| name | pdfjs-syntax-text-layer |
| description | Use when adding selectable/searchable text overlay to rendered PDF pages or extracting text content from PDFs. Prevents the common mistake of using the deprecated renderTextLayer() function instead of the v5 TextLayer class. Covers TextLayer class, getTextContent(), streamTextContent(), CSS overlay positioning, text selection, and plain text extraction. Keywords: TextLayer, getTextContent, text selection, text extraction, textContentSource, select text in PDF, copy text from PDF, searchable PDF, extract text, text overlay.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdfjs-dist 5.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
pdfjs-syntax-text-layer
Quick Reference
Text Layer Pipeline
| Step | Method | Output |
|---|
| 1. Render page to canvas | page.render({ canvasContext, viewport }) | Rendered canvas |
| 2. Get text content | page.getTextContent() | TextContent object |
| 3. Create container div | Position absolutely over canvas | <div> element |
| 4. Create TextLayer | new TextLayer({ textContentSource, container, viewport }) | TextLayer instance |
| 5. Render text layer | textLayer.render() | Promise (text spans in DOM) |
Critical Warnings
NEVER use the deprecated renderTextLayer() function -- it was removed in pdfjs-dist 5.x. ALWAYS use the TextLayer class constructor and its render() method.
ALWAYS position the text layer container with position: absolute directly over the canvas -- without this, text selection coordinates will not align with the rendered PDF content.
ALWAYS set the text layer container to the same CSS dimensions as the canvas -- a size mismatch causes text spans to appear offset from their corresponding rendered glyphs.
NEVER forget to set pointer-events: none on the text layer container while keeping pointer-events: all on the individual text spans -- this allows click-through to the canvas for areas without text while preserving text selection.
ALWAYS include the PDF.js text layer CSS (pdfjs-dist/web/pdf_viewer.css or custom equivalent) -- without it, text spans render as visible black text on top of the canvas instead of invisible selectable overlays.
ALWAYS call textLayer.cancel() before creating a new TextLayer for the same page (e.g., on zoom/rotation) -- failing to cancel causes duplicate text spans and memory leaks.
Essential Patterns
Basic Text Layer Over Canvas
import { getDocument, GlobalWorkerOptions, TextLayer } from "pdfjs-dist";
import type { PDFPageProxy, PageViewport } from "pdfjs-dist";
GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
async function renderPageWithTextLayer(
url: string,
pageNumber: number,
container: HTMLDivElement,
scale: number = 1.5
): Promise<void> {
const doc = await getDocument(url).promise;
const page = await doc.getPage(pageNumber);
const viewport = page.getViewport({ scale });
container.style.position = "relative";
container.style. = ;
container.. = ;
canvas = .();
dpr = . || ;
canvas. = .(viewport. * dpr);
canvas. = .(viewport. * dpr);
canvas.. = ;
canvas.. = ;
canvas.. = ;
canvas.. = ;
canvas.. = ;
container.(canvas);
ctx = canvas.()!;
ctx.(dpr, dpr);
page.({ : ctx, viewport }).;
textContent = page.();
textLayerDiv = .();
textLayerDiv. = ;
textLayerDiv.. = ;
textLayerDiv.. = ;
textLayerDiv.. = ;
textLayerDiv.. = ;
textLayerDiv.. = ;
container.(textLayerDiv);
textLayer = ({
: textContent,
: textLayerDiv,
: viewport,
});
textLayer.();
}
Required CSS for Text Layer
.textLayer {
position: absolute;
text-align: initial;
inset: 0;
overflow: hidden;
opacity: 1;
line-height: 1;
-webkit-text-size-adjust: none;
-moz-text-size-adjust: none;
text-size-adjust: none;
forced-color-adjust: none;
z-index: 1;
}
.textLayer span,
.textLayer br {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
pointer-events: all;
}
.textLayer ::selection {
background: rgba(0, 0, 255, 0.25);
}
NOTE: For production use, ALWAYS import the full CSS from pdfjs-dist/web/pdf_viewer.css instead of maintaining custom CSS. The snippet above shows the minimum required properties for understanding.
Plain Text Extraction
async function extractPageText(page: PDFPageProxy): Promise<string> {
const textContent = await page.getTextContent();
const lines: string[] = [];
let currentLine = "";
for (const item of textContent.items) {
if ("str" in item) {
currentLine += item.str;
if (item.hasEOL) {
lines.push(currentLine);
currentLine = "";
}
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines.join("\n");
}
Streaming Text Content
async function extractTextStreaming(page: PDFPageProxy): Promise<string> {
const stream = page.streamTextContent({
includeMarkedContent: false,
disableNormalization: false,
});
const reader = stream.getReader();
const parts: string[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const item of value.items) {
if ("str" in item) {
parts.push(item.str);
if (item.hasEOL) {
parts.push("\n");
}
}
}
}
return parts.join("");
}
Update Text Layer on Zoom/Rotation
import { TextLayer } from "pdfjs-dist";
let currentTextLayer: TextLayer | null = null;
async function updateTextLayer(
page: PDFPageProxy,
textLayerDiv: HTMLDivElement,
newScale: number,
rotation: number = 0
): Promise<void> {
const newViewport = page.getViewport({ scale: newScale, rotation });
if (currentTextLayer) {
currentTextLayer.update({
viewport: newViewport,
onBefore() {
textLayerDiv.style.width = `${Math.floor(newViewport.width)}px`;
textLayerDiv.style.height = `${Math.floor(newViewport.height)}px`;
},
});
} else {
const textContent = await page.getTextContent();
currentTextLayer = ({
: textContent,
: textLayerDiv,
: newViewport,
});
currentTextLayer.();
}
}
Cancel and Cleanup
function destroyTextLayer(textLayer: TextLayer | null): void {
if (textLayer) {
textLayer.cancel();
}
TextLayer.cleanup();
}
Decision Tree: Text Layer Setup
Need text functionality on PDF page?
├── Need selectable/searchable text overlay?
│ ├── YES → Use TextLayer class with container over canvas
│ │ ├── First render? → new TextLayer() + render()
│ │ ├── Zoom/rotation changed? → textLayer.update({ viewport })
│ │ └── Page destroyed? → textLayer.cancel() + remove container
│ └── NO → Use page.getTextContent() for data only
│
├── Need plain text extraction?
│ ├── Small document → page.getTextContent() + concatenate item.str
│ └── Large document → page.streamTextContent() for streaming
│
├── Need text positions/coordinates?
│ └── Use TextItem.transform matrix from getTextContent()
│ transform[4] = x position, transform[5] = y position
│
└── Need to search within PDF text?
└── Extract text with getTextContent() → search item.str values
→ highlight matching spans via textLayer.textDivs
Reference Links
Official Sources