fix(documents): read the text layer through the same pdf.js that renders

Under Node, pdf.js runs its worker in the server process and keeps it on
a global, and whichever copy of pdf.js opens a document first owns that
global until a restart. The local text read went through pdf-parse, which
pins its own pdfjs-dist 5.4.296; the rasterizer uses the app's 6.4.299. A
text read first made every later rasterization fail with "The API version
does not match the Worker version" (thumbnails raster-failed, the vision
read of a scan 422), and a rasterization first made every later local text
read fail, which left the document out of the search index with nothing in
the logs.

src/lib/documents/pdfjs.ts is now the one place pdf.js is loaded and
opened, with one set of options (asset directories, no eval, no system
fonts). The text read walks the pages with getTextContent and lays the
text out the way pdf-parse did with the options it was called with: line
breaks, tabs between far-apart items, a blank line after each page, the
200-page cap. Text extracted from the PDF fixtures and from generated lab
tables, encrypted files and doctor reports is identical before and after.

A failed text read now emits documents.local_extract.failed with the
error pdf.js threw, and annotates the index event with it, instead of
returning a bare error.

The report tests read PDF text through the same reader. Two tests, one per
order, run a text read and a rasterization in one process; both failed
before this change.

Refs #1277
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 19:32:13 +02:00
1 parent 567cb19afc
commit 0b1757ff6f
15 files changed
+612 -244

No files matched your search

@@ -14,9 +14,10 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
import { PDFParse } from "pdf-parse";
import { unzipSync } from "fflate";
import { readPdfText } from "@/lib/documents/pdfjs";
vi.mock("@/lib/db", () => ({
prisma: {
// `groupBy` sizes the report's dense windows (#1023): a sparse account.
@@ -155,22 +156,11 @@ function mkReq(
}
async function pdfText(res: Response): Promise<string> {
const bytes = new Uint8Array(await res.arrayBuffer());
const parser = new PDFParse({ data: bytes });
try {
return (await parser.getText()).text;
} finally {
await parser.destroy();
}
return readPdfText(new Uint8Array(await res.arrayBuffer()));
}
async function pdfBytesText(bytes: Uint8Array): Promise<string> {
const parser = new PDFParse({ data: bytes });
try {
return (await parser.getText()).text;
} finally {
await parser.destroy();
}
function pdfBytesText(bytes: Uint8Array): Promise<string> {
return readPdfText(bytes);
}
beforeEach(() => {