Select text in a PDF, and find PDFs by their contents
Stage five. pdf.js draws pages as pictures, so selecting means laying transparent spans over them exactly where each word was painted. The layer carries the same scale as the canvas — they drift apart otherwise and a selection lands on the wrong words. While drawing it stops taking pointer events entirely, or a stroke would come out as a selection. The text is also extracted and handed to the index that already holds the notes, so one search box finds a sentence whether it was typed or imported. Extraction runs after the document is on screen and yields between pages: it must never stand between the reader and the first page. The extracted text is deliberately not sent to the server. It is derivable from a file the server already holds, so storing it again costs space and a second thing to encrypt for nothing gained. The price, stated plainly in the readme, is that a PDF becomes searchable on a device once that device has opened it — the same shape as the limitation notes have. Text runs are joined on pdf.js's hasEOL rather than concatenated. Without it the last word of a line glues to the first of the next and matches nothing when searched for. The minimal text-layer CSS is written out rather than importing pdfjs-dist/web/pdf_viewer.css, which carries the whole reference viewer's toolbars and dialogs. Suite now checks the layer sits over the page rather than stacked in a corner, that the text is transparent, that dragging selects real words, that a word from inside the file finds the note and one that is not in it finds nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1be3a9b9ba
commit
b12b0d6142
6 files changed
+303
-6
No files matched your search
+55
@@ -100,6 +100,52 @@ try {
|
||||
"pdf.js was fetched only once it was needed"
|
||||
);
|
||||
|
||||
/* ── selectable text ── */
|
||||
const layerText = await p
|
||||
.locator('[data-pdf-page="1"] .pdf-text-layer')
|
||||
.innerText()
|
||||
.catch(() => "");
|
||||
check(layerText.includes("Zebrastreifen"), "the page text is laid over the picture", layerText.slice(0, 60));
|
||||
|
||||
const spanBox = await p.locator('[data-pdf-page="1"] .pdf-text-layer span').first().boundingBox();
|
||||
const pageBox = await p.locator('[data-pdf-page="1"]').boundingBox();
|
||||
check(
|
||||
spanBox && spanBox.width > 4 && spanBox.x > pageBox.x && spanBox.y > pageBox.y,
|
||||
"and positioned over the page rather than stacked in a corner",
|
||||
JSON.stringify(spanBox && { x: Math.round(spanBox.x - pageBox.x), y: Math.round(spanBox.y - pageBox.y) })
|
||||
);
|
||||
const invisible = await p
|
||||
.locator('[data-pdf-page="1"] .pdf-text-layer span')
|
||||
.first()
|
||||
.evaluate((e) => getComputedStyle(e).color);
|
||||
check(invisible.includes("rgba(0, 0, 0, 0)"), "the overlaid text is transparent", invisible);
|
||||
|
||||
/* Select it the way a reader would: drag across the line. */
|
||||
await p.mouse.move(spanBox.x + 2, spanBox.y + spanBox.height / 2);
|
||||
await p.mouse.down();
|
||||
await p.mouse.move(spanBox.x + spanBox.width - 2, spanBox.y + spanBox.height / 2, { steps: 10 });
|
||||
await p.mouse.up();
|
||||
const selected = await p.evaluate(() => String(window.getSelection()));
|
||||
check(selected.trim().length > 0, "dragging across the line selects text", JSON.stringify(selected.slice(0, 40)));
|
||||
await p.screenshot({ path: "/out/pdf-selection.png" });
|
||||
|
||||
/* ── the document is findable ── */
|
||||
await p.waitForTimeout(2500); // extraction runs after the first page is shown
|
||||
await p.getByPlaceholder("Search notes...").fill("Zebrastreifen");
|
||||
await p.waitForTimeout(1500);
|
||||
check(
|
||||
(await p.locator("[data-note-row]").count()) === 1,
|
||||
"searching a word from inside the PDF finds the note",
|
||||
`${await p.locator("[data-note-row]").count()} rows`
|
||||
);
|
||||
await p.getByPlaceholder("Search notes...").fill("Wortdasnichtvorkommt");
|
||||
await p.waitForTimeout(1200);
|
||||
check((await p.locator("[data-note-row]").count()) === 0, "and a word that is not in it finds nothing");
|
||||
await p.getByPlaceholder("Search notes...").fill("");
|
||||
await p.waitForTimeout(1000);
|
||||
await p.locator("[data-note-row]").first().click();
|
||||
await p.waitForTimeout(3000);
|
||||
|
||||
/* ── drawing on a page ── */
|
||||
const inkOnPage = (n) =>
|
||||
p.evaluate((page) => {
|
||||
@@ -125,6 +171,15 @@ try {
|
||||
await p.locator("[data-pdf-mode]").click();
|
||||
check((await p.locator('[data-pdf-mode="draw"]').count()) === 1, "drawing mode can be switched on");
|
||||
|
||||
const textEvents = await p
|
||||
.locator('[data-pdf-page="1"] .pdf-text-layer')
|
||||
.evaluate((e) => getComputedStyle(e).pointerEvents);
|
||||
check(
|
||||
textEvents === "none",
|
||||
"while drawing, the text layer stops taking the pointer so a stroke is not a selection",
|
||||
textEvents
|
||||
);
|
||||
|
||||
await drawOn(1, 0.2, 0.25, 0.7, 0.3);
|
||||
const inkedFirst = await inkOnPage(1);
|
||||
check(inkedFirst > 100, "the stroke appears on the page", `${inkedFirst} painted pixels`);
|
||||
|
||||
Reference in new issue
Block a user