Main's #6663 added InterpreterWarning/RenderWarning::Type3WidthsMissing (patch
90) and a decoded-image cache that records warnings as flags (patch 91). Both
warning variants are kept in every match (renderer, automation, CLI), and
MalformedIndexedImage takes flag 32 in warning_bit/warnings_in. The ledger
keeps main's rows with (80) added; both sides' raster test modules are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Keeps main's vendor ledger with hayro-interpret patch (61) added, main's
BlendMode import beside the patch's Point and Cow in path.rs, and both sides'
stroke test modules (main's stroke_degenerate_cubic and stroke_zero_end_tangent
fix vello_common's stroker; this patch shapes subpaths in hayro-interpret).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Main's #6595 made a soft hyphen invisible and a line ending in one run on, so
the line-end hyphen readings now cover HYPHEN-MINUS and HYPHEN only, pass over
invisible characters, and keep main's zero width space word boundaries in each
reading. The reference oracle combines both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A cubic whose second control point is its end point (as the `y` operator builds,
ISO 32000-2 8.5.2.1 Table 58) has a zero derivative at its end. kurbo 0.13.1's
offset curve looks for cusps with a root search on the curvature, which grows
without bound towards that end; at some widths the search stops on the end point
and normalising the zero tangent there gives NaN. vello_common's fill drops any
outline with NaN, so the whole stroke vanished (a two-curve `y` path drew nothing
at 16, 32 and 63 pt; a PDFBox tracker file lost its 32 pt "XX" outline).
The NaN fallback that #4913 added to `flatten::stroke` now also moves such a
control point the stroke tolerance towards the previous control point before
stroking again (the curve moves by at most 4/9 of the tolerance). Outlines that
come back finite take the old path unchanged. The vendor ledger row also gets
back #4913's entry, which a merge had dropped.
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- is_symbol_name: after `,` or `-` only a style name (SYMBOL_STYLES) keeps
the Symbol font, so `Symbol-Helvetica` keeps its declared encoding and
Latin stand-in.
- Tests: the negative names draw the same letters as Helvetica; a SymbolMT
with /Differences [109 /alpha] draws alpha; an embedded SymbolMT draws
its own program.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- CffFontBlob::new moves the subfonts it validated into the yoke instead
of reading every font dict a second time.
- a_program_with_no_usable_font_dict_draws_no_square now checks that the
page renders exactly as it does with no embedded program; the old
inequality passed with the empty-dict check removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A non-embedded TrueType `SymbolMT` (Symbol's TrueType name) with
/WinAnsiEncoding and the nonsymbolic flag was not recognised as the
standard Symbol font and fell back to Helvetica, so code 0x6D drew "m"
where PDFium, MuPDF and Poppler draw "mu". `SymbolMT`, and `Symbol` or
`SymbolMT` with a `,`/`-` style suffix, now select Symbol, which draws
through its built-in encoding like `/Symbol` already does.
ISO 32000-2 §9.6.2.2, §9.6.5.1. PDFBOX-5844 (13069708-exportByExcel.pdf).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A CID CFF program whose subsetter emptied the font dicts no kept glyph
uses (no Private DICT) dropped the whole font: CffFontBlob collected its
subfonts with `collect::<Option<Vec<_>>>()?`. The subfonts are now kept
as `Option`s; a glyph whose font dict can't be read is missing, the rest
draw. The program is still refused when no font dict is usable.
ISO 32000-2 §9.7.4.2, §9.9 Table 124; Adobe TN #5176 (FDArray, FDSelect,
Private DICT). PDFBOX-5843 (13069603-123.pdf) now matches PDFium, MuPDF
and Poppler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
vendor/README.md conflicted: taken from main, with this PR's Indexed
lookup patch added to the hayro-interpret row as patch (80), past main's
highest (75) and the numbers open PRs use (76-79). Its code comments
(cache.rs, color.rs, context.rs, interpret/mod.rs, x_object.rs) and the
render tests that name it say (80) too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Conflicts:
- vendor/README.md (hayro-interpret row): kept main's row; main does not
use patch number (67), so this PR's (67) entries go in numeric position
(before (71)) in both the Patches and Test columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Conflicts:
- vendor/README.md: the hayro and hayro-jbig2 rows are main's (this PR
does not change them). The hayro-interpret row is main's, with this
PR's (61) entry inserted after (59) in both the patch column and the
tests column; no other number moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Conflict in vendor/README.md (hayro-interpret row): main added patches (71), (72) and (74); this branch adds (65). main's row is kept, with (65) placed after (64) in the patch list and in the tests list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
main gave the colour-space constructors a nesting depth (ColorSpace::within) and read the
Indexed lookup once by type (patch (71)). Indexed::new keeps this PR's recovery (hival
truncated and clamped, black past a short table, no table skipped) with main's depth bound;
vendor/README.md takes main's rows with this PR's patch (60) entries after (59).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
disconnected_subpaths_on_one_line_leave_the_gap_between_them_unpainted
checked overlapping subpaths drawn in opposite directions under f* only.
It now checks the W* clip of the same path keeps their union too: with
the clip area pushed under the path's own even-odd rule instead of
non-zero, the overlap goes missing and the new assertion fails.
Review on #4433 (CodeRabbit, crates/render/tests/it/zero_area_fill_clip.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
ISO 32000-2 §8.5.2.1, Table 58: h "terminates the current subpath.
Appending another segment to the current path shall begin a new subpath,
even if the new segment begins at the endpoint reached by the h
operation." kurbo carries a segment after ClosePath on from the subpath
start without a MoveTo, so resolve_degenerate_subpaths saw "h" then an "l"
back to that point as part of the closed subpath, and the round-cap dot
of that degenerate subpath was lost (main drew it, through the nudge this
PR removed). l, c, v and y after h now start with a MoveTo to the point h
reached.
Review on #4410 (CodeRabbit, vendor/hayro-interpret/src/interpret/mod.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Tests for the mutation pass on c709dd99e:
- a_fill_colour_in_a_space_with_no_table_keeps_the_fallback now covers
CS/SC too;
- a_shading_in_a_space_with_no_table_is_not_drawn checks that only
images and the colour operators see a tableless space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Seven readers were compared on synthetic files. Most draw the entries a
short table holds and black past them, and none draws an image whose
lookup is missing.
- Indexed::new keeps how many entries the table holds in full. Indices
at or past that are set to black after conversion, so DeviceCMYK,
ICCBased, Separation, DeviceN and Lab come out black too (zero bytes
gave white, or blue in Lab). Fill and stroke colours follow the same
rule. An incomplete last entry counts as missing.
- A lookup that is missing, null, a number, or a stream that does not
decode (or decodes to nothing although it holds data) is no table.
ColorSpace::new_allowing_tableless builds such a space marked
tableless (for the resource cache and ImageXObject::new only), and
the image is skipped with MalformedIndexedImage. A cs/CS colour in it
keeps the upstream DeviceGray fallback with ColorFallback. Every other
caller still gets None, as upstream.
- hival clamping and truncation are unchanged, and so is case C.
- The warning now reads "an image with a malformed Indexed colour table
may be drawn wrongly or not at all".
- vendor/README.md: the patch (60) row is one line again. 3275f853c
split it.
Tests: indexed_lookup.rs now has 25 tests, including one per base space
(DeviceGray, DeviceCMYK, Lab, Separation, DeviceN, and ICCBased N=4
drawn through its alternate). Against 6179cf795, 13 fail, including
every base space except DeviceGray, where zero is black. Against
b6f93a3ae, 19 fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
* M2: a shrunk image on a turned or mirrored page snaps to whole pixels (#5530)
hayro patch 14 (`snap_minified_image`) shrinks an interpolated image once
to its whole-pixel device rectangle and draws that 1:1, instead of
shrinking to a rounded-up size and resampling again at a fractional
scale and offset. It only accepted an upright transform (a > 0, d > 0,
b = c = 0), so every image on a page under /Rotate 90, 180 or 270 -- and
any image placed turned or mirrored by its own matrix -- still took the
double resample. That is #5530: a scanned page that matches PDFium
upright drifts and softens when the same page is rotated.
The snap now accepts every transform whose axes lie along the device's
(the eight quarter-turns and mirrors) and returns the 1:1 transform
that draws the shrunk raster from the snapped corner, one source-axis
step a whole device pixel, in whichever direction that axis runs.
Skewed and freely rotated images keep the old path.
On a synthetic page (crop box and scale on whole pixels, image at
fractional coordinates), /Rotate 90, 180 and 270 renders turned back
differed from the upright render in 72% of pixels, by up to 200 grey
levels; now they are identical. Non-interpolated images were already
identical and stay so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: snap only when the dropped skew is invisible, and only a resize that succeeded
Review of #5640:
- The snap judged an image axis-aligned by a tolerance on its
coefficients (1e-4 of the scale). For a very wide image a skew that
small still moves the far edge by more than a pixel (40 000 samples
at 4e-5: 1.6 px), and the snap flattened it. It now measures what it
drops: how far each off-axis term moves the far edges, at most a
hundredth of a device pixel (SNAP_SKEW). Upright and turned alike.
- A resize that fails hands back the source raster at source size; the
1:1 snapped transform then drew it at that size. The snapped
transform is now used only when the shrink produced the snapped
size; otherwise the full transform, as before.
- The mirrored case of the edge test drew a uniform image, which looks
the same mirrored; it now also mirrors a half-black image and checks
which side the black lands on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: the turned-page test draws every kind of image the snap reaches
The /Rotate oracle test drew one 8-bit grey image. Stencil masks (CCITT
scans often are one), 1-bit images and images with a soft mask reach
the snap through other paths, so it now draws all five: 8-bit and
1-bit grey, a stencil mask, a soft mask of the image's size and one of
another size. On main, every interpolated kind is off by 113-217 levels
over 63-73% of the image when turned, and so is a non-interpolated
image with a smaller soft mask (110 levels, 16%): hayro patch 15
filters such a mask whatever /Interpolate says, so it is snapped
upright and was not when turned. All are identical with the fix; the
other non-interpolated kinds were already identical and stay as guards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Mutation pass on patch (60):
- keying the resource colour-space cache with one key for every space
survived the earlier tests, which used one Indexed resource a page:
raster::tests::indexed_lookup_cache::two_indexed_resources_are_cached_apart
kills it;
- the ColorFallback report for a recovered space set with CS had no
test (only cs was tested): a stroke case in
a_fill_or_stroke_colour_in_a_short_table_is_padded_and_reported fails
with that report removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Patch (60) skipped such an image. A reviewer's side-by-side tests on #4393
(seven cases against main and Chrome's viewer) showed the skip dropped
colours other readers draw, and regressed a real hival that main draws.
Indexed::new now recovers instead:
- a lookup table shorter than m x (hival + 1) bytes, missing, or not a
string or decodable stream is padded with zero bytes;
- a hival past 255 is taken as 255;
- a real hival is truncated toward zero (as upstream read it).
The space records that it was recovered. ImageXObject::new raises
MalformedIndexedImage from the space it built; a cs/CS colour in such a
space reports ColorFallback, as the upstream DeviceGray fallback did. A
hival below zero or not a number has no reading: the space falls back as
upstream, and the image reports the warning. Index values past hival
stay clamped (8.6.6.3).
The separate is_malformed_indexed check is gone, so no lookup table is
read twice: inline images naming an Indexed resource read its table once
through the resource cache (CodeRabbit, interpret/mod.rs:945). The read
count is exposed as InterpreterCache::indexed_lookup_reads.
Tests: indexed_lookup.rs rewritten as draw tests (17, cases A-G and
bounds); raster::tests::indexed_lookup_cache. On b6f93a3ae logic 14 of
the 17 fail (the three that pass are guards: full table, case C, device
name) and the cache test reads [21, 41] instead of [1, 1].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
- render_warning_text (crates/automation/src/pages.rs, on main since this
branch was cut) matched RenderWarning exhaustively and did not build
with the new variant. It now names it, in the CLI's words, so
page_render and render_preview report the skip. A workspace search
finds no other match on RenderWarning outside pdfcraft-render.
- An Indexed lookup is a string or a stream (8.6.6.3). Indexed::new and
is_malformed_indexed tried it as a stream first; a failed stream read
of a reference consumed the reference, so a string lookup given by
reference was never found and the image was skipped. The lookup is now
read once and dispatched by type, in both places.
- hival is judged before the base: an out-of-range hival rules the space
out even when the base cannot be read (the table size still needs the
base, so a usable hival keeps today's handling there).
The size check is unchanged: hival is range-checked to 0..=255 before
(hival + 1) * m is formed with checked_add/checked_mul.
Tests (each seen failing with a fault injected): bounds at DeviceCMYK,
DeviceGray and hival 255 (exact and one short); hivals past u32/u64/any
integer, negative and fractional (with unchecked arithmetic and the range
widened the test panics with "attempt to multiply with overflow"); a
lookup string by reference (fails with the old stream-then-string read);
an unreadable base with hival 300; an undecodable lookup stream; and
page_render reporting the skip (without the new arm the crate does not
build, E0004).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv