Build an Automated Visual Report Pipeline: Parse, Transform, Render

7 min read

Reports Are Still Manual

Someone sends you a monthly report as a PDF. You need to extract the key numbers, clean up the attached charts, and produce a visual summary card for the team dashboard. Every month.

The manual version: open the PDF, copy the numbers into a spreadsheet, screenshot the charts, crop them in an image editor, paste everything into a Figma template, export, upload. An hour of work for a task that should take seconds.

The semi-automated version: a Python script that uses pdfplumber to extract text, regex to find the numbers, Pillow to crop the charts, and Jinja2 with wkhtmltopdf to render the summary. Two hundred lines of brittle code that breaks when the report layout changes.

There’s a better way. Three API calls. Parse the document for structured data. Transform extracted images to the right size and format. Render the final visual report. No servers, no libraries, no layout parsing code.

The Three APIs

This pipeline uses three Iteration Layer products, each handling one step:

  • Document Extraction API — extracts structured data from the source document. Confidence scores, built-in OCR.
  • Image Transformation API — resizes, crops, and converts images. 24 operations, chainable in a single request.
  • Image Generation API — composes the final visual from layers: backgrounds, shapes, text, and images.

Each API is independent. You chain them by passing one API’s output as the next API’s input. The orchestration is just code — a few fetch calls in sequence.

Step 1: Parse the Source Document

Start with the document. A monthly sales report PDF containing a revenue figure, a growth percentage, the reporting period, and a chart image.

import { IterationLayer } from "iterationlayer";

const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const parseResult = await client.extract({
  files: [
    { type: "url", name: "sales-report-jan-2026.pdf", url: reportPdfUrl },
  ],
  schema: {
    fields: [
      { name: "reporting_period", type: "TEXT", description: "The month and year of the report" },
      { name: "total_revenue", type: "CURRENCY_AMOUNT", description: "Total revenue for the period" },
      { name: "currency_code", type: "CURRENCY_CODE", description: "Currency of the revenue" },
      { name: "revenue_growth_percent", type: "DECIMAL", description: "Revenue growth percentage compared to previous period" },
      {
        name: "top_products",
        type: "ARRAY",
        description: "Top selling products",
        item_schema: {
          fields: [
            { name: "product_name", type: "TEXT", description: "Product name" },
            { name: "units_sold", type: "INTEGER", description: "Number of units sold" },
          ],
        },
      },
    ],
  },
});

const { results: [reportData] } = parseResult.data;

The parser returns structured JSON with every field typed and scored for confidence:

{
  "reportingPeriod": { "value": "January 2026", "confidence": 0.97 },
  "totalRevenue": { "value": 847500.00, "confidence": 0.96 },
  "currencyCode": { "value": "USD", "confidence": 0.99 },
  "revenueGrowthPercent": { "value": 12.3, "confidence": 0.91 },
  "topProducts": {
    "value": [
      [
        { "value": "Enterprise Plan", "confidence": 0.95 },
        { "value": 342, "confidence": 0.93 }
      ],
      [
        { "value": "Team Plan", "confidence": 0.94 },
        { "value": 1205, "confidence": 0.92 }
      ]
    ],
    "confidence": 0.93
  }
}

No regex. No layout coordinates. No brittle text extraction. You describe the fields, the parser finds them.

Step 2: Transform Supporting Images

If your report includes charts, logos, or other images that need to be included in the final visual, transform them to the right dimensions and format first.

Say you have a company logo that needs to fit a 120x40 space in the report card:

const { data: { buffer: logoBase64 } } = await client.transform({
  file: { type: "url", name: "company-logo.png", url: companyLogoUrl },
  operations: [
    { type: "resize", width_in_px: 120, height_in_px: 40, fit: "contain" },
    { type: "convert", format: "png" },
  ],
});

The contain fit ensures the logo scales to fit within 120x40 without cropping or distortion. If the logo is landscape, it fills the width. If it’s square, it fits the height.

This step is optional — skip it if your visual report only uses text and shapes. But most real-world reports include logos, charts, or reference images that need sizing.

Step 3: Render the Visual Report

Now compose everything into a final image using the Image Generation API. The extracted data becomes text layers. The transformed logo becomes a static-image layer. Background and shapes tie it together.

const topProductsSummary = reportData.top_products.value
  .map(([name, units]) => `${name.value}: ${units.value.toLocaleString()} units`)
  .join("\n");

