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.