diff --git a/src/tag.ts b/src/tag.ts index ed95200..0526b72 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -188,7 +188,7 @@ function tagPage(page: mupdf.PDFPage, i: number, html: string, ctx: PageCtx): { throw new IrisPdfError("too_many_words", `Page ${n} has more than ${MAX_WORDS} words.`); const { match, complete } = align(ordered.map((o) => o.word.norm), tokens.map((t) => t.norm)); if (!complete) warn({ code: "alignment_incomplete", detail: "The page and the HTML differ too much to match every word in time." }); - const blockOf = new Map(); + const blockOf = new Map(), bounds = page.getBounds() as Box; const matched = new Set(); match.forEach((t, k) => { if (t < 0) { @@ -197,10 +197,9 @@ function tagPage(page: mupdf.PDFPage, i: number, html: string, ctx: PageCtx): { } report.matched++; matched.add(ordered[k].word); - ordered[k].word.at = placed(words[tokens[t].words[0]]); + ordered[k].word.at = placed(words[tokens[t].words[0]], bounds); blockOf.set(t, ordered[k].block); }); - const bounds = page.getBounds(); // With no page words, each block starts a new line, so blocks do not run together. const breaks = new Set(words.length ? [] : ordered.filter((o, k) => k && o.newLine).map((o) => o.word)); fillPositions(ordered.map((o) => o.word), bounds, (s) => ctx.fonts.width(s), breaks); @@ -227,7 +226,7 @@ function tagPage(page: mupdf.PDFPage, i: number, html: string, ctx: PageCtx): { } report.lost++; if (!run) lost.set(block, [...(lost.get(block) ?? []), (run = { words: [] })]); - run.words.push({ text: w.text, norm: "", at: placed(w) }); + run.words.push({ text: w.text, norm: "", at: placed(w, bounds) }); } }); for (const runs of lost.values()) { @@ -275,7 +274,9 @@ type Link = { obj: mupdf.PDFObject; box: Box; uri: string; used: boolean }; type Emitter = PageCtx & { pageObj: mupdf.PDFObject; overlay: Overlay; links: Link[]; mcids: number }; const asP = (r: Run): Node => ({ type: "P", kids: [r] }); -const placed = (w: PageWord): Placed => ({ box: w.box, baseline: w.baseline, size: w.size }); +// A word running off the page is squeezed onto it: extractors drop glyphs off the page. +const placed = (w: PageWord, page: Box): Placed => + ({ box: [Math.max(w.box[0], page[0]), w.box[1], Math.min(w.box[2], page[2]), w.box[3]], baseline: w.baseline, size: w.size }); // An HTML word with no page word follows the word before it, at its natural // width, wrapping at the page edge: extractors drop text off the page, and diff --git a/test/tag.test.ts b/test/tag.test.ts index a658d74..744df8f 100644 --- a/test/tag.test.ts +++ b/test/tag.test.ts @@ -234,6 +234,18 @@ test("a word in a very narrow box keeps its repeated letters", () => { assert.doesNotMatch(overlay.toString(), /NaN|Infinity/); }); +test("a word running off the page edge keeps every letter of its tagged text", () => { + // Extractors drop a glyph off the page. The source word is monospaced, so its + // last letter starts just inside; stretched to the same box, ours would not. + // So too at the left edge, for its first letter. + const doc = new mupdf.PDFDocument(); + const res = doc.addObject({ Font: { F1: doc.addSimpleFont(new mupdf.Font("Helvetica")), F2: doc.addSimpleFont(new mupdf.Font("Courier")) } }); + doc.insertPage(-1, doc.addPage([0, 0, 300, 300], 0, res, "BT /F1 12 Tf 10 200 Td (Thanks) Tj ET BT /F2 12 Tf 276.4 200 Td (Hail) Tj ET BT /F2 12 Tf -4 180 Td (iWall) Tj ET")); + const report = newReport(); + tag(doc.saveToBuffer("").asUint8Array().slice(), { lang: "en", title: "t", pages: [{ sourcePage: 1, html: "

Thanks Hail

iWall

" }] }, {}, report); + assert.equal(report.verification.textPreserved, true); +}); + test("a character no font has is reported and left out, not a verification failure", () => { const report = newReport(); tag(readFixture("text-simple.pdf"), { lang: "en", pages: [{ sourcePage: 1, html: "

Parking Permit 🦄 x🦄

" }] }, {}, report);