const { data: { buffer: reportCardBase64 } } = await client.generateImage({
  widthInPx: 1200,
  heightInPx: 630,
  output_format: "png",
  layers: [
    {
      type: "solid-color-background",
      index: 0,
      hex_color: "#0f172a",
    },
    {
      type: "rectangle",
      index: 1,
      xInPx: 0,
      yInPx: 0,
      widthInPx: 1200,
      heightInPx: 6,
      hex_color: "#3b82f6",
    },
    {
      type: "static-image",
      index: 2,
      file: { type: "base64", name: "logo.png", base64: logoBase64 },
      xInPx: 60,
      yInPx: 40,
      widthInPx: 120,
      heightInPx: 40,
    },
    {
      type: "text",
      index: 3,
      text: `**${reportData.reporting_period.value}** Sales Report`,
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 18,
      hex_color: "#94a3b8",
      xInPx: 60,
      yInPx: 100,
      maxWidthInPx: 1080,
      maxHeightInPx: 30,
    },
    {
      type: "text",
      index: 4,
      text: `$${reportData.total_revenue.value.toLocaleString()}`,
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 64,
      hex_color: "#ffffff",
      xInPx: 60,
      yInPx: 140,
      maxWidthInPx: 600,
      maxHeightInPx: 80,
    },
    {
      type: "text",
      index: 5,
      text: `+${reportData.revenue_growth_percent.value}% vs previous period`,
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 20,
      hex_color: "#4ade80",
      xInPx: 60,
      yInPx: 230,
      maxWidthInPx: 600,
      maxHeightInPx: 30,
    },
    {
      type: "rectangle",
      index: 6,
      xInPx: 60,
      yInPx: 280,
      widthInPx: 1080,
      heightInPx: 1,
      hex_color: "#1e293b",
    },
    {
      type: "text",
      index: 7,
      text: "**Top Products**",
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 16,
      hex_color: "#94a3b8",
      xInPx: 60,
      yInPx: 310,
      maxWidthInPx: 1080,
      maxHeightInPx: 24,
    },
    {
      type: "text",
      index: 8,
      text: topProductsSummary,
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 18,
      hex_color: "#e2e8f0",
      xInPx: 60,
      yInPx: 350,
      maxWidthInPx: 1080,
      maxHeightInPx: 200,
    },
  ],
  fonts: [
    {
      name: "Inter",
      weight: "Regular",
      style: "normal",
      file: { type: "url", name: "Inter-Regular.ttf", url: "https://example.com/fonts/Inter-Regular.ttf" },
    },
    {
      name: "Inter",
      weight: "Bold",
      style: "normal",
      file: { type: "url", name: "Inter-Bold.ttf", url: "https://example.com/fonts/Inter-Bold.ttf" },
    },
  ],
});

const reportCardBuffer = Buffer.from(reportCardBase64, "base64");

Three API calls. The PDF goes in. A branded, data-driven visual comes out.

Why Three Separate APIs

You might wonder why this isn’t one API that does everything. The answer is composability.

Each API does one thing well. The Document Extraction doesn’t know about images. The Image Transformation API doesn’t know about documents. The Image Generation API doesn’t know about either. They communicate through data — JSON with structured results and base64-encoded images.

This means you can swap out any step. Parse the data from a spreadsheet instead of a PDF — the generation step doesn’t change. Skip the transformation step if you don’t need images in the output. Add a fourth step that posts the result to Slack or uploads it to S3.

The pipeline is just API calls in sequence. No orchestration framework. No workflow engine. No event bus. Just fetch, await, fetch.

Adapting the Pattern

Weekly team updates. Parse a project tracking document for milestone status, blockers, and metrics. Transform team member avatars to uniform size. Render a status card for Slack.

Client deliverables. Parse a scope document for project name, deliverables, and timeline. Render a branded project summary card for the client portal.

Financial snapshots. Parse bank statements or financial reports for account balances, transaction totals, and period comparisons. Render a dashboard tile for the finance team.

The pattern is always the same: structured data in, visual out. The Document Extraction handles the “structured data” part regardless of the document format. The Image Generation API handles the “visual” part regardless of the data source.

Get Started

Check the full API references — Document Extraction, Image Transformation, and Image Generation. All three have TypeScript and Python SDKs.

Sign up for a free account — no credit card required. Try the pipeline with a real document. Parse it, transform what you need, and render the result. Three API calls, one visual output.

Start building in minutes

Free trial included. No credit card required.