Download static/reader/pdf/reader-pdf-text.js from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 9.6 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/pdf/reader-pdf-text.js
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader/pdf/reader-pdf-text.js
-
curl -L -o reader-pdf-text.js https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/pdf/reader-pdf-text.js
9.6 kB
| // PDF.js already emits word spaces. Adding a space after every item breaks | |
| // Chinese words and words split across fonts. Keep the source's explicit spaces. | |
| export function pdfTextContent(items) { | |
| return items.filter((item) => typeof item.str === "string") | |
| .map((item) => item.str + (item.hasEOL ? "\n" : "")).join(""); | |
| } | |
| // Embedded OCR often spaces individual Han characters, including vertical text. | |
| // Keep punctuation and Latin word boundaries, and map hits back to source offsets. | |
| const hanWordSpacing = /(?<=\p{Script=Han})\s+(?=\p{Script=Han})/gu; | |
| export function normalizePdfSearchText(raw) { | |
| return raw.replace(hanWordSpacing, ""); | |
| } | |
| export function pdfSourceOffset(mapping, index) { | |
| const offsets = mapping.offsets; | |
| if (!offsets) return index; | |
| let low = 0, high = offsets.length / 2; | |
| while (low < high) { | |
| const middle = (low + high) >> 1; | |
| if (offsets[middle * 2] <= index) low = middle + 1; | |
| else high = middle; | |
| } | |
| return index + (low ? offsets[(low - 1) * 2 + 1] : 0); | |
| } | |
| export function pdfSearchText(raw) { | |
| const offsets = [], chunks = []; | |
| let start = 0, removed = 0; | |
| for (const match of raw.matchAll(hanWordSpacing)) { | |
| chunks.push(raw.slice(start, match.index)); | |
| const boundary = match.index - removed; | |
| removed += match[0].length; | |
| offsets.push(boundary, removed); | |
| start = match.index + match[0].length; | |
| } | |
| if (!offsets.length) return { text: raw, offsets: null }; | |
| chunks.push(raw.slice(start)); | |
| return { text: chunks.join(""), offsets: new Uint32Array(offsets) }; | |
| } | |
| function textRun(layer, text) { | |
| const run = layer.ownerDocument.createElement("span"); | |
| run.className = "reader-pdf-text-run"; | |
| run.textContent = text; | |
| return run; | |
| } | |
| const measurements = new WeakMap(); | |
| function normalizeBox(value) { | |
| return Math.max(0, Math.min(1, Number(value) || 0)); | |
| } | |
| function measureRunScale(context, text, family, targetWidth) { | |
| context.font = `100px ${family}`; | |
| const measured = context.measureText(text).width / 100; | |
| return measured > 0 && targetWidth > 0 ? targetWidth / measured : 1; | |
| } | |
| function positionedRun(layer, text, x, y, height, width, family, angle = 0, vertical = false) { | |
| const box = layer.ownerDocument.createElement("span"); | |
| box.className = "reader-pdf-text-position reader-pdf-text-block"; | |
| box.style.left = `${x * 100}%`; | |
| box.style.top = `${y * 100}%`; | |
| // cqw uses the page width, so text scales with zoom and responsive layout. | |
| box.style.fontSize = `${height * 100}cqw`; | |
| box.style.fontFamily = family; | |
| if (angle) box.style.transform = `rotate(${angle}rad)`; | |
| const run = textRun(layer, text); | |
| let context = measurements.get(layer.ownerDocument); | |
| if (!context) { | |
| context = layer.ownerDocument.createElement("canvas").getContext("2d"); | |
| measurements.set(layer.ownerDocument, context); | |
| } | |
| if (vertical) { | |
| run.style.writingMode = "vertical-rl"; | |
| run.style.textOrientation = "upright"; | |
| // Estimate the scale until native PDF text is attached for measurement. | |
| const measured = Array.from(text).length; | |
| run._readerVerticalAdvance = width / height; | |
| if (width > 0 && measured) run.style.transform = `scaleY(${width / (height * measured)})`; | |
| } else { | |
| const scaleX = measureRunScale(context, text, family, width / height); | |
| run.style.setProperty("--reader-pdf-run-scale-x", String(scaleX)); | |
| run.style.transform = "scaleX(var(--reader-pdf-run-scale-x))"; | |
| } | |
| box.appendChild(run); | |
| return box; | |
| } | |
| export function populatePdfTextLayer(layer, items, viewport, styles = {}) { | |
| const fragment = layer.ownerDocument.createDocumentFragment(); | |
| const w = Math.max(1, viewport.width), h = Math.max(1, viewport.height); | |
| for (const item of items) { | |
| if (typeof item.str !== "string") continue; | |
| if (item.str) { | |
| const t = item.transform, v = viewport.transform; | |
| const point = t && viewport.convertToViewportPoint?.(t[4], t[5]); | |
| if (point && t.every(Number.isFinite)) { | |
| const style = styles[item.fontName] || {}; | |
| const family = style.fontFamily || "sans-serif"; | |
| const vertical = style.vertical || item.dir === "ttb"; | |
| const scale = viewport.scale || 1; | |
| const height = Math.hypot(t[2], t[3]) * scale || 12; | |
| const angle = v ? Math.atan2(v[1] * t[0] + v[3] * t[1], v[0] * t[0] + v[2] * t[1]) : 0; | |
| const ascent = height * (Number.isFinite(style.ascent) ? style.ascent : 0.8); | |
| const x = vertical ? point[0] - height / 2 : point[0] + ascent * Math.sin(angle); | |
| const y = vertical ? point[1] : point[1] - ascent * Math.cos(angle); | |
| fragment.appendChild(positionedRun(layer, item.str, x / w, y / h, height / w, | |
| (vertical ? item.height : item.width) * scale / w, family, angle, vertical)); | |
| } else { | |
| // Text without coordinates remains accessible, but never forms a visible | |
| // second text panel beneath the page image. | |
| fragment.appendChild(textRun(layer, item.str)); | |
| } | |
| } | |
| if (item.hasEOL) fragment.appendChild(layer.ownerDocument.createTextNode("\n")); | |
| } | |
| layer.replaceChildren(fragment); | |
| // Vertical spaces and fallback glyphs have font-dependent advances. Measure | |
| // the attached run so selection covers the PDF's actual vertical extent. | |
| for (const run of layer.querySelectorAll('.reader-pdf-text-run')) { | |
| if (!(run._readerVerticalAdvance > 0) || !run.offsetHeight) continue; | |
| const em = parseFloat(layer.ownerDocument.defaultView.getComputedStyle(run).fontSize); | |
| run.style.transform = `scaleY(${run._readerVerticalAdvance * em / run.offsetHeight})`; | |
| } | |
| } | |
| export function populateOcrTextLayer(layer, blocks, layout = {}) { | |
| const fragment = layer.ownerDocument.createDocumentFragment(); | |
| const ratio = layer.parentElement.getBoundingClientRect(); | |
| const vertical = layout?.writing_mode === "vertical-rl" || layout?.writing_mode === "vertical-lr"; | |
| let positioned = 0; | |
| const fallback = []; | |
| for (const block of blocks || []) { | |
| const text = String(block?.t || ""), b = block?.b; | |
| if (!text) continue; | |
| if (!Array.isArray(b) || b.length !== 4 || !b.every(Number.isFinite)) { | |
| fallback.push(text); | |
| continue; | |
| } | |
| const [x1, y1, x2, y2] = b.map(normalizeBox); | |
| const width = Math.abs(x2 - x1), height = Math.abs(y2 - y1); | |
| if (!width || !height) { | |
| fallback.push(text); | |
| continue; | |
| } | |
| fragment.appendChild(positionedRun(layer, text, Math.min(x1, x2), Math.min(y1, y2), | |
| height * ratio.height / Math.max(1, ratio.width), width, "sans-serif", 0, vertical)); | |
| fragment.appendChild(layer.ownerDocument.createTextNode("\n")); | |
| positioned++; | |
| } | |
| if (!positioned && fallback.length) { | |
| // Native-text extraction can provide correct text without coordinates. | |
| // Keep it selectable instead of dropping the entire page text layer. | |
| const box = layer.ownerDocument.createElement("div"); | |
| box.className = "reader-pdf-text-fallback"; | |
| if (vertical) { | |
| box.style.writingMode = layout.writing_mode; | |
| box.style.textOrientation = "upright"; | |
| } | |
| box.appendChild(textRun(layer, fallback.join("\n"))); | |
| fragment.appendChild(box); | |
| } | |
| layer.replaceChildren(fragment); | |
| } | |
| export function populateIndependentTextLayer(layer, payload) { | |
| const fragment = layer.ownerDocument.createDocumentFragment(); | |
| const chars = Array.from(payload.text), bounds = layer.parentElement.getBoundingClientRect(); | |
| const ratio = bounds.height / Math.max(1, bounds.width); | |
| let end = 0; | |
| for (const region of payload.regions) { | |
| fragment.appendChild(layer.ownerDocument.createTextNode(chars.slice(end, region.start).join(""))); | |
| const vertical = region.writing_mode.startsWith("vertical"); | |
| const q = region.quad; | |
| const dx = q[1][0] - q[0][0], dy = (q[1][1] - q[0][1]) * ratio; | |
| const angle = Math.atan2(dy, dx); | |
| const across = Math.hypot(dx, dy); | |
| const down = Math.hypot(q[3][0] - q[0][0], (q[3][1] - q[0][1]) * ratio); | |
| const height = vertical ? across : down; | |
| const width = vertical ? down : across; | |
| const positioned = positionedRun(layer, region.text, q[0][0], | |
| q[0][1], Math.max(height, .0001), width, "sans-serif", angle, vertical); | |
| const run = positioned.querySelector(".reader-pdf-text-run"); | |
| run.dir = ["rtl", "ltr"].includes(region.direction) ? region.direction : "auto"; | |
| run.style.unicodeBidi = "plaintext"; | |
| run.dataset.regionId = region.id; | |
| run.dataset.mappingPrecision = region.mapping_precision; | |
| if (vertical) run.style.writingMode = region.writing_mode === "vertical-lr" ? "vertical-lr" : "vertical-rl"; | |
| fragment.appendChild(positioned); | |
| end = region.end; | |
| } | |
| fragment.appendChild(layer.ownerDocument.createTextNode(chars.slice(end).join(""))); | |
| layer.replaceChildren(fragment); | |
| // Page surfaces disable selection; separators must be selectable along with runs. | |
| layer.style.userSelect = "text"; | |
| layer.style.webkitUserSelect = "text"; | |
| layer.dataset.textGeneration = payload.generation; | |
| } | |
| export function independentTextCaret(doc, x, y) { | |
| const position = doc.caretPositionFromPoint?.(x, y); | |
| const range = position ? null : doc.caretRangeFromPoint?.(x, y); | |
| const node = position?.offsetNode || range?.startContainer; | |
| let offset = position?.offset ?? range?.startOffset; | |
| if (node?.nodeType !== 3 || !node.parentElement?.closest( | |
| '.reader-pdf-text[data-text-generation] .reader-pdf-text-run[data-region-id]')) return null; | |
| if (offset > 0 && offset < node.length && /[\uDC00-\uDFFF]/.test(node.data[offset]) && | |
| /[\uD800-\uDBFF]/.test(node.data[offset - 1])) offset--; | |
| return { node, offset }; | |
| } | |