Files
zeekayandHanzo Dev d15022d194 fix(pdf): tear a pdf.js document down through its loading task
pdfjs-dist went from 5 to 6 and PDFDocumentProxy lost destroy(). In 5 it
was one line, `return this.loadingTask.destroy()`, so the task is what
there is to destroy. Three calls were left:

- pdfToImages called it after rendering. The call threw before its .catch
  could attach, so every render ended in "pdf.destroy is not a function"
  and the AI features that read a document got nothing. The task it also
  destroyed is the same task, so that call is the one kept.
- The PDF viewer called it when a new document replaced an old one, inside
  the load's try, so switching documents showed the load error; and in the
  effect's cleanup. Both now destroy the loading task, as 5 did for them.

pdf-to-images.test.ts renders a two-page PDF and checks each page comes
back as a letter-size JPEG in order; against the old code it fails with the
TypeError.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-19 04:31:23 -07:00

98 lines
2.5 KiB
TypeScript

import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { Canvas, Image, Path2D } from 'skia-canvas';
// @ts-expect-error napi-rs/canvas satisfies the requirements
globalThis.Path2D = Path2D;
// @ts-expect-error napi-rs/canvas satisfies the requirements
globalThis.Image = Image;
class SkiaCanvasFactory {
_createCanvas(width: number, height: number) {
const canvas = new Canvas(width, height);
canvas.gpu = false;
return canvas;
}
create(width: number, height: number) {
const canvas = this._createCanvas(width, height);
return {
canvas,
context: canvas.getContext('2d'),
};
}
reset(canvasAndContext: { canvas: Canvas }, width: number, height: number) {
canvasAndContext.canvas.width = width;
canvasAndContext.canvas.height = height;
}
destroy(canvasAndContext: { canvas: Canvas | null; context: unknown }) {
if (canvasAndContext.canvas) {
canvasAndContext.canvas.width = 0;
canvasAndContext.canvas.height = 0;
}
canvasAndContext.canvas = null;
canvasAndContext.context = null;
}
}
export type PdfToImagesOptions = {
scale?: number;
};
export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOptions = {}) => {
const { scale = 2 } = options;
const task = await pdfjsLib.getDocument({
data: pdfBytes,
CanvasFactory: SkiaCanvasFactory,
});
const pdf = await task.promise;
const images = await pMap(
Array.from({ length: pdf.numPages }),
async (_, index) => {
const pageNumber = index + 1;
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
// @ts-expect-error napi-rs/canvas satifies the requirements
canvas,
// @ts-expect-error napi-rs/canvas satifies the requirements
canvasContext,
viewport,
}).promise;
const result = {
pageNumber,
image: await canvas.toBuffer('jpeg'),
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
mimeType: 'image/jpeg',
};
void page.cleanup();
return result;
},
{ concurrency: 10 },
);
// Destroying the loading task destroys the document it loaded.
void task.destroy().catch((e) => console.error(e));
return images;
};