Commit graph

5 commits

Author SHA1 Message Date
1740da3f34 feat(pdf): render a form XObject to pixels — the golden caught what the
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
assertions missed

`Rasteriser::register_xobject` takes a form's recorded commands from a
caller that can resolve the page dictionary, so Phase 7's last golden-corpus
criterion is met with pixels instead of with a request recorded by name.

The first golden of that page showed the form drawn at the **page origin**,
ignoring the `1 0 0 1 20 20 cm` that placed it. The recorded commands'
`SetTransform`s are absolute in form space, and replaying them overwrote the
page's CTM rather than composing with it. Nested lists now compose against
the CTM in force at the `Do`.

The colour assertions written next to that golden all passed while the bug
was live — a red square two pixels from where it belongs is still a red
square somewhere. That is the argument for pixel goldens in one sentence,
and it is why the golden is compared after the assertions and not instead
of them. The offset now has its own assertion too.

The new fixture's form deliberately overflows its own /BBox, so the clip is
visible in the golden as an absence rather than being taken on trust.

Phase 7's golden-corpus exit criterion is now met in full. The `ui.rs` smoke
tests remain blocked on the Makepad headless backend, as they have been
since Phase 1, and are still not claimed as done.

1426 tests pass.
2026-08-18 20:06:50 +00:00
0bef30a6d5 feat(pdf): compositing and overprint — the blend maths had no backdrop
Some checks failed
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
`transparency.rs` implemented all sixteen blend modes and unit-tested them
against the specification's formulas. Nothing ever called them with a
backdrop. The Makepad renderer's `SetBlendMode` pushed a
`TransparencyError::Unsupported` and then painted the source colour, so a
/Multiply highlight and a /Normal one produced byte-identical output and
every test passed — because every test asked "was the right command
issued", not "does the page look right".

Overprint had no code at all. /OP, /op and /OPM were not parsed, so an
overprinting object knocked out the inks under it. That is not a missing
feature, it is the inverse of the instruction: on a press it is the
difference between a colour and a hole.

- `composite.rs`: a straight-alpha RGBA `Canvas` implementing §11.3.6's
  union formula, weighted by backdrop alpha so a Multiply over transparency
  is the source rather than black. Constant alpha and per-pixel soft masks.
  Transparency groups composite as a unit; knockout groups are refused by
  name rather than silently treated as non-knockout.
- Overprint as `composite_cmyk`, separate from the RGB path rather than a
  flag on it: overprint is a statement about inks and RGB has none. /op
  defaults to /OP per table 58 — defaulting it to false makes the common
  `<< /OP true >>` knock out every fill. §10.7.5's "no effect on an RGB
  device" is asserted, so our doing nothing there is the spec rather than
  an omission.
- `raster.rs`: a CPU rasteriser that replays a command list onto a canvas.
  Not on the display path, no anti-aliasing, no fonts; it exists so
  compositing has a verifiable output. In pdf-graphics and not pdf-makepad
  because a test that needs a GPU is a test that does not run.
- Golden **pixels** for shading, mesh, blend and overprint pages — Phase
  7's exit criterion, which the Phase 2 command-text goldens cannot meet.
  ASCII grids with a colour legend, quantised to quarter steps; each test
  asserts its exact colours before comparing, so a wrong-but-stable render
  cannot be blessed by an UPDATE_GOLDEN run.

Six mutations, all killed, including the two that describe the old
behaviour: discarding the blend result, and ignoring the overprint flag.

1364 tests pass. ADR 0030.
2026-08-18 19:25:15 +00:00
6d2e3fb696 fix(pdf): repair the tree a hand-resolved merge left red
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Commit 258fa32 ("Merge origin/main: resolve xref/document conflicts") is not
a merge - it has one parent - and it landed a PDF tree that does not pass
its own gates. Found by running the suite, not by reading the diff.

Four separate breakages:

1. xref_streams::a_wrong_type_at_startxref_is_named_not_guessed FAILED.
   The conflict resolution replaced the diagnostic error with a generic
   "expected xref or cross-reference stream". The fixture writes
   /Type /Frobnicate and the parser has that name in hand; throwing it away
   leaves the reader knowing only that this is not what we wanted, which is
   the least useful half of the story. Restored, and extended to name a
   stream with no /Type at all. This is an ADR 0013 merge criterion.

2. golden_render::golden_text_line_ops FAILED - the merge added the test
   but not its golden file. Verified the output by hand against the spec
   before blessing it: leading 14, lines at 700/684/670/656/642, which is
   correct for Td/TD/T*/'/". A golden file blessed without reading it locks
   in whatever bug exists.

   That golden cannot witness the " operator's two spacing operands: word
   and character spacing live in the graphics state and emit no render
   command, so dropping both would leave the file byte-identical. Added a
   test that asserts the state directly. Mutation-checked: removing the two
   set_*_spacing calls fails it, and leaves the golden untouched.

3. clippy -D warnings failed with 6 errors on pdf-cos and pdf-document, so
   CI's engine job could not have passed. Two unused imports, a dead
   read_be (superseded by the `field` closure in parse_xref_stream), a
   collapsible if-let, and - committed into the source -

     // ... keep the entire top of the file unchanged until the resolve_num function ...

   an editing instruction left in document.rs as a doc comment.

4. cargo fmt --check failed on document.rs and xref.rs.

