Add searchable and flattened export, history re-export and diagnostics self-test

issueAndArchive keeps an issued invoice even when rendering or archiving
fails (Issued - not archived, with a Render & archive action). The export
menu offers a searchable PDF or a 300 DPI image-only PDF whose metadata is
the invoice number alone; flatten failures offer Retry or searchable export
and never silently lower the resolution. History re-exports the original
as issued by default and warns when a re-render's layout fingerprint
differs. The render client gains a FIFO assemble lane that previews cannot
supersede. A Diagnostics panel runs fixtures in the real worker and
compares against goldens generated from Node.
This commit is contained in:
2026-10-04 06:11:23 +05:30
parent 7162dac6e7
commit f1b7502e9d
30 changed files with 2727 additions and 154 deletions
+57
View File
@@ -0,0 +1,57 @@
import { flattenPages, type CanvasLike, type FlattenResult, type PdfDocLike } from "../pdf/flatten";
/** Lets the browser paint and handle input; falls back to a timer when rAF is paused (hidden window). */
export const yieldToBrowser = (): Promise<void> =>
new Promise<void>((resolve) => {
let done = false;
const finish = () => {
if (!done) {
done = true;
resolve();
}
};
setTimeout(finish, 50);
requestAnimationFrame(() => setTimeout(finish, 0));
});
/** JPEG bytes of a canvas via the browser's encoder. */
export function encodeCanvasJpeg(canvas: CanvasLike, quality: number): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
(canvas as unknown as HTMLCanvasElement).toBlob(
(blob) => {
if (!blob) return reject(new Error("the browser could not encode the page image (out of memory?)"));
blob.arrayBuffer().then((buf) => resolve(new Uint8Array(buf)), reject);
},
"image/jpeg",
quality,
);
});
}
export interface BrowserFlattenOptions {
signal?: AbortSignal;
onProgress?: (page: number, pages: number) => void;
}
/** Flattens PDF bytes at 300 DPI with pdf.js and a DOM canvas; opens and closes the document itself. */
export async function flattenInBrowser(bytes: Uint8Array, opts: BrowserFlattenOptions = {}): Promise<FlattenResult> {
const { openPdf, closePdf } = await import("./pdfjs");
const doc = await openPdf(bytes);
try {
return await flattenPages({
pdf: doc as unknown as PdfDocLike,
createCanvas: (width, height) => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
return canvas as unknown as CanvasLike;
},
encodeJpeg: encodeCanvasJpeg,
yieldToUi: yieldToBrowser,
signal: opts.signal,
onProgress: opts.onProgress,
});
} finally {
await closePdf(doc).catch(() => {});
}
}