Repository navigation
pdfce v0.7.0 — transparency composites for real
Transparency composites for real
v0.6.0's notes said, in as many words, "Transparency-group compositing and soft masks are not implemented… do not read this release as 'transparency works'." They are implemented now, and this is the release where that sentence changes.
pdfce owns the compositing arithmetic — ISO 32000-1 §11.4.4's element formula, §11.4.8's knockout variant, §11.4.4's backdrop removal, and all thirteen of Table 136's separable blend functions in one module rather than scattered across three call sites.
- Non-isolated groups see the backdrop beneath them. §11.4.4 NOTE 2 gives the reason: most blend modes depend on both operands, and painting a group into a fresh transparent buffer hands every interior blend a backdrop of nothing. The contents are walked twice — once transparent for
α_gn, once over the backdrop forC_n— and the second walk is skipped under §11.4.4 NOTE 5's own condition, so ordinary content pays nothing. - Knockout groups (
/K true) are implemented, including §11.4.6 NOTE 6's nesting rule: a non-isolated group inside a knockout group inherits the outer group's initial backdrop, not the immediate one. Ghent GWG 16.1 goes from 14 wrong cells to 2. - Soft masks apply to the group's RESULT (§11.4.5), not to each object inside it. Reference-strip correlation against the suite's own inline reference:
GWG 16.100.576 → 0.962,GWG 16.80.725 → 0.978,GWG 16.90.905 → 0.986. Folding a mask into the clip is still what happens to an elementary object and is correct there — §11.6.4.1 makes the mask value that object'sq_m.
The defect that had been shipping since the non-separable modes landed
Both hand-written composites substituted a white backdrop wherever the destination was transparent. That is §11.4.4's formula specialised to α_b = 1, and it is wrong exactly where §11.4.7 makes transparency common: the page buffer starts transparent and the white medium is composited in once at the end, so transparent means no backdrop, not paper. Sat(white) = 0 and Lum(white) = 1, so Hue/Saturation/Color of anything over white is white.
Measured on Ghent 16.2 at scale 2.0, against pdfium as an independent second opinion:
| cell | before | after | pdfium |
|---|---|---|---|
Hue |
(255,255,255) |
(184,184,184) |
(184,184,184) |
Saturation |
(255,255,255) |
(184,184,184) |
(184,184,184) |
Color |
(255,255,255) |
(184,184,184) |
(184,184,184) |
Luminosity |
(106,106,106) |
(255,20,159) |
(255,22,158) |
A second bug fell out of the same rewrite: accumulated alpha used max(a, b) where §11.3.7.3 says Union(a, b) = a + b − ab. Two half-covering elements leave a quarter of the pixel uncovered, not a half — so alpha was under-reported on every anti-aliased edge inside a group.
Editing reaches text inside form XObjects
99 % of the text worth editing in a CAD drawing is not in the page's own content stream. On the reference A3 sheet the page /Contents holds 3,007 single-character show operators of producer watermark, while one form XObject holds 1,696 carrying every label, the title block and every callout. edit-text and format-text now reach it.
A form XObject may legally be painted from several pages, and no clause in either ISO edition binds one to a page — so an edit changes every sheet, there is exactly one stream, and pdfce discloses that rather than pretending otherwise. Copy-on-write was weighed and declined as a default, on the ground that a form invoked from inside another form cannot be re-bound without editing the parent, which may itself be shared.
Two defects only a real file could find, both in code years older than the Pass that exposed them:
- A character code present in both a
bfcharand abfrangewas materialised twice, and the injectivity check reported it colliding with itself — "codes 361 and 361 both map to 'Ʃ'". A false refusal on a perfectly invertible font, and a refusal message is the one string nobody expects to see. - One four-character edit reported 1,676 followers repositioned and changed 34,059 pixels across the page. Reflow shifted every following
Tmuntil aTd-family boundary — and a CAD stream positions everything withTmand never emitsTd, so there was no boundary. After the fix: 42 pixels in a 20×7 box.
An object clipboard
copy_objects / cut_objects / paste_objects, cross-document, plus a serialisable clip file.
The hard part is not copying indirect objects — it is that a page's content objects are byte ranges whose operators name their resources by page-local name. On the destination page /F1 is a different font, so pasting verbatim draws the right glyphs in the wrong typeface and nothing errors, because a resource name is not a reference and there is nothing in the graph to remap. So copy records the names each item consumes and carries the objects behind them; paste re-binds each to a fresh name and rewrites the names inside the copied bytes.
Annotations get their own address space: markup and dimensions paste through pdfce's own models, widgets are refused by name because a renamed field is a different field.
A third real-file defect: text state is graphics state, so a producer may set /F8 12 Tf once and emit many BT…ET blocks that inherit it. A text object's byte span is its BT…ET, so the Tf is not in it — copy bound no font and paste emitted fontless text, silently. Fixed with a prelude carrying the inherited state, captured at copy time.
Transform a selection
transform_objects — scale, rotate, shear or move a selection by one page-space matrix, on every object kind including text and images.
The open question closed by impossibility, not preference: operand rewriting cannot do the job, because a rotated rectangle has no re spelling, line width is a user-space scalar a coordinate scale leaves behind, and text and images carry no coordinate operands at all. Wrapping each run in q <cm> … Q never looks at an operand, so kind-agnosticism is a property of the mechanism rather than a match arm somebody must remember to extend.
★ The matrix emitted is not the matrix passed in. matrix is page space; cm composes into the CTM in force at that point. pdfce emits X = CTM × M × CTM⁻¹ from each object's own captured CTM — emitting the request directly is correct only where the CTM is the identity and silently wrong everywhere else.
Also in this release
add_imagewrote files pdfce could not reopen — and the bug was in two independent copies of the same six lines.- A perimeter ce dimension, and per-vertex editing of one.
quad_point_orderwas a setting that did nothing. A setting is a promise.merge-documentcomputed three disclosures and printed none of them — and then a gate (check-outcome-disclosed.py) for the whole class, because an outcome field no shell reads is a disclosure that does not happen.- CI ran six of fifteen gates. It runs fifteen of sixteen now — the sixteenth takes a fixture directory and is a sweep tool, named in the workflow so the next person counting does not record it as a miss.
- A gate that compares the local gate sweep against CI.
cargo fuzz buildhad been red for three days while every local check was green — and neither signal was wrong, because "run the gates" means fourteentools/check-*scripts and that is one of CI's nine jobs.check-ci-parity.pyclassifies everyrun:command in the workflow as covered locally, covered by a named local stand-in, or deliberately CI-only, and fails on anything unaccounted for. Sabotage-verified before shipping. - The fuzz target for markup annotations had never fuzzed one of its eight variants. The dispatch read
% 8against an eight-variant enum with one arm spent twice. Nothing reported it; it surfaced only because the compile broke.
What this release does NOT fix, and it is now measured rather than assumed
The Ghent transparency panels are blocked on the blending colour space (§11.3.4), not on the group model. Worked by hand on 16.2's Difference cell, whose operands are printed in the file — magenta 0 1 0 0 k under black 0 0 0 1 k:
§11.3.4 complement: cb′ = (1,0,1,1) cs′ = (1,1,1,0)
|cb′ − cs′| = (0,1,0,1)
complement back = (1,0,1,0) = DeviceCMYK 1 0 1 0 = GREEN
That is the surround the patch is authored around, exactly. pdfce renders (237,1,140); pdfium renders (202,29,108) — both blend in RGB, both wrong, differently. Every Ghent transparency patch declares /Group /CS /DeviceCMYK on the page, including the one whose own objects are ICCBased RGB.
So the Ghent board is unchanged at 26 pass / 14 fail / 11 unresolved while the trap count across the failing patches fell 67 → 55. Read docs/FEATURES.md, which says row by row what works.
Still not implemented: implicit knockout (§9.3.8's /TK default-true text, §11.7.4.4's B/b, §11.6.7's shading patterns — only explicit /K true is honoured), /TR transfer functions on a soft mask, per-sample overprint on images (now counted, as overprint_images_unsupported), and the per-colorant buffer everything above needs.
Verification
cargo test --workspace green; cargo fmt --check and cargo clippy --all-targets -- -D warnings clean; pdfce-core / pdfce-render / pdfce-cli carry zero GUI dependencies; THIRD_PARTY_LICENSES.md regenerated and byte-identical, so no dependency changed.
Full-corpus render parity — 4,023 files — was run twice, once on this tree and once on a worktree built at the commit before the compositor work, and is identical bucket for bucket, band for band, naming the same single unexplained file. That is the intended result: the new arithmetic is confined to the cases that were already wrong.
166 commits since v0.6.0. v0.6.1 was never cut, so v0.6.0's notes — which say in as many words "soft masks are not implemented" — have been the published state of this project for three days longer than they were true.
Windows x64, single-folder portable. No installer, nothing written outside the folder — smoke-tested by copying the folder to a fresh path and running both binaries from it, including the GUI, which creates its userdata/ inside its own directory rather than anywhere else on the machine.