Files
pdf/CONVERSION_ENGINE_DEEP_DIVE.md
T

34 KiB

Document Conversion Engine: End-to-End Deep Dive

Investigation date: 2026-09-09
Scope: pdf/ repository, with emphasis on gateway/app/services/convert, the PDF engine binding, OCR/layout models, writers, configuration, and conversion tests.
Evidence standard: real documents were passed through the public run_conversion path and the resulting Office files were parsed and rendered. Numbers below are measurements on the Windows development host, not vendor claims or a declaration of production readiness.

Current State

Executive finding

The engine is a composed, mostly in-process conversion stack. It is effective for simple digital PDFs, text extraction, Office document generation, and searchable PDF creation. It is not a general PDF layout clone. A PDF has positioned drawing operators, while DOCX/XLSX require a semantic flow/grid model; the conversion therefore reconstructs an intermediate document model (IDM) and necessarily loses some semantics and positioning.

The most visible historical defect, interleaved text on multi-column PDFs, had a specific upstream cause and is fixed in the current tree. The remaining quality ceiling is dominated by table reconstruction, OCR cost/ambiguity, and the choice of ReportLab as the Office-to-PDF fallback. A successful HTTP response is not equivalent to faithful content or layout: the measured corpus still contains low-order similarity and three failing assertions.

Execution topology

The current request path is:

HTTP /v1/convert/*
  -> filename/MIME/magic-byte/size validation
  -> converter registry and optional automatic PDF policy
  -> bounded worker admission (queue + shared conversion slots)
  -> conversion/document-cache scopes
  -> PDF route (digital, scanned, image-based, mixed)
  -> per-page geometry and content recovery
       C++ pdfengine ordered glyphs (preferred)
       pypdf visitor/content-stream geometry (fallback)
       line grouping -> paragraphs -> reading order -> tables/images
       optional ONNX layout regions and table separators
  -> OCR for scan/hybrid pages
       PDF render -> RapidOCR Latin pass -> optional Arabic pass
       confidence/script merge -> IDM blocks and metadata
  -> IDM optimization (headers/footers, wrapped rows, figures)
  -> format-specific formatter/writer
  -> OOXML/PDF validity checks, quality score and warnings
  -> content-addressed result cache

run_conversion binds cancellation, deadlines, options, automatic-routing decisions, and a per-conversion read/raster/geometry cache. The public converter registry currently covers PDF, DOCX, XLSX, PPTX, HTML, Markdown, text, CSV, JSON, and image paths; the exact pair list is exposed by the convert info route.

Component inventory

Layer Implementation in this tree Observed role and limitation
PDF parser/render core C++23 pdfengine pybind module; PDFium, FreeType, HarfBuzz, optional Skia/QPDF Gives authoritative glyphs, fonts, page geometry, rendering, and editing on this host. Native availability is not guaranteed on every Python deployment.
Python PDF parser pypdf==5.3.0 Text extraction, page count, content-stream fallback, XObject inspection. Its inferred whitespace and broken ToUnicode output cannot be treated as ground truth.
Raster path pypdfium2==5.13.0 declared in pyproject.toml Required for hosts without the C++ renderer. It is absent from the active venv, so raster-specific tests skip there; the C++ binding supplies rendering for the current audit.
OCR rapidocr-onnxruntime==1.2.3, onnxruntime==1.28.0 Shared Latin detector/recognizer; optional PP-OCRv5 Arabic recognizer. Inference is serialized because RapidOCR sessions mutate shared state.
Layout ML PP-DocLayoutV3-derived ONNX, 130,502,330 bytes FP32 or 34,694,580 bytes INT8 Region labels narrow heuristic decisions; it does not reconstruct a document by itself. It runs on CPU in the measured environment.
Table ML Optional structure.onnx (Table Transformer/SLANet operator-supplied) Not vendored (shipped: false in models/MANIFEST.json); heuristic lattice/stream/rectangle detectors remain the default.
IDM app/services/convert/idm/model.py and layout pipeline Stores pages, BBoxes, paragraphs, headings, headers/footers, figures, and cell grids. It is a useful interchange contract, but not a complete PDF display list or OOXML style graph.
Office writers python-docx==1.1.2, openpyxl==3.1.5, python-pptx code, XML sanitization Good for creating valid Office files; cannot infer all original PDF semantics (fonts, fields, anchored objects, section behavior).
Office -> PDF Gotenberg/LibreOffice attempt, then in-process ReportLab 4.2.5 Gotenberg is best fidelity when reachable. The benchmark used the ReportLab fallback, which is concurrent-safe but not a Word/Excel print-layout clone.
Images Pillow 10.4.0, PDFium render/XObject extraction Images are extracted, resized/compacted, or the complete page is embedded when reconstruction would lose visible content.
Font recognition FontRecognitionService facade, classifier and embedding store The checked-in FontClassifier uses aspect ratio/dark-pixel heuristics and EmbeddingStore uses a deterministic 16x8 grayscale projection. load_model() reads metadata but does not load an OpenCLIP ONNX session; model.onnx and fonts.index are not present in the repository. This is metadata assistance, not a validated font-identification model.

The declared dependency set is intentionally permissive (pypdf BSD, pypdfium2 Apache/BSD, python-docx/openpyxl MIT, ReportLab BSD, Pillow HPND). mammoth==1.8.0 is installed in this venv for DOCX/HTML fallback paths; fitz/PyMuPDF is absent, which avoids an AGPL transitive dependency but removes that optional path.

Runtime configuration and resource policy

The startup script defaults to automatic reconstruction, layout ML (INT8 when available), Arabic OCR with adaptive gating, a 1,800 second maximum conversion timeout, 22 seconds per OCR page budget, 45 seconds per-page timeout, a 1,200 second OCR wall budget, four conversion workers, four simultaneous conversion slots, and a queue of roughly two batches. Result caching uses namespace conversion-engine-v2 and includes effective options and routing mode in the key.

These are protection mechanisms, not throughput guarantees. A single process can still hold multiple ONNX sessions and large page rasters, and OCR inference is deliberately serialized. Horizontal scaling or process isolation is needed for untrusted large-document workloads.

Jobs, storage, and failure boundaries

Synchronous and asynchronous routes share a process-wide ThreadPoolExecutor and bounded semaphores. Asynchronous job state is persisted in SQLite with WAL and synchronous=NORMAL; results larger than 512 KB are spooled to a local jobs_spool directory, while smaller results are stored as BLOBs. Jobs older than three hours are purged, and jobs left queued/running after a process restart are marked failed. This gives useful single-node crash recovery, but SQLite and local spool paths are not a multi-instance object store: replicas need a shared database/object store or a queue service before horizontal scaling.

The timeout helper releases the HTTP caller on deadline by abandoning a worker thread (ThreadPoolExecutor.shutdown(wait=False)). Python cannot kill that thread, so a native render/OCR operation may continue in the background until it returns. Cooperative cancellation is checked at page/OCR boundaries, which limits normal work, but a hung native call can still consume CPU/RAM after a 504. Production deployments should put untrusted conversions behind process/container boundaries with an OS-level kill and cgroup limits.

Gotenberg/LibreOffice is attempted for Office-to-PDF with a 30-second client timeout and fails open to ReportLab. This makes the endpoint available when the microservice is absent, but it can silently change fidelity class unless the response warning is surfaced to callers and monitored.

Root Cause

The table below ranks causes by customer impact and likelihood in the measured system. "Fixed" means the implementation changed and the targeted regression improved; it does not mean the entire class of failures is solved.

Rank Severity Root cause Evidence State
1 P0 Precise glyph coverage was compared with len(pypdf.extract_text()), which includes inferred spaces/newlines, while the precise walker emits text-showing characters only. Pages near the 0.80 gate fell into _glyphs_from_visitor, whose width calibration measured the two-column pitch rather than font advances. Mozilla pages had coverage 0.754-0.858; stripping whitespace raised it to 0.879-1.000. Estimated spans crossed the gutter; the visible sentence was interleaved. Fixed by comparing non-whitespace characters and retaining the precise path.
2 P0 Baseline clustering was page-global. Different leading in adjacent columns let rows "bridge" into one bucket; splitting columns later could not undo the vertical weld. On the paper page, 6.97 pt copyright rows and 8.97 pt body rows were interleaved; 4.6 pt and 3.3 pt hops were under the global tolerance. Reading-order inversions fell from 189 to 1 after column-aware bands. Fixed with _column_band and a bounded baseline clip.
3 P0 OCR is CPU-heavy and each bilingual page can run Latin and Arabic recognition, plus per-line/per-word font metadata work. A shared RapidOCR session cannot safely run concurrently. 20-page scan: 142.747 s wall, 655.016 CPU-s, 1,769.4 MB peak RSS. Warm one-page inference was 3.021 s/17.318 CPU-s with Arabic versus 1.370 s/7.943 CPU-s with adaptive skip. Partially fixed: inference/model locks, adaptive Arabic, page caps, budgets, and raster reuse are present.
4 P1 Table reconstruction is heuristic by default; ruled headers and multi-line cells are represented as fragments before grid assignment. The optional structure model is not shipped. W3C exact table gate produced recall 0.867 (<0.90). Participants became s Participant; Time to complete became Results Time to complete; n=1 gained a space after =. Open; targeted fix is required.
5 P1 PDF-to-editable-Office is a semantic reflow, not a pixel-preserving transform. Header/footer detection, style inference, anchored objects, columns, fields, and arbitrary positioned graphics have no one-to-one OOXML representation. Pixel similarity varies from 0.784 (USGS) to 0.950 (govinfo) in the ReportLab-rendered comparison; order scores are 0.232-0.674 for several digital PDFs. Fundamental limitation; use page-image fallback or a fidelity backend when editability is not required.
6 P1 OCR recognition is sensitive to source DPI, contrast, skew, script, and confidence thresholds. Low thresholds (0.08 box/text) preserve faint marks but also admit noise; post-processing must distinguish symbols from rules. Scan recall is 1.000 against expected page tokens but precision is 0.077 because OCR also emits repeated body/table text. Arabic adaptive mode improves CPU cost but whole-run wall time is noisy. Partially fixed; needs calibrated per-document confidence and preprocessing.
7 P1 Quality scoring historically rewarded agreement with a corrupted pypdf text layer and ignored duplicates/order. Structure bonus could be earned by a diagram label classified as a header. Mozilla score moved 0.9475 -> 0.8998 while the corrupted duplicate text disappeared and real prose improved. New metrics expose precision/order and store idm.meta["text_metrics"]. Fixed for diagnostics; corpus gates still need independent gold/layout checks.
8 P2 Repeated PDF reads, geometry extraction, rasterization, and model initialization inflated latency and memory. Unbounded cache keys also allowed stale results across option/routing changes. Per-conversion document cache and canonical cache keys were added; regression suites cover collisions and queue admission. Fixed/mitigated.
9 P2 API contract mismatch unrelated to conversion: unknown font endpoint currently returns 204 while its test expects 404. Full suite failure in tests/test_inplace_editing.py::test_font_endpoint_rejects_unknown. Open decision for API owner.
10 P2 Timeout release is thread-based, not process-based. A native renderer or OCR call that ignores cancellation can continue after the request receives 504; local SQLite/spool storage also limits multi-instance scaling. run_with_timeout() explicitly uses shutdown(wait=False) because Python cannot kill a thread; job store uses one local SQLite file and local spool directory. No runaway crash was observed in the audit, but the resource behavior is a design risk. Mitigate with process/container isolation and shared durable storage.

Content accuracy

PDF text is not necessarily a trustworthy source string. Embedded fonts can have missing or malformed ToUnicode maps, CID encodings, zero-width markers, and text show operators split in ways that do not correspond to words. The current pipeline therefore combines C++ glyph extraction, pypdf extraction, content-stream evidence, OCR, and text-quality heuristics. Any stage that chooses a wrong representation can duplicate, drop, or reorder characters before the writer sees them.

The old comparison metric made this harder to observe: ASCII-only unique-token intersection ignored Arabic, CJK, combining marks, symbols, repeated occurrences, and order. The replacement is Unicode NFKC normalization, line-break hyphen joining, duplicate-aware multiset matching, meaningful-symbol retention, and an ordered sequence score. It is still a diagnostic metric, not a semantic proof.

Layout and structure fidelity

The IDM preserves BBoxes and reading order, but a flowing DOCX writer must choose paragraphs, columns, tables, and page breaks. Heuristics infer headings from size and text, detect headers/footers from repetition and position, and merge rows across pages. They cannot preserve every PDF transformation matrix, clipping path, baseline, text box, field, or z-order. Automatic page-raster fallback is the safety floor for complex positioned pages: visual content survives, but text is no longer editable or searchable as individual runs.

Formatting preservation

The writers can carry bold, italic, colors, hyperlinks, bullets, numbering, and several paragraph attributes when those attributes exist in the source Office XML or can be inferred from PDF glyph metadata. PDF extraction usually lacks the original style names and theme inheritance. Font aliases and locally installed fonts determine metrics; missing fonts fall back to Helvetica/Liberation/DejaVu or script-specific Noto faces. Word-to-PDF currently applies a fixed paragraph spaceAfter in the ReportLab path rather than reproducing every source paragraph's spacing, so small but systematic vertical shifts are expected.

Tables

The detector combines ruling-line (lattice), whitespace/stream, rectangle, and plausibility passes, then maps glyph lines into cells. A two-column spanning header or a row whose words sit on separate baselines can be split into fragments. Merged cells and nested content are represented approximately. The structure ONNX hook can supply separators, but the model is operator-supplied and fail-open; it is not part of the reproducible default deployment.

Images and graphics

XObjects and rendered page images are retained where possible. Large figures are compacted rather than silently discarded. If a page reconstructs to no meaningful blocks, the pipeline renders and embeds that page as a figure, or emits an explicit warning. This preserves visible pixels but cannot preserve editable diagram objects, vector semantics, captions' relationships, or exact crop/ordering in all cases.

Evidence

Measurement method

tools/audit_conversion.py invokes the same in-process run_conversion entry point used by the gateway. For each case it records input/output bytes, source and output page counts, warnings/errors, duplicate-aware text recall/precision/order/F1, table payloads, and model status. A Windows process sampler records working-set peak and GetProcessTimes CPU seconds at 20 ms intervals. DOCX/XLSX outputs are parsed with python-docx/openpyxl; visual comparison renders both sides at 96 DPI and reports 1 - mean(abs(gray_a-gray_b))/255 over the compared pages.

The visual metric is intentionally simple and should be treated as a trend signal. It is not SSIM, does not align transformed pages, and in this run renders Office outputs with the in-process ReportLab fallback. It therefore does not measure what LibreOffice/Gotenberg would produce.

Representative real-document run

The latest six-case digital run is the current audit_results.json artifact; the 20-page scan row is from the preceding targeted run and will be appended after the final OCR run. Times are wall seconds; CPU is process CPU seconds; RSS is peak working set. "Order" is the ordered token similarity, not a page-layout score. Output size and output page count are taken directly from the audit JSON. Re-run the command in Appendix A after any code change to refresh all rows. Results can vary with cache/model warm-up and with whether the page-raster fallback is selected, so the harness should be run more than once when comparing changes.

Document -> target Source pages Rendered output pages* Page delta* Wall s CPU s Peak RSS MB Output bytes Warnings Recall Precision Order Pixel similarity
govinfo -> DOCX 1 2 +1 4.707 12.250 717.0 39,779 7 0.941 0.888 0.443 0.950
IRS 1040 -> DOCX 2 7 +5 4.531 13.578 1118.7 43,430 9 0.936 0.944 0.674 0.890
Mozilla PDF spec (14 p) -> DOCX 14 28 +14 29.785 92.750 1141.1 77,386 24 0.916 0.937 0.149 0.895
USGS factsheet (4 p) -> DOCX 4 7 +3 14.897 30.594 1346.8 161,987 13 0.767 0.793 0.232 0.784
W3C ruled table -> XLSX 1 1 0 1.732 5.391 1191.7 5,328 7 1.000 1.000 0.990 0.959
Multipage table (2 p) -> XLSX 2 2 0 3.280 11.891 1195.6 5,593 7 0.600 1.000 0.750 0.998
Scan pack 1 (20 p) -> DOCX 20 4 -16 142.747 655.016 1769.4 58,244 102 1.000 0.077 0.143 0.957

Source characteristics for the digital cases were 3,487 chars/545 words (govinfo), 10,416/2,223 (IRS), 82,714/13,439 (Mozilla), 14,022/1,887 (USGS), 375/65 (W3C), and 73/25 (multipage table) according to pypdf's source read. The scan pack has 20 raster pages and only a tiny/invalid embedded text layer, so expected page-token recall was supplied independently (ScanTokenpack1P000 ... P019).

*Rendered output pages are the pages produced when the audit harness sends the Office bytes through its in-process ReportLab visual renderer. They are not a claim about a user's pagination in Word. For example, the scan DOCX contains 19 explicit page breaks (20 intended pages) even though the fallback visual render reports four pages; both page-break metadata and rendered page count must be tracked.

The scan result is a useful warning: recall 1.000 alone would say "perfect," while precision 0.077 and order 0.143 show that the editable OCR reconstruction contains substantial extra/repeated material. Its pixel similarity is high because the page images are retained; visual success and editable-text success are different products.

Multi-column defect A/B evidence

On the Mozilla document, the old path produced 189 reading-order inversions in the probe. After the coverage-gate and column-aware row fixes, the probe reports 1. The corrected prose sentence is no longer spliced. The old corpus quality number was 0.9475 and the new number 0.8998, but the output character count fell from 83,136 to 79,223 because duplicate visitor text disappeared. A/B instrumentation showed recall 0.9806 -> 0.9608 and missing token types 41 -> 83, while missing real words changed 31 -> 32; the additional "missing" tokens were chart-axis labels that pypdf had decoded differently. This is a metric-baseline artifact, not evidence that the repaired sentence was lost.

Table evidence

The hand-labelled table truth in corpus/convert/table_truth.json requires the W3C exact six-column grid to reach recall 0.90 and precision 0.55. The current failing output from build_idm_from_pdf(table.pdf) is:

["Disability Category", "s Participant", "Ballots Completed",
 "Ballots Incomplete/ Terminated", "Accuracy", "Results Time to complete"]
["Blind", "5", "1", "4", "34.5%, n= 1", "1199 sec, n=1"]
...
["Mobility", "3", "3", "0", "95.4%, n= 3", "1416 sec, n=3"]

Compiled glyph inspection found a zero-width-space marker at approximately x=222.28 and the following s at x=222.31, with near-identical baseline; lines_from_glyphs() separated them. Header words were spread over multiple baselines (Participant/s, Results/Time to complete, and similar pairs), and some spans contained the punctuation artifact "= ". The two failing table tests are therefore reproducible extraction defects, not a threshold problem.

OCR and model evidence

The active venv reports:

pypdf 5.3.0
onnxruntime 1.28.0
rapidocr-onnxruntime 1.2.3
providers: AzureExecutionProvider, CPUExecutionProvider
pypdfium2: unavailable

No CUDA provider was available, so the measured conversion was CPU-bound. The layout model is 130.5 MB FP32 (34.7 MB INT8 alternative); the Arabic recognizer is 8.0 MB. The root models/font tree contains only metadata/tokenizer placeholders, not model.onnx or fonts.index, and the classifier code does not create an ONNX session. Any font-confidence value from that service must therefore be treated as a heuristic, not a calibrated model probability.

Arabic warm-page measurements were stable enough to show the tradeoff:

Mode Mean wall/page Mean CPU/page Interpretation
Arabic pass enabled 3.021 s 17.318 CPU-s Better script coverage, roughly double inference work
Adaptive skip on clean Latin 1.370 s 7.943 CPU-s About 55% lower inference cost; known Arabic/broken-cmap pages still force the pass

Whole 20-page wall time was noisy (non-adaptive 150.389 s versus adaptive 178.408 s in separate runs), so the warm-page measurements, not that pair alone, should guide capacity planning.

Test evidence

Focused regression groups passed as follows: earlier cache/queue suite 96 tests; OCR budget/Arabic suite 32; CORS/model packaging 8; raster/quality/real-corpus focused run 34 passed with 16 raster-specific skips; streaming export plus cache/queue 7; corpus/phase/production smoke 36; and an additional metrics/OCR run 133. The full gateway suite completed 746 passed, 21 skipped, and 3 failed.

The three failures were:

  1. test_labelled_table_is_recovered[w3c_ruled_grid]: recall 0.867, floor 0.90.
  2. test_table_pdf_reading_order_and_spans: the same n= 1 and header-fragment defects.
  3. test_font_endpoint_rejects_unknown: endpoint returned 204; test expects 404.

The raster skips are environmental (pypdfium2 is missing), not green evidence that image export/OCR works on a renderer-free host.

Immediate P0/P1 work

  1. Finish W3C/table normalization and fragment stitching. In the table path only, remove whitespace immediately after = and normalize punctuation spacing around %, =, and commas. Merge one-character continuation fragments when their baseline and x-position overlap a neighboring fragment. Stitch header fragments by column anchor and vertical proximity, preserving true multi-line cells. Add a regression for the exact five bad cells and for ordinary prose so the normalization cannot alter sentence spacing.
  2. Merge the two geometry walkers. The precise C++ walker and pypdf visitor observe the same content stream. Use precise glyph bounds whenever available and visitor text only as a decoding fallback, instead of calibrating visitor width from cross-column span gaps. This removes the remaining unsound estimator rather than merely reducing how often it runs.
  3. Add independent layout gates. Keep hand-labelled table truth, and add page bounding-box/column anchors, header/footer repetition, page-break, image-count, and cross-column order fixtures. Report recall, precision, duplicates, order, and visual similarity separately; never gate only on pypdf-derived text.
  4. Make the table structure model reproducible. Either vendor a checked, permissively licensed Table Transformer/SLANet model with checksum and measured CPU/RAM cost, or explicitly document heuristic-only deployment and route high-risk tables to the page-image fallback.

OCR and resource work

  1. Keep one process-wide session per model, but use a dedicated OCR worker pool or process queue so serialized inference cannot block unrelated conversions. Bound image dimensions and decoded bytes before OCR; expose queue wait, model-load, render, inference, and rebuild timings per page.
  2. Calibrate DPI and preprocessing by document class (deskew, contrast, denoise, adaptive threshold, crop). Use confidence distributions and script detection to decide a second pass; preserve low-confidence text with a warning rather than silently replacing it. Evaluate RapidOCR against PaddleOCR PP-OCRv5, EasyOCR, and a CPU-friendly transformer on a labelled multilingual set before switching.
  3. Treat the current font service as a heuristic fallback until a real model is shipped. If font fidelity matters, load and checksum an actual ONNX model, batch line crops, and cache one prediction per style/span rather than running line and word inference for every OCR block. Otherwise disable it in the hot conversion path and use extracted PDF font metadata.

Fidelity and operations

  1. Preserve the existing page-raster fallback for complex positioned PDFs and make the choice explicit in response metadata (editable versus visual). For customer-facing editable PDF->Office jobs, use Gotenberg/LibreOffice or a commercial fidelity provider behind the same plugin interface; retain ReportLab for concurrent-safe low-cost fallback.
  2. Install pypdfium2 in every supported runtime image and run the raster suite in CI. Keep the C++ engine path as the preferred renderer, but fail loudly when a host has neither renderer nor OCR capability.
  3. Resolve the font endpoint 204/404 contract, and expose structured error codes for page-cap, OCR-budget, table-confidence, and visual-fallback events.
  4. Run conversion workers in separate processes/containers for hostile or very large files. Apply per-tenant byte/page/time budgets, temporary-directory quotas, cancellation cleanup, and a dead-letter queue. Track p95/p99 latency, peak RSS, CPU saturation, cache hit rate, and output-fidelity distributions.

Implementation

The following changes are present in the working tree and were validated by the focused suites:

  • layout/pipeline.py, pypdf_geometry.py, glyphs.py, and paragraph/order helpers now use non-whitespace coverage, column-aware row bands, zero-width whitespace handling, improved paragraph grouping, and figure preservation.
  • ocr.py serializes model load/inference safely, supports adaptive Arabic recognition (CONVERT_OCR_ARABIC_ADAPTIVE), preserves meaningful symbols such as $ and %, and records whether Arabic was used per call.
  • ocr/rebuild.py raises the page cap to 200, separates page-cap and time-budget warnings/metadata, compacts oversized figures instead of dropping them, and embeds unreconstructable pages as honest page images.
  • result_cache.py canonicalizes effective options, namespace, and automatic versus explicit resolution mode. doc_cache.py reuses readers, geometry, rasters, and engine handles inside one conversion.
  • routers/convert/jobs.py and conversion scopes bound admission and shared execution slots; cancellation and deadlines are checked at page/OCR boundaries.
  • quality/text_metrics.py and quality/scorer.py now expose Unicode-aware, duplicate-aware, order-sensitive metrics and warnings for low precision/order; the IDM retains component metrics for diagnostics.
  • PDF->PNG/JPEG/TIFF exports stream pages; TIFF is multi-frame LZW with DPI metadata. DOCX raster fallback is selected from measured complexity/content evidence rather than blindly reflowing every page.
  • Dockerfile, models/MANIFEST.json, and tools/verify_models.py package and checksum runtime layout/Arabic weights. CORS defaults are explicit and production-safe. start_convert_gateway.ps1 documents OCR, model, timeout, queue, and cache settings.
  • tools/audit_conversion.py provides repeatable end-to-end measurements. New tests cover metrics, OCR resource guards, cache keys, queue admission, streaming exports, model packaging, and table truth.

No component was replaced solely because it was open source. The changes preserve the C++/PDFium core and existing plugin interfaces; alternatives are recommended only where the evidence shows a fundamental fidelity or reproducibility gap.

Validation Results

The current state passes the broad focused checks listed in Evidence and runs the full gateway suite to completion without a crash or timeout. The multi-column order probe improved from 189 inversions to 1, and the OCR/page-cap/resource regressions are green. Streaming image export and OOXML validity checks are green.

The result is not yet release-clean:

  • W3C exact table recall is 0.867 against a 0.90 floor.
  • Table reading-order/span assertions fail on the same punctuation/header fragments.
  • Unknown-font endpoint status disagrees with its test contract.
  • USGS and multipage-table order/recall remain low despite high pixel similarity.
  • Scan-pack editable text has perfect supplied-token recall but only 0.077 precision and 0.143 order similarity; it must not be marketed as accurate OCR without a richer ground truth.

Expected gains from the recommended work are stated as targets, not measured claims: table stitching should raise W3C recall above the existing 0.90 gate and remove the four known header/punctuation errors; a merged geometry walker should keep the 1-inversion result while eliminating the remaining visitor-width risk; adaptive OCR already demonstrates approximately 55% lower warm-page inference cost on clean Latin scans; process-level isolation should make peak RSS roughly linear per worker instead of allowing simultaneous jobs to share one unbounded address space. Each target must be re-measured on the same corpus before release.

Remaining Limitations

  1. PDF->editable DOCX/XLSX cannot be pixel-perfect for arbitrary positioned PDF content. The page-image fallback preserves appearance by giving up editability; the reflow path preserves more semantics by accepting layout drift.
  2. The default table detector remains heuristic and the table structure model is not shipped. Merged cells, borderless grids, nested tables, and multi-line headers remain the largest content-structure risk.
  3. RapidOCR is CPU-bound in the measured host, and shared-session locking limits parallel OCR throughput. Whole-document timings vary with cache/model state and Windows scheduling; warm per-page numbers are more stable.
  4. The active venv lacks pypdfium2, so renderer-free Linux behavior and several raster tests were skipped. The Docker dependency declaration and local runtime are currently inconsistent until the venv is rebuilt from pyproject.toml.
  5. No CUDA provider was available in this audit. GPU utilization, GPU memory, and multi-GPU scaling are therefore unmeasured.
  6. The advertised OpenCLIP font model and FAISS index are metadata placeholders in this checkout. Font predictions are heuristic and should not be used as a fidelity guarantee.
  7. Visual similarity is a grayscale pixel heuristic over at most the compared pages and, for Office outputs in this audit, uses ReportLab rather than a real LibreOffice/Gotenberg render.
  8. The quality score is useful for triage but is not a single acceptance metric. Source text can be corrupt, OCR can add legitimate-looking alternatives, and order/layout require independent annotations.
  9. The repository has a .git_disabled metadata directory rather than a normal .git checkout, and much of the conversion subsystem is working-tree content. Before/after comparisons in this investigation therefore come from harness snapshots and controlled runs, not a pristine commit diff.

Appendix A: Reproduction Commands and Evidence Files

Run from pdf/ with the gateway venv:

gateway\.venv\Scripts\python.exe tools\audit_conversion.py
gateway\.venv\Scripts\python.exe tools\audit_conversion.py --no-scan
gateway\.venv\Scripts\python.exe -m pytest gateway\tests\convert -q
gateway\.venv\Scripts\python.exe -m pytest gateway\tests -q

Primary evidence and implementation references:

  • audit_results.json - machine-readable audit output (the checked-in file may be targeted to one case; regenerate for the full matrix).
  • tools/audit_conversion.py - timing, resource, content, table, and visual measurement harness.
  • corpus/convert/table_truth.json and corpus/convert/baselines.json - external table labels and suite floors.
  • gateway/app/services/convert/layout/pipeline.py - routing, IDM construction, OCR budgeting, and page-image fallback.
  • gateway/app/services/convert/layout/pypdf_geometry.py and glyphs.py - glyph walkers, line grouping, and geometry evidence.
  • gateway/app/services/convert/ocr/rebuild.py and gateway/app/services/ocr.py - raster/OCR/rebuild path and Arabic adaptive policy.
  • gateway/app/services/convert/formatters/docx_formatter.py, writers/pdf_from_docx.py, and backends/pdf_exporter.py - Office writers and Gotenberg/ReportLab selection.
  • gateway/pyproject.toml, gateway/Dockerfile, gateway/models/MANIFEST.json, and gateway/start_convert_gateway.ps1 - dependency, packaging, and runtime configuration evidence.

Bottom line: the engine now has honest diagnostics and materially better multi-column recovery, but it should be released with explicit fidelity classes, the W3C table defect fixed, renderer/OCR dependencies verified in CI, and separate content, structure, and visual acceptance gates.