Files
documenso/apps/remix/server/api/document-analysis/debug-visualizer.ts
Ephraim Atta-Duncan 654fc57639 chore: review
2025-11-19 10:57:54 +00:00

184 lines
5.2 KiB
TypeScript

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { Canvas, Image } from 'skia-canvas';
import type { TDetectFormFieldsResponse } from './types';
export type RenderedPage = {
image: Buffer;
pageNumber: number;
width: number;
height: number;
};
const GRID_PADDING = { left: 80, top: 20, right: 20, bottom: 40 };
const GRID_INTERVAL = 100;
const FIELD_COLORS = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'];
/**
* Saves debug visualizations of detected form fields for development purposes.
* Creates annotated images showing field bounding boxes and coordinate grids.
*/
export async function saveDebugVisualization(
renderedPages: RenderedPage[],
detectedFields: TDetectFormFieldsResponse,
): Promise<void> {
const debugDir = join(process.cwd(), '..', '..', 'packages', 'assets', 'ai-previews');
await mkdir(debugDir, { recursive: true });
const timestamp = new Date()
.toISOString()
.replace(/[-:]/g, '')
.replace(/\..+/, '')
.replace('T', '_');
for (const page of renderedPages) {
const canvas = createAnnotatedCanvas(page, detectedFields);
await saveCanvasToFile(canvas, debugDir, timestamp, page.pageNumber);
}
}
function createAnnotatedCanvas(
page: RenderedPage,
detectedFields: TDetectFormFieldsResponse,
): Canvas {
const canvas = new Canvas(
page.width + GRID_PADDING.left + GRID_PADDING.right,
page.height + GRID_PADDING.top + GRID_PADDING.bottom,
);
const ctx = canvas.getContext('2d');
// Draw the original page image
const img = new Image();
img.src = page.image;
ctx.drawImage(img, GRID_PADDING.left, GRID_PADDING.top);
// Draw coordinate grid
drawCoordinateGrid(ctx, page.width, page.height);
// Draw field bounding boxes
drawFieldBoundingBoxes(ctx, page, detectedFields);
// Draw axis labels
drawAxisLabels(ctx, page.width, page.height);
return canvas;
}
function drawCoordinateGrid(
ctx: ReturnType<Canvas['getContext']>,
pageWidth: number,
pageHeight: number,
): void {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
ctx.lineWidth = 1;
// Draw vertical grid lines
for (let i = 0; i <= 1000; i += GRID_INTERVAL) {
const x = GRID_PADDING.left + (i / 1000) * pageWidth;
ctx.beginPath();
ctx.moveTo(x, GRID_PADDING.top);
ctx.lineTo(x, pageHeight + GRID_PADDING.top);
ctx.stroke();
}
// Draw horizontal grid lines
for (let i = 0; i <= 1000; i += GRID_INTERVAL) {
const y = GRID_PADDING.top + (i / 1000) * pageHeight;
ctx.beginPath();
ctx.moveTo(GRID_PADDING.left, y);
ctx.lineTo(pageWidth + GRID_PADDING.left, y);
ctx.stroke();
}
}
function drawFieldBoundingBoxes(
ctx: ReturnType<Canvas['getContext']>,
page: RenderedPage,
detectedFields: TDetectFormFieldsResponse,
): void {
const pageFields = detectedFields.filter((field) => field.pageNumber === page.pageNumber);
pageFields.forEach((field, index) => {
const { ymin, xmin, ymax, xmax } = field.boundingBox;
const x = (xmin / 1000) * page.width + GRID_PADDING.left;
const y = (ymin / 1000) * page.height + GRID_PADDING.top;
const width = ((xmax - xmin) / 1000) * page.width;
const height = ((ymax - ymin) / 1000) * page.height;
const color = FIELD_COLORS[index % FIELD_COLORS.length];
ctx.strokeStyle = color;
ctx.lineWidth = 5;
ctx.strokeRect(x, y, width, height);
// Draw field label
ctx.fillStyle = color;
ctx.font = '20px Arial';
ctx.fillText(field.label, x, y - 5);
});
}
function drawAxisLabels(
ctx: ReturnType<Canvas['getContext']>,
pageWidth: number,
pageHeight: number,
): void {
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.font = '26px Arial';
// Draw Y-axis
ctx.beginPath();
ctx.moveTo(GRID_PADDING.left, GRID_PADDING.top);
ctx.lineTo(GRID_PADDING.left, pageHeight + GRID_PADDING.top);
ctx.stroke();
// Y-axis labels
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let i = 0; i <= 1000; i += GRID_INTERVAL) {
const y = GRID_PADDING.top + (i / 1000) * pageHeight;
ctx.fillStyle = '#000000';
ctx.fillText(i.toString(), GRID_PADDING.left - 5, y);
ctx.beginPath();
ctx.moveTo(GRID_PADDING.left - 5, y);
ctx.lineTo(GRID_PADDING.left, y);
ctx.stroke();
}
// Draw X-axis
ctx.beginPath();
ctx.moveTo(GRID_PADDING.left, pageHeight + GRID_PADDING.top);
ctx.lineTo(pageWidth + GRID_PADDING.left, pageHeight + GRID_PADDING.top);
ctx.stroke();
// X-axis labels
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let i = 0; i <= 1000; i += GRID_INTERVAL) {
const x = GRID_PADDING.left + (i / 1000) * pageWidth;
ctx.fillStyle = '#000000';
ctx.fillText(i.toString(), x, pageHeight + GRID_PADDING.top + 5);
ctx.beginPath();
ctx.moveTo(x, pageHeight + GRID_PADDING.top);
ctx.lineTo(x, pageHeight + GRID_PADDING.top + 5);
ctx.stroke();
}
}
async function saveCanvasToFile(
canvas: Canvas,
debugDir: string,
timestamp: string,
pageNumber: number,
): Promise<void> {
const outputFilename = `detected_form_fields_${timestamp}_page_${pageNumber}.png`;
const outputPath = join(debugDir, outputFilename);
const pngBuffer = await canvas.toBuffer('png');
await writeFile(outputPath, new Uint8Array(pngBuffer));
}