Skip to content

fix(render): evaluate multi-input Type 0 tint transforms for DeviceN - #949

Merged
yfedoseev merged 1 commit into
mainfrom
fix/issue-859-devicen-multi-input-type0-tint
Jul 26, 2026
Merged

fix(render): evaluate multi-input Type 0 tint transforms for DeviceN#949
yfedoseev merged 1 commit into
mainfrom
fix/issue-859-devicen-multi-input-type0-tint

Conversation

@yfedoseev

Copy link
Copy Markdown
Owner

Summary

Closes #859.

Corrected root cause. The issue title (and its own follow-up triage comment) suspected Type 4 (PostScript calculator) tint transforms. That's stale — the actual repro file (devicen.pdf, a pdf.js corpus fixture) has an incremental update, so the live content stream (verified with pikepdf, which resolves the xref/generation chain correctly — naive byte-search over the raw file finds a stale first revision) uses a Type 0 (sampled) tint transform on a 3-channel /DeviceN [/Red /Green /Blue] color space, /Domain [0 1 0 1 0 1], /Size [13 13 13] — genuinely 3-dimensional.

resolve_separation_or_devicen in src/rendering/resolution/color.rs only ever forwarded components[0] (the first scn operand) to the Type 0/3 evaluator, discarding every other input channel. That's invisible for the common single-channel Separation case, but this DeviceN space always supplies 3 components. The 1-D-only sampled-function evaluator (evaluate_type0_sampled) then rejected the request (Size.len() == 3 != 1) and fell back to the long-standing gray = 1 - components[0] heuristic. Shape 1's first component was 1.0 → black. Shapes 2 and 3's first component was 0.0 → pure white, invisible against the white page — the "missing shapes" symptom. Type 4 dispatch (already correctly threading the full component vector, from #849) was never the problem.

Fix. Generalized evaluate_type0_sampled to N-dimensional multilinear interpolation across the 2^N sample-grid corners (ISO 32000-1 §7.10.2's general algorithm, of which the prior 1-D linear interpolation is the N=1 special case), with a MAX_SAMPLED_FUNCTION_DIMS = 8 guard against a pathological /Size array. Threaded the full components slice through for Type 0 (Type 2/3 stay single-input, matching their spec definition).

Test plan

  • cargo build, cargo fmt --check, cargo clippy --features rendering -- -D warnings — clean.
  • cargo test --lib --features rendering rendering::resolution::color — 16/16 pass, including two new tests: type0_sampled_function_single_dimension_matches_prior_linear_interpolation (pins the N=1 case is byte-for-byte unchanged) and type0_sampled_function_two_dimensional_input_uses_all_components (hand-computed 2-D grid, asserts a non-first-channel-dominant input produces the correct interpolated result).
  • cargo test --features rendering — full suite green.
  • Corpus regression: corpus_sig over the full local corpus (3530 PDFs) — zero-diff, identical output between main and this branch. (Expected: corpus_sig measures text-extraction signatures; this change only affects rendering-time color resolution, so it's a true negative-control confirmation that nothing else moved, not a direct exercise of the fix.)
  • Visual verification against the real repro file (devicen.pdf, not committed — CONTRIBUTING.md fixture policy): before, one black triangle + two invisible-white ones; after, three distinct green/blue/red triangles, matching poppler's reference rendering described in the issue.

Separation/DeviceN colour spaces with a Type 0 (sampled) tint
transform only ever passed the first scn/SCN component to the
evaluator, discarding every other input channel. That was invisible
for the common single-channel Separation case, but a multi-channel
DeviceN colour space always supplies more than one component: the
first-component-only read silently reduced a genuinely N-dimensional
sampled function to a 1-D lookup on one axis, which then usually fell
outside evaluate_type0_sampled's declared 1-D-only envelope and fell
back to the old "gray = 1 - tint" heuristic. Depending on the value of
just the first component that produced solid black or solid white
(invisible against a white page) regardless of what the other
components actually were.

Generalise the sampled-function evaluator to N-dimensional multilinear
interpolation over the sample grid (the existing linear-interpolation
code is exactly the N=1 case), and thread the full component vector
through for Type 0. Type 2 (exponential) and Type 3 (stitching) stay
single-input, matching their spec definition.

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
@yfedoseev
yfedoseev force-pushed the fix/issue-859-devicen-multi-input-type0-tint branch from d68f6fb to ed16f36 Compare July 26, 2026 18:48
@yfedoseev
yfedoseev merged commit 261ec8d into main Jul 26, 2026
236 checks passed
@yfedoseev
yfedoseev deleted the fix/issue-859-devicen-multi-input-type0-tint branch July 26, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rendering: Separation/DeviceN with a Type 4 (PostScript) tint transform renders black / drops shapes

1 participant