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();