Commit graph

2 commits

Author SHA1 Message Date
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
d8d29c226d feat(pdf): transparency, and four operator-parsing bugs it exposed
Some checks failed
nigig-build.yml / feat(pdf): transparency, and four operator-parsing bugs it exposed (push) Failing after 0s
pdf.yml / feat(pdf): transparency, and four operator-parsing bugs it exposed (push) Failing after 0s
Phase 8 #5 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md
("Advanced transparency"). Design and merge criteria in
REVIEWS/adr/0009-pdf-transparency.md.

The headline defect was not that transparency was missing. `gs` was
MIS-PARSED as CloseStroke:

  /GS0 gs 1 0 0 rg 100 100 200 200 re f
    -> [CloseStroke, RgbFill, Rectangle, FillWinding]

so every page setting a graphics state gained a stroked path the
document never asked for, and lost its alpha, blend mode and soft mask
silently. Downstream everything was dead: StrokeExtGState/FillExtGState
were never constructed, set_fill_opacity was never called and emitted no
command when it was, and PdfPage::ext_gstate was read by no code at all.

New pdf-graphics/src/transparency.rs: full /ExtGState (ca, CA, BM,
SMask, LW, LC, LJ, ML, D, AIS, TK), all sixteen blend modes including
the four non-separable ones, soft masks with /S, /G, /BC and a /TR
evaluated through ADR 0006's PdfFunction, and /Group parsing. Wired end
to end: parser -> PdfOp::SetExtGState -> device -> RenderCommand ->
Makepad renderer.

Compositing is NOT claimed. Backdrop blending needs render-to-texture,
which an engine-neutral crate has no framebuffer for. Constant alpha is
applied because it needs no backdrop; blend modes and soft masks are
reported through TransparencyError::Unsupported rather than dropped,
because a silently ignored /Multiply looks exactly like a correct
/Normal.

The ADR required a test enumerating every multi-character operator, on
the grounds that fixing the third instance of a shadowing bug (after cm
and rg) without preventing the fourth is not a fix. It immediately found
three more live bugs, none of them transparency-related:

- `b` and `b*` dropped their close-path, mapping to FillStroke* instead
  of CloseFillStroke*, so every closed-and-stroked path drew with a gap.
- Text operators within three bytes of the end of a content stream were
  mis-parsed: the b'T' arm guarded `*i + 3 < len` while reading only two
  bytes, so a stream ending in `/F1 12 Tf` parsed as FillWinding.

And the transparency-group fixture found a fourth:

- PdfPage::xobjects was empty for every page of every document.
  extract_xobjects called doc.resolve(), which follows the reference,
  then asked the resolved object for as_ref() - always None - and only
  accepted a bare dict when every XObject is a stream. No `Do` operator
  could be resolved through the page model. Same defect as the one that
  once destroyed annotation object references; it now has its own
  regression test.

T* and BMC are genuinely unimplemented and are deliberately excluded
from the guard's table rather than papered over.

7 corpus fixtures, 12 acceptance tests, 32 unit tests asserting the
§11.3.5 formulas (not our own output), and a parse_ext_gstate fuzz
target.

TEST_TARGET=pdf 451 -> 497, TEST_TARGET=pdf-ui 495 -> 541.
rustfmt and clippy -D warnings clean.
2026-07-31 18:58:31 +00:00