Mutation-checked both fixes: reverting the xref message fails 1 test,
dropping the " spacing operands fails 1.

pdf: 730 passed. pdf-ui: 775 passed. Coverage 84.15%, floors met;
destinations.rs holds at 98.65%.
2026-08-16 20:09:48 +00:00
7d21532ebf feat(pdf): real colour spaces, ICC profiles and PDF functions
Some checks failed
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phase 8 #4 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md
("Advanced colour"). Design and merge criteria in
REVIEWS/adr/0006-pdf-advanced-color.md.

The interpreter tracked only the *name* of the active colour space and
then passed sc/scn operands to the device as if they were already RGBA.
Every non-device space therefore rendered a confident wrong colour with
no error:

  /Spot cs 1.0 scn      full tint of a spot ink   -> pure red
  /Idx  cs 3   scn      palette entry 3           -> near-black
  /Lab  cs 50 0 0 scn   mid gray                  -> white (clamped)
  /DevN cs (5 inks)     five colorants            -> inks 5+ discarded
  /ICCBased             profile-defined colour    -> profile discarded

DeviceCMYK also used the additive 1-c-k conversion, which crushes any
colour printed over black.

Three new modules in pdf-graphics:

- function.rs  PDF functions, all four types. Type 4 runs on a bounded
               interpreter: depth 32, 32768 tokens, stack 100, 100000
               steps, and an unknown operator is an error rather than a
               no-op that would leave a plausible wrong colour.
- icc.rs       ICC matrix/TRC and gray kTRC profiles, applied exactly.
               LUT-class profiles are reported as such and the caller
               falls back to /Alternate; they are never pretended to be
               matrix profiles.
- colorspace.rs  All eleven families, converting through XYZ with
               Bradford adaptation and a real sRGB transfer function.

Wiring:

- PdfDevice gains set_stroke_components/set_fill_components, so SC/SCN
  reach the device as components of the active space instead of being
  read positionally as RGBA.
- cs/CS now resets to the space's initial colour (table 74), which is
  why golden/colors.txt gains a line.
- PdfPage::color_spaces carries /Resources /ColorSpace fully
  dereferenced with streams decoded; a half-resolved space would make
  every ICC profile, palette and type 0/4 transform silently fall back.
- A space that cannot be resolved keeps the previous colour and records
  a typed ColorError. No colour is invented, and no error is swallowed.

Tests: 12 corpus fixtures under tests/corpus/color/, 14 acceptance
tests in pdf-document/tests/color.rs asserting numeric RGB (the broken
code produced a colour for every one of these; only the value was
wrong), plus unit tests per function type and per curve type. Two fuzz
targets added: eval_function and parse_colorspace.

TEST_TARGET=pdf 387 -> 447 passing, TEST_TARGET=pdf-ui 431 -> 491.
rustfmt and clippy -D warnings clean.
2026-07-28 20:48:55 +00:00
b7d23f26bc feat(pdf): complete Phase 2 render pipeline with golden tests
Phase 2 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md. The exit
criterion is that a real content stream renders through the device path and
is verifiable; previously nothing could fail, so nothing was proven.

Step 2.3, the operations the review calls "the lies", all reached a dead end
in the interpreter. Each now reaches the device:

- Inline images: BI/ID/EI were emitted as two empty marker ops and the
  payload was discarded, so an inline image could never be drawn. They are
  now parsed into a single InlineImage op carrying the dictionary and bytes.
  Abbreviated keys (/W /H /BPC /CS /F) and colour-space and filter
  abbreviations are expanded. The EI scan requires delimiters on both sides
  so binary data containing the bytes "EI" does not truncate the image, and
  an unterminated image yields no image rather than invented pixels.
- Do (XObject) was an empty match arm. The interpreter cannot resolve a
  resource name, so it now reports it through PdfDevice::paint_x_object and
  the device performs the lookup.
- set_dash and set_miter_limit only mutated interpreter-local state and
  emitted no command, so dashes never reached any renderer.

Three further defects surfaced while reviewing the generated goldens, all
of which silently corrupted output rather than failing:

- `cm` was never parsed at all. The single-character `m` arm matched first
  and consumed it as a moveto, so every CTM change in every document was
  lost and content drew at the wrong position. Two-character operators are
  now tested before their one-character prefixes.
- `rg` and `RG` were shadowed by the `r` and `R` arms, so an RGB fill was
  read as a single-component grey: `0.1 0.2 0.3 rg` produced 0.1 0.1 0.1.
- ImageInfo defaulted a missing /Filter to FlateDecode. An absent /Filter
  means the data is stored raw, so every uncompressed image was undecodable.

Testing: adds format_commands(), a deterministic one-line-per-command text
form of a RenderCommand list, and eight golden files covering vector paths,
text, kerning and spacing, dash and stroke parameters, fill rules, inline
images, XObjects and colour operators. Floats are fixed-precision and
negative zero is normalised so no spurious diffs appear. UPDATE_GOLDEN=1
regenerates them for review.

Also fixes .gitignore: blanket *.txt and *.pdf rules were silently excluding
the golden expectations and the AcroForm fixture added in the previous
commit, which would have left both test suites unable to run on a fresh
clone.

Validation: TEST_TARGET=pdf ./tools/test-rust-clean.sh
132 tests pass; rustfmt and clippy -D warnings clean.
2026-07-27 15:55:53 +00:00