> ## Documentation Index
> Fetch the complete documentation index at: https://docs.landing.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Visualize Parsed Blocks: Draw Bounding Boxes

> Draw a labeled, color-coded bounding box around every block the Parse API found, with each table cell outlined, one annotated PNG per page.

Use this script to see how the [Parse API](./parse) divided a document into blocks. It draws a box around every block on each page, colored by [block type](./parse-response#block-types) and labeled with the block's ID (such as `text-0` or `table-0`). Inside each table, every cell gets a thin box with no label, so the labels don't cover the table text.

The script reads the `structure` tree of the parse response. Every page node lists the blocks on that page as `children`, a `table` block lists its cells, and each node carries a `grounding` object with its page number and `box`. See [Structure: Pages and Blocks](./parse-response#structure-pages-and-blocks).

Two details matter when you draw on the page:

* **Page numbers are 1-indexed.** `grounding.page` is `1` for the first page, while PDF libraries such as PyMuPDF number pages from 0. The script subtracts 1 when it opens a page.
* **Boxes are fractions of the page.** `xmin` and `xmax` are fractions of the page width, and `ymin` and `ymax` are fractions of the page height, from 0 to 1. Multiply them by the width and height of the rendered page image. See [Working with Boxes](./parse-response#working-with-boxes).

For PDFs, the script saves one annotated PNG per page (`page_1_annotated.png`, `page_2_annotated.png`). For image files, it saves `page_annotated.png`. The Python script draws on PDFs and every [image type](./file-types) Parse accepts. The TypeScript script draws on PDF, PNG, and JPEG files. For any other file, such as a DOCX file or a spreadsheet, the script prints a message and saves nothing:

* **Text documents and presentations:** Parse converts them to PDF before parsing, so there is no page image to match the boxes to. To see the boxes, convert the document to PDF and parse the PDF.
* **Spreadsheets:** sheets have no page boxes. Their cells are grounded by `address`.

The script also skips any page that failed to parse, and saves nothing when no page is left (an image whose only page failed).

> [!NOTE]
>
> These examples require the [Python](./ade-python) or [TypeScript](./ade-typescript) client library. Before running a script, set your API key and install the library and any required dependencies.

## Scripts

```python Python [expandable] theme={null}
from pathlib import Path
from landingai_ade import LandingAIADE
from PIL import Image, ImageDraw, ImageOps
import pymupdf

# Define colors for each block type
BLOCK_TYPE_COLORS = {
    "text": (40, 167, 69),          # Green
    "table": (0, 123, 255),         # Blue
    "table_cell": (173, 216, 230),  # Light blue
    "marginalia": (111, 66, 193),   # Purple
    "figure": (255, 0, 255),        # Magenta
    "logo": (144, 238, 144),        # Light green
    "card": (255, 165, 0),          # Orange
    "attestation": (0, 255, 255),   # Cyan
    "scan_code": (255, 193, 7),     # Yellow
}

# Image types the Parse API accepts. The script draws on these and on PDFs.
IMAGE_TYPES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff", ".webp"}


def iter_blocks(node):
    """Yield every block nested below a node, such as the cells inside a table."""
    for child in node.children or []:
        yield child
        yield from iter_blocks(child)


def to_pixels(box, img_width, img_height):
    """Convert normalized box coordinates to pixel coordinates."""
    return [int(box.xmin * img_width), int(box.ymin * img_height),
            int(box.xmax * img_width), int(box.ymax * img_height)]


def draw_blocks(image, page):
    """Create an annotated copy of a page image with a labeled box around each block."""
    annotated_img = image.copy()
    draw = ImageDraw.Draw(annotated_img)
    img_width, img_height = image.size

    for block in page.children or []:
        x1, y1, x2, y2 = to_pixels(block.grounding.box, img_width, img_height)

        # Draw bounding box
        color = BLOCK_TYPE_COLORS.get(block.type, (128, 128, 128))  # Default to gray
        draw.rectangle([x1, y1, x2, y2], outline=color, width=3)

        # Draw label background and text (the block ID, such as text-0)
        label_y = max(0, y1 - 20)
        draw.rectangle([x1, label_y, x1 + len(block.id) * 8, y1], fill=color)
        draw.text((x1 + 2, label_y + 2), block.id, fill=(255, 255, 255))

        # Outline the cells inside a table at 1px with no label, so labels don't cover the table text
        for cell in iter_blocks(block):
            cell_color = BLOCK_TYPE_COLORS.get(cell.type, (128, 128, 128))
            draw.rectangle(to_pixels(cell.grounding.box, img_width, img_height), outline=cell_color, width=1)

    return annotated_img


def draw_bounding_boxes(parse_response, document_path):
    """Save an annotated PNG for each parsed page."""
    # Office documents and spreadsheets have no page image to draw on
    file_type = document_path.suffix.lower()
    if file_type != ".pdf" and file_type not in IMAGE_TYPES:
        print(f"Nothing to draw: the script draws on PDFs and images, not {document_path.name}")
        return

    # Skip pages that failed to parse
    pages = [
        node for node in parse_response.structure.children
        if node.type == "page" and node.status != "failed"
    ]
    if not pages:
        print("Nothing to draw: the document has no successfully parsed pages")
        return

    if file_type == ".pdf":
        pdf = pymupdf.open(document_path)

        for page in pages:
            page_number = page.grounding.page  # 1-indexed
            pix = pdf[page_number - 1].get_pixmap(matrix=pymupdf.Matrix(2, 2))  # 2x scaling
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

            annotated_path = f"page_{page_number}_annotated.png"
            draw_blocks(img, page).save(annotated_path)
            print(f"Annotated image saved to: {annotated_path}")

        pdf.close()
    else:
        # Boxes match the upright image, so apply any EXIF orientation first
        img = ImageOps.exif_transpose(Image.open(document_path)).convert("RGB")

        annotated_path = "page_annotated.png"
        draw_blocks(img, pages[0]).save(annotated_path)
        print(f"Annotated image saved to: {annotated_path}")


# Initialize client (uses the API key from the VISION_AGENT_API_KEY environment variable)
client = LandingAIADE()

# Replace with your file path
document_path = Path("/path/to/file/document")

# Parse the document
print("Parsing document...")
parse_response = client.v2.parse(
    document=document_path,
    model="dpt-3-pro-latest",
)
print("Parsing complete!")

# Draw bounding boxes and create annotated images
draw_bounding_boxes(parse_response, document_path)
```

```typescript TypeScript [expandable] theme={null}
import LandingAIADE from "landingai-ade";
import fs from "fs";
import path from "path";
import { createCanvas, loadImage } from "canvas";
import { pdf } from "pdf-to-img";

type Block = LandingAIADE.V2ParseElement;
type Page = LandingAIADE.V2ParsePage;

// Define colors for each block type
const BLOCK_TYPE_COLORS: Record<string, string> = {
  text: "rgb(40, 167, 69)",          // Green
  table: "rgb(0, 123, 255)",         // Blue
  table_cell: "rgb(173, 216, 230)",  // Light blue
  marginalia: "rgb(111, 66, 193)",   // Purple
  figure: "rgb(255, 0, 255)",        // Magenta
  logo: "rgb(144, 238, 144)",        // Light green
  card: "rgb(255, 165, 0)",          // Orange
  attestation: "rgb(0, 255, 255)",   // Cyan
  scan_code: "rgb(255, 193, 7)",     // Yellow
};

// Image types the script draws on, besides PDFs (convert other images, such as TIFF or WebP, to PNG first)
const IMAGE_TYPES = new Set([".png", ".jpg", ".jpeg"]);

// Yield every block nested below a node, such as the cells inside a table
function* iterBlocks(node: { children?: Block[] | null }): Generator<Block> {
  for (const child of node.children ?? []) {
    yield child;
    yield* iterBlocks(child);
  }
}

// Convert normalized box coordinates to pixel coordinates
function toPixels(block: Block, width: number, height: number): [number, number, number, number] {
  const box = block.grounding!.box;
  return [
    Math.floor(box.xmin * width),
    Math.floor(box.ymin * height),
    Math.floor(box.xmax * width),
    Math.floor(box.ymax * height),
  ];
}

// Create an annotated copy of a page image with a labeled box around each block
async function drawBlocks(imageSource: Buffer | string, page: Page): Promise<Buffer> {
  const image = await loadImage(imageSource);
  const canvas = createCanvas(image.width, image.height);
  const ctx = canvas.getContext("2d");
  ctx.drawImage(image, 0, 0);

  for (const block of page.children ?? []) {
    const [x1, y1, x2, y2] = toPixels(block, image.width, image.height);

    // Draw bounding box
    const color = BLOCK_TYPE_COLORS[block.type] ?? "rgb(128, 128, 128)"; // Default to gray
    ctx.strokeStyle = color;
    ctx.lineWidth = 3;
    ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);

    // Draw label background and text (the block ID, such as text-0)
    const labelY = Math.max(0, y1 - 20);
    ctx.fillStyle = color;
    ctx.fillRect(x1, labelY, block.id.length * 8, 20);
    ctx.fillStyle = "white";
    ctx.font = "12px sans-serif";
    ctx.fillText(block.id, x1 + 2, labelY + 14);

    // Outline the cells inside a table at 1px with no label, so labels don't cover the table text
    for (const cell of iterBlocks(block)) {
      const [cx1, cy1, cx2, cy2] = toPixels(cell, image.width, image.height);
      ctx.strokeStyle = BLOCK_TYPE_COLORS[cell.type] ?? "rgb(128, 128, 128)";
      ctx.lineWidth = 1;
      ctx.strokeRect(cx1, cy1, cx2 - cx1, cy2 - cy1);
    }
  }

  return canvas.toBuffer("image/png");
}

// Save an annotated PNG for each parsed page
async function drawBoundingBoxes(parseResponse: LandingAIADE.V2ParseResponse, documentPath: string) {
  // Office documents and spreadsheets have no page image to draw on
  const fileType = path.extname(documentPath).toLowerCase();
  if (fileType !== ".pdf" && !IMAGE_TYPES.has(fileType)) {
    console.log(`Nothing to draw: the script draws on PDF, PNG and JPEG files, not ${path.basename(documentPath)}`);
    return;
  }

  // Skip pages that failed to parse
  const pages = (parseResponse.structure?.children ?? []).filter(
    (node) => node.type === "page" && node.status !== "failed",
  );
  if (pages.length === 0) {
    console.log("Nothing to draw: the document has no successfully parsed pages");
    return;
  }

  if (fileType === ".pdf") {
    const pagesByNumber = new Map(pages.map((page) => [page.grounding!.page, page] as const));

    // pdf-to-img renders the pages in order; grounding.page is 1-indexed
    const document = await pdf(documentPath, { scale: 2.0 });
    let pageNumber = 1;
    for await (const pageImage of document) {
      const page = pagesByNumber.get(pageNumber);
      if (page) {
        const outputPath = `page_${pageNumber}_annotated.png`;
        fs.writeFileSync(outputPath, await drawBlocks(pageImage, page));
        console.log(`Annotated image saved to: ${outputPath}`);
      }
      pageNumber++;
    }
  } else {
    // Boxes match the upright image: rotate photos that carry EXIF orientation before running the script
    const outputPath = "page_annotated.png";
    fs.writeFileSync(outputPath, await drawBlocks(documentPath, pages[0]));
    console.log(`Annotated image saved to: ${outputPath}`);
  }
}

// Initialize client (uses the API key from the VISION_AGENT_API_KEY environment variable)
const client = new LandingAIADE();

async function visualizeBlocks() {
  // Replace with your file path
  const documentPath = "/path/to/file/document";

  // Parse the document
  console.log("Parsing document...");
  const parseResponse = await client.v2.parse({
    document: fs.createReadStream(documentPath),
    model: "dpt-3-pro-latest",
  });
  console.log("Parsing complete!");

  // Draw bounding boxes and create annotated images
  await drawBoundingBoxes(parseResponse, documentPath);
}

visualizeBlocks();
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.