> ## 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.

# Save Parsed Blocks as Images

> Crop each block the Parse API found and save it as a separate PNG named by its block ID, to build datasets or review parsing quality.

Use this script to crop each block the [Parse API](./parse) found and save it as a separate PNG. Use the crops to build datasets, review parsing quality block by block, or process individual regions of a document.

The script reads the `structure` tree of the parse response: each page node lists the blocks on that page as `children`, and each block carries a `grounding` object with its 1-indexed page number and a `box` in fractions of the page width and height. See [Structure: Pages and Blocks](./parse-response#structure-pages-and-blocks). Each table is saved as one image.

The Python script crops PDFs and every [image type](./file-types) Parse accepts. The TypeScript script crops PDF, PNG, and JPEG files. For any other file, the script prints a message and saves nothing. Parse converts text documents and presentations to PDF before parsing, so to crop blocks from a DOCX or PPTX file, convert it to PDF and parse the PDF. Spreadsheets have no page boxes. The script also skips pages 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 datetime import datetime
from landingai_ade import LandingAIADE
from PIL import Image, ImageOps
import pymupdf

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


def save_blocks_as_images(parse_response, document_path, output_base_dir="blocks"):
    """Save each parsed block as a separate image file."""

    # Office documents and spreadsheets have no page image to crop from
    file_type = document_path.suffix.lower()
    if file_type != ".pdf" and file_type not in IMAGE_TYPES:
        print(f"No blocks to save: the script crops PDFs and images, not {document_path.name}")
        return None

    # Create timestamped output directory
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_dir = Path(output_base_dir) / f"{document_path.stem}_{timestamp}"

    def save_page_blocks(image, page):
        """Save all blocks for one page."""
        img_width, img_height = image.size

        # Create page-specific directory (page numbers are 1-indexed)
        page_dir = output_dir / f"page_{page.grounding.page}"
        page_dir.mkdir(parents=True, exist_ok=True)

        for block in page.children:
            box = block.grounding.box

            # Convert normalized coordinates to pixel coordinates
            x1 = int(box.xmin * img_width)
            y1 = int(box.ymin * img_height)
            x2 = int(box.xmax * img_width)
            y2 = int(box.ymax * img_height)
            if x2 <= x1 or y2 <= y1:
                continue  # Nothing to crop

            # Save the crop, named by block ID (for example, text-0.png or table-0.png)
            output_path = page_dir / f"{block.id}.png"
            image.crop((x1, y1, x2, y2)).save(output_path)
            print(f"Saved block: {output_path}")

    # 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("No blocks to save: the document has no successfully parsed pages")
        return None

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

        for page in pages:
            # PyMuPDF numbers pages from 0, and grounding.page from 1
            pix = pdf[page.grounding.page - 1].get_pixmap(matrix=pymupdf.Matrix(2, 2))  # 2x scaling for clarity
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            save_page_blocks(img, page)

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

    print(f"\nAll blocks saved to: {output_dir}")
    return output_dir


# 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!")

# Save blocks as images
save_blocks_as_images(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";

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

async function saveBlocksAsImages(
  parseResponse: LandingAIADE.V2ParseResponse,
  documentPath: string,
  outputBaseDir: string = "blocks"
) {
  // Office documents and spreadsheets have no page image to crop from
  const fileType = path.extname(documentPath).toLowerCase();
  if (fileType !== ".pdf" && !IMAGE_TYPES.has(fileType)) {
    console.log(`No blocks to save: the script crops PDF, PNG and JPEG files, not ${path.basename(documentPath)}`);
    return null;
  }

  // Create timestamped output directory
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
  const documentName = path.basename(documentPath, path.extname(documentPath));
  const outputDir = path.join(outputBaseDir, `${documentName}_${timestamp}`);

  async function savePageBlocks(imageSource: Buffer | string, page: LandingAIADE.V2ParsePage) {
    // Load the page image
    const image = await loadImage(imageSource);

    // Create page-specific directory (page numbers are 1-indexed)
    const pageDir = path.join(outputDir, `page_${page.grounding!.page}`);
    fs.mkdirSync(pageDir, { recursive: true });

    for (const block of page.children ?? []) {
      const box = block.grounding!.box;

      // Convert normalized coordinates to pixel coordinates
      const x1 = Math.floor(box.xmin * image.width);
      const y1 = Math.floor(box.ymin * image.height);
      const x2 = Math.floor(box.xmax * image.width);
      const y2 = Math.floor(box.ymax * image.height);
      const width = x2 - x1;
      const height = y2 - y1;
      if (width <= 0 || height <= 0) continue; // Nothing to crop

      // Draw the cropped region on its own canvas
      const canvas = createCanvas(width, height);
      canvas.getContext("2d").drawImage(image, x1, y1, width, height, 0, 0, width, height);

      // Save the crop, named by block ID (for example, text-0.png or table-0.png)
      const outputPath = path.join(pageDir, `${block.id}.png`);
      fs.writeFileSync(outputPath, canvas.toBuffer("image/png"));
      console.log(`Saved block: ${outputPath}`);
    }
  }

  // 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("No blocks to save: the document has no successfully parsed pages");
    return null;
  }

  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) await savePageBlocks(pageImage, page);
      pageNumber++;
    }
  } else {
    // Boxes match the upright image: rotate photos that carry EXIF orientation before running the script
    await savePageBlocks(documentPath, pages[0]);
  }

  console.log(`\nAll blocks saved to: ${outputDir}`);
  return outputDir;
}

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

async function extractBlocks() {
  // 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!");

  // Save each block as a separate image
  await saveBlocksAsImages(parseResponse, documentPath);
}

extractBlocks();
```

## Directory Structure for Saved Images

The scripts save the images in this structure:

```text theme={null}
blocks/
└── document_TIMESTAMP/
    └── page_1/
        └── BLOCK_ID.png
```

Where:

* `TIMESTAMP` is the date and time the script ran (format: `YYYYMMDD_HHMMSS` for Python, ISO format for TypeScript).
* `page_1` is the 1-indexed page number, the same value as the block's `grounding.page`.
* `BLOCK_ID` is the block's `id`, in the format `<type>-<index>`. The index counts blocks of each type in reading order across the whole document, so `text-2` is the document's third text block, wherever it appears. See [Common Block Fields](./parse-response#common-block-fields).

For example, a two-page document with a table and a figure on the first page could produce:

```text theme={null}
blocks/
└── document_20261012_143022/
    ├── page_1/
    │   ├── text-0.png
    │   ├── text-1.png
    │   ├── table-0.png
    │   └── figure-0.png
    └── page_2/
        ├── text-2.png
        └── marginalia-0.png
```


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