@stabrise/scaledp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +3 -2
- package/dist/{data-to-image-DoZ4jQ3R.js → data-to-image-CRvh92wt.js} +2 -2
- package/dist/{data-to-image-DoZ4jQ3R.js.map → data-to-image-CRvh92wt.js.map} +1 -1
- package/dist/detect/index.js +1 -1
- package/dist/{detect-q8AI_Jdj.js → detect-DpTc5Wtc.js} +3 -3
- package/dist/{detect-q8AI_Jdj.js.map → detect-DpTc5Wtc.js.map} +1 -1
- package/dist/{image-CAH2rLv9.js → image-DRBsbv7G.js} +16 -2
- package/dist/{image-CAH2rLv9.js.map → image-DRBsbv7G.js.map} +1 -1
- package/dist/{image-draw-boxes-De0QbFv9.js → image-draw-boxes-DuR8eBeg.js} +2 -2
- package/dist/{image-draw-boxes-De0QbFv9.js.map → image-draw-boxes-DuR8eBeg.js.map} +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +4 -4
- package/dist/ner/index.js +1 -1
- package/dist/{ner-SsZLZ6ed.js → ner-BB3vGNxO.js} +2 -2
- package/dist/{ner-SsZLZ6ed.js.map → ner-BB3vGNxO.js.map} +1 -1
- package/dist/ocr/index.d.ts +100 -4
- package/dist/ocr/index.js +3 -3
- package/dist/{ocr-OHX2WM3e.js → ocr-Dkkbk0Wl.js} +282 -25
- package/dist/ocr-Dkkbk0Wl.js.map +1 -0
- package/dist/{ort-CXDoPrtw.js → ort-DZEG14nY.js} +25 -4
- package/dist/ort-DZEG14nY.js.map +1 -0
- package/dist/pdf/index.js +1 -1
- package/dist/{pdf-BQl0dneD.js → pdf-qbdnOnqZ.js} +3 -3
- package/dist/{pdf-BQl0dneD.js.map → pdf-qbdnOnqZ.js.map} +1 -1
- package/dist/registry/index.js +97 -8
- package/dist/registry/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/ocr-OHX2WM3e.js.map +0 -1
- package/dist/ort-CXDoPrtw.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocr-Dkkbk0Wl.js","names":["boxesOf","boxesOf"],"sources":["../src/ocr/db-postprocess.ts","../src/ocr/dbnet-onnx.ts","../src/ocr/detector-registry.ts","../src/ocr/line-orientation.ts","../src/ocr/line-orientation-stage.ts","../src/ocr/presets.ts","../src/ocr/paddle-service.ts","../src/ocr/paddle.ts","../src/ocr/paddle-recognizer.ts","../src/ocr/script-detect.ts","../src/ocr/tesseract.ts","../src/ocr/tesseract-recognizer.ts"],"sourcesContent":["/**\n * Differentiable Binarization post-processing.\n *\n * Port of `scaledp/models/detectors/paddle_onnx/db_postprocess.py`. Takes the\n * model's probability map and turns it into text quads, with the same\n * thresholds, ordering and filtering as Python so box output matches.\n */\n\nimport { boxPoints, minAreaRect, type Point, polygonArea, polygonPerimeter } from '../core/geometry.js'\n\nexport interface DbPostProcessOptions {\n /** Probability above which a pixel counts as text. */\n thresh: number\n /** Mean in-box probability a candidate must reach to survive. */\n boxThresh: number\n /** Cap on contours considered, guarding against pathological maps. */\n maxCandidates: number\n /** How far to grow each box; DB shrinks regions during training. */\n unclipRatio: number\n /** Minimum short side, in model pixels. */\n minSize: number\n}\n\n/**\n * ScaleDP's own values, from `predict_det.py`. Note `boxThresh` is 0.3 there\n * and the stage-level `scoreThreshold` never reaches this code -- a Python bug\n * that is not reproduced: here `boxThresh` is what the stage actually sets.\n */\nexport const DB_POSTPROCESS_DEFAULTS: DbPostProcessOptions = Object.freeze({\n thresh: 0.5,\n boxThresh: 0.3,\n maxCandidates: 1000,\n unclipRatio: 2.5,\n minSize: 3,\n})\n\nexport interface ProbabilityMap {\n data: Float32Array\n width: number\n height: number\n}\n\nexport interface DetectedQuad {\n /** Four corners, ordered top-left, top-right, bottom-right, bottom-left. */\n points: [Point, Point, Point, Point]\n score: number\n}\n\n/**\n * Boundary pixels of each 8-connected foreground component.\n *\n * cv2.findContours traces outlines; we collect boundary pixels instead. For the\n * only consumers here -- minAreaRect and its convex hull -- the two are\n * equivalent, and this avoids porting Suzuki-Abe border following.\n */\nexport function findComponentBoundaries(\n map: ProbabilityMap,\n thresh: number,\n maxCandidates: number\n): Point[][] {\n const { data, width, height } = map\n const visited = new Uint8Array(width * height)\n const components: Point[][] = []\n\n const isForeground = (x: number, y: number): boolean =>\n x >= 0 && y >= 0 && x < width && y < height && (data[y * width + x] as number) > thresh\n\n const stack: number[] = []\n for (let start = 0; start < visited.length && components.length < maxCandidates; start++) {\n if (visited[start] || !((data[start] as number) > thresh)) continue\n\n const boundary: Point[] = []\n stack.length = 0\n stack.push(start)\n visited[start] = 1\n\n while (stack.length > 0) {\n const index = stack.pop() as number\n const x = index % width\n const y = (index - x) / width\n\n let onBoundary = false\n for (let dy = -1; dy <= 1; dy++) {\n for (let dx = -1; dx <= 1; dx++) {\n if (dx === 0 && dy === 0) continue\n const nx = x + dx\n const ny = y + dy\n if (!isForeground(nx, ny)) {\n onBoundary = true\n continue\n }\n const neighbour = ny * width + nx\n if (!visited[neighbour]) {\n visited[neighbour] = 1\n stack.push(neighbour)\n }\n }\n }\n if (onBoundary) boundary.push([x, y])\n }\n if (boundary.length >= 3) components.push(boundary)\n }\n return components\n}\n\n/**\n * Corners of the minimum-area rect, ordered top-left, top-right, bottom-right,\n * bottom-left, together with the rect's shorter side.\n *\n * Port of `get_mini_boxes`: sort the four corners by x, then decide within each\n * pair which is the upper one.\n */\nexport function miniBox(points: readonly Point[]): { points: [Point, Point, Point, Point]; sside: number } {\n const rect = minAreaRect(points)\n const corners = [...boxPoints(rect)].sort((a, b) => a[0] - b[0])\n\n const [p0, p1, p2, p3] = corners as [Point, Point, Point, Point]\n const [topLeft, bottomLeft] = p0[1] <= p1[1] ? [p0, p1] : [p1, p0]\n const [topRight, bottomRight] = p2[1] <= p3[1] ? [p2, p3] : [p3, p2]\n\n return {\n points: [topLeft, topRight, bottomRight, bottomLeft],\n sside: Math.min(rect.size[0], rect.size[1]),\n }\n}\n\n/**\n * Mean probability inside a quad -- port of `box_score_fast`.\n *\n * Python builds an integer mask with `cv2.fillPoly` and averages the\n * probability map under it, so this reproduces OpenCV's fill convention:\n * vertices are truncated to integers and treated as pixel *centres*, scanlines\n * run at integer y, and both ends of each span are inclusive. Sampling at\n * pixel centres instead (the more usual rasterisation rule) drops the boundary\n * row and column, which shifts the score by around 1% and changes which\n * candidates clear `boxThresh`.\n */\nexport function boxScore(map: ProbabilityMap, points: readonly Point[]): number {\n const { data, width, height } = map\n const xs = points.map((p) => p[0])\n const ys = points.map((p) => p[1])\n\n const clamp = (value: number, max: number) => Math.min(Math.max(value, 0), max)\n const xmin = clamp(Math.floor(Math.min(...xs)), width - 1)\n const xmax = clamp(Math.ceil(Math.max(...xs)), width - 1)\n const ymin = clamp(Math.floor(Math.min(...ys)), height - 1)\n const ymax = clamp(Math.ceil(Math.max(...ys)), height - 1)\n if (xmax < xmin || ymax < ymin) return 0\n\n // Translate into mask space and truncate, matching numpy's astype(int32).\n const local = points.map(([x, y]) => [Math.trunc(x - xmin), Math.trunc(y - ymin)] as Point)\n const maskWidth = xmax - xmin\n const maskHeight = ymax - ymin\n\n let sum = 0\n let count = 0\n for (let y = 0; y <= maskHeight; y++) {\n let left = Number.POSITIVE_INFINITY\n let right = Number.NEGATIVE_INFINITY\n\n for (let i = 0; i < local.length; i++) {\n const a = local[i] as Point\n const b = local[(i + 1) % local.length] as Point\n if (a[1] === b[1]) {\n // A horizontal edge contributes both endpoints on its own row.\n if (a[1] !== y) continue\n left = Math.min(left, a[0], b[0])\n right = Math.max(right, a[0], b[0])\n continue\n }\n if (y < Math.min(a[1], b[1]) || y > Math.max(a[1], b[1])) continue\n const x = a[0] + ((y - a[1]) / (b[1] - a[1])) * (b[0] - a[0])\n left = Math.min(left, x)\n right = Math.max(right, x)\n }\n if (left > right) continue\n\n // The quads here are always convex, so the span between the extreme\n // crossings is exactly the covered run.\n const from = Math.max(0, Math.ceil(left))\n const to = Math.min(maskWidth, Math.floor(right))\n const rowOffset = (y + ymin) * width + xmin\n for (let x = from; x <= to; x++) {\n sum += data[rowOffset + x] as number\n count++\n }\n }\n return count === 0 ? 0 : sum / count\n}\n\n/**\n * Grow a detected rect outward -- port of `unclip`.\n *\n * Python offsets the polygon with a Clipper round join and then takes the\n * minimum-area rect of the result. For a rectangle those two steps compose\n * exactly: offsetting outward by `d` produces a rounded rectangle whose\n * min-area rect is the original grown by `d` on each of the four sides. That\n * identity is what lets this avoid a Clipper dependency entirely.\n */\nexport function unclipRect(points: readonly Point[], unclipRatio: number): [Point, Point, Point, Point] {\n const rect = minAreaRect(points)\n const perimeter = polygonPerimeter(points)\n const distance = perimeter === 0 ? 0 : (polygonArea(points) * unclipRatio) / perimeter\n\n return boxPoints({\n center: rect.center,\n size: [rect.size[0] + distance * 2, rect.size[1] + distance * 2],\n angle: rect.angle,\n })\n}\n\n/**\n * Reorder four points clockwise from the top-left -- port of\n * `order_points_clockwise`. Top-left has the smallest x+y, bottom-right the\n * largest; the remaining two are separated by y-x.\n */\nexport function orderPointsClockwise(points: readonly Point[]): [Point, Point, Point, Point] {\n const bySum = [...points].sort((a, b) => a[0] + a[1] - (b[0] + b[1]))\n const topLeft = bySum[0] as Point\n const bottomRight = bySum[bySum.length - 1] as Point\n\n const rest = bySum.slice(1, -1).sort((a, b) => a[1] - a[0] - (b[1] - b[0]))\n return [topLeft, rest[0] as Point, bottomRight, rest[1] as Point]\n}\n\n/**\n * Probability map -> text quads in source-image coordinates.\n *\n * `scale` is the uniform factor the source was resized by. Coordinates restore\n * by *dividing* by it, with no offset to subtract, because the letterbox pads\n * bottom and right only.\n */\nexport function quadsFromProbabilityMap(\n map: ProbabilityMap,\n source: { width: number; height: number },\n scale: number,\n options: Partial<DbPostProcessOptions> = {}\n): DetectedQuad[] {\n const opts = { ...DB_POSTPROCESS_DEFAULTS, ...options }\n const quads: DetectedQuad[] = []\n\n for (const boundary of findComponentBoundaries(map, opts.thresh, opts.maxCandidates)) {\n const candidate = miniBox(boundary)\n if (candidate.sside < opts.minSize) continue\n\n const score = boxScore(map, candidate.points)\n if (score < opts.boxThresh) continue\n\n const expanded = miniBox(unclipRect(candidate.points, opts.unclipRatio))\n // Python's second gate is `min_size + 2`, i.e. tighter than the first --\n // unclipping can only grow a box, so anything still tiny is noise.\n if (expanded.sside < opts.minSize + 2) continue\n\n // Python clips to [0, dest], inclusive of the far edge -- not [0, dest-1].\n const restored = expanded.points.map(\n ([x, y]) =>\n [\n Math.min(Math.max(Math.round(x / scale), 0), source.width),\n Math.min(Math.max(Math.round(y / scale), 0), source.height),\n ] as Point\n )\n\n const ordered = orderPointsClockwise(restored)\n // Drop slivers: Python filters boxes whose sides are 3px or less.\n const width = Math.hypot(ordered[0][0] - ordered[1][0], ordered[0][1] - ordered[1][1])\n const height = Math.hypot(ordered[0][0] - ordered[3][0], ordered[0][1] - ordered[3][1])\n if (width <= 3 || height <= 3) continue\n\n quads.push({ points: ordered, score })\n }\n return quads\n}\n","/**\n * DBNet ONNX text detection -- the direct mirror of\n * `scaledp/models/detectors/DBNetOnnxDetector.py`, so ScaleDP's own detection\n * model runs unchanged in the browser.\n *\n * Preprocessing reproduces `paddle_onnx/operators.py` exactly, including one\n * quirk that matters: the Python path converts RGB to BGR and never converts\n * back, so the model is fed BGR channels normalised against *RGB* ImageNet\n * statistics. Feeding true RGB instead shifts the boxes.\n *\n * The model detects text *lines*, not words -- one region per line, in Python\n * as here. Nothing downstream subdivides them either: `TesseractRecognizer`\n * reads each region with PSM.SINGLE_WORD and returns one box per region, which\n * is what ScaleDP does too. `TesseractOcr`, which runs tesseract's own layout\n * analysis over the whole page, is the stage that yields word boxes.\n */\n\nimport { getConfig } from '../core/config.js'\nimport { DetectionError } from '../core/errors.js'\nimport {\n decodeImage,\n IMAGENET_MEAN,\n IMAGENET_STD,\n letterbox,\n toImageData,\n toNchwFloat32,\n} from '../core/image.js'\nimport { ensureModelFiles, type ModelSpec } from '../core/model-cache.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { type Box, boxFromPolygon, mergeOverlappingBoxes } from '../schemas/box.js'\nimport { createDetectorOutput, type DetectorOutput } from '../schemas/detector-output.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\nimport { DB_POSTPROCESS_DEFAULTS, type ProbabilityMap, quadsFromProbabilityMap } from './db-postprocess.js'\nimport { createSession } from './ort.js'\n\n/** Fixed input size from ScaleDP's `DetResizeForTest` config. */\nexport const DBNET_INPUT_SIZE = 1280\n\n/** Model ScaleDP's DBNetOnnxDetector documents and its tests use. */\nexport const DEFAULT_DBNET_MODEL = 'StabRise/text_detection_dbnet_ml_v0.2'\n\nexport interface DbnetOnnxDetectorParams extends BaseStageParams {\n /** Hugging Face repo id, or a URL when self-hosting. */\n model: string\n /** Mean in-box probability a candidate must reach. */\n scoreThreshold: number\n /** Probability above which a pixel counts as text. */\n binaryThreshold: number\n /** How far to grow each box; DB shrinks text regions during training. */\n unclipRatio: number\n /**\n * Merge boxes that overlap and share a line. ScaleDP uses an unusually low\n * IoU of 0.02 here, because adjacent words in a line barely overlap.\n *\n * In practice it rarely changes anything for this model: the regions are\n * already whole lines and do not overlap. ScaleDP merges unconditionally\n * and its box count is likewise unchanged. Kept as a parameter because it\n * does matter for detectors that emit overlapping candidates.\n */\n mergeBoxes: boolean\n}\n\nexport const DBNET_DETECTOR_DEFAULTS: DbnetOnnxDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'boxes',\n keepInputData: true,\n model: DEFAULT_DBNET_MODEL,\n scoreThreshold: DB_POSTPROCESS_DEFAULTS.boxThresh,\n binaryThreshold: DB_POSTPROCESS_DEFAULTS.thresh,\n unclipRatio: DB_POSTPROCESS_DEFAULTS.unclipRatio,\n mergeBoxes: true,\n})\n\nfunction modelSpec(model: string): ModelSpec {\n return /^https?:\\/\\//.test(model)\n ? { repo: 'dbnet', files: [{ path: model }] }\n : { repo: model, files: [{ path: 'model.onnx' }] }\n}\n\nexport class DbnetOnnxDetector extends Stage<DbnetOnnxDetectorParams> {\n readonly name = 'DbnetOnnxDetector'\n\n private session: import('onnxruntime-web').InferenceSession | null = null\n private loading: Promise<import('onnxruntime-web').InferenceSession> | null = null\n\n constructor(options: Partial<DbnetOnnxDetectorParams> = {}) {\n super(resolveParams(DBNET_DETECTOR_DEFAULTS, options))\n }\n\n override async init(): Promise<void> {\n await this.getSession()\n }\n\n private getSession(): Promise<import('onnxruntime-web').InferenceSession> {\n if (this.session) return Promise.resolve(this.session)\n if (this.loading) return this.loading\n\n this.loading = (async () => {\n const spec = modelSpec(this.params.model)\n const files = await ensureModelFiles(spec)\n const bytes = files[spec.files[0]?.path ?? '']\n if (!bytes) throw new DetectionError(`Model ${this.params.model} not found`, this.name)\n\n const session = await createSession(bytes, {\n executionProviders: getConfig().executionProviders,\n })\n this.session = session\n return session\n })()\n\n // Clear on failure so a transient network error can be retried rather\n // than cached as a permanently rejected promise.\n this.loading.catch(() => {\n this.loading = null\n })\n return this.loading\n }\n\n override async dispose(): Promise<void> {\n await this.session?.release()\n this.session = null\n this.loading = null\n }\n\n protected async apply(input: unknown, row: Row): Promise<DetectorOutput> {\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new DetectionError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n let boxes: Box[]\n try {\n boxes = await this.detect(bitmap)\n } finally {\n bitmap.close()\n }\n\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'dbnet-onnx',\n bboxes: boxes,\n })\n }\n\n /** Run the model over one decoded image and return boxes in its coordinates. */\n async detect(source: ImageBitmap | OffscreenCanvas): Promise<Box[]> {\n const session = await this.getSession()\n\n // Letterbox to a fixed 1280x1280, padding bottom and right only with\n // white. Because nothing is padded at the top or left, coordinates\n // restore by dividing by the scale with no offset to subtract.\n const fitted = letterbox(\n source,\n { width: DBNET_INPUT_SIZE, height: DBNET_INPUT_SIZE },\n {\n padding: 'end',\n fill: '#ffffff',\n }\n )\n const tensorData = toNchwFloat32(toImageData(fitted.canvas), {\n mean: IMAGENET_MEAN,\n std: IMAGENET_STD,\n bgr: true,\n })\n\n const { Tensor } = await import('onnxruntime-web')\n const inputName = session.inputNames[0]\n const outputName = session.outputNames[0]\n if (!inputName || !outputName) {\n throw new DetectionError('Model exposes no input or output', this.name)\n }\n\n const outputs = await session.run({\n [inputName]: new Tensor('float32', tensorData, [1, 3, DBNET_INPUT_SIZE, DBNET_INPUT_SIZE]),\n })\n const output = outputs[outputName]\n if (!output) throw new DetectionError(`Model produced no \"${outputName}\" output`, this.name)\n\n // Output is [N, 1, H, W]; the first channel is the probability map.\n const [, , height = DBNET_INPUT_SIZE, width = DBNET_INPUT_SIZE] = output.dims\n const map: ProbabilityMap = {\n data: output.data as Float32Array,\n width,\n height,\n }\n\n const quads = quadsFromProbabilityMap(\n map,\n { width: source.width, height: source.height },\n fitted.scale,\n {\n thresh: this.params.binaryThreshold,\n boxThresh: this.params.scoreThreshold,\n unclipRatio: this.params.unclipRatio,\n }\n )\n\n const boxes = quads.map((quad) => boxFromPolygon(quad.points, { score: quad.score }))\n return this.params.mergeBoxes ? mergeOverlappingBoxes(boxes, 0.02, 10, 0.3) : boxes\n }\n\n protected onError(message: string, row: Row): DetectorOutput {\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'dbnet-onnx',\n exception: message,\n })\n }\n}\n","/**\n * Text detectors that can run in the browser.\n *\n * Detection and recognition are separable in ScaleDP -- a detector stage feeds\n * boxes to a recognizer -- so which detector produced a page's boxes is a real\n * choice worth making addressable, the same way OCR presets and NER models are.\n */\n\nexport type DetectorKind = 'paddle' | 'dbnet-onnx'\n\nexport interface DetectorModel {\n /** Short id callers pass to the picker. */\n id: string\n /** Human-readable name including the download size. */\n name: string\n kind: DetectorKind\n /**\n * Hugging Face repo, for ONNX detectors. Paddle detectors come from the\n * OCR preset instead, so this is unset for them.\n */\n repo?: string\n approxBytes?: number\n notes: string\n}\n\nexport const DETECTOR_MODELS: readonly DetectorModel[] = Object.freeze([\n {\n id: 'paddle',\n name: 'PaddleOCR DB (follows the OCR preset)',\n kind: 'paddle',\n notes: 'Shares the detection model already downloaded for the selected OCR preset.',\n },\n {\n id: 'dbnet-v0.2',\n name: 'StabRise DBNet ONNX v0.2 (~5 MB)',\n kind: 'dbnet-onnx',\n repo: 'StabRise/text_detection_dbnet_ml_v0.2',\n approxBytes: 4_800_000,\n notes: 'The same detection model ScaleDP uses server-side.',\n },\n {\n id: 'dbnet-v0.1',\n name: 'StabRise DBNet ONNX v0.1 (~5 MB)',\n kind: 'dbnet-onnx',\n repo: 'StabRise/text_detection_dbnet_ml_v0.1',\n approxBytes: 4_800_000,\n notes: 'The earlier revision, kept for comparison.',\n },\n])\n\nexport const DEFAULT_DETECTOR_ID = 'paddle'\n\nexport function getDetectorModel(id: string): DetectorModel | undefined {\n return DETECTOR_MODELS.find((m) => m.id === id)\n}\n","/**\n * 0 / 180 degree line-orientation classifier.\n *\n * Port of `scaledp/models/detectors/HasDetectLineOrientation.py`. Tiny, and it\n * meaningfully improves recognition on rotated crops -- an upside-down line\n * otherwise recognises as noise.\n *\n * Reproduces the same BGR-with-RGB-statistics quirk as the DBNet path: the\n * Python code converts to BGR and normalises with RGB ImageNet constants.\n */\n\nimport { DetectionError } from '../core/errors.js'\nimport {\n context2d,\n createCanvas,\n IMAGENET_MEAN,\n IMAGENET_STD,\n toImageData,\n toNchwFloat32,\n} from '../core/image.js'\nimport { ensureModelFiles } from '../core/model-cache.js'\nimport { createSession } from './ort.js'\n\nexport const DEFAULT_ORIENTATION_MODEL = 'StabRise/line_orientation_detection_v0.1'\n\n/** Model input, width x height. */\nexport const ORIENTATION_INPUT = { width: 160, height: 80 } as const\n\nexport type LineOrientation = '0_degree' | '180_degree'\n\nconst LABELS: readonly LineOrientation[] = ['0_degree', '180_degree']\n\nexport class LineOrientationClassifier {\n private session: import('onnxruntime-web').InferenceSession | null = null\n private loading: Promise<import('onnxruntime-web').InferenceSession> | null = null\n\n constructor(readonly model: string = DEFAULT_ORIENTATION_MODEL) {}\n\n private getSession(): Promise<import('onnxruntime-web').InferenceSession> {\n if (this.session) return Promise.resolve(this.session)\n if (this.loading) return this.loading\n\n this.loading = (async () => {\n const files = await ensureModelFiles({\n repo: this.model,\n files: [{ path: 'model.onnx' }],\n })\n const bytes = files['model.onnx']\n if (!bytes) {\n throw new DetectionError(`Model ${this.model} has no model.onnx`, 'LineOrientation')\n }\n const session = await createSession(bytes)\n this.session = session\n return session\n })()\n\n this.loading.catch(() => {\n this.loading = null\n })\n return this.loading\n }\n\n async classify(source: ImageBitmap | OffscreenCanvas): Promise<LineOrientation> {\n const session = await this.getSession()\n\n // Plain resize, not a letterbox: the Python path calls cv2.resize\n // directly and lets the aspect ratio distort.\n const canvas = createCanvas(ORIENTATION_INPUT.width, ORIENTATION_INPUT.height)\n context2d(canvas).drawImage(source, 0, 0, canvas.width, canvas.height)\n\n const data = toNchwFloat32(toImageData(canvas), {\n mean: IMAGENET_MEAN,\n std: IMAGENET_STD,\n bgr: true,\n })\n\n const { Tensor } = await import('onnxruntime-web')\n const inputName = session.inputNames[0]\n const outputName = session.outputNames[0]\n if (!inputName || !outputName) {\n throw new DetectionError('Model exposes no input or output', 'LineOrientation')\n }\n\n const outputs = await session.run({\n [inputName]: new Tensor('float32', data, [\n 1,\n 3,\n ORIENTATION_INPUT.height,\n ORIENTATION_INPUT.width,\n ]),\n })\n const logits = outputs[outputName]?.data as Float32Array | undefined\n if (!logits || logits.length < 2) {\n throw new DetectionError('Classifier produced no logits', 'LineOrientation')\n }\n\n return (logits[1] as number) > (logits[0] as number)\n ? (LABELS[1] as LineOrientation)\n : (LABELS[0] as LineOrientation)\n }\n\n async dispose(): Promise<void> {\n await this.session?.release()\n this.session = null\n this.loading = null\n }\n}\n","/**\n * Line-orientation detection and correction.\n *\n * Port of ScaleDP's `HasDetectLineOrientation`, which `TesseractRecognizer`\n * mixes in: each detected box is cropped, classified 0 or 180 degrees, and the\n * upside-down ones are turned before recognition. An inverted line otherwise\n * recognises as noise.\n *\n * Python does this per crop inside the recognizer. Here it is a stage of its\n * own, because `PaddleTextRecognizer` detects and recognises in a single pass\n * and has no seam to hook into. Flipping each inverted region in place on a\n * copy of the page gets the same result and costs one recognition pass rather\n * than one per box; the regions are rectangles, so a 180-degree turn leaves\n * every box's coordinates untouched and downstream stages need no adjustment.\n */\n\nimport { DetectionError } from '../core/errors.js'\nimport { context2d, createCanvas, cropBox, decodeImage, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { type Box, isRotated } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport type { Document } from '../schemas/document.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\nimport {\n DEFAULT_ORIENTATION_MODEL,\n type LineOrientation,\n LineOrientationClassifier,\n} from './line-orientation.js'\n\nexport interface LineOrientationDetectorParams extends BaseStageParams {\n /** [imageColumn, boxColumn]. */\n inputCols: string[]\n /** Column the corrected image is written to. */\n outputCol: string\n /** Column the per-box orientation labels are written to. */\n orientationCol: string\n model: string\n /** Turn the inverted regions. Off classifies only, leaving the page as-is. */\n correct: boolean\n /**\n * Classify only boxes that are already rotated, as ScaleDP's `onlyRotated`\n * does, and defaults to.\n *\n * This is not just a saved inference per box. The classifier has a real\n * false-positive rate: on an upright invoice it called 1 of 81 upright\n * regions inverted, and turning that region cost about 40 characters of\n * recognition. Restricting it to boxes that are already rotated is where\n * the signal actually is.\n *\n * Set false for pages that may contain upside-down *horizontal* text, which\n * is the case this misses.\n */\n onlyRotated: boolean\n /** Grow each box before cropping, so glyph edges are not clipped. */\n padding: number\n imageType: ImageFormat\n}\n\nexport const LINE_ORIENTATION_DEFAULTS: LineOrientationDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'oriented',\n orientationCol: 'orientations',\n keepInputData: true,\n model: DEFAULT_ORIENTATION_MODEL,\n correct: true,\n onlyRotated: true,\n padding: 2,\n imageType: 'png' as ImageFormat,\n})\n\nfunction boxesOf(source: unknown): Box[] {\n if (typeof source !== 'object' || source === null) return []\n return (source as DetectorOutput | Document).bboxes ?? []\n}\n\nexport class LineOrientationDetector extends Stage<LineOrientationDetectorParams> {\n readonly name = 'LineOrientationDetector'\n\n private classifier: LineOrientationClassifier | null = null\n\n constructor(options: Partial<LineOrientationDetectorParams> = {}) {\n super(\n resolveParams(LINE_ORIENTATION_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length !== 2) {\n throw new RangeError('inputCols must be [imageColumn, boxColumn]')\n }\n },\n })\n )\n }\n\n override async init(): Promise<void> {\n this.classifier ??= new LineOrientationClassifier(this.params.model)\n }\n\n /** One row in, one row out -- but two columns written, so expand not apply. */\n protected override async expand(_input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { inputCols, outputCol, orientationCol, correct, onlyRotated, padding } = this.params\n const [imageCol, boxCol] = inputCols as [string, string]\n const image = row[imageCol] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new DetectionError('Expected an Image with decoded bytes', this.name)\n }\n\n await this.init()\n const classifier = this.classifier as LineOrientationClassifier\n const boxes = boxesOf(row[boxCol])\n\n const bitmap = await decodeImage(image.data)\n try {\n const canvas = createCanvas(bitmap.width, bitmap.height)\n const ctx2d = context2d(canvas)\n ctx2d.drawImage(bitmap, 0, 0)\n\n const orientations: LineOrientation[] = []\n let flipped = 0\n\n for (const box of boxes) {\n ctx.signal?.throwIfAborted()\n\n if (onlyRotated && !isRotated(box)) {\n orientations.push('0_degree')\n continue\n }\n\n const crop = cropBox(bitmap, box, { padding })\n const orientation = await classifier.classify(crop)\n orientations.push(orientation)\n\n if (orientation === '180_degree' && correct) {\n flipRegion(ctx2d, canvas, box)\n flipped++\n }\n }\n\n const corrected =\n flipped > 0\n ? createImage({\n path: image.path,\n resolution: image.resolution,\n data: await encodeImage(canvas, `image/${this.params.imageType}` as never),\n imageType: this.params.imageType,\n width: canvas.width,\n height: canvas.height,\n })\n : // Nothing was inverted, so hand the original through rather\n // than paying a re-encode for an identical image.\n image\n\n return [{ ...row, [outputCol]: corrected, [orientationCol]: orientations }]\n } finally {\n bitmap.close()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new DetectionError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({\n path: String(row[this.params.pathCol] ?? 'memory'),\n exception: message,\n })\n }\n\n override async dispose(): Promise<void> {\n await this.classifier?.dispose()\n this.classifier = null\n }\n}\n\n/**\n * Turn one box's region 180 degrees in place.\n *\n * A rectangle maps onto itself under a 180-degree rotation about its own\n * centre, whatever its angle, so the box's coordinates stay valid and\n * downstream stages need no adjustment.\n *\n * The draw is clipped to the box's own rotated outline. Without that a rotated\n * box turns its whole axis-aligned envelope, dragging neighbouring text through\n * the rotation with it -- and the envelope of a skewed box is meaningfully\n * larger than the box.\n */\nfunction flipRegion(ctx: OffscreenCanvasRenderingContext2D, canvas: OffscreenCanvas, box: Box): void {\n const pad = 1\n const x = Math.max(0, Math.min(Math.round(box.x) - pad, canvas.width - 1))\n const y = Math.max(0, Math.min(Math.round(box.y) - pad, canvas.height - 1))\n const width = Math.min(Math.round(box.width) + pad * 2, canvas.width - x)\n const height = Math.min(Math.round(box.height) + pad * 2, canvas.height - y)\n if (width < 1 || height < 1) return\n\n // Drawing a canvas onto itself through a transform reads and writes the same\n // backing store, so take a copy first.\n const region = createCanvas(width, height)\n context2d(region).drawImage(canvas, x, y, width, height, 0, 0, width, height)\n\n const cx = x + width / 2\n const cy = y + height / 2\n\n ctx.save()\n ctx.beginPath()\n if (Math.abs(box.angle) < 3) {\n ctx.rect(x, y, width, height)\n } else {\n const rad = (box.angle * Math.PI) / 180\n const cos = Math.cos(rad)\n const sin = Math.sin(rad)\n const corners: [number, number][] = [\n [-box.width / 2, -box.height / 2],\n [box.width / 2, -box.height / 2],\n [box.width / 2, box.height / 2],\n [-box.width / 2, box.height / 2],\n ]\n corners.forEach(([px, py], index) => {\n const rx = px * cos - py * sin + cx\n const ry = px * sin + py * cos + cy\n if (index === 0) ctx.moveTo(rx, ry)\n else ctx.lineTo(rx, ry)\n })\n ctx.closePath()\n }\n ctx.clip()\n\n ctx.translate(cx, cy)\n ctx.rotate(Math.PI)\n ctx.drawImage(region, -width / 2, -height / 2)\n ctx.restore()\n}\n","/**\n * PaddleOCR language presets.\n *\n * Each preset pairs a detection model, a recognition model and a character\n * dictionary. There is no single model covering every script, so the choice is\n * a real one: pick the pairing that matches the documents being processed.\n *\n * Curated from ppu-paddle-ocr's catalogue -- the full list also carries v3/v4\n * models that v5/v6 supersede.\n */\n\nexport interface OcrPreset {\n /** Key accepted by `PaddleTextDetector`/`PaddleTextRecognizer`. */\n value: string\n /** Human-readable name, suitable for a language picker. */\n label: string\n /** Scripts this preset can read. */\n scripts: readonly string[]\n}\n\nexport const PADDLE_OCR_PRESETS: readonly OcrPreset[] = Object.freeze([\n { value: 'v6-small', label: 'Latin / CJK (default)', scripts: ['Latin', 'Han', 'Hiragana', 'Katakana'] },\n {\n value: 'v6-medium',\n label: 'Latin / CJK (medium, more accurate)',\n scripts: ['Latin', 'Han', 'Hiragana', 'Katakana'],\n },\n { value: 'v6-tiny', label: 'Latin / CJK (tiny, fastest)', scripts: ['Latin', 'Han'] },\n { value: 'v5-latin-mobile', label: 'Latin (French, German, Spanish, ...)', scripts: ['Latin'] },\n {\n value: 'v5-eslav-mobile',\n label: 'Latin + Cyrillic (Russian, Ukrainian, ...)',\n scripts: ['Latin', 'Cyrillic'],\n },\n { value: 'v5-cyrillic-mobile', label: 'Cyrillic only', scripts: ['Cyrillic'] },\n { value: 'v5-devanagari-mobile', label: 'Latin + Hindi (Devanagari)', scripts: ['Latin', 'Devanagari'] },\n { value: 'v5-arabic-mobile', label: 'Latin + Arabic', scripts: ['Latin', 'Arabic'] },\n { value: 'v5-greek-mobile', label: 'Latin + Greek', scripts: ['Latin', 'Greek'] },\n { value: 'v5-korean-mobile', label: 'Latin + Korean', scripts: ['Latin', 'Hangul'] },\n { value: 'v5-thai-mobile', label: 'Latin + Thai', scripts: ['Latin', 'Thai'] },\n { value: 'v5-tamil-mobile', label: 'Latin + Tamil', scripts: ['Latin', 'Tamil'] },\n { value: 'v5-telugu-mobile', label: 'Latin + Telugu', scripts: ['Latin', 'Telugu'] },\n { value: 'v5-en-mobile', label: 'English only (fastest)', scripts: ['Latin'] },\n])\n\nexport const DEFAULT_OCR_PRESET = 'v6-small'\n\nexport function isKnownPreset(value: string): boolean {\n return PADDLE_OCR_PRESETS.some((p) => p.value === value)\n}\n\n/** Shared by every Paddle stage, so an unknown preset fails at construction. */\nexport function validatePreset(value: string): void {\n if (!isKnownPreset(value)) {\n throw new RangeError(`Unknown OCR preset \"${value}\". See PADDLE_OCR_PRESETS for valid values.`)\n }\n}\n\n/** Presets able to read a script name as reported by OSD script detection. */\nexport function presetsForScript(script: string): OcrPreset[] {\n return PADDLE_OCR_PRESETS.filter((p) => p.scripts.includes(script))\n}\n","/**\n * PaddleOcrService lifecycle: model fetching, caching and per-preset reuse.\n *\n * Models are pulled through our own ModelCache and handed to the service as\n * ArrayBuffers, rather than letting ppu-paddle-ocr fetch them itself. Its\n * browser build re-downloads ~6 MB on every page load, relying only on the HTTP\n * cache; routing through IndexedDB makes a repeat visit instant and offline-safe.\n *\n * Two entry points, because the two halves of a preset are separable:\n * `getPaddleService` builds the full detect-then-read service, while\n * `getPaddleRecognizer` loads only the recognition model and its dictionary --\n * what `PaddleRecognizer` needs when some other detector already found the\n * boxes. Both key their cache on the same repo, so a pipeline using both pays\n * for each file once.\n */\n\nimport { getConfig } from '../core/config.js'\nimport type { ModelSpec } from '../core/model-cache.js'\nimport { ensureModelFiles, evict, isCached } from '../core/model-cache.js'\nimport { createSession, loadOrt } from './ort.js'\nimport { DEFAULT_OCR_PRESET } from './presets.js'\n\ntype OrtSession = Awaited<ReturnType<typeof createSession>>\n\ntype PpuWeb = typeof import('ppu-paddle-ocr/web')\ntype PaddleOcrService = InstanceType<PpuWeb['PaddleOcrService']>\n\n/**\n * ppu's recognition half, bound to a session: `run(canvas, boxes)` reads the\n * regions it is handed and does no detection of its own.\n */\nexport type PaddleRecognitionService = InstanceType<PpuWeb['RecognitionService']>\n\n/** Recognition tuning a stage may vary without paying for a second session. */\nexport interface PaddleRecognizerOptions {\n /** Crops per batched inference. 1 disables batching. */\n recBatchSize?: number\n /** Recover inter-word spaces the greedy CTC decode drops. */\n spaceRecovery?: boolean\n}\n\nlet modulePromise: Promise<PpuWeb> | null = null\n\nasync function loadPpu(): Promise<PpuWeb> {\n if (modulePromise) return modulePromise\n modulePromise = (async () => {\n // ORT must be configured before ppu-paddle-ocr creates any session,\n // otherwise its own wasmPaths default wins the \"if unset\" check.\n await loadOrt()\n try {\n return await import('ppu-paddle-ocr/web')\n } catch (cause) {\n throw new Error(\n 'ppu-paddle-ocr is required for PaddleOCR stages. Install it: npm i ppu-paddle-ocr',\n { cause }\n )\n }\n })()\n return modulePromise\n}\n\n/** The three files a preset needs, as a ModelSpec our cache understands. */\nasync function specForPreset(preset: string): Promise<{ spec: ModelSpec; roles: string[] }> {\n const ppu = await loadPpu()\n const urls =\n preset in ppu.MODEL_PRESETS\n ? ppu.MODEL_PRESETS[preset as keyof typeof ppu.MODEL_PRESETS]\n : ppu.DEFAULT_MODEL\n\n const roles = ['detection', 'recognition', 'charactersDictionary'] as const\n // ppu gives absolute URLs; the cache keys on repo-relative paths, so the\n // full URL doubles as the key and `modelHost` is bypassed for these.\n return {\n spec: { repo: `ppu-paddle-ocr/${preset}`, files: roles.map((r) => ({ path: urls[r] })) },\n roles: [...roles],\n }\n}\n\n/**\n * Absolute model URLs bypass `modelHost`: ppu-paddle-ocr publishes its own\n * catalogue and the paths are meaningful only against that host.\n */\nasync function fetchPresetFiles(preset: string): Promise<Record<string, ArrayBuffer>> {\n const { spec, roles } = await specForPreset(preset)\n const files = await ensureModelFiles({\n ...spec,\n files: spec.files.map((f) => ({ ...f })),\n })\n const out: Record<string, ArrayBuffer> = {}\n for (const [i, role] of roles.entries()) {\n const path = spec.files[i]?.path\n if (path && files[path]) out[role] = files[path] as ArrayBuffer\n }\n return out\n}\n\n// Keyed by preset: switching language keeps the previous service alive, so\n// toggling back is instant rather than a re-download plus re-init.\nconst services = new Map<string, Promise<PaddleOcrService>>()\n\nexport async function getPaddleService(preset = DEFAULT_OCR_PRESET): Promise<PaddleOcrService> {\n const existing = services.get(preset)\n if (existing) return existing\n\n const promise = (async () => {\n const ppu = await loadPpu()\n const model = await fetchPresetFiles(preset)\n const service = new ppu.PaddleOcrService({\n model,\n session: {\n executionProviders: [...getConfig().executionProviders] as never,\n graphOptimizationLevel: 'all',\n },\n })\n await service.initialize()\n return service\n })()\n\n // Evict on failure so a transient network error is retried rather than\n // cached as a permanently rejected promise.\n promise.catch(() => services.delete(preset))\n services.set(preset, promise)\n return promise\n}\n\n/**\n * The recognition ONNX session and its character dictionary, per preset.\n *\n * Deliberately separate from `services`: a recognizer running behind someone\n * else's detector never needs the detection model, and skipping it saves a\n * download of a few MB. The cache `repo` is the same either way, so the two\n * files are shared with the full service rather than stored twice.\n */\nconst recognitionAssets = new Map<string, Promise<{ session: OrtSession; dictionary: string[] }>>()\n\n/**\n * Split a PaddleOCR dictionary into an ordered array, one entry per line.\n *\n * ppu exports `parseDictionary` from a module its `exports` map does not\n * publish, so this reproduces it -- and it is the whole of it. Blank entries\n * are preserved: the index is the class id the model emits, so dropping one\n * would shift every character after it.\n */\nfunction parseDictionary(source: ArrayBuffer): string[] {\n return new TextDecoder('utf-8').decode(source).split(/\\r?\\n/)\n}\n\nasync function loadRecognitionAssets(preset: string): Promise<{ session: OrtSession; dictionary: string[] }> {\n const existing = recognitionAssets.get(preset)\n if (existing) return existing\n\n const promise = (async () => {\n // Nothing below imports ppu, but loading it first keeps the \"ORT\n // configured before any session exists\" ordering `loadPpu` enforces --\n // and importing `ppu-paddle-ocr/web` is also what registers ppu-ocv's\n // web canvas platform, which the crop path needs. Skipping\n // `PaddleOcrService.initialize()` skips nothing else: the web recognizer\n // runs the canvas-native engine and never touches OpenCV.\n await loadPpu()\n const { spec, roles } = await specForPreset(preset)\n // Same `spec.repo`, so these two land on the cache keys the full\n // service already uses -- the detection file is simply never asked for.\n const wanted = ['recognition', 'charactersDictionary'].map(\n (role) => spec.files[roles.indexOf(role)]?.path ?? ''\n )\n const files = await ensureModelFiles({ ...spec, files: wanted.map((path) => ({ path })) })\n const model = files[wanted[0] as string]\n const dict = files[wanted[1] as string]\n if (!model || !dict) {\n throw new Error(`PaddleOCR preset \"${preset}\" is missing its recognition model or dictionary.`)\n }\n\n const dictionary = parseDictionary(dict)\n if (dictionary.length === 0) {\n throw new Error(`PaddleOCR preset \"${preset}\" has an empty character dictionary.`)\n }\n // WebGPU cannot run PP-OCR's recognition graph -- it rewrites the\n // convolutions into `com.ms.internal.nhwc` and has no kernel for them,\n // which fails at session creation. `PaddleOcrService` retries on WASM\n // for exactly this, so a session built here has to as well.\n return { session: await createSession(model, { fallbackToWasm: true }), dictionary }\n })()\n\n promise.catch(() => recognitionAssets.delete(preset))\n recognitionAssets.set(preset, promise)\n return promise\n}\n\n/**\n * Recognition only: read the regions you hand it, no detection.\n *\n * `minimumConfidence` is pinned to 0 and `maxCropSourceSideLength` is set past\n * any canvas we pass. ppu would otherwise silently drop low-scoring results --\n * breaking the caller's box-to-result mapping, since `run` also sorts what it\n * returns into reading order -- and downscale a tall batch of stacked crops.\n * Filtering is the calling stage's job, where the threshold is a parameter.\n */\nexport async function getPaddleRecognizer(\n preset = DEFAULT_OCR_PRESET,\n options: PaddleRecognizerOptions = {}\n): Promise<PaddleRecognitionService> {\n const ppu = await loadPpu()\n const { session, dictionary } = await loadRecognitionAssets(preset)\n return new ppu.RecognitionService(session, {\n charactersDictionary: dictionary,\n minimumConfidence: 0,\n maxCropSourceSideLength: Number.MAX_SAFE_INTEGER,\n ...(options.recBatchSize === undefined ? {} : { recBatchSize: options.recBatchSize }),\n ...(options.spaceRecovery === undefined ? {} : { spaceRecovery: options.spaceRecovery }),\n })\n}\n\nasync function releaseRecognition(preset: string): Promise<void> {\n const pending = recognitionAssets.get(preset)\n if (!pending) return\n recognitionAssets.delete(preset)\n await pending.then(({ session }) => session.release()).catch(() => undefined)\n}\n\nexport async function isPresetCached(preset: string): Promise<boolean> {\n const { spec } = await specForPreset(preset)\n return isCached(spec)\n}\n\n/** Pre-warm a preset so the first OCR call does not pay the download. */\nexport async function loadPreset(preset: string): Promise<void> {\n await getPaddleService(preset)\n}\n\nexport async function removePreset(preset: string): Promise<void> {\n const { spec } = await specForPreset(preset)\n const service = services.get(preset)\n services.delete(preset)\n await service?.then((s) => s.destroy()).catch(() => undefined)\n await releaseRecognition(preset)\n await evict(spec)\n}\n\n/** Tear down every cached service, recognition-only sessions included. */\nexport async function disposePaddleServices(): Promise<void> {\n const pending = [...services.values()]\n services.clear()\n const recognition = [...recognitionAssets.keys()]\n await Promise.all([\n ...pending.map((p) => p.then((s) => s.destroy()).catch(() => undefined)),\n ...recognition.map((preset) => releaseRecognition(preset)),\n ])\n}\n","/**\n * PaddleOCR text detection and recognition -- the default OCR engines.\n *\n * `PaddleTextDetector` mirrors ScaleDP's detector stages (image -> boxes) and\n * `PaddleTextRecognizer` mirrors its OCR stages (image -> Document with text\n * and boxes). Both run PP-OCR models through ppu-paddle-ocr on onnxruntime-web.\n */\n\nimport { OcrError } from '../core/errors.js'\nimport { decodeImage, imageDataToCanvas } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { boxesToFormattedText, boxesToText } from '../core/text.js'\nimport { type Box, boxFromBBox } from '../schemas/box.js'\nimport { createDetectorOutput, type DetectorOutput } from '../schemas/detector-output.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\nimport { getPaddleService } from './paddle-service.js'\nimport { DEFAULT_OCR_PRESET, validatePreset } from './presets.js'\n\n/** Recognizing per box gives word-level output; per line merges them first. */\nexport type RecognitionStrategy = 'per-box' | 'per-line' | 'cross-line'\n\nexport interface PaddleOcrParams extends BaseStageParams {\n /** Language/script preset. See PADDLE_OCR_PRESETS. */\n preset: string\n /** Drop results below this confidence (0-1). */\n scoreThreshold: number\n}\n\nexport interface PaddleTextDetectorParams extends PaddleOcrParams {}\n\nexport const PADDLE_TEXT_DETECTOR_DEFAULTS: PaddleTextDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'boxes',\n keepInputData: true,\n preset: DEFAULT_OCR_PRESET,\n scoreThreshold: 0,\n})\n\n/** Decode a stage input into something ppu-paddle-ocr accepts. */\nasync function toCanvas(input: unknown): Promise<OffscreenCanvas> {\n if (typeof OffscreenCanvas !== 'undefined' && input instanceof OffscreenCanvas) return input\n if (typeof ImageData !== 'undefined' && input instanceof ImageData) {\n return imageDataToCanvas(input)\n }\n\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new OcrError(`Upstream stage failed: ${image.exception}`, 'toCanvas')\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new OcrError('Expected an Image with decoded bytes', 'toCanvas')\n }\n\n const bitmap = await decodeImage(image.data)\n try {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)\n const ctx = canvas.getContext('2d')\n if (!ctx) throw new OcrError('Failed to acquire a 2D context', 'toCanvas')\n ctx.drawImage(bitmap, 0, 0)\n return canvas\n } finally {\n bitmap.close()\n }\n}\n\n/** Text detection only: image -> word boxes, no recognition. */\nexport class PaddleTextDetector extends Stage<PaddleTextDetectorParams> {\n readonly name = 'PaddleTextDetector'\n\n constructor(options: Partial<PaddleTextDetectorParams> = {}) {\n super(resolveParams(PADDLE_TEXT_DETECTOR_DEFAULTS, options, { preset: validatePreset }))\n }\n\n override async init(): Promise<void> {\n await getPaddleService(this.params.preset)\n }\n\n protected async apply(input: unknown, row: Row): Promise<DetectorOutput> {\n const service = await getPaddleService(this.params.preset)\n const canvas = await toCanvas(input)\n const { boxes } = await service.detect(canvas as never)\n\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'paddle',\n // ppu returns axis-aligned boxes in original image coordinates.\n bboxes: boxes.map((b) => boxFromBBox([b.x, b.y, b.x + b.width, b.y + b.height], { score: 1 })),\n })\n }\n\n protected onError(message: string, row: Row): DetectorOutput {\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'paddle',\n exception: message,\n })\n }\n}\n\nexport interface PaddleTextRecognizerParams extends PaddleOcrParams {\n /**\n * How recognized regions are grouped. 'per-box' keeps them as detected,\n * 'per-line' merges them into lines.\n *\n * ScaleDP's docstring calls 'per-box' word-level. That describes the\n * grouping, not the geometry: the boxes are whatever the preset's detector\n * produced, and PaddleOCR's detector is line-level. No strategy can\n * subdivide a region. `TesseractOcr` is the stage that returns words.\n */\n strategy: RecognitionStrategy\n /** Rebuild the original layout with spaces and blank lines. */\n keepFormatting: boolean\n /** Line-grouping tolerance in pixels; 0 derives it from character height. */\n lineTolerance: number\n}\n\nexport const PADDLE_TEXT_RECOGNIZER_DEFAULTS: PaddleTextRecognizerParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'text',\n keepInputData: true,\n preset: DEFAULT_OCR_PRESET,\n scoreThreshold: 0.5,\n strategy: 'per-box' as RecognitionStrategy,\n keepFormatting: false,\n lineTolerance: 0,\n})\n\n/** Full OCR: image -> Document with text and word-level boxes. */\nexport class PaddleTextRecognizer extends Stage<PaddleTextRecognizerParams> {\n readonly name = 'PaddleTextRecognizer'\n\n constructor(options: Partial<PaddleTextRecognizerParams> = {}) {\n super(resolveParams(PADDLE_TEXT_RECOGNIZER_DEFAULTS, options, { preset: validatePreset }))\n }\n\n override async init(): Promise<void> {\n await getPaddleService(this.params.preset)\n }\n\n protected async apply(input: unknown, row: Row): Promise<Document> {\n const { preset, strategy, scoreThreshold, keepFormatting, lineTolerance } = this.params\n const service = await getPaddleService(preset)\n const canvas = await toCanvas(input)\n\n const result = await service.recognize(canvas as never, {\n flatten: true,\n strategy,\n // ppu caches globally, keyed on pixels. Without this, switching\n // preset returns the *previous* model's result for the same image.\n noCache: true,\n })\n\n const items = 'results' in result ? result.results : []\n const bboxes: Box[] = items\n .filter((item) => item.confidence >= scoreThreshold)\n .map((item) =>\n boxFromBBox(\n [item.box.x, item.box.y, item.box.x + item.box.width, item.box.y + item.box.height],\n { text: item.text, score: item.confidence }\n )\n )\n\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'ocr',\n text: keepFormatting ? boxesToFormattedText(bboxes, lineTolerance) : boxesToText(bboxes),\n bboxes,\n })\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'ocr',\n exception: message,\n })\n }\n}\n","/**\n * Read the boxes a detector already found, with a PaddleOCR preset.\n *\n * The Paddle counterpart to `TesseractRecognizer`, and for the same reason:\n * `PaddleTextRecognizer` detects and recognises in a single pass over the page,\n * so boxes produced by a *separate* detector never reach it. This stage takes\n * those boxes, straightens each one, and reads it -- which is what lets DBNet or\n * YOLO feed PaddleOCR recognition, rotated regions included.\n *\n * There is no `strategy` parameter. ppu's 'per-line' and 'cross-line' merge\n * boxes before reading them; the contract here is one result per box handed in,\n * which only 'per-box' can honour.\n */\n\nimport { OcrError } from '../core/errors.js'\nimport { context2d, createCanvas, cropBox, decodeImage, resize, rotate180 } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { boxesToFormattedText, boxesToText } from '../core/text.js'\nimport { type Box, isRotated } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\nimport { DEFAULT_ORIENTATION_MODEL, LineOrientationClassifier } from './line-orientation.js'\nimport { getPaddleRecognizer, type PaddleRecognitionService } from './paddle-service.js'\nimport { DEFAULT_OCR_PRESET, validatePreset } from './presets.js'\n\nexport interface PaddleRecognizerParams extends BaseStageParams {\n /** [imageColumn, boxColumn]. */\n inputCols: string[]\n /** Language/script preset. See PADDLE_OCR_PRESETS. */\n preset: string\n /** Resize the page by this factor before cropping. */\n scaleFactor: number\n /** Grow each box before cropping. ScaleDP hardcodes 5. */\n padding: number\n /** Drop regions below this confidence (0-1). */\n scoreThreshold: number\n /** Rebuild the original layout with spaces and blank lines. */\n keepFormatting: boolean\n /** Line-grouping tolerance in pixels; 0 derives it from character height. */\n lineTolerance: number\n /**\n * Classify each crop 0/180 degrees and turn the inverted ones.\n *\n * Off by default, unlike `TesseractRecognizer`. Paddle already turns a crop\n * that is markedly taller than wide, so only the 180 degree flip is missing\n * -- and catching it costs a separate ~9 MB model an ordinary page never\n * needs. Turn it on for scans that come in upside down.\n */\n detectLineOrientation: boolean\n /** Recognize only boxes that are rotated or came back inverted. */\n onlyRotated: boolean\n oriModel: string\n /**\n * Recover inter-word spaces the greedy CTC decode drops. Helps Latin text\n * where the model collapses word gaps; can add spurious ones in dense\n * symbol runs.\n */\n spaceRecovery: boolean\n /** Crops per batched inference. 1 disables batching. */\n recBatchSize: number\n}\n\nexport const PADDLE_RECOGNIZER_DEFAULTS: PaddleRecognizerParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'text',\n keepInputData: true,\n preset: DEFAULT_OCR_PRESET,\n scaleFactor: 1,\n padding: 5,\n scoreThreshold: 0.5,\n keepFormatting: false,\n lineTolerance: 0,\n detectLineOrientation: false,\n onlyRotated: false,\n oriModel: DEFAULT_ORIENTATION_MODEL,\n spaceRecovery: false,\n recBatchSize: 6,\n})\n\n/**\n * Tallest sheet crops are stacked onto.\n *\n * Well under every browser's canvas ceiling, and low enough that a sheet stays\n * cheap to allocate. A single crop taller than this gets a sheet to itself.\n */\nconst MAX_SHEET_HEIGHT = 8192\n\nfunction boxesOf(source: unknown): Box[] {\n if (typeof source !== 'object' || source === null) return []\n return (source as DetectorOutput).bboxes ?? []\n}\n\nexport class PaddleRecognizer extends Stage<PaddleRecognizerParams> {\n readonly name = 'PaddleRecognizer'\n\n private orientation: LineOrientationClassifier | null = null\n private recognition: PaddleRecognitionService | null = null\n\n constructor(options: Partial<PaddleRecognizerParams> = {}) {\n super(\n resolveParams(PADDLE_RECOGNIZER_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length !== 2) {\n throw new RangeError('inputCols must be [imageColumn, boxColumn]')\n }\n },\n preset: validatePreset,\n })\n )\n }\n\n override async init(): Promise<void> {\n const { preset, recBatchSize, spaceRecovery, detectLineOrientation, oriModel } = this.params\n this.recognition ??= await getPaddleRecognizer(preset, { recBatchSize, spaceRecovery })\n if (detectLineOrientation) {\n this.orientation ??= new LineOrientationClassifier(oriModel)\n }\n }\n\n protected async apply(_input: unknown, row: Row, ctx: StageContext): Promise<Document> {\n const {\n inputCols,\n scaleFactor,\n padding,\n scoreThreshold,\n keepFormatting,\n lineTolerance,\n detectLineOrientation,\n onlyRotated,\n } = this.params\n const [imageCol, boxCol] = inputCols as [string, string]\n const image = row[imageCol] as ScaleDpImage | undefined\n\n // Check `exception` first. A failed upstream stage returns a well-formed\n // but empty Image, so testing the bytes first would report \"no decoded\n // bytes\" and bury the real cause.\n if (image?.exception) {\n throw new OcrError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new OcrError('Expected an Image with decoded bytes', this.name)\n }\n\n const source = row[boxCol]\n if (source === undefined) {\n throw new OcrError(\n `No boxes in column \"${boxCol}\". This stage reads a detector's output; ` +\n 'run a text detector before it.',\n this.name\n )\n }\n\n await this.init()\n const recognition = this.recognition as PaddleRecognitionService\n const bitmap = await decodeImage(image.data)\n\n // ScaleDP resizes the *page* and scales each box to index into it, which\n // is how a small line is handed to the model at a readable size. The\n // boxes it reports back are the originals, untouched -- so nothing here\n // has to map coordinates out again.\n const canvas = scaleFactor === 1 ? bitmap : resize(bitmap, scaleFactor)\n\n // Kept index-aligned: `crops[i]` is what `kept[i]` became.\n const kept: Box[] = []\n const crops: OffscreenCanvas[] = []\n\n try {\n for (const box of boxesOf(source)) {\n ctx.signal?.throwIfAborted()\n\n // Straightens rotated boxes rather than taking their envelope,\n // which is what makes a skewed line readable at all -- and what\n // ppu's own cropping, being axis-aligned, cannot do.\n let crop = cropBox(canvas, box, { scaleFactor, padding })\n\n let inverted = false\n if (detectLineOrientation && this.orientation) {\n inverted = (await this.orientation.classify(crop)) === '180_degree'\n if (inverted) crop = rotate180(crop)\n }\n\n // ScaleDP's onlyRotated: an upright, right-way-up box was\n // already handled by whatever pass produced it.\n if (onlyRotated && !isRotated(box) && !inverted) continue\n\n kept.push(box)\n crops.push(crop)\n }\n } finally {\n bitmap.close()\n }\n\n const read = await recognizeCrops(recognition, crops, ctx)\n\n const bboxes: Box[] = []\n for (const [i, box] of kept.entries()) {\n const result = read[i]\n if (!result?.text) continue\n if (result.score < scoreThreshold) continue\n bboxes.push({ ...box, text: result.text, score: result.score })\n }\n\n return createDocument({\n path: String(row[this.params.pathCol] ?? image.path),\n type: 'paddle-recognizer',\n text: keepFormatting ? boxesToFormattedText(bboxes, lineTolerance) : boxesToText(bboxes),\n bboxes,\n })\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'paddle-recognizer',\n exception: message,\n })\n }\n\n override async dispose(): Promise<void> {\n await this.orientation?.dispose()\n this.orientation = null\n // The recognition session is shared per preset and owned by\n // paddle-service; `disposePaddleServices()` is what releases it.\n this.recognition = null\n }\n}\n\ninterface ReadResult {\n text: string\n score: number\n}\n\n/**\n * Read every crop, batching across them.\n *\n * ppu batches `recBatchSize` crops into a single inference, but only within one\n * `run()` call, and `run()` cuts its crops out of the one canvas it is given. So\n * the crops are stacked onto sheet canvases -- each at x 0, one below the last --\n * and handed back as the boxes to read. ppu re-crops exactly those rects, so the\n * unused width beside a narrow crop is never sampled. Calling `run()` once per\n * box would instead pay one inference, and one main-thread yield, per line.\n *\n * Results come back index-aligned to `crops`; a crop ppu rejected or did not\n * return is left `undefined`. `run()` sorts what it returns into reading order,\n * so results are matched on the slot's y offset, never on array position.\n */\nasync function recognizeCrops(\n recognition: PaddleRecognitionService,\n crops: readonly OffscreenCanvas[],\n ctx: StageContext\n): Promise<(ReadResult | undefined)[]> {\n const out: (ReadResult | undefined)[] = new Array(crops.length)\n\n for (let start = 0; start < crops.length; ) {\n ctx.signal?.throwIfAborted()\n\n // Take as many crops as fit on one sheet, but always at least one, so a\n // crop taller than the ceiling is still read.\n let end = start\n let height = 0\n let width = 0\n while (end < crops.length) {\n const crop = crops[end] as OffscreenCanvas\n if (end > start && height + crop.height > MAX_SHEET_HEIGHT) break\n height += crop.height\n width = Math.max(width, crop.width)\n end++\n }\n\n const sheet = createCanvas(width, height)\n const sheetCtx = context2d(sheet)\n const slots: { x: number; y: number; width: number; height: number }[] = []\n const atOffset = new Map<number, number>()\n let offset = 0\n for (let i = start; i < end; i++) {\n const crop = crops[i] as OffscreenCanvas\n sheetCtx.drawImage(crop, 0, offset)\n slots.push({ x: 0, y: offset, width: crop.width, height: crop.height })\n atOffset.set(offset, i)\n offset += crop.height\n }\n\n const results = await recognition.run(sheet as never, slots, undefined, 'per-box')\n for (const result of results) {\n const index = atOffset.get(result.box.y)\n if (index !== undefined) out[index] = { text: result.text.trim(), score: result.confidence }\n }\n\n start = end\n }\n\n return out\n}\n","/**\n * Writing-script detection via Tesseract's OSD model.\n *\n * Kept separate from recognition on purpose: when the recognition model is\n * wrong for the page the text comes back garbled, and that is precisely when\n * knowing the script is most useful. Running OSD independently means the\n * answer stays trustworthy.\n *\n * Feeds the preset picker -- see `presetsForScript`.\n */\n\nimport { OcrError } from '../core/errors.js'\nimport { decodeImage, imageDataToCanvas, toImageData } from '../core/image.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\nimport { type OcrPreset, presetsForScript } from './presets.js'\n\nexport interface DetectedScript {\n /** Script name as Tesseract reports it, e.g. 'Latin', 'Cyrillic', 'Han'. */\n script: string\n /** Tesseract's own confidence; not a probability. */\n confidence: number\n}\n\ntype TesseractJs = typeof import('tesseract.js')\ntype OsdWorker = Awaited<ReturnType<TesseractJs['createWorker']>>\n\nlet workerPromise: Promise<OsdWorker> | null = null\n\nasync function getOsdWorker(): Promise<OsdWorker> {\n if (workerPromise) return workerPromise\n\n workerPromise = (async () => {\n let mod: TesseractJs\n try {\n mod = await import('tesseract.js')\n } catch (cause) {\n throw new Error('tesseract.js is required for script detection. Install it: npm i tesseract.js', {\n cause,\n })\n }\n // OEM 0 (legacy engine) is the only one that carries OSD data.\n return mod.createWorker('osd', mod.OEM.TESSERACT_ONLY)\n })()\n\n workerPromise.catch(() => {\n workerPromise = null\n })\n return workerPromise\n}\n\n/** Detect the dominant writing script on a page. Returns null when unsure. */\nexport async function detectScript(\n source: ImageBitmap | OffscreenCanvas | ImageData | ScaleDpImage\n): Promise<DetectedScript | null> {\n const worker = await getOsdWorker()\n\n let canvas: OffscreenCanvas\n if (typeof OffscreenCanvas !== 'undefined' && source instanceof OffscreenCanvas) {\n canvas = source\n } else if (typeof ImageData !== 'undefined' && source instanceof ImageData) {\n canvas = imageDataToCanvas(source)\n } else if (typeof ImageBitmap !== 'undefined' && source instanceof ImageBitmap) {\n canvas = imageDataToCanvas(toImageData(source))\n } else {\n const image = source as ScaleDpImage\n if (!(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new OcrError('Expected an Image with decoded bytes', 'detectScript')\n }\n const bitmap = await decodeImage(image.data)\n try {\n canvas = imageDataToCanvas(toImageData(bitmap))\n } finally {\n bitmap.close()\n }\n }\n\n const blob = await canvas.convertToBlob({ type: 'image/png' })\n const { data } = await worker.detect(blob)\n if (!data?.script) return null\n return { script: data.script, confidence: data.script_confidence ?? 0 }\n}\n\n/** Presets able to read the detected script, best-first. */\nexport async function suggestPresets(\n source: ImageBitmap | OffscreenCanvas | ImageData | ScaleDpImage\n): Promise<OcrPreset[]> {\n const detected = await detectScript(source)\n return detected ? presetsForScript(detected.script) : []\n}\n\n/** Tear down the shared OSD worker. */\nexport async function disposeScriptDetection(): Promise<void> {\n const pending = workerPromise\n workerPromise = null\n await pending?.then((w) => w.terminate()).catch(() => undefined)\n}\n","/**\n * Tesseract OCR, mirroring ScaleDP's `TesseractOcr` stage.\n *\n * Recognition runs on tesseract-wasm; script detection uses tesseract.js's OSD\n * model. The two are deliberately independent -- script detection still works\n * when the recognition model is wrong for the page and garbles the text, which\n * is exactly when you most want to know the script.\n *\n * Both the worker URL and the traineddata location come from `configure()`.\n * The pdftools prototype hardcoded `/tesseract-worker.js` and a raw GitHub URL,\n * neither of which a library can assume.\n */\n\nimport { getConfig } from '../core/config.js'\nimport { OcrError } from '../core/errors.js'\nimport { decodeImage, toImageData } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { boxesToFormattedText, boxesToText } from '../core/text.js'\nimport { type Box, boxFromBBox } from '../schemas/box.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\n\n/** tessdata_fast is a good default: far smaller than tessdata, barely less accurate. */\nexport const DEFAULT_TESSDATA_URL = 'https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/main/'\n\nexport interface TesseractOcrParams extends BaseStageParams {\n /** Language codes, e.g. ['eng'] or ['eng', 'deu']. */\n lang: readonly string[]\n /** Drop words below this confidence (0-1). */\n scoreThreshold: number\n keepFormatting: boolean\n lineTolerance: number\n}\n\nexport const TESSERACT_OCR_DEFAULTS: TesseractOcrParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'text',\n keepInputData: true,\n lang: ['eng'] as readonly string[],\n scoreThreshold: 0.5,\n keepFormatting: false,\n lineTolerance: 0,\n})\n\ntype TesseractWasm = typeof import('tesseract-wasm')\ntype OcrClient = InstanceType<TesseractWasm['OCRClient']>\n\nlet clientPromise: Promise<OcrClient> | null = null\nlet loadedLanguage: string | null = null\n\nfunction trainedDataUrl(lang: string): string {\n const base = (getConfig().tesseract.dataUrl ?? DEFAULT_TESSDATA_URL).replace(/\\/?$/, '/')\n return `${base}${lang}.traineddata`\n}\n\n/**\n * The shared Tesseract client, loading the model for `lang` if needed.\n *\n * Exported so the crop-based recognizer reuses this one client rather than\n * standing up a second worker and loading the same traineddata twice.\n */\nexport async function getTesseractClient(lang: string): Promise<OcrClient> {\n if (clientPromise && loadedLanguage === lang) return clientPromise\n\n // A client holds one model; switching language means a fresh one.\n if (clientPromise) {\n const previous = clientPromise\n clientPromise = null\n await previous.then((c) => c.destroy()).catch(() => undefined)\n }\n\n loadedLanguage = lang\n clientPromise = (async () => {\n let mod: TesseractWasm\n try {\n mod = await import('tesseract-wasm')\n } catch (cause) {\n throw new Error(\n 'tesseract-wasm is required for the Tesseract engine. Install it: npm i tesseract-wasm',\n { cause }\n )\n }\n\n const { workerUrl } = getConfig().tesseract\n const client = new mod.OCRClient(workerUrl ? { workerURL: workerUrl } : {})\n const response = await fetch(trainedDataUrl(lang))\n if (!response.ok) {\n throw new OcrError(\n `Failed to fetch ${lang}.traineddata: ${response.status} ${response.statusText}`,\n 'TesseractOcr'\n )\n }\n await client.loadModel(new Uint8Array(await response.arrayBuffer()))\n return client\n })()\n\n clientPromise.catch(() => {\n clientPromise = null\n loadedLanguage = null\n })\n return clientPromise\n}\n\n/** Tear down the shared Tesseract client. */\nexport async function disposeTesseract(): Promise<void> {\n const pending = clientPromise\n clientPromise = null\n loadedLanguage = null\n await pending?.then((c) => c.destroy()).catch(() => undefined)\n}\n\nexport class TesseractOcr extends Stage<TesseractOcrParams> {\n readonly name = 'TesseractOcr'\n\n constructor(options: Partial<TesseractOcrParams> = {}) {\n super(\n resolveParams(TESSERACT_OCR_DEFAULTS, options, {\n lang: (value) => {\n if (value.length === 0) throw new RangeError('lang must not be empty')\n },\n })\n )\n }\n\n /** tesseract-wasm loads one model, so multi-language means a joined code. */\n private get language(): string {\n return this.params.lang.join('+')\n }\n\n override async init(): Promise<void> {\n await getTesseractClient(this.language)\n }\n\n protected async apply(input: unknown, row: Row): Promise<Document> {\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new OcrError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new OcrError('Expected an Image with decoded bytes', this.name)\n }\n\n const client = await getTesseractClient(this.language)\n const bitmap = await decodeImage(image.data)\n let words: import('tesseract-wasm').TextItem[]\n try {\n await client.loadImage(toImageData(bitmap))\n // getTextBoxes, not getBoundingBoxes: the latter is layout analysis\n // only and returns geometry with no text and no confidence.\n words = await client.getTextBoxes('word')\n } finally {\n bitmap.close()\n }\n\n const { scoreThreshold, keepFormatting, lineTolerance } = this.params\n const bboxes: Box[] = words\n // tesseract-wasm reports confidence on a 0-1 scale, not 0-100.\n .filter((word) => word.confidence >= scoreThreshold && word.text.trim().length > 0)\n .map((word) =>\n boxFromBBox([word.rect.left, word.rect.top, word.rect.right, word.rect.bottom], {\n text: word.text.trim(),\n score: word.confidence,\n })\n )\n\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'tesseract',\n text: keepFormatting ? boxesToFormattedText(bboxes, lineTolerance) : boxesToText(bboxes),\n bboxes,\n })\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'tesseract',\n exception: message,\n })\n }\n\n override async dispose(): Promise<void> {\n await disposeTesseract()\n }\n}\n","/**\n * Recognize text inside boxes a detector already found.\n *\n * Port of ScaleDP's `TesseractRecognizer` (a `BaseRecognizer`), which is the\n * half of the OCR story `PaddleTextRecognizer` cannot cover: Paddle detects and\n * recognises in a single pass over the page, so boxes produced by a *separate*\n * detector never reach it -- rotated boxes in particular. This stage takes those\n * boxes, straightens each one, and reads it.\n */\n\nimport { OcrError } from '../core/errors.js'\nimport type { Point } from '../core/geometry.js'\nimport { cropBox, cropGeometry, decodeImage, rotate180, toImageData } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { boxesToFormattedText, boxesToText } from '../core/text.js'\nimport { type Box, boxFromPolygon, isRotated } from '../schemas/box.js'\nimport type { DetectorOutput } from '../schemas/detector-output.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\nimport { DEFAULT_ORIENTATION_MODEL, LineOrientationClassifier } from './line-orientation.js'\nimport { getTesseractClient } from './tesseract.js'\n\nexport interface TesseractRecognizerParams extends BaseStageParams {\n /** [imageColumn, boxColumn]. */\n inputCols: string[]\n lang: readonly string[]\n /** Resize the page by this factor before cropping. */\n scaleFactor: number\n /** Grow each box before cropping. ScaleDP hardcodes 5. */\n padding: number\n /** Drop words below this confidence (0-1). */\n scoreThreshold: number\n keepFormatting: boolean\n lineTolerance: number\n /**\n * Granularity of the boxes returned.\n *\n * 'region' keeps ScaleDP's behaviour: one box per region the detector\n * found, carrying everything read inside it. Since the detectors here are\n * line-level, so are those boxes.\n *\n * 'word' returns instead the boxes tesseract reports for each word, mapped\n * back through the crop -- rotation included -- into page coordinates. Not\n * in Python ScaleDP, which always returns one box per region.\n */\n boxLevel: 'region' | 'word'\n /** Classify each crop 0/180 degrees and turn the inverted ones. */\n detectLineOrientation: boolean\n /**\n * Recognize only boxes that are rotated or came back inverted.\n *\n * ScaleDP defaults this to true because there the stage refines an OCR pass\n * that already ran. Standalone it is the primary recognizer, and skipping\n * every upright box would return an empty document for the ordinary case,\n * so the default is flipped here. See docs/porting.md.\n */\n onlyRotated: boolean\n oriModel: string\n}\n\nexport const TESSERACT_RECOGNIZER_DEFAULTS: TesseractRecognizerParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n inputCols: ['image', 'boxes'],\n outputCol: 'text',\n keepInputData: true,\n lang: ['eng'] as readonly string[],\n scaleFactor: 1,\n padding: 5,\n scoreThreshold: 0.5,\n keepFormatting: false,\n lineTolerance: 0,\n boxLevel: 'region' as 'region' | 'word',\n detectLineOrientation: true,\n onlyRotated: false,\n oriModel: DEFAULT_ORIENTATION_MODEL,\n})\n\nfunction boxesOf(source: unknown): Box[] {\n if (typeof source !== 'object' || source === null) return []\n return (source as DetectorOutput).bboxes ?? []\n}\n\nexport class TesseractRecognizer extends Stage<TesseractRecognizerParams> {\n readonly name = 'TesseractRecognizer'\n\n private orientation: LineOrientationClassifier | null = null\n\n constructor(options: Partial<TesseractRecognizerParams> = {}) {\n super(\n resolveParams(TESSERACT_RECOGNIZER_DEFAULTS, options, {\n inputCols: (value) => {\n if (value.length !== 2) {\n throw new RangeError('inputCols must be [imageColumn, boxColumn]')\n }\n },\n lang: (value) => {\n if (value.length === 0) throw new RangeError('lang must not be empty')\n },\n })\n )\n }\n\n private get language(): string {\n return this.params.lang.join('+')\n }\n\n override async init(): Promise<void> {\n await getTesseractClient(this.language)\n if (this.params.detectLineOrientation) {\n this.orientation ??= new LineOrientationClassifier(this.params.oriModel)\n }\n }\n\n protected async apply(_input: unknown, row: Row, ctx: StageContext): Promise<Document> {\n const {\n inputCols,\n scaleFactor,\n padding,\n scoreThreshold,\n keepFormatting,\n lineTolerance,\n boxLevel,\n detectLineOrientation,\n onlyRotated,\n } = this.params\n const [imageCol, boxCol] = inputCols as [string, string]\n const image = row[imageCol] as ScaleDpImage | undefined\n\n if (image?.exception) {\n throw new OcrError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new OcrError('Expected an Image with decoded bytes', this.name)\n }\n\n const source = row[boxCol]\n if (source === undefined) {\n throw new OcrError(\n `No boxes in column \"${boxCol}\". This stage reads a detector's output; ` +\n 'run a text detector before it.',\n this.name\n )\n }\n\n await this.init()\n const client = await getTesseractClient(this.language)\n const bitmap = await decodeImage(image.data)\n const recognized: Box[] = []\n\n try {\n for (const box of boxesOf(source)) {\n ctx.signal?.throwIfAborted()\n\n // Straightens rotated boxes rather than taking their envelope,\n // which is what makes a skewed line readable at all. The same\n // geometry maps word boxes back out again.\n const geometry = cropGeometry(box, { scaleFactor, padding })\n let crop = cropBox(bitmap, box, { scaleFactor, padding })\n\n let inverted = false\n if (detectLineOrientation && this.orientation) {\n inverted = (await this.orientation.classify(crop)) === '180_degree'\n if (inverted) crop = rotate180(crop)\n }\n\n // ScaleDP's onlyRotated: an upright, right-way-up box was\n // already handled by whatever pass produced it.\n if (onlyRotated && !isRotated(box) && !inverted) continue\n\n await client.loadImage(toImageData(crop))\n const items = await client.getTextBoxes('word')\n\n const words = items.filter((item) => item.text.trim())\n if (words.length === 0) continue\n\n // tesseract-wasm reports confidence on a 0-1 scale, not 0-100.\n // ScaleDP writes its confidence to `conf` and then filters on\n // `score`, so its threshold silently applies to the detector's\n // score instead; the value goes where it is read here.\n // Averaged over every item tesseract returned, empty ones\n // included, which is what the region mode did before word boxes\n // existed -- so a pipeline that was passing this gate still is.\n const score = items.reduce((sum, item) => sum + item.confidence, 0) / (items.length || 1)\n\n // The gate is the region's score in both modes, so `boxLevel`\n // changes how finely the result is cut up and nothing else. Per\n // word it would also change *what was read*: a word scoring 0.3\n // between two at 0.9 rides out on the region's mean, and would\n // vanish on its own -- so the same page would yield different\n // text depending on the box size asked for. Each word still\n // carries its own confidence for filtering further downstream.\n if (score < scoreThreshold) continue\n\n if (boxLevel === 'word') {\n for (const item of words) {\n recognized.push(\n wordBox(\n item.rect,\n geometry,\n inverted,\n scaleFactor,\n item.text.trim(),\n item.confidence\n )\n )\n }\n continue\n }\n\n recognized.push({ ...box, text: words.map((item) => item.text.trim()).join(' '), score })\n }\n } finally {\n bitmap.close()\n }\n\n // Coordinates came from the scaled page, so bring them back. Word boxes\n // are already in page coordinates -- `wordBox` divides as it maps, so it\n // rounds once rather than twice.\n const bboxes =\n scaleFactor === 1 || boxLevel === 'word'\n ? recognized\n : recognized.map((box) => ({\n ...box,\n x: Math.round(box.x / scaleFactor),\n y: Math.round(box.y / scaleFactor),\n width: Math.round(box.width / scaleFactor),\n height: Math.round(box.height / scaleFactor),\n }))\n\n return createDocument({\n path: String(row[this.params.pathCol] ?? image.path),\n type: 'tesseract-recognizer',\n text: keepFormatting ? boxesToFormattedText(bboxes, lineTolerance) : boxesToText(bboxes),\n bboxes,\n })\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'tesseract-recognizer',\n exception: message,\n })\n }\n\n override async dispose(): Promise<void> {\n await this.orientation?.dispose()\n this.orientation = null\n }\n}\n\n/**\n * One word's box, in the page's coordinates.\n *\n * tesseract reports the rect in the crop's own space: straightened, padded,\n * scaled, and turned the right way up if the line was upside down. Undo the\n * turn, then push the four corners back through the crop's own transform, so a\n * word inside a skewed line comes back skewed the same way.\n */\nfunction wordBox(\n rect: { left: number; top: number; right: number; bottom: number },\n geometry: ReturnType<typeof cropGeometry>,\n inverted: boolean,\n scaleFactor: number,\n text: string,\n score: number\n): Box {\n const { width, height, map } = geometry\n // rotate180 maps (x, y) to (w - x, h - y), so the rect's corners swap.\n const [left, top, right, bottom] = inverted\n ? [width - rect.right, height - rect.bottom, width - rect.left, height - rect.top]\n : [rect.left, rect.top, rect.right, rect.bottom]\n\n const toPage = (x: number, y: number): Point => {\n const [px, py] = map(x, y)\n return [px / scaleFactor, py / scaleFactor]\n }\n const corners: Point[] = [\n toPage(left, top),\n toPage(right, top),\n toPage(right, bottom),\n toPage(left, bottom),\n ]\n return boxFromPolygon(corners, { text, score })\n}\n"],"mappings":"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"}
|
|
@@ -39,15 +39,36 @@ async function loadOrt() {
|
|
|
39
39
|
function resetOrt() {
|
|
40
40
|
modulePromise = null;
|
|
41
41
|
}
|
|
42
|
+
/** Providers that exist in every onnxruntime-web build. */
|
|
43
|
+
const ALWAYS_AVAILABLE = /* @__PURE__ */ new Set(["cpu", "wasm"]);
|
|
44
|
+
/**
|
|
45
|
+
* The provider list to retry with, or `null` when there is nothing safer left.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors ppu-paddle-ocr's own fallback, so a stage building its own session
|
|
48
|
+
* behaves the same as one going through `PaddleOcrService`.
|
|
49
|
+
*/
|
|
50
|
+
function wasmFallbackProviders(providers) {
|
|
51
|
+
if (providers.length === 0 || providers.every((name) => ALWAYS_AVAILABLE.has(name))) return null;
|
|
52
|
+
return [providers.find((name) => ALWAYS_AVAILABLE.has(name)) ?? "wasm"];
|
|
53
|
+
}
|
|
42
54
|
/** Create an inference session from model bytes. */
|
|
43
55
|
async function createSession(model, options = {}) {
|
|
44
56
|
const ort = await loadOrt();
|
|
45
57
|
const bytes = model instanceof Uint8Array ? model : new Uint8Array(model);
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
const providers = [...options.executionProviders ?? getConfig().executionProviders];
|
|
59
|
+
const create = (executionProviders) => ort.InferenceSession.create(bytes, {
|
|
60
|
+
executionProviders,
|
|
48
61
|
graphOptimizationLevel: "all",
|
|
49
62
|
...options.externalData ? { externalData: options.externalData } : {}
|
|
50
63
|
});
|
|
64
|
+
try {
|
|
65
|
+
return await create(providers);
|
|
66
|
+
} catch (cause) {
|
|
67
|
+
const fallback = options.fallbackToWasm ? wasmFallbackProviders(providers) : null;
|
|
68
|
+
if (!fallback) throw cause;
|
|
69
|
+
console.warn(`[scaledp] executionProviders ${JSON.stringify(providers)} could not run this model (${cause instanceof Error ? cause.message : String(cause)}); falling back to ${JSON.stringify(fallback)}.`);
|
|
70
|
+
return create(fallback);
|
|
71
|
+
}
|
|
51
72
|
}
|
|
52
73
|
/** True when the browser exposes a usable WebGPU adapter. */
|
|
53
74
|
async function isWebGpuAvailable() {
|
|
@@ -68,6 +89,6 @@ function isCrossOriginIsolated() {
|
|
|
68
89
|
return typeof crossOriginIsolated !== "undefined" && crossOriginIsolated === true;
|
|
69
90
|
}
|
|
70
91
|
//#endregion
|
|
71
|
-
export { resetOrt as a, loadOrt as i, isCrossOriginIsolated as n, isWebGpuAvailable as r, createSession as t };
|
|
92
|
+
export { resetOrt as a, loadOrt as i, isCrossOriginIsolated as n, wasmFallbackProviders as o, isWebGpuAvailable as r, createSession as t };
|
|
72
93
|
|
|
73
|
-
//# sourceMappingURL=ort-
|
|
94
|
+
//# sourceMappingURL=ort-DZEG14nY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ort-DZEG14nY.js","names":[],"sources":["../src/ocr/ort.ts"],"sourcesContent":["/**\n * onnxruntime-web setup, shared by every ONNX-backed stage.\n *\n * onnxruntime-web is an optional peer dependency, imported lazily so the core\n * pulls in no ML runtime.\n */\n\nimport { getConfig, resolveNumThreads } from '../core/config.js'\n\ntype OrtModule = typeof import('onnxruntime-web')\n\nlet modulePromise: Promise<OrtModule> | null = null\n\n/**\n * onnxruntime-web's JS glue and its .wasm binary must come from the same build\n * variant AND the same version -- a mismatch fails at session creation with an\n * opaque error. Deriving the CDN URL from the resolved package version is what\n * keeps them in step; the pdftools prototype hardcoded 1.23.2 in three files\n * while actually running 1.26/1.27.\n */\nfunction cdnWasmPaths(version: string): string {\n return `https://cdn.jsdelivr.net/npm/onnxruntime-web@${version}/dist/`\n}\n\nexport async function loadOrt(): Promise<OrtModule> {\n if (modulePromise) return modulePromise\n\n modulePromise = (async () => {\n let ort: OrtModule\n try {\n ort = await import('onnxruntime-web')\n } catch (cause) {\n throw new Error(\n 'onnxruntime-web is required for OCR, NER and detection. Install it: npm i onnxruntime-web',\n { cause }\n )\n }\n\n const config = getConfig()\n if (config.ortWasmPaths) {\n ort.env.wasm.wasmPaths = config.ortWasmPaths\n } else if (!ort.env.wasm.wasmPaths && ort.env.versions?.web) {\n // Set eagerly, before any session exists, so a bundled library's own\n // \"if not already set\" default never wins.\n ort.env.wasm.wasmPaths = cdnWasmPaths(ort.env.versions.web)\n }\n\n // Threading only engages on a cross-origin-isolated page (COOP/COEP).\n // Without that, ORT silently falls back to its single-threaded build and\n // this value has no effect -- not an error, just a slower run.\n ort.env.wasm.numThreads = resolveNumThreads()\n ort.env.logLevel = 'error'\n return ort\n })()\n\n return modulePromise\n}\n\n/** Reset the cached module. Tests only. */\nexport function resetOrt(): void {\n modulePromise = null\n}\n\nexport interface SessionOptions {\n executionProviders?: readonly string[]\n /** Companion `.onnx_data` files for models with external weights. */\n externalData?: { path: string; data: ArrayBuffer | Uint8Array }[]\n /**\n * Retry on WASM when the preferred providers cannot run the graph.\n *\n * Off by default, so a misconfigured provider still fails loudly. Turn it on\n * for a model known to be picky: WebGPU rewrites convolutions into its\n * `com.ms.internal.nhwc` domain and a graph it has no kernel for dies at\n * session creation, not at inference.\n */\n fallbackToWasm?: boolean\n}\n\n/** Providers that exist in every onnxruntime-web build. */\nconst ALWAYS_AVAILABLE = new Set(['cpu', 'wasm'])\n\n/**\n * The provider list to retry with, or `null` when there is nothing safer left.\n *\n * Mirrors ppu-paddle-ocr's own fallback, so a stage building its own session\n * behaves the same as one going through `PaddleOcrService`.\n */\nexport function wasmFallbackProviders(providers: readonly string[]): string[] | null {\n if (providers.length === 0 || providers.every((name) => ALWAYS_AVAILABLE.has(name))) return null\n return [providers.find((name) => ALWAYS_AVAILABLE.has(name)) ?? 'wasm']\n}\n\n/** Create an inference session from model bytes. */\nexport async function createSession(\n model: ArrayBuffer | Uint8Array,\n options: SessionOptions = {}\n): Promise<import('onnxruntime-web').InferenceSession> {\n const ort = await loadOrt()\n const bytes = model instanceof Uint8Array ? model : new Uint8Array(model)\n const providers = [...(options.executionProviders ?? getConfig().executionProviders)] as string[]\n const create = (executionProviders: string[]) =>\n ort.InferenceSession.create(bytes, {\n executionProviders,\n graphOptimizationLevel: 'all',\n ...(options.externalData ? { externalData: options.externalData } : {}),\n })\n\n try {\n return await create(providers)\n } catch (cause) {\n const fallback = options.fallbackToWasm ? wasmFallbackProviders(providers) : null\n if (!fallback) throw cause\n console.warn(\n `[scaledp] executionProviders ${JSON.stringify(providers)} could not run this model ` +\n `(${cause instanceof Error ? cause.message : String(cause)}); ` +\n `falling back to ${JSON.stringify(fallback)}.`\n )\n return create(fallback)\n }\n}\n\n/** True when the browser exposes a usable WebGPU adapter. */\nexport async function isWebGpuAvailable(): Promise<boolean> {\n const gpu = (navigator as { gpu?: { requestAdapter(): Promise<unknown> } }).gpu\n if (!gpu) return false\n try {\n return (await gpu.requestAdapter()) !== null\n } catch {\n return false\n }\n}\n\n/**\n * True when the page is cross-origin isolated, which is what SharedArrayBuffer\n * and therefore multi-threaded WASM require. A library cannot set COOP/COEP for\n * its consumer, so this is reported rather than enforced.\n */\nexport function isCrossOriginIsolated(): boolean {\n return typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated === true\n}\n"],"mappings":";;;;;;;;AAWA,IAAI,gBAA2C;;;;;;;;AAS/C,SAAS,aAAa,SAAyB;CAC3C,OAAO,gDAAgD,QAAQ;AACnE;AAEA,eAAsB,UAA8B;CAChD,IAAI,eAAe,OAAO;CAE1B,iBAAiB,YAAY;EACzB,IAAI;EACJ,IAAI;GACA,MAAM,MAAM,OAAO;EACvB,SAAS,OAAO;GACZ,MAAM,IAAI,MACN,6FACA,EAAE,MAAM,CACZ;EACJ;EAEA,MAAM,SAAS,UAAU;EACzB,IAAI,OAAO,cACP,IAAI,IAAI,KAAK,YAAY,OAAO;OAC7B,IAAI,CAAC,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,UAAU,KAGpD,IAAI,IAAI,KAAK,YAAY,aAAa,IAAI,IAAI,SAAS,GAAG;EAM9D,IAAI,IAAI,KAAK,aAAa,kBAAkB;EAC5C,IAAI,IAAI,WAAW;EACnB,OAAO;CACX,EAAA,CAAG;CAEH,OAAO;AACX;;AAGA,SAAgB,WAAiB;CAC7B,gBAAgB;AACpB;;AAkBA,MAAM,mCAAmB,IAAI,IAAI,CAAC,OAAO,MAAM,CAAC;;;;;;;AAQhD,SAAgB,sBAAsB,WAA+C;CACjF,IAAI,UAAU,WAAW,KAAK,UAAU,OAAO,SAAS,iBAAiB,IAAI,IAAI,CAAC,GAAG,OAAO;CAC5F,OAAO,CAAC,UAAU,MAAM,SAAS,iBAAiB,IAAI,IAAI,CAAC,KAAK,MAAM;AAC1E;;AAGA,eAAsB,cAClB,OACA,UAA0B,CAAC,GACwB;CACnD,MAAM,MAAM,MAAM,QAAQ;CAC1B,MAAM,QAAQ,iBAAiB,aAAa,QAAQ,IAAI,WAAW,KAAK;CACxE,MAAM,YAAY,CAAC,GAAI,QAAQ,sBAAsB,UAAU,CAAC,CAAC,kBAAmB;CACpF,MAAM,UAAU,uBACZ,IAAI,iBAAiB,OAAO,OAAO;EAC/B;EACA,wBAAwB;EACxB,GAAI,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;CACzE,CAAC;CAEL,IAAI;EACA,OAAO,MAAM,OAAO,SAAS;CACjC,SAAS,OAAO;EACZ,MAAM,WAAW,QAAQ,iBAAiB,sBAAsB,SAAS,IAAI;EAC7E,IAAI,CAAC,UAAU,MAAM;EACrB,QAAQ,KACJ,gCAAgC,KAAK,UAAU,SAAS,EAAE,6BAClD,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,qBACxC,KAAK,UAAU,QAAQ,EAAE,EACpD;EACA,OAAO,OAAO,QAAQ;CAC1B;AACJ;;AAGA,eAAsB,oBAAsC;CACxD,MAAM,MAAO,UAA+D;CAC5E,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI;EACA,OAAQ,MAAM,IAAI,eAAe,MAAO;CAC5C,QAAQ;EACJ,OAAO;CACX;AACJ;;;;;;AAOA,SAAgB,wBAAiC;CAC7C,OAAO,OAAO,wBAAwB,eAAe,wBAAwB;AACjF"}
|
package/dist/pdf/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as extractTextBoxes, a as relativeCharWidth, c as splitRunsIntoWords, d as PdfToImage, f as renderPage, g as TEXT_LAYER_SCORE, h as resetPdfjs, i as cssFontFromPdfName, l as PDF_TO_IMAGE_DEFAULTS, m as loadPdfjs, n as PdfToDocument, o as resetMeasurementContext, p as documentOptions, r as hasUsableTextLayer, s as splitRunIntoWords, t as PDF_TO_DOCUMENT_DEFAULTS, u as POINTS_PER_INCH, v as isTextItem, y as textItemToBox } from "../pdf-
|
|
1
|
+
import { _ as extractTextBoxes, a as relativeCharWidth, c as splitRunsIntoWords, d as PdfToImage, f as renderPage, g as TEXT_LAYER_SCORE, h as resetPdfjs, i as cssFontFromPdfName, l as PDF_TO_IMAGE_DEFAULTS, m as loadPdfjs, n as PdfToDocument, o as resetMeasurementContext, p as documentOptions, r as hasUsableTextLayer, s as splitRunIntoWords, t as PDF_TO_DOCUMENT_DEFAULTS, u as POINTS_PER_INCH, v as isTextItem, y as textItemToBox } from "../pdf-qbdnOnqZ.js";
|
|
2
2
|
export { PDF_TO_DOCUMENT_DEFAULTS, PDF_TO_IMAGE_DEFAULTS, POINTS_PER_INCH, PdfToDocument, PdfToImage, TEXT_LAYER_SCORE, cssFontFromPdfName, documentOptions, extractTextBoxes, hasUsableTextLayer, isTextItem, loadPdfjs, relativeCharWidth, renderPage, resetMeasurementContext, resetPdfjs, splitRunIntoWords, splitRunsIntoWords, textItemToBox };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { c as ImageError, h as getConfig, i as Stage } from "./pipeline-DACqGkpN.js";
|
|
2
|
-
import { c as encodeImage, i as createCanvas, r as context2d } from "./image-
|
|
2
|
+
import { c as encodeImage, i as createCanvas, r as context2d } from "./image-DRBsbv7G.js";
|
|
3
3
|
import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
|
|
4
4
|
import { n as createDocument, t as createImage } from "./image-DoZDJkcR.js";
|
|
5
|
-
import { r as toBytes } from "./data-to-image-
|
|
5
|
+
import { r as toBytes } from "./data-to-image-CRvh92wt.js";
|
|
6
6
|
//#region src/pdf/extract-text.ts
|
|
7
7
|
/** Glyphs sit above the baseline by roughly three quarters of the line height. */
|
|
8
8
|
const ASCENT_RATIO = .75;
|
|
@@ -414,4 +414,4 @@ function hasUsableTextLayer(document, minimumBoxes = 1) {
|
|
|
414
414
|
//#endregion
|
|
415
415
|
export { extractTextBoxes as _, relativeCharWidth as a, splitRunsIntoWords as c, PdfToImage as d, renderPage as f, TEXT_LAYER_SCORE as g, resetPdfjs as h, cssFontFromPdfName as i, PDF_TO_IMAGE_DEFAULTS as l, loadPdfjs as m, PdfToDocument as n, resetMeasurementContext as o, documentOptions as p, hasUsableTextLayer as r, splitRunIntoWords as s, PDF_TO_DOCUMENT_DEFAULTS as t, POINTS_PER_INCH as u, isTextItem as v, textItemToBox as y };
|
|
416
416
|
|
|
417
|
-
//# sourceMappingURL=pdf-
|
|
417
|
+
//# sourceMappingURL=pdf-qbdnOnqZ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdf-BQl0dneD.js","names":[],"sources":["../src/pdf/extract-text.ts","../src/pdf/pdfjs.ts","../src/pdf/pdf-to-image.ts","../src/pdf/split-words.ts","../src/pdf/pdf-to-document.ts"],"sourcesContent":["/**\n * Word-level text extraction from a PDF's embedded text layer.\n *\n * Ported from the pdftools prototype's `extractTextFromPage`, whose rotation\n * handling is the hard-won part and is reproduced here with its reasoning.\n *\n * A pdf.js text item carries a full transform matrix [a, b, c, d, e, f]. Naively\n * reading `a` as a width scale breaks on rotated text, so the reading direction\n * and the \"up\" direction are recovered as *unit* vectors -- otherwise the font\n * size gets counted twice. The four corners are then pushed through\n * `convertToViewportPoint` and reduced to an axis-aligned box.\n *\n * No semantic `angle` is emitted, deliberately. A rotated glyph matrix is\n * indistinguishable from page-level /Rotate compensation or an embedded\n * FontMatrix, so composing the corners geometrically and taking their bounding\n * box is correct regardless of which caused it. `readDirX`/`readDirY` carry the\n * direction that word-splitting needs.\n */\n\nimport type { Box } from '../schemas/box.js'\n\n/** A box plus the reading direction, which splitting needs but ScaleDP's Box lacks. */\nexport interface TextBox extends Box {\n /** Unit vector, in viewport space, along which reading progresses. */\n readDirX: number\n readDirY: number\n /** pdf.js font identifier, e.g. 'g_d0_f1'. */\n fontName: string\n}\n\n/** Glyphs sit above the baseline by roughly three quarters of the line height. */\nconst ASCENT_RATIO = 0.75\n\n/** Confidence assigned to text read from a PDF's own text layer. */\nexport const TEXT_LAYER_SCORE = 0.99\n\ninterface TextItemLike {\n str: string\n transform: number[]\n width: number\n height: number\n fontName?: string\n}\n\ninterface ViewportLike {\n convertToViewportPoint(x: number, y: number): number[]\n}\n\nexport function isTextItem(item: unknown): item is TextItemLike {\n return (\n typeof item === 'object' &&\n item !== null &&\n 'str' in item &&\n 'transform' in item &&\n Array.isArray((item as TextItemLike).transform)\n )\n}\n\n/** Convert one pdf.js text item into a viewport-space box. */\nexport function textItemToBox(item: TextItemLike, viewport: ViewportLike): TextBox | null {\n if (item.str.length === 0) return null\n\n const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = item.transform\n\n // Unit direction vectors. Dividing out the magnitudes is what stops the\n // font size being applied twice, since item.width/height already include it.\n const abMag = Math.hypot(a, b) || 1\n const cdMag = Math.hypot(c, d) || 1\n const dirX = a / abMag\n const dirY = b / abMag\n const upX = c / cdMag\n const upY = d / cdMag\n\n // The transform's origin is the baseline; shift up to the glyph tops.\n const ascent = item.height * ASCENT_RATIO\n const startX = e + upX * ascent\n const startY = f + upY * ascent\n\n const corners = [\n viewport.convertToViewportPoint(startX, startY),\n viewport.convertToViewportPoint(startX + dirX * item.width, startY + dirY * item.width),\n viewport.convertToViewportPoint(startX - upX * item.height, startY - upY * item.height),\n viewport.convertToViewportPoint(\n startX + dirX * item.width - upX * item.height,\n startY + dirY * item.width - upY * item.height\n ),\n ]\n\n const xs = corners.map((p) => p[0] as number)\n const ys = corners.map((p) => p[1] as number)\n const x = Math.min(...xs)\n const y = Math.min(...ys)\n\n // Reading direction in viewport space, taken from the start and end points\n // rather than from the matrix, so the viewport's own flip is accounted for.\n const [startScreenX = 0, startScreenY = 0] = corners[0] ?? []\n const [endScreenX = 0, endScreenY = 0] = corners[1] ?? []\n const readMag = Math.hypot(endScreenX - startScreenX, endScreenY - startScreenY) || 1\n\n return {\n text: item.str,\n score: TEXT_LAYER_SCORE,\n x: Math.floor(x),\n y: Math.floor(y),\n width: Math.max(1, Math.ceil(Math.max(...xs) - x)),\n height: Math.max(1, Math.ceil(Math.max(...ys) - y)),\n angle: 0,\n readDirX: (endScreenX - startScreenX) / readMag,\n readDirY: (endScreenY - startScreenY) / readMag,\n fontName: item.fontName ?? '',\n }\n}\n\n/** Extract every text item on a page as a viewport-space box. */\nexport async function extractTextBoxes(\n page: { getTextContent(): Promise<{ items: unknown[] }> },\n viewport: ViewportLike\n): Promise<TextBox[]> {\n const content = await page.getTextContent()\n const boxes: TextBox[] = []\n for (const item of content.items) {\n if (!isTextItem(item)) continue\n const box = textItemToBox(item, viewport)\n if (box) boxes.push(box)\n }\n return boxes\n}\n","/**\n * Lazy pdf.js loader.\n *\n * pdfjs-dist is an optional peer dependency, so it is imported only when a PDF\n * stage actually runs. Every asset path comes from `configure()` -- unlike the\n * pdftools prototype, which hardcoded `/pdf.worker.min.mjs`, a path only its\n * own Next app could serve.\n */\n\nimport { getConfig } from '../core/config.js'\n\ntype PdfjsModule = typeof import('pdfjs-dist')\n\nlet modulePromise: Promise<PdfjsModule> | null = null\n\nexport async function loadPdfjs(): Promise<PdfjsModule> {\n if (modulePromise) return modulePromise\n\n modulePromise = (async () => {\n let pdfjs: PdfjsModule\n try {\n pdfjs = await import('pdfjs-dist')\n } catch (cause) {\n throw new Error('pdfjs-dist is required for PDF support. Install it: npm i pdfjs-dist', { cause })\n }\n\n const { workerSrc } = getConfig().pdf\n if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc\n return pdfjs\n })()\n\n return modulePromise\n}\n\n/** Reset the cached module. Tests only. */\nexport function resetPdfjs(): void {\n modulePromise = null\n}\n\n/**\n * Turn pdf.js's worker-setup failure into something actionable.\n *\n * When `workerSrc` is unset or 404s, pdf.js reports \"Setting up fake worker\n * failed\" with a bare module URL, which says nothing about what to do. The\n * worker is not bundled with this library on purpose -- it has to be served by\n * the consuming application -- so the fix is always the same two steps.\n */\nexport function describePdfError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error)\n if (!/fake worker|worker/i.test(message)) {\n return error instanceof Error ? error : new Error(message)\n }\n\n const { workerSrc } = getConfig().pdf\n const cause = workerSrc\n ? `pdf.js could not load its worker from \"${workerSrc}\".`\n : 'pdf.js has no worker configured.'\n\n return new Error(\n `${cause}\\n` +\n 'Copy it out of the package and point the config at it:\\n' +\n ' cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/\\n' +\n \" configure({ pdf: { workerSrc: '/pdf.worker.min.mjs' } })\\n\" +\n `Original error: ${message}`,\n { cause: error }\n )\n}\n\n/** Document-level options assembled from the global config. */\nexport function documentOptions(data: Uint8Array): Record<string, unknown> {\n const { cMapUrl, standardFontDataUrl, wasmUrl } = getConfig().pdf\n // pdf.js takes ownership of the buffer it is given and detaches it, so hand\n // over a copy: callers routinely reuse the row's `content` afterwards.\n const owned = new Uint8Array(data.byteLength)\n owned.set(data)\n\n const options: Record<string, unknown> = { data: owned }\n if (cMapUrl) {\n options.cMapUrl = cMapUrl\n options.cMapPacked = true\n }\n if (standardFontDataUrl) options.standardFontDataUrl = standardFontDataUrl\n if (wasmUrl) options.wasmUrl = wasmUrl\n return options\n}\n","/**\n * Port of `scaledp/pdf/PdfDataToImage.py`: a PDF into one `Image` row per page.\n *\n * Python renders with PyMuPDF at a DPI; pdf.js works in scale factors, so the\n * DPI converts through the PDF unit of 72 points per inch.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { createCanvas, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\nimport { toBytes } from '../stages/data-to-image.js'\nimport { describePdfError, documentOptions, loadPdfjs } from './pdfjs.js'\n\n/** PDF user space is defined in points; 72 of them make an inch. */\nexport const POINTS_PER_INCH = 72\n\nexport interface PdfToImageParams extends BaseStageParams {\n /** Render DPI. 300 matches ScaleDP's default and suits OCR. */\n resolution: number\n /** Maximum pages to render; 0 renders all of them. */\n pageLimit: number\n imageType: ImageFormat\n}\n\nexport const PDF_TO_IMAGE_DEFAULTS: PdfToImageParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'content',\n outputCol: 'image',\n resolution: 300,\n pageLimit: 0,\n imageType: 'png' as ImageFormat,\n})\n\nconst MIME: Record<ImageFormat, 'image/png' | 'image/webp' | 'image/jpeg'> = {\n png: 'image/png',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n}\n\nexport class PdfToImage extends Stage<PdfToImageParams> {\n readonly name = 'PdfToImage'\n\n constructor(options: Partial<PdfToImageParams> = {}) {\n super(\n resolveParams(PDF_TO_IMAGE_DEFAULTS, options, {\n resolution: (value) => {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`resolution must be positive, received ${value}`)\n }\n },\n pageLimit: (value) => {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`pageLimit must be a non-negative integer, received ${value}`)\n }\n },\n })\n )\n }\n\n /** One input PDF becomes N rows, each carrying its page index. */\n protected override async expand(input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { outputCol, pageCol, pathCol, resolution, pageLimit, imageType } = this.params\n const path = String(row[pathCol] ?? 'memory')\n\n const pdfjs = await loadPdfjs()\n const task = pdfjs.getDocument(documentOptions(toBytes(input)))\n\n try {\n // pdf.js defers worker setup, so a missing worker surfaces on first\n // page access rather than from task.promise. Wrap the whole\n // operation so the failure is described wherever it lands.\n const document = await task.promise\n const pageCount = pageLimit > 0 ? Math.min(pageLimit, document.numPages) : document.numPages\n const rows: Row[] = []\n\n for (let index = 0; index < pageCount; index++) {\n ctx.signal?.throwIfAborted()\n const image = await renderPage(document, index + 1, {\n resolution,\n imageType,\n path,\n })\n rows.push({ ...row, [pageCol]: index, [outputCol]: image })\n }\n return rows\n } catch (error) {\n throw describePdfError(error)\n } finally {\n // destroy() lives on the loading task, not the document proxy, and\n // is what releases the pdf.js worker's copy of the file.\n await task.destroy()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({ path: String(row[this.params.pathCol] ?? 'memory'), exception: message })\n }\n}\n\n/** Rasterise a single 1-based page to encoded image bytes. */\nexport async function renderPage(\n document: Awaited<ReturnType<typeof import('pdfjs-dist').getDocument>['promise']>,\n pageNumber: number,\n opts: { resolution: number; imageType: ImageFormat; path: string }\n): Promise<ScaleDpImage> {\n const page = await document.getPage(pageNumber)\n try {\n const viewport = page.getViewport({ scale: opts.resolution / POINTS_PER_INCH })\n const canvas = createCanvas(viewport.width, viewport.height)\n\n // pdf.js >= 5 takes `canvas`, not `canvasContext`. Passing the context\n // still works but is the documented legacy path.\n await page.render({\n canvas: canvas as unknown as HTMLCanvasElement,\n viewport,\n }).promise\n\n return createImage({\n path: opts.path,\n resolution: opts.resolution,\n data: await encodeImage(canvas, MIME[opts.imageType]),\n imageType: opts.imageType,\n width: canvas.width,\n height: canvas.height,\n })\n } finally {\n page.cleanup()\n }\n}\n","/**\n * Split a pdf.js text run into word-level boxes.\n *\n * pdf.js emits runs at line granularity (\"Client: Raja Raman\"), but detection\n * boxes and NER offsets both want words. Each word is measured with Canvas 2D\n * `measureText` using a font reconstructed from the pdf.js font name, then the\n * measured widths are scaled so they sum to the run's actual width -- the\n * substitute font is never metrically identical to the embedded one, so the\n * measurements are only useful as *proportions*.\n *\n * Walking along `readDirX`/`readDirY` rather than assuming left-to-right is what\n * makes rotated, bottom-to-top and right-to-left runs come out correctly.\n */\n\nimport { context2d, createCanvas } from '../core/image.js'\nimport type { Box } from '../schemas/box.js'\nimport type { TextBox } from './extract-text.js'\n\n/** Widen each word slightly so glyph overhang is not clipped. */\nconst WORD_PADDING_RATIO = 0.1\n\nlet measureCtx: OffscreenCanvasRenderingContext2D | null = null\n\nfunction measurementContext(): OffscreenCanvasRenderingContext2D | null {\n if (measureCtx) return measureCtx\n try {\n measureCtx = context2d(createCanvas(1, 1))\n return measureCtx\n } catch {\n // No canvas (e.g. a non-browser test run): fall back to glyph heuristics.\n return null\n }\n}\n\n/**\n * Rebuild a CSS font string from a pdf.js font name.\n *\n * Names look like `AAAAAA+Helvetica-BoldOblique`: a six-letter subset prefix,\n * then the real family and style suffixes.\n */\nexport function cssFontFromPdfName(fontName: string, size: number): string {\n const name = fontName.replace(/^[A-Z]{6}\\+/, '')\n const lower = name.toLowerCase()\n const weight = /bold|black|heavy|semibold/.test(lower) ? 'bold' : 'normal'\n const style = /italic|oblique/.test(lower) ? 'italic' : 'normal'\n const family = /serif|times|georgia|garamond|roman/.test(lower)\n ? 'serif'\n : /mono|courier|consol/.test(lower)\n ? 'monospace'\n : 'sans-serif'\n return `${style} ${weight} ${Math.max(1, Math.round(size))}px ${family}`\n}\n\n/**\n * Relative advance width per character, used when no canvas is available.\n * Buckets rather than real metrics -- enough to keep proportions sane.\n */\nexport function relativeCharWidth(char: string): number {\n if (\"iljI|.,:;'`!\".includes(char)) return 0.6\n if ('ftr()[]{}-'.includes(char)) return 0.8\n if ('MWmw@%'.includes(char)) return 1.6\n if (char === ' ') return 0.6\n if (char >= 'A' && char <= 'Z') return 1.3\n return 1.0\n}\n\nfunction measureWord(word: string, font: string): number {\n const ctx = measurementContext()\n if (ctx) {\n ctx.font = font\n return ctx.measureText(word).width\n }\n let total = 0\n for (const char of word) total += relativeCharWidth(char)\n return total\n}\n\n/**\n * Split one run into word boxes.\n *\n * Returns the run itself when it holds a single word, so the common case costs\n * nothing.\n */\nexport function splitRunIntoWords(run: TextBox): Box[] {\n const trimmed = run.text.trim()\n if (trimmed.length === 0) return []\n\n const tokens = trimmed.split(/(\\s+)/).filter((t) => t.length > 0)\n const words = tokens.filter((t) => !/^\\s+$/.test(t))\n if (words.length <= 1) {\n return [{ ...run, text: trimmed }]\n }\n\n const font = cssFontFromPdfName(run.fontName, run.height)\n const measured = tokens.map((token) => measureWord(token, font))\n const totalMeasured = measured.reduce((sum, w) => sum + w, 0) || 1\n\n // The substitute font's absolute metrics are meaningless; only the ratios\n // matter, so normalise them onto the run's real extent.\n const runLength = Math.hypot(run.width * run.readDirX, run.height * run.readDirY) || run.width\n const scale = runLength / totalMeasured\n\n // Walking starts at whichever corner the reading direction comes *from*, so\n // a right-to-left or bottom-to-top run starts at the opposite edge.\n let cursorX = run.readDirX >= 0 ? run.x : run.x + run.width\n let cursorY = run.readDirY >= 0 ? run.y : run.y + run.height\n\n const out: Box[] = []\n for (const [i, token] of tokens.entries()) {\n const advance = (measured[i] as number) * scale\n if (!/^\\s+$/.test(token)) {\n const pad = run.height * WORD_PADDING_RATIO\n const spanX = Math.abs(run.readDirX) > Math.abs(run.readDirY) ? advance : run.width\n const spanY = Math.abs(run.readDirY) > Math.abs(run.readDirX) ? advance : run.height\n\n const left = run.readDirX >= 0 ? cursorX : cursorX - spanX\n const top = run.readDirY >= 0 ? cursorY : cursorY - spanY\n\n out.push({\n text: token,\n score: run.score,\n x: Math.floor(left - pad),\n y: Math.floor(top - pad),\n width: Math.max(1, Math.ceil(spanX + pad * 2)),\n height: Math.max(1, Math.ceil(spanY + pad * 2)),\n angle: run.angle,\n })\n }\n cursorX += run.readDirX * advance\n cursorY += run.readDirY * advance\n }\n return out\n}\n\n/** Split every run on a page into word boxes. */\nexport function splitRunsIntoWords(runs: readonly TextBox[]): Box[] {\n return runs.flatMap(splitRunIntoWords)\n}\n\n/** Reset the cached measurement canvas. Tests only. */\nexport function resetMeasurementContext(): void {\n measureCtx = null\n}\n","/**\n * Port of `scaledp/pdf/PdfDataToText.py`: a PDF's embedded text layer into one\n * `Document` row per page, with word-level boxes.\n *\n * Coordinates are emitted in the same pixel space `PdfToImage` renders at, so\n * boxes from this stage and boxes from OCR are directly comparable. Python\n * leaves PdfDataToText in PDF points and scales only in PdfDataToDocument; a\n * single consistent space is more useful and avoids a class of silent mismatch.\n *\n * The output feeds the `bypassCol` optimisation: a page that already has a text\n * layer does not need OCR.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport { toBytes } from '../stages/data-to-image.js'\nimport { extractTextBoxes } from './extract-text.js'\nimport { POINTS_PER_INCH } from './pdf-to-image.js'\nimport { describePdfError, documentOptions, loadPdfjs } from './pdfjs.js'\nimport { splitRunsIntoWords } from './split-words.js'\n\nexport interface PdfToDocumentParams extends BaseStageParams {\n /** Pixel space the boxes are expressed in; match PdfToImage to align them. */\n resolution: number\n pageLimit: number\n /** Split pdf.js line runs into word boxes. Off yields run-level boxes. */\n splitWords: boolean\n}\n\nexport const PDF_TO_DOCUMENT_DEFAULTS: PdfToDocumentParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'content',\n outputCol: 'document',\n keepInputData: true,\n resolution: 300,\n pageLimit: 0,\n splitWords: true,\n})\n\nexport class PdfToDocument extends Stage<PdfToDocumentParams> {\n readonly name = 'PdfToDocument'\n\n constructor(options: Partial<PdfToDocumentParams> = {}) {\n super(resolveParams(PDF_TO_DOCUMENT_DEFAULTS, options))\n }\n\n protected override async expand(input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { outputCol, pageCol, pathCol, resolution, pageLimit, splitWords } = this.params\n const path = String(row[pathCol] ?? 'memory')\n\n const pdfjs = await loadPdfjs()\n const task = pdfjs.getDocument(documentOptions(toBytes(input)))\n\n try {\n // pdf.js defers worker setup, so a missing worker surfaces on first\n // page access rather than from task.promise.\n const pdf = await task.promise\n const pageCount = pageLimit > 0 ? Math.min(pageLimit, pdf.numPages) : pdf.numPages\n const rows: Row[] = []\n\n for (let index = 0; index < pageCount; index++) {\n ctx.signal?.throwIfAborted()\n const page = await pdf.getPage(index + 1)\n try {\n const viewport = page.getViewport({ scale: resolution / POINTS_PER_INCH })\n const runs = await extractTextBoxes(page, viewport)\n const bboxes = splitWords ? splitRunsIntoWords(runs) : runs\n\n rows.push({\n ...row,\n [pageCol]: index,\n [outputCol]: createDocument({\n path,\n type: 'pdf',\n text: runs.map((r) => r.text).join('\\n'),\n bboxes,\n }),\n })\n } finally {\n page.cleanup()\n }\n }\n return rows\n } catch (error) {\n throw describePdfError(error)\n } finally {\n await task.destroy()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'pdf',\n exception: message,\n })\n }\n}\n\n/** True when a page's text layer is substantive enough to skip OCR. */\nexport function hasUsableTextLayer(document: Document, minimumBoxes = 1): boolean {\n return document.exception === '' && document.bboxes.length >= minimumBoxes\n}\n"],"mappings":";;;;;;;AA+BA,MAAM,eAAe;;AAGrB,MAAa,mBAAmB;AAchC,SAAgB,WAAW,MAAqC;CAC5D,OACI,OAAO,SAAS,YAChB,SAAS,QACT,SAAS,QACT,eAAe,QACf,MAAM,QAAS,KAAsB,SAAS;AAEtD;;AAGA,SAAgB,cAAc,MAAoB,UAAwC;CACtF,IAAI,KAAK,IAAI,WAAW,GAAG,OAAO;CAElC,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,KAAK;CAIxD,MAAM,QAAQ,KAAK,MAAM,GAAG,CAAC,KAAK;CAClC,MAAM,QAAQ,KAAK,MAAM,GAAG,CAAC,KAAK;CAClC,MAAM,OAAO,IAAI;CACjB,MAAM,OAAO,IAAI;CACjB,MAAM,MAAM,IAAI;CAChB,MAAM,MAAM,IAAI;CAGhB,MAAM,SAAS,KAAK,SAAS;CAC7B,MAAM,SAAS,IAAI,MAAM;CACzB,MAAM,SAAS,IAAI,MAAM;CAEzB,MAAM,UAAU;EACZ,SAAS,uBAAuB,QAAQ,MAAM;EAC9C,SAAS,uBAAuB,SAAS,OAAO,KAAK,OAAO,SAAS,OAAO,KAAK,KAAK;EACtF,SAAS,uBAAuB,SAAS,MAAM,KAAK,QAAQ,SAAS,MAAM,KAAK,MAAM;EACtF,SAAS,uBACL,SAAS,OAAO,KAAK,QAAQ,MAAM,KAAK,QACxC,SAAS,OAAO,KAAK,QAAQ,MAAM,KAAK,MAC5C;CACJ;CAEA,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,EAAY;CAC5C,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,EAAY;CAC5C,MAAM,IAAI,KAAK,IAAI,GAAG,EAAE;CACxB,MAAM,IAAI,KAAK,IAAI,GAAG,EAAE;CAIxB,MAAM,CAAC,eAAe,GAAG,eAAe,KAAK,QAAQ,MAAM,CAAC;CAC5D,MAAM,CAAC,aAAa,GAAG,aAAa,KAAK,QAAQ,MAAM,CAAC;CACxD,MAAM,UAAU,KAAK,MAAM,aAAa,cAAc,aAAa,YAAY,KAAK;CAEpF,OAAO;EACH,MAAM,KAAK;EACX,OAAO;EACP,GAAG,KAAK,MAAM,CAAC;EACf,GAAG,KAAK,MAAM,CAAC;EACf,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,IAAI,GAAG,EAAE,IAAI,CAAC,CAAC;EACjD,QAAQ,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,IAAI,GAAG,EAAE,IAAI,CAAC,CAAC;EAClD,OAAO;EACP,WAAW,aAAa,gBAAgB;EACxC,WAAW,aAAa,gBAAgB;EACxC,UAAU,KAAK,YAAY;CAC/B;AACJ;;AAGA,eAAsB,iBAClB,MACA,UACkB;CAClB,MAAM,UAAU,MAAM,KAAK,eAAe;CAC1C,MAAM,QAAmB,CAAC;CAC1B,KAAK,MAAM,QAAQ,QAAQ,OAAO;EAC9B,IAAI,CAAC,WAAW,IAAI,GAAG;EACvB,MAAM,MAAM,cAAc,MAAM,QAAQ;EACxC,IAAI,KAAK,MAAM,KAAK,GAAG;CAC3B;CACA,OAAO;AACX;;;;;;;;;;;ACjHA,IAAI,gBAA6C;AAEjD,eAAsB,YAAkC;CACpD,IAAI,eAAe,OAAO;CAE1B,iBAAiB,YAAY;EACzB,IAAI;EACJ,IAAI;GACA,QAAQ,MAAM,OAAO;EACzB,SAAS,OAAO;GACZ,MAAM,IAAI,MAAM,wEAAwE,EAAE,MAAM,CAAC;EACrG;EAEA,MAAM,EAAE,cAAc,UAAU,CAAC,CAAC;EAClC,IAAI,WAAW,MAAM,oBAAoB,YAAY;EACrD,OAAO;CACX,EAAA,CAAG;CAEH,OAAO;AACX;;AAGA,SAAgB,aAAmB;CAC/B,gBAAgB;AACpB;;;;;;;;;AAUA,SAAgB,iBAAiB,OAAuB;CACpD,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACrE,IAAI,CAAC,sBAAsB,KAAK,OAAO,GACnC,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO;CAG7D,MAAM,EAAE,cAAc,UAAU,CAAC,CAAC;CAClC,MAAM,QAAQ,YACR,0CAA0C,UAAU,MACpD;CAEN,OAAO,IAAI,MACP,GAAG,MAAM;;;kBAIc,WACvB,EAAE,OAAO,MAAM,CACnB;AACJ;;AAGA,SAAgB,gBAAgB,MAA2C;CACvE,MAAM,EAAE,SAAS,qBAAqB,YAAY,UAAU,CAAC,CAAC;CAG9D,MAAM,QAAQ,IAAI,WAAW,KAAK,UAAU;CAC5C,MAAM,IAAI,IAAI;CAEd,MAAM,UAAmC,EAAE,MAAM,MAAM;CACvD,IAAI,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ,aAAa;CACzB;CACA,IAAI,qBAAqB,QAAQ,sBAAsB;CACvD,IAAI,SAAS,QAAQ,UAAU;CAC/B,OAAO;AACX;;;;;;;;;;ACpEA,MAAa,kBAAkB;AAU/B,MAAa,wBAA0C,OAAO,OAAO;CACjE,GAAG;CACH,UAAU;CACV,WAAW;CACX,YAAY;CACZ,WAAW;CACX,WAAW;AACf,CAAC;AAED,MAAM,OAAuE;CACzE,KAAK;CACL,MAAM;CACN,MAAM;AACV;AAEA,IAAa,aAAb,cAAgC,MAAwB;CACpD,OAAgB;CAEhB,YAAY,UAAqC,CAAC,GAAG;EACjD,MACI,cAAc,uBAAuB,SAAS;GAC1C,aAAa,UAAU;IACnB,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GACpC,MAAM,IAAI,WAAW,yCAAyC,OAAO;GAE7E;GACA,YAAY,UAAU;IAClB,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GACpC,MAAM,IAAI,WAAW,sDAAsD,OAAO;GAE1F;EACJ,CAAC,CACL;CACJ;;CAGA,MAAyB,OAAO,OAAgB,KAAU,KAAmC;EACzF,MAAM,EAAE,WAAW,SAAS,SAAS,YAAY,WAAW,cAAc,KAAK;EAC/E,MAAM,OAAO,OAAO,IAAI,YAAY,QAAQ;EAG5C,MAAM,QAAO,MADO,UAAU,EAAA,CACX,YAAY,gBAAgB,QAAQ,KAAK,CAAC,CAAC;EAE9D,IAAI;GAIA,MAAM,WAAW,MAAM,KAAK;GAC5B,MAAM,YAAY,YAAY,IAAI,KAAK,IAAI,WAAW,SAAS,QAAQ,IAAI,SAAS;GACpF,MAAM,OAAc,CAAC;GAErB,KAAK,IAAI,QAAQ,GAAG,QAAQ,WAAW,SAAS;IAC5C,IAAI,QAAQ,eAAe;IAC3B,MAAM,QAAQ,MAAM,WAAW,UAAU,QAAQ,GAAG;KAChD;KACA;KACA;IACJ,CAAC;IACD,KAAK,KAAK;KAAE,GAAG;MAAM,UAAU;MAAQ,YAAY;IAAM,CAAC;GAC9D;GACA,OAAO;EACX,SAAS,OAAO;GACZ,MAAM,iBAAiB,KAAK;EAChC,UAAU;GAGN,MAAM,KAAK,QAAQ;EACvB;CACJ;CAEA,MAAgB,QAAwB;EACpC,MAAM,IAAI,WAAW,yCAAyC,KAAK,IAAI;CAC3E;CAEA,QAAkB,SAAiB,KAAwB;EACvD,OAAO,YAAY;GAAE,MAAM,OAAO,IAAI,KAAK,OAAO,YAAY,QAAQ;GAAG,WAAW;EAAQ,CAAC;CACjG;AACJ;;AAGA,eAAsB,WAClB,UACA,YACA,MACqB;CACrB,MAAM,OAAO,MAAM,SAAS,QAAQ,UAAU;CAC9C,IAAI;EACA,MAAM,WAAW,KAAK,YAAY,EAAE,OAAO,KAAK,aAAA,GAA6B,CAAC;EAC9E,MAAM,SAAS,aAAa,SAAS,OAAO,SAAS,MAAM;EAI3D,MAAM,KAAK,OAAO;GACN;GACR;EACJ,CAAC,CAAC,CAAC;EAEH,OAAO,YAAY;GACf,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK,UAAU;GACpD,WAAW,KAAK;GAChB,OAAO,OAAO;GACd,QAAQ,OAAO;EACnB,CAAC;CACL,UAAU;EACN,KAAK,QAAQ;CACjB;AACJ;;;;;;;;;;;;;;;;;ACnHA,MAAM,qBAAqB;AAE3B,IAAI,aAAuD;AAE3D,SAAS,qBAA+D;CACpE,IAAI,YAAY,OAAO;CACvB,IAAI;EACA,aAAa,UAAU,aAAa,GAAG,CAAC,CAAC;EACzC,OAAO;CACX,QAAQ;EAEJ,OAAO;CACX;AACJ;;;;;;;AAQA,SAAgB,mBAAmB,UAAkB,MAAsB;CAEvE,MAAM,QADO,SAAS,QAAQ,eAAe,EAC5B,CAAC,CAAC,YAAY;CAC/B,MAAM,SAAS,4BAA4B,KAAK,KAAK,IAAI,SAAS;CAClE,MAAM,QAAQ,iBAAiB,KAAK,KAAK,IAAI,WAAW;CACxD,MAAM,SAAS,qCAAqC,KAAK,KAAK,IACxD,UACA,sBAAsB,KAAK,KAAK,IAC9B,cACA;CACR,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,EAAE,KAAK;AACpE;;;;;AAMA,SAAgB,kBAAkB,MAAsB;CACpD,IAAI,eAAe,SAAS,IAAI,GAAG,OAAO;CAC1C,IAAI,aAAa,SAAS,IAAI,GAAG,OAAO;CACxC,IAAI,SAAS,SAAS,IAAI,GAAG,OAAO;CACpC,IAAI,SAAS,KAAK,OAAO;CACzB,IAAI,QAAQ,OAAO,QAAQ,KAAK,OAAO;CACvC,OAAO;AACX;AAEA,SAAS,YAAY,MAAc,MAAsB;CACrD,MAAM,MAAM,mBAAmB;CAC/B,IAAI,KAAK;EACL,IAAI,OAAO;EACX,OAAO,IAAI,YAAY,IAAI,CAAC,CAAC;CACjC;CACA,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,MAAM,SAAS,kBAAkB,IAAI;CACxD,OAAO;AACX;;;;;;;AAQA,SAAgB,kBAAkB,KAAqB;CACnD,MAAM,UAAU,IAAI,KAAK,KAAK;CAC9B,IAAI,QAAQ,WAAW,GAAG,OAAO,CAAC;CAElC,MAAM,SAAS,QAAQ,MAAM,OAAO,CAAC,CAAC,QAAQ,MAAM,EAAE,SAAS,CAAC;CAEhE,IADc,OAAO,QAAQ,MAAM,CAAC,QAAQ,KAAK,CAAC,CAC1C,CAAC,CAAC,UAAU,GAChB,OAAO,CAAC;EAAE,GAAG;EAAK,MAAM;CAAQ,CAAC;CAGrC,MAAM,OAAO,mBAAmB,IAAI,UAAU,IAAI,MAAM;CACxD,MAAM,WAAW,OAAO,KAAK,UAAU,YAAY,OAAO,IAAI,CAAC;CAC/D,MAAM,gBAAgB,SAAS,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC,KAAK;CAKjE,MAAM,SADY,KAAK,MAAM,IAAI,QAAQ,IAAI,UAAU,IAAI,SAAS,IAAI,QAAQ,KAAK,IAAI,SAC/D;CAI1B,IAAI,UAAU,IAAI,YAAY,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;CACtD,IAAI,UAAU,IAAI,YAAY,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;CAEtD,MAAM,MAAa,CAAC;CACpB,KAAK,MAAM,CAAC,GAAG,UAAU,OAAO,QAAQ,GAAG;EACvC,MAAM,UAAW,SAAS,KAAgB;EAC1C,IAAI,CAAC,QAAQ,KAAK,KAAK,GAAG;GACtB,MAAM,MAAM,IAAI,SAAS;GACzB,MAAM,QAAQ,KAAK,IAAI,IAAI,QAAQ,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI,UAAU,IAAI;GAC9E,MAAM,QAAQ,KAAK,IAAI,IAAI,QAAQ,IAAI,KAAK,IAAI,IAAI,QAAQ,IAAI,UAAU,IAAI;GAE9E,MAAM,OAAO,IAAI,YAAY,IAAI,UAAU,UAAU;GACrD,MAAM,MAAM,IAAI,YAAY,IAAI,UAAU,UAAU;GAEpD,IAAI,KAAK;IACL,MAAM;IACN,OAAO,IAAI;IACX,GAAG,KAAK,MAAM,OAAO,GAAG;IACxB,GAAG,KAAK,MAAM,MAAM,GAAG;IACvB,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,MAAM,CAAC,CAAC;IAC7C,QAAQ,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,MAAM,CAAC,CAAC;IAC9C,OAAO,IAAI;GACf,CAAC;EACL;EACA,WAAW,IAAI,WAAW;EAC1B,WAAW,IAAI,WAAW;CAC9B;CACA,OAAO;AACX;;AAGA,SAAgB,mBAAmB,MAAiC;CAChE,OAAO,KAAK,QAAQ,iBAAiB;AACzC;;AAGA,SAAgB,0BAAgC;CAC5C,aAAa;AACjB;;;;;;;;;;;;;;;AC/GA,MAAa,2BAAgD,OAAO,OAAO;CACvE,GAAG;CACH,UAAU;CACV,WAAW;CACX,eAAe;CACf,YAAY;CACZ,WAAW;CACX,YAAY;AAChB,CAAC;AAED,IAAa,gBAAb,cAAmC,MAA2B;CAC1D,OAAgB;CAEhB,YAAY,UAAwC,CAAC,GAAG;EACpD,MAAM,cAAc,0BAA0B,OAAO,CAAC;CAC1D;CAEA,MAAyB,OAAO,OAAgB,KAAU,KAAmC;EACzF,MAAM,EAAE,WAAW,SAAS,SAAS,YAAY,WAAW,eAAe,KAAK;EAChF,MAAM,OAAO,OAAO,IAAI,YAAY,QAAQ;EAG5C,MAAM,QAAO,MADO,UAAU,EAAA,CACX,YAAY,gBAAgB,QAAQ,KAAK,CAAC,CAAC;EAE9D,IAAI;GAGA,MAAM,MAAM,MAAM,KAAK;GACvB,MAAM,YAAY,YAAY,IAAI,KAAK,IAAI,WAAW,IAAI,QAAQ,IAAI,IAAI;GAC1E,MAAM,OAAc,CAAC;GAErB,KAAK,IAAI,QAAQ,GAAG,QAAQ,WAAW,SAAS;IAC5C,IAAI,QAAQ,eAAe;IAC3B,MAAM,OAAO,MAAM,IAAI,QAAQ,QAAQ,CAAC;IACxC,IAAI;KAEA,MAAM,OAAO,MAAM,iBAAiB,MADnB,KAAK,YAAY,EAAE,OAAO,aAAA,GAA6B,CAC9B,CAAQ;KAClD,MAAM,SAAS,aAAa,mBAAmB,IAAI,IAAI;KAEvD,KAAK,KAAK;MACN,GAAG;OACF,UAAU;OACV,YAAY,eAAe;OACxB;OACA,MAAM;OACN,MAAM,KAAK,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI;OACvC;MACJ,CAAC;KACL,CAAC;IACL,UAAU;KACN,KAAK,QAAQ;IACjB;GACJ;GACA,OAAO;EACX,SAAS,OAAO;GACZ,MAAM,iBAAiB,KAAK;EAChC,UAAU;GACN,MAAM,KAAK,QAAQ;EACvB;CACJ;CAEA,MAAgB,QAAwB;EACpC,MAAM,IAAI,WAAW,yCAAyC,KAAK,IAAI;CAC3E;CAEA,QAAkB,SAAiB,KAAoB;EACnD,OAAO,eAAe;GAClB,MAAM,OAAO,IAAI,KAAK,OAAO,YAAY,QAAQ;GACjD,MAAM;GACN,WAAW;EACf,CAAC;CACL;AACJ;;AAGA,SAAgB,mBAAmB,UAAoB,eAAe,GAAY;CAC9E,OAAO,SAAS,cAAc,MAAM,SAAS,OAAO,UAAU;AAClE"}
|
|
1
|
+
{"version":3,"file":"pdf-qbdnOnqZ.js","names":[],"sources":["../src/pdf/extract-text.ts","../src/pdf/pdfjs.ts","../src/pdf/pdf-to-image.ts","../src/pdf/split-words.ts","../src/pdf/pdf-to-document.ts"],"sourcesContent":["/**\n * Word-level text extraction from a PDF's embedded text layer.\n *\n * Ported from the pdftools prototype's `extractTextFromPage`, whose rotation\n * handling is the hard-won part and is reproduced here with its reasoning.\n *\n * A pdf.js text item carries a full transform matrix [a, b, c, d, e, f]. Naively\n * reading `a` as a width scale breaks on rotated text, so the reading direction\n * and the \"up\" direction are recovered as *unit* vectors -- otherwise the font\n * size gets counted twice. The four corners are then pushed through\n * `convertToViewportPoint` and reduced to an axis-aligned box.\n *\n * No semantic `angle` is emitted, deliberately. A rotated glyph matrix is\n * indistinguishable from page-level /Rotate compensation or an embedded\n * FontMatrix, so composing the corners geometrically and taking their bounding\n * box is correct regardless of which caused it. `readDirX`/`readDirY` carry the\n * direction that word-splitting needs.\n */\n\nimport type { Box } from '../schemas/box.js'\n\n/** A box plus the reading direction, which splitting needs but ScaleDP's Box lacks. */\nexport interface TextBox extends Box {\n /** Unit vector, in viewport space, along which reading progresses. */\n readDirX: number\n readDirY: number\n /** pdf.js font identifier, e.g. 'g_d0_f1'. */\n fontName: string\n}\n\n/** Glyphs sit above the baseline by roughly three quarters of the line height. */\nconst ASCENT_RATIO = 0.75\n\n/** Confidence assigned to text read from a PDF's own text layer. */\nexport const TEXT_LAYER_SCORE = 0.99\n\ninterface TextItemLike {\n str: string\n transform: number[]\n width: number\n height: number\n fontName?: string\n}\n\ninterface ViewportLike {\n convertToViewportPoint(x: number, y: number): number[]\n}\n\nexport function isTextItem(item: unknown): item is TextItemLike {\n return (\n typeof item === 'object' &&\n item !== null &&\n 'str' in item &&\n 'transform' in item &&\n Array.isArray((item as TextItemLike).transform)\n )\n}\n\n/** Convert one pdf.js text item into a viewport-space box. */\nexport function textItemToBox(item: TextItemLike, viewport: ViewportLike): TextBox | null {\n if (item.str.length === 0) return null\n\n const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = item.transform\n\n // Unit direction vectors. Dividing out the magnitudes is what stops the\n // font size being applied twice, since item.width/height already include it.\n const abMag = Math.hypot(a, b) || 1\n const cdMag = Math.hypot(c, d) || 1\n const dirX = a / abMag\n const dirY = b / abMag\n const upX = c / cdMag\n const upY = d / cdMag\n\n // The transform's origin is the baseline; shift up to the glyph tops.\n const ascent = item.height * ASCENT_RATIO\n const startX = e + upX * ascent\n const startY = f + upY * ascent\n\n const corners = [\n viewport.convertToViewportPoint(startX, startY),\n viewport.convertToViewportPoint(startX + dirX * item.width, startY + dirY * item.width),\n viewport.convertToViewportPoint(startX - upX * item.height, startY - upY * item.height),\n viewport.convertToViewportPoint(\n startX + dirX * item.width - upX * item.height,\n startY + dirY * item.width - upY * item.height\n ),\n ]\n\n const xs = corners.map((p) => p[0] as number)\n const ys = corners.map((p) => p[1] as number)\n const x = Math.min(...xs)\n const y = Math.min(...ys)\n\n // Reading direction in viewport space, taken from the start and end points\n // rather than from the matrix, so the viewport's own flip is accounted for.\n const [startScreenX = 0, startScreenY = 0] = corners[0] ?? []\n const [endScreenX = 0, endScreenY = 0] = corners[1] ?? []\n const readMag = Math.hypot(endScreenX - startScreenX, endScreenY - startScreenY) || 1\n\n return {\n text: item.str,\n score: TEXT_LAYER_SCORE,\n x: Math.floor(x),\n y: Math.floor(y),\n width: Math.max(1, Math.ceil(Math.max(...xs) - x)),\n height: Math.max(1, Math.ceil(Math.max(...ys) - y)),\n angle: 0,\n readDirX: (endScreenX - startScreenX) / readMag,\n readDirY: (endScreenY - startScreenY) / readMag,\n fontName: item.fontName ?? '',\n }\n}\n\n/** Extract every text item on a page as a viewport-space box. */\nexport async function extractTextBoxes(\n page: { getTextContent(): Promise<{ items: unknown[] }> },\n viewport: ViewportLike\n): Promise<TextBox[]> {\n const content = await page.getTextContent()\n const boxes: TextBox[] = []\n for (const item of content.items) {\n if (!isTextItem(item)) continue\n const box = textItemToBox(item, viewport)\n if (box) boxes.push(box)\n }\n return boxes\n}\n","/**\n * Lazy pdf.js loader.\n *\n * pdfjs-dist is an optional peer dependency, so it is imported only when a PDF\n * stage actually runs. Every asset path comes from `configure()` -- unlike the\n * pdftools prototype, which hardcoded `/pdf.worker.min.mjs`, a path only its\n * own Next app could serve.\n */\n\nimport { getConfig } from '../core/config.js'\n\ntype PdfjsModule = typeof import('pdfjs-dist')\n\nlet modulePromise: Promise<PdfjsModule> | null = null\n\nexport async function loadPdfjs(): Promise<PdfjsModule> {\n if (modulePromise) return modulePromise\n\n modulePromise = (async () => {\n let pdfjs: PdfjsModule\n try {\n pdfjs = await import('pdfjs-dist')\n } catch (cause) {\n throw new Error('pdfjs-dist is required for PDF support. Install it: npm i pdfjs-dist', { cause })\n }\n\n const { workerSrc } = getConfig().pdf\n if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc\n return pdfjs\n })()\n\n return modulePromise\n}\n\n/** Reset the cached module. Tests only. */\nexport function resetPdfjs(): void {\n modulePromise = null\n}\n\n/**\n * Turn pdf.js's worker-setup failure into something actionable.\n *\n * When `workerSrc` is unset or 404s, pdf.js reports \"Setting up fake worker\n * failed\" with a bare module URL, which says nothing about what to do. The\n * worker is not bundled with this library on purpose -- it has to be served by\n * the consuming application -- so the fix is always the same two steps.\n */\nexport function describePdfError(error: unknown): Error {\n const message = error instanceof Error ? error.message : String(error)\n if (!/fake worker|worker/i.test(message)) {\n return error instanceof Error ? error : new Error(message)\n }\n\n const { workerSrc } = getConfig().pdf\n const cause = workerSrc\n ? `pdf.js could not load its worker from \"${workerSrc}\".`\n : 'pdf.js has no worker configured.'\n\n return new Error(\n `${cause}\\n` +\n 'Copy it out of the package and point the config at it:\\n' +\n ' cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/\\n' +\n \" configure({ pdf: { workerSrc: '/pdf.worker.min.mjs' } })\\n\" +\n `Original error: ${message}`,\n { cause: error }\n )\n}\n\n/** Document-level options assembled from the global config. */\nexport function documentOptions(data: Uint8Array): Record<string, unknown> {\n const { cMapUrl, standardFontDataUrl, wasmUrl } = getConfig().pdf\n // pdf.js takes ownership of the buffer it is given and detaches it, so hand\n // over a copy: callers routinely reuse the row's `content` afterwards.\n const owned = new Uint8Array(data.byteLength)\n owned.set(data)\n\n const options: Record<string, unknown> = { data: owned }\n if (cMapUrl) {\n options.cMapUrl = cMapUrl\n options.cMapPacked = true\n }\n if (standardFontDataUrl) options.standardFontDataUrl = standardFontDataUrl\n if (wasmUrl) options.wasmUrl = wasmUrl\n return options\n}\n","/**\n * Port of `scaledp/pdf/PdfDataToImage.py`: a PDF into one `Image` row per page.\n *\n * Python renders with PyMuPDF at a DPI; pdf.js works in scale factors, so the\n * DPI converts through the PDF unit of 72 points per inch.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { createCanvas, encodeImage } from '../core/image.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { createImage, type ImageFormat, type ScaleDpImage } from '../schemas/image.js'\nimport { toBytes } from '../stages/data-to-image.js'\nimport { describePdfError, documentOptions, loadPdfjs } from './pdfjs.js'\n\n/** PDF user space is defined in points; 72 of them make an inch. */\nexport const POINTS_PER_INCH = 72\n\nexport interface PdfToImageParams extends BaseStageParams {\n /** Render DPI. 300 matches ScaleDP's default and suits OCR. */\n resolution: number\n /** Maximum pages to render; 0 renders all of them. */\n pageLimit: number\n imageType: ImageFormat\n}\n\nexport const PDF_TO_IMAGE_DEFAULTS: PdfToImageParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'content',\n outputCol: 'image',\n resolution: 300,\n pageLimit: 0,\n imageType: 'png' as ImageFormat,\n})\n\nconst MIME: Record<ImageFormat, 'image/png' | 'image/webp' | 'image/jpeg'> = {\n png: 'image/png',\n webp: 'image/webp',\n jpeg: 'image/jpeg',\n}\n\nexport class PdfToImage extends Stage<PdfToImageParams> {\n readonly name = 'PdfToImage'\n\n constructor(options: Partial<PdfToImageParams> = {}) {\n super(\n resolveParams(PDF_TO_IMAGE_DEFAULTS, options, {\n resolution: (value) => {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`resolution must be positive, received ${value}`)\n }\n },\n pageLimit: (value) => {\n if (!Number.isInteger(value) || value < 0) {\n throw new RangeError(`pageLimit must be a non-negative integer, received ${value}`)\n }\n },\n })\n )\n }\n\n /** One input PDF becomes N rows, each carrying its page index. */\n protected override async expand(input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { outputCol, pageCol, pathCol, resolution, pageLimit, imageType } = this.params\n const path = String(row[pathCol] ?? 'memory')\n\n const pdfjs = await loadPdfjs()\n const task = pdfjs.getDocument(documentOptions(toBytes(input)))\n\n try {\n // pdf.js defers worker setup, so a missing worker surfaces on first\n // page access rather than from task.promise. Wrap the whole\n // operation so the failure is described wherever it lands.\n const document = await task.promise\n const pageCount = pageLimit > 0 ? Math.min(pageLimit, document.numPages) : document.numPages\n const rows: Row[] = []\n\n for (let index = 0; index < pageCount; index++) {\n ctx.signal?.throwIfAborted()\n const image = await renderPage(document, index + 1, {\n resolution,\n imageType,\n path,\n })\n rows.push({ ...row, [pageCol]: index, [outputCol]: image })\n }\n return rows\n } catch (error) {\n throw describePdfError(error)\n } finally {\n // destroy() lives on the loading task, not the document proxy, and\n // is what releases the pdf.js worker's copy of the file.\n await task.destroy()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): ScaleDpImage {\n return createImage({ path: String(row[this.params.pathCol] ?? 'memory'), exception: message })\n }\n}\n\n/** Rasterise a single 1-based page to encoded image bytes. */\nexport async function renderPage(\n document: Awaited<ReturnType<typeof import('pdfjs-dist').getDocument>['promise']>,\n pageNumber: number,\n opts: { resolution: number; imageType: ImageFormat; path: string }\n): Promise<ScaleDpImage> {\n const page = await document.getPage(pageNumber)\n try {\n const viewport = page.getViewport({ scale: opts.resolution / POINTS_PER_INCH })\n const canvas = createCanvas(viewport.width, viewport.height)\n\n // pdf.js >= 5 takes `canvas`, not `canvasContext`. Passing the context\n // still works but is the documented legacy path.\n await page.render({\n canvas: canvas as unknown as HTMLCanvasElement,\n viewport,\n }).promise\n\n return createImage({\n path: opts.path,\n resolution: opts.resolution,\n data: await encodeImage(canvas, MIME[opts.imageType]),\n imageType: opts.imageType,\n width: canvas.width,\n height: canvas.height,\n })\n } finally {\n page.cleanup()\n }\n}\n","/**\n * Split a pdf.js text run into word-level boxes.\n *\n * pdf.js emits runs at line granularity (\"Client: Raja Raman\"), but detection\n * boxes and NER offsets both want words. Each word is measured with Canvas 2D\n * `measureText` using a font reconstructed from the pdf.js font name, then the\n * measured widths are scaled so they sum to the run's actual width -- the\n * substitute font is never metrically identical to the embedded one, so the\n * measurements are only useful as *proportions*.\n *\n * Walking along `readDirX`/`readDirY` rather than assuming left-to-right is what\n * makes rotated, bottom-to-top and right-to-left runs come out correctly.\n */\n\nimport { context2d, createCanvas } from '../core/image.js'\nimport type { Box } from '../schemas/box.js'\nimport type { TextBox } from './extract-text.js'\n\n/** Widen each word slightly so glyph overhang is not clipped. */\nconst WORD_PADDING_RATIO = 0.1\n\nlet measureCtx: OffscreenCanvasRenderingContext2D | null = null\n\nfunction measurementContext(): OffscreenCanvasRenderingContext2D | null {\n if (measureCtx) return measureCtx\n try {\n measureCtx = context2d(createCanvas(1, 1))\n return measureCtx\n } catch {\n // No canvas (e.g. a non-browser test run): fall back to glyph heuristics.\n return null\n }\n}\n\n/**\n * Rebuild a CSS font string from a pdf.js font name.\n *\n * Names look like `AAAAAA+Helvetica-BoldOblique`: a six-letter subset prefix,\n * then the real family and style suffixes.\n */\nexport function cssFontFromPdfName(fontName: string, size: number): string {\n const name = fontName.replace(/^[A-Z]{6}\\+/, '')\n const lower = name.toLowerCase()\n const weight = /bold|black|heavy|semibold/.test(lower) ? 'bold' : 'normal'\n const style = /italic|oblique/.test(lower) ? 'italic' : 'normal'\n const family = /serif|times|georgia|garamond|roman/.test(lower)\n ? 'serif'\n : /mono|courier|consol/.test(lower)\n ? 'monospace'\n : 'sans-serif'\n return `${style} ${weight} ${Math.max(1, Math.round(size))}px ${family}`\n}\n\n/**\n * Relative advance width per character, used when no canvas is available.\n * Buckets rather than real metrics -- enough to keep proportions sane.\n */\nexport function relativeCharWidth(char: string): number {\n if (\"iljI|.,:;'`!\".includes(char)) return 0.6\n if ('ftr()[]{}-'.includes(char)) return 0.8\n if ('MWmw@%'.includes(char)) return 1.6\n if (char === ' ') return 0.6\n if (char >= 'A' && char <= 'Z') return 1.3\n return 1.0\n}\n\nfunction measureWord(word: string, font: string): number {\n const ctx = measurementContext()\n if (ctx) {\n ctx.font = font\n return ctx.measureText(word).width\n }\n let total = 0\n for (const char of word) total += relativeCharWidth(char)\n return total\n}\n\n/**\n * Split one run into word boxes.\n *\n * Returns the run itself when it holds a single word, so the common case costs\n * nothing.\n */\nexport function splitRunIntoWords(run: TextBox): Box[] {\n const trimmed = run.text.trim()\n if (trimmed.length === 0) return []\n\n const tokens = trimmed.split(/(\\s+)/).filter((t) => t.length > 0)\n const words = tokens.filter((t) => !/^\\s+$/.test(t))\n if (words.length <= 1) {\n return [{ ...run, text: trimmed }]\n }\n\n const font = cssFontFromPdfName(run.fontName, run.height)\n const measured = tokens.map((token) => measureWord(token, font))\n const totalMeasured = measured.reduce((sum, w) => sum + w, 0) || 1\n\n // The substitute font's absolute metrics are meaningless; only the ratios\n // matter, so normalise them onto the run's real extent.\n const runLength = Math.hypot(run.width * run.readDirX, run.height * run.readDirY) || run.width\n const scale = runLength / totalMeasured\n\n // Walking starts at whichever corner the reading direction comes *from*, so\n // a right-to-left or bottom-to-top run starts at the opposite edge.\n let cursorX = run.readDirX >= 0 ? run.x : run.x + run.width\n let cursorY = run.readDirY >= 0 ? run.y : run.y + run.height\n\n const out: Box[] = []\n for (const [i, token] of tokens.entries()) {\n const advance = (measured[i] as number) * scale\n if (!/^\\s+$/.test(token)) {\n const pad = run.height * WORD_PADDING_RATIO\n const spanX = Math.abs(run.readDirX) > Math.abs(run.readDirY) ? advance : run.width\n const spanY = Math.abs(run.readDirY) > Math.abs(run.readDirX) ? advance : run.height\n\n const left = run.readDirX >= 0 ? cursorX : cursorX - spanX\n const top = run.readDirY >= 0 ? cursorY : cursorY - spanY\n\n out.push({\n text: token,\n score: run.score,\n x: Math.floor(left - pad),\n y: Math.floor(top - pad),\n width: Math.max(1, Math.ceil(spanX + pad * 2)),\n height: Math.max(1, Math.ceil(spanY + pad * 2)),\n angle: run.angle,\n })\n }\n cursorX += run.readDirX * advance\n cursorY += run.readDirY * advance\n }\n return out\n}\n\n/** Split every run on a page into word boxes. */\nexport function splitRunsIntoWords(runs: readonly TextBox[]): Box[] {\n return runs.flatMap(splitRunIntoWords)\n}\n\n/** Reset the cached measurement canvas. Tests only. */\nexport function resetMeasurementContext(): void {\n measureCtx = null\n}\n","/**\n * Port of `scaledp/pdf/PdfDataToText.py`: a PDF's embedded text layer into one\n * `Document` row per page, with word-level boxes.\n *\n * Coordinates are emitted in the same pixel space `PdfToImage` renders at, so\n * boxes from this stage and boxes from OCR are directly comparable. Python\n * leaves PdfDataToText in PDF points and scales only in PdfDataToDocument; a\n * single consistent space is more useful and avoids a class of silent mismatch.\n *\n * The output feeds the `bypassCol` optimisation: a page that already has a text\n * layer does not need OCR.\n */\n\nimport { ImageError } from '../core/errors.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage, type StageContext } from '../core/pipeline.js'\nimport { createDocument, type Document } from '../schemas/document.js'\nimport { toBytes } from '../stages/data-to-image.js'\nimport { extractTextBoxes } from './extract-text.js'\nimport { POINTS_PER_INCH } from './pdf-to-image.js'\nimport { describePdfError, documentOptions, loadPdfjs } from './pdfjs.js'\nimport { splitRunsIntoWords } from './split-words.js'\n\nexport interface PdfToDocumentParams extends BaseStageParams {\n /** Pixel space the boxes are expressed in; match PdfToImage to align them. */\n resolution: number\n pageLimit: number\n /** Split pdf.js line runs into word boxes. Off yields run-level boxes. */\n splitWords: boolean\n}\n\nexport const PDF_TO_DOCUMENT_DEFAULTS: PdfToDocumentParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'content',\n outputCol: 'document',\n keepInputData: true,\n resolution: 300,\n pageLimit: 0,\n splitWords: true,\n})\n\nexport class PdfToDocument extends Stage<PdfToDocumentParams> {\n readonly name = 'PdfToDocument'\n\n constructor(options: Partial<PdfToDocumentParams> = {}) {\n super(resolveParams(PDF_TO_DOCUMENT_DEFAULTS, options))\n }\n\n protected override async expand(input: unknown, row: Row, ctx: StageContext): Promise<Row[]> {\n const { outputCol, pageCol, pathCol, resolution, pageLimit, splitWords } = this.params\n const path = String(row[pathCol] ?? 'memory')\n\n const pdfjs = await loadPdfjs()\n const task = pdfjs.getDocument(documentOptions(toBytes(input)))\n\n try {\n // pdf.js defers worker setup, so a missing worker surfaces on first\n // page access rather than from task.promise.\n const pdf = await task.promise\n const pageCount = pageLimit > 0 ? Math.min(pageLimit, pdf.numPages) : pdf.numPages\n const rows: Row[] = []\n\n for (let index = 0; index < pageCount; index++) {\n ctx.signal?.throwIfAborted()\n const page = await pdf.getPage(index + 1)\n try {\n const viewport = page.getViewport({ scale: resolution / POINTS_PER_INCH })\n const runs = await extractTextBoxes(page, viewport)\n const bboxes = splitWords ? splitRunsIntoWords(runs) : runs\n\n rows.push({\n ...row,\n [pageCol]: index,\n [outputCol]: createDocument({\n path,\n type: 'pdf',\n text: runs.map((r) => r.text).join('\\n'),\n bboxes,\n }),\n })\n } finally {\n page.cleanup()\n }\n }\n return rows\n } catch (error) {\n throw describePdfError(error)\n } finally {\n await task.destroy()\n }\n }\n\n protected async apply(): Promise<never> {\n throw new ImageError('unreachable: expand handles every row', this.name)\n }\n\n protected onError(message: string, row: Row): Document {\n return createDocument({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: 'pdf',\n exception: message,\n })\n }\n}\n\n/** True when a page's text layer is substantive enough to skip OCR. */\nexport function hasUsableTextLayer(document: Document, minimumBoxes = 1): boolean {\n return document.exception === '' && document.bboxes.length >= minimumBoxes\n}\n"],"mappings":"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"}
|