mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The text read and the rasterizer each opened pdf.js on their own: the text read with bare options, the rasterizer through src/lib/documents/pdfjs.ts with the standard font, CMap and wasm directories. The loader now serves both, so a CID font with a predefined CMap is mapped to characters on the text path too, and there is one set of options to keep in step. The loader is typed with pdf.js' own declarations instead of a hand-written shape. That drops isEvalSupported, which pdf.js 6 no longer has and which did nothing. Only the render slice the DOM-typed declarations cannot describe (the native canvas factory) keeps a named local type in the rasterizer. The run joiner with its named steps moves into the loader as joinTextRuns, and readPdfText is its one page walk; the second layout copy that mimicked the previous library's tabs and blank lines is gone. extractPdfText keeps its outcomes and page cap on top of it, and a failed read still says what pdf.js threw (documents.local_extract.failed). A test draws a word in two fonts into a real PDF and reads it back whole. Refs #1277
129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { jsPDF } from "jspdf";
|
|
import type {
|
|
TextItem,
|
|
TextMarkedContent,
|
|
} from "pdfjs-dist/types/src/display/api";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { joinTextRuns, readPdfText } from "../pdfjs";
|
|
|
|
/**
|
|
* The text layer as the content index reads it (#1277): runs in order, a line
|
|
* break where a run ends its line or the next one sits on another, a space
|
|
* where two runs on one line leave a gap, and nothing where they touch, so a
|
|
* word drawn in two fonts stays one word and a search for it still finds it.
|
|
*/
|
|
|
|
/** A 10 pt run starting at (x, y), 5 pt per character. */
|
|
function run(
|
|
str: string,
|
|
x: number,
|
|
y: number,
|
|
extra: Partial<TextItem> = {},
|
|
): TextItem {
|
|
return {
|
|
str,
|
|
dir: "ltr",
|
|
transform: [10, 0, 0, 10, x, y],
|
|
width: str.length * 5,
|
|
height: 10,
|
|
fontName: "g_d0_f1",
|
|
hasEOL: false,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
describe("joinTextRuns", () => {
|
|
it("keeps a word drawn in two fonts whole", () => {
|
|
expect(
|
|
joinTextRuns([
|
|
run("Haem", 10, 700),
|
|
run("oglobin", 30, 700, { fontName: "g_d0_f2" }),
|
|
]),
|
|
).toBe("Haemoglobin");
|
|
});
|
|
|
|
it("puts a space between runs on one line that leave a gap", () => {
|
|
expect(joinTextRuns([run("Glucose", 10, 700), run("92", 200, 700)])).toBe(
|
|
"Glucose 92",
|
|
);
|
|
});
|
|
|
|
it("breaks once where a run ends its line", () => {
|
|
expect(
|
|
joinTextRuns([
|
|
run("Haemoglobin", 10, 700, { hasEOL: true }),
|
|
run("14.2 g/dL", 10, 688),
|
|
]),
|
|
).toBe("Haemoglobin\n14.2 g/dL");
|
|
});
|
|
|
|
it("breaks where the next run sits on another line without a marked end", () => {
|
|
expect(
|
|
joinTextRuns([run("Creatinine", 10, 700), run("0.9", 10, 680)]),
|
|
).toBe("Creatinine\n0.9");
|
|
});
|
|
|
|
it("keeps a raised run on its line", () => {
|
|
expect(joinTextRuns([run("10", 10, 700), run("9", 20, 703)])).toBe("109");
|
|
});
|
|
|
|
it("adds no blank line for an empty run that only ends a line", () => {
|
|
expect(
|
|
joinTextRuns([
|
|
run("Line", 10, 700, { hasEOL: true }),
|
|
run("", 30, 700, { hasEOL: true }),
|
|
run("Next", 10, 688),
|
|
]),
|
|
).toBe("Line\nNext");
|
|
});
|
|
|
|
it("skips marked-content markers, which carry no text", () => {
|
|
const marker: TextMarkedContent = {
|
|
type: "beginMarkedContent",
|
|
id: "mc0",
|
|
};
|
|
expect(joinTextRuns([marker, run("Text", 10, 700)])).toBe("Text");
|
|
});
|
|
});
|
|
|
|
describe("readPdfText", () => {
|
|
function pages(count: number): Buffer {
|
|
const doc = new jsPDF();
|
|
for (let p = 1; p <= count; p++) {
|
|
if (p > 1) doc.addPage();
|
|
doc.text(`Page ${p} lab report`, 10, 10);
|
|
doc.text("second line", 10, 20);
|
|
}
|
|
return Buffer.from(doc.output("arraybuffer"));
|
|
}
|
|
|
|
it("reads the pages one after another and adds no page markers", async () => {
|
|
const text = await readPdfText(pages(2));
|
|
expect(text).toBe(
|
|
"Page 1 lab report\nsecond line\nPage 2 lab report\nsecond line",
|
|
);
|
|
});
|
|
|
|
it("reads no further than the page cap", async () => {
|
|
const text = await readPdfText(pages(3), 2);
|
|
expect(text).toContain("Page 2");
|
|
expect(text).not.toContain("Page 3");
|
|
});
|
|
|
|
it("keeps a word whole that a real PDF draws in two fonts", async () => {
|
|
const doc = new jsPDF();
|
|
doc.setFont("helvetica", "bold");
|
|
doc.text("Haem", 10, 10);
|
|
const joint = 10 + doc.getTextWidth("Haem");
|
|
doc.setFont("times", "normal");
|
|
doc.text("oglobin 14.2 g/dL", joint, 10);
|
|
const text = await readPdfText(Buffer.from(doc.output("arraybuffer")));
|
|
expect(text).toBe("Haemoglobin 14.2 g/dL");
|
|
});
|
|
|
|
it("throws what pdf.js throws on a file that is not a PDF", async () => {
|
|
await expect(readPdfText(Buffer.from("not a pdf"))).rejects.toThrow();
|
|
});
|
|
});
|