Skip to content

PDF fidelity report and partial acceptance (#4406)

The fidelity report is the gate in front of PDF vector conversion: before any geometry is prepared it states what the planner would omit, where on the page, and whether that loss is visible. This evidence records the canonical IfcAPI.preparePdfVectorPage verdict for nine controlled pages and two real drawings; one accepted partial and one exact conversion reopened by an independent reader and compared pixel by pixel against an independent raster of the page; and the viewer journey through the report step in a real browser. Every JSON here holds counts, extents and digests only — never page content beyond the two tiny CC0 control PDFs. It is generated by tools/texture-authoring/pdf-fidelity-evidence.mjs through the production PDF.js 6.3.289 adapter and the freshly built WASM.

Nothing here claims that a converted subset resembles its source page. The report exists so the user sees the omissions and accepts them explicitly; the contract defines the kinds, visibility and digest binding. The raster comparison below checks the complementary claim: that the report locates everything the conversion leaves out.

Controlled pages (original, CC0)

apps/viewer/src/lib/appearance/pdf/fixtures.ts (fidelityControls) builds every control on the same page: CropBox [10 20 110 92], /Rotate 90, /UserUnit 2, calibrated at one model metre per 30 PDF units. Extents are in unrotated PDF user space (the raw bboxPdf values; the viewer multiplies them by UserUnit before labelling them in points, so the same text run shows as x 20–136.0 pt on screen). apps/viewer/src/lib/appearance/pdf/fidelity-report-wasm.test.ts asserts the same values through the real decoder and WASM on every run.

control (control-*.json) verdict convertible paths omissions visible/total extent (PDF user-space units)
transforms (nonuniform cm, cubic fill) exact 2 — —
text (Helvetica 12 pt run) partial 2 text 1/1 [10, 16.4, 68.02, 32]
invisibleText (render mode 3) exact 1 text 0/1 —
clip (rectangle not containing the page) partial 1 clip 1/1 [0, 0, 80, 80]
transparency (/ca 0.5 ExtGState) partial 1 transparency 1/1 [20, 30, 30, 40]
strokes (round join, dash, solid) exact 3 — —
form (XObject with Matrix and clipping BBox) partial 1 clip 1/1 [5, 5, 25, 25]
hiddenLayer (optional content OFF) exact 1 hidden 0/1 —
rasterOnly (one inline image) raster-only 0 image 1/1 [10, 20, 110, 92]

Text extents are em-box estimates from font advances (here x 10–68.02, y 16.4–32 for a 12 pt run at (10, 20)); they locate the run, they do not reproduce glyphs. The invisible-text and hidden-layer pages are exact because nothing visible is lost, and their omissions stay listed with visibleCount 0 and omittedPaints 0 (only visible painted parts count as omitted paints). The clipped, transparent and form-scoped fills are reported instead of being drawn without their clip or effect; the later unclipped fill on each page still converts.

Real drawings

The HouseFlrPlan acceptance is a complete, GPL-2.0-or-later architectural vector floor plan. At a declared 2 mm tolerance it converts exactly as one compound path and three native regions, reopens with IfcOpenShell, and matches both native 3D and reopened 2D output pixel for pixel. Its independent MuPDF comparison has zero unexplained pixels. That focused directory carries the pinned source, deterministic PDF derivative, IFC and visual evidence.

featherston-report.json is the CC BY-SA 3.0 Zaidaudi ground-floor plan already used by the pinned decoder investigation (attribution and digest in ../pdf-vectors/sources.json; 515,902 bytes, SHA-256 c8c9…87d66; downloaded locally, not committed). The adapter produced 12,758 typed operations from the 12,773 raw operator-list entries (text-state setup folds into 11 text runs) with 3,167 painted paths. That committed report records the earlier qualifier, before round-stroke conversion: 1,141 paths converted and 2,025 painted parts were omitted, including roundCapJoin 1,436 (the drawing's default line style, extent [215.2, 33.9, 623.9, 570.6]), hairline 556 ([485.1, 215.0, 542.6, 358.7]), clip 33 fills inside one clipping path ([229.0, 35.5, 623.9, 570.6]) and text 11 labels ([244.1, 93.6, 617.4, 501.6]). All 2,036 individual entries are listed; nothing was truncated. Current preparation accepts the round cap/join state, as the controlled report above proves. This retained real-file record is not silently rewritten into a current verdict without the original PDF and a fresh complete run. Hairlines, clipped content and text still prevent an exact conversion, and the drawing is not presented as one.

habs-report.json is the public-domain HABS PA-6709 sheet 4 (Cyclorama Building floor plans; source metadata in ../pdf-appearance/habs-source.json; 675,723 bytes, SHA-256 f753…305a; downloaded locally). Its 19 operations are four save/restore pairs, eight transforms and three image paints covering the CropBox [0, 487.7, 596, 1224]; there are no paths. The verdict is raster-only: the viewer disables the vector action with a raster-reference message, and planPdfFillAnnotation refuses the page even when a digest is quoted as accepted.

Accepted partial conversion, reopened independently

control-text-accepted.ifc is the text control planned with its report digest quoted as the user's acceptance, applied through StoreEditor and exported with StepExporter against the textured product fixture (accepted block in control-text.json). IfcOpenShell 0.8.3.post2 reopened it through tools/texture-authoring/pdf-fidelity-oracle.py (control-text-oracle.json): one IfcAnnotation (IfcLite:PdfVectorFills) contained in "Level" with two representation items, Description PDF vectors, page 1: partial conversion; omitted 1 text run, and the IfcLite_PdfVectorConversion property set with 21 properties — source PDF digest, page 1, CropBox and conversion boundary [10.0,20.0,110.0,92.0], UserUnit 2, rotation 90, decoder PDF.js 6.3.289, calibration key and affine, ToleranceMetres 0.0001, grid size, request and fidelity digests, ExactConversion false, AcceptedPartialConversion true, 2 converted paths, 2 fill regions, 0 omitted paints and Omissions [{"kind":"text","count":1,"visible":1}]. The oracle shares no code with ifc-lite and checks that the recorded verdict agrees with the recorded omissions.

Independent raster versus native 2D and 3D output

The same oracle renders the control PDF with MuPDF 1.28.2 (antialiasing off, render scale 4, 576 × 800 pixels = 460,800 pixel centres over the rotated CropBox) and predicts two rasters from the conversion: the native plan meshes in IFC world metres, projected back onto the page through the recorded frame and calibration affine (what the 3D scene draws), and the exported IfcAnnotationFillArea boundaries as IfcOpenShell reads them, rasterised in paint order with holes (what the 2D drawing draws). Every differing pixel must lie inside a visible omission extent the report listed or within one pixel of a native boundary, and a partial page must actually differ inside its extents. Pixel-to-page mapping is derived from CropBox, /Rotate and UserUnit and asserted against MuPDF's own page size.

control verdict pixels differing inside reported extent on native boundary unexplained 3D vs 2D prediction
text (0.1 mm) partial, 1 text 9,576 9,576 0 0 identical
transforms (1 mm) exact 83 0 83 (≤ 0.26 px) 0 identical

control-text-mupdf.png is the MuPDF page (text and two fills), control-text-native3d.png the native output (two fills), and control-text-native3d-mismatch.png the difference: exactly the omitted text run, entirely inside the reported extent [10, 16.4, 68.02, 32]. The transforms control plans at 1 mm (PDF_FIDELITY_TOLERANCE_METRES=0.001) because its cubic fill exceeds the composition work budget at 0.1 mm — a geometric qualification refusal that the report does not hide; its 83 differing pixels sit on the flattened curve edge where the rasterizer's edge inclusion differs. This is a raster support check for the report's extents, not a metre-tolerance proof from pixels.

Viewer journey through the report step

browser-proof.mjs drives the local development viewer with Playwright and installed Chrome 153 (PROOF_CHANNEL=chrome), headless with WebGPU. It opens tests/models/various/issue-604-door.ifc (IFC4, two storeys, 11 meshes) through Open, uploads the text control PDF as a reference, calibrates two landmarks 2 m apart on the XZ plane, places the reference and removes the catalog source, then chooses Save into model → PDF vectors and presses Prepare vector preview. browser-report.png shows the outcome: the report line "Partial conversion: 1 visible omission would be left out; 2 paths convert.", the omission row "1 × Text runs — region x 20–136.0, y 32.8–64 pt" (the page's user-space extent scaled by its /UserUnit 2), the unchecked "Create a partial conversion" acknowledgement, a disabled Prepare partial conversion button and no Create button. Ticking the acknowledgement enables preparation; the native preview reports two coloured regions of the accepted partial conversion; Create selects one IfcAnnotation with two flat parts and no texture. Ordinary Undo removes it and Redo restores both parts; with the drawing hidden, actual pointer input selects it (browser-created-selected.png). File → Export IFC (with changes) writes issue-604-door_export.ifc; a fresh tab opens it through the ordinary loader with no original PDF, finds the IfcAnnotation with its Name, both colours and a maximum world-corner difference of 0, and pointer selection works again (browser-reopened-selected.png). The IfcOpenShell oracle then reads the exported file (browser-reopened-oracle.json): Description partial conversion; omitted 1 text run, AcceptedPartialConversion true, ToleranceMetres 0.001, the registration calibration key and affine, and Omissions [{"kind":"text","count":1,"visible":1}]. Measurements, browser and WASM digest are in browser-journey.json.

What was and was not verified

Verified on this host: the nine controls and both real drawings through the production adapter and WASM; the accepted partial plan, host export, ordinary parser reopen (fidelity-report-wasm.test.ts) and the IfcOpenShell reopen above; the MuPDF raster comparison of the partial text and exact transforms controls against native 3D meshes and reopened 2D fill areas; the viewer flow in PdfAnnotationFields.test.tsx (report shown, Prepare disabled until the acknowledgement, creation records the property set and one Undo entry, raster-only pages disabled, late cancelled results refused); and the real browser journey above (report, acknowledgement, preview, create, Undo/Redo, picking, export, fresh-tab reopen, independent reader).

Not verified here: no fresh shared-room join (rooms disable authoring in this build, as the panel test asserts), and no browser run of the raster-only or exact paths beyond the earlier registered PDF vectors UI journey, which covers the exact control page and predates the report step.

Reproduce

export TSX_TSCONFIG_PATH=apps/viewer/tsconfig.json
for c in transforms text invisibleText clip transparency strokes form hiddenLayer rasterOnly; do
  node --import tsx --import ./apps/viewer/src/test/vite-module-hooks.mjs \
    tools/texture-authoring/pdf-fidelity-evidence.mjs control:$c 1 docs/architecture/evidence/pdf-fidelity-report/control-$c.json
done
E=docs/architecture/evidence/pdf-fidelity-report
node --import tsx --import ./apps/viewer/src/test/vite-module-hooks.mjs tools/texture-authoring/pdf-fidelity-evidence.mjs \
  control:text 1 $E/control-text.json $E/control-text-accepted.ifc
PDF_FIDELITY_TOLERANCE_METRES=0.001 node --import tsx --import ./apps/viewer/src/test/vite-module-hooks.mjs \
  tools/texture-authoring/pdf-fidelity-evidence.mjs control:transforms 1 $E/control-transforms.json $E/control-transforms-accepted.ifc
for c in text transforms; do   # needs ifcopenshell, pymupdf, numpy, matplotlib, pillow
  python tools/texture-authoring/pdf-fidelity-oracle.py $E/control-$c-accepted.ifc $E/control-$c-oracle.json \
    $E/control-$c-accepted-plan.json $E/control-$c-accepted.pdf
done
# Browser journey: start the viewer (`pnpm --filter viewer exec vite --port 4390`), then
PROOF_TARGET_IFC=tests/models/various/issue-604-door.ifc PROOF_PDF=$E/control-text-accepted.pdf PROOF_CHANNEL=chrome \
  PROOF_OUT=/tmp/pdf-fidelity-proof node $E/browser-proof.mjs
node --import tsx --import ./apps/viewer/src/test/vite-module-hooks.mjs tools/texture-authoring/pdf-fidelity-evidence.mjs \
  /path/to/Floor_Plan.pdf 1 docs/architecture/evidence/pdf-fidelity-report/featherston-report.json

The real drawings use a nominal calibration (one model metre per 100 PDF units, tolerance 1 mm); extents do not depend on it, only the tolerance and the bound calibration identity do.