Commit Graph
98 Commits
Author SHA1 Message Date
aiongg 69e755c439 Merge r2 track t08: fonts
# Conflicts:
#	crates/automation/src/tools.rs
#	vendor/README.md
2026-10-22 02:20:50 +00:00
aiongg 4d5025c6da Merge r2 track t07: text-extract-select
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/render/src/raster.rs
#	vendor/README.md
2026-10-22 02:20:03 +00:00
aiongg e22767f706 Integration: #2974 knockout erase pass reads form resources through Resources::for_form (#3637 made them optional) 2026-10-22 02:15:20 +00:00
aiongg ec6a8166bb Integration: #3020 number its hayro-interpret patch 48 (40 is #2987's) 2026-10-22 01:51:55 +00:00
aiongg e2d3f30c63 Merge PR #3020: fix(render): draw each ICC profile through itself when two have the same stream dictionary (vendored hayro-interpret patch 40)
vendor/README.md: main's row plus the PR's hayro-interpret patch and tests, numbered 48 (40 is #2987's)
2026-10-22 01:51:47 +00:00
aiongg e61515f324 Merge PR #2987: fix(render): a visibility expression decides what an OCMD shows (#2986)
vendor/README.md: main's row plus the PR's hayro-interpret patch 40 and its test
2026-10-22 01:51:22 +00:00
aiongg b84121e978 Merge PR #3327: Render: a five-point polyline is no longer painted as its bounding box
vendor/README.md: main's row plus the PR's hayro-interpret patch and tests, numbered 47 (35 is #3296's)
2026-10-22 01:51:10 +00:00
aiongg 554d9105f3 Merge PR #2974: fix(render): knockout transparency groups knock out (#2971)
crates/render/src/raster.rs: both new tests (#2975 stencil masks, #2971 knockout groups)
vendor/hayro/src/renderer.rs: set_paint returns main's PaintSetup; the PR's knockout erase early return wraps its clip in PaintSetup::Clip
vendor/README.md: main's rows plus the PR's hayro-interpret patch 39 and hayro patch numbered 22 (19 is #3296's), with tests
2026-10-22 01:49:59 +00:00
aiongg 61cbaf3e23 Merge PR #3246: Shading patterns use the stroke or fill alpha current at paint time (#3176)
vendor/hayro-interpret/src/pattern.rs: ShadingPattern::new takes the default colour spaces (#3018) and no opacity (this PR); without_opacity (#2977) and set_alpha both kept
2026-10-22 01:49:02 +00:00
aiongg c3c0bea0a1 Integration: #2977 number its hayro-interpret patch 46 (35 is #3296's) 2026-10-22 01:48:40 +00:00
aiongg 1ddbb5f3ff Merge PR #2977: fix(render): a stencil mask is drawn at the fill opacity once (#2975)
vendor/hayro/src/renderer.rs: main's PaintSetup::Clip match with the PR's fill paint (is_stroke false) and comment
vendor/README.md: main's row plus the PR's hayro-interpret patch and test, numbered 46 (35 is #3296's)
2026-10-22 01:48:34 +00:00
aiongg a8b7d14709 Merge PR #3035: fix(render): mesh triangles that share an edge leave no seam
vendor/hayro-interpret/src/encode.rs: both sides; a pixel sampled by either test (corner held, or centre covered) widens the mesh extent from #3296
vendor/README.md: main's row plus the PR's patch 36 and its test
2026-10-22 01:47:35 +00:00
aiongg 5f256d53fd Merge PR #3958: perf(render): look CFF glyph names up in a map (vendored hayro-interpret patch 44)
vendor/README.md: hayro-interpret row keeps patches (38) and (44) and both tests, in number order
2026-10-22 01:47:18 +00:00
aiongg 40d4566c61 Merge PR #3296: Renderer: sample a mesh shading over its triangles alone, not the whole path
vendor/README.md: main's rows plus the PR's hayro-interpret patch 35 and hayro patch 19 with their tests
2026-10-22 01:47:04 +00:00
aiongg e47b64b009 Merge PR #2949: M2: a post table version with swapped halves doesn't make a font unusable (#2946)
vendor/README.md: hayro-interpret row keeps #2914's patch (26) extension and #2949's new patch (38) with both tests
2026-10-22 01:46:55 +00:00
aiongg b1c8ae231a Merge PR #3643: fix(render): draw missing spacing glyphs in Japanese CID substitutes as spaces (#3601)
ROADMAP.md: progress log keeps both sides' lines
2026-10-22 01:46:04 +00:00
aiongg fd0c47f7be Merge PR #3637: Fix named properties in forms without resources
ROADMAP.md: progress log union, newest first
2026-10-22 01:45:57 +00:00
aiongg 0b51ea4baa Merge PR #3018: fix(render): draw device colour in the resources' /DefaultGray, /DefaultRGB and /DefaultCMYK (vendored hayro-interpret patch 38)
vendor/README.md: main's single hayro-interpret row plus the PR's patch 38 and its tests, placed before 41
2026-10-22 01:45:41 +00:00
aiongg add6907f6a Merge PR #3239: M3: extract text from glyph names outside the reference-app Glyph List (#3169, #3170) 2026-10-22 01:45:33 +00:00
aiongg dbb71754d0 Merge PR #3497: M2: Type 0 fonts with /ToUnicode /Identity-H read their codes as Unicode
contributors/people.toml: kept both new contributor entries.
2026-10-22 01:45:33 +00:00
aiongg 4a73cacb85 Merge PR #3116: fix(render): emit clip-only text (7 Tr) to devices for text extraction (#3109) 2026-10-22 01:44:50 +00:00
mtlcl410andClaude Opus 5.5 09668ce885 M10: a swapped post 2.0 table's glyph names serve /Differences (#2946)
Review follow-up on #2949.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-22 08:40:08 +08:00
mtlcl410andClaude Opus 5.5 b67a8bf268 M2: a post table version with swapped halves doesn't make a font unusable (#2946)
Vendored hayro-interpret refuses an embedded font whose `post` table
version isn't 1.0, 2.0, 2.5 or 3.0 (a guard from pdf.js issue 9462).
ScanSoft PDF Create! writes the version of every font it embeds with its
16-bit halves swapped (`00 00 00 03` for 3.0), so those fonts were refused
and their text drawn with a substitute: Hebrew came out as Latin
look-alikes or not at all.

1.0, 2.0 and 3.0 with swapped halves are now accepted too; any other
unknown version is still refused. PdfCraft patch (38) in vendor/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-22 08:40:08 +08:00
narain 9c2f4e3529 perf(render): look CFF glyph names up in a map (vendored hayro-interpret patch)
CffFontBlob::glyph_index_by_name searched the whole charset, with a string lookup per glyph, for every character code of a font (there was a TODO noting it). Build a name to glyph map on first use and share it between clones. The first glyph with a name still wins, so results are unchanged.
2026-10-21 17:14:05 -07:00
Hamza e3fc82938f A rectangle's corners are equal, not nearly equal
is_nearly_equal allows a 256th of a path unit, and stroke_path_impl
applies the matrix after path_as_rect, so a cm of 10,000 turns that
tolerance into tens of device units: a path that only nearly closes was
painted as the rectangle, with a side it does not have, and an edge off
true by that much counted as axis-aligned.

The fast path now compares coordinates exactly. Nothing that is a
rectangle loses it: a rectangle's coordinates are equal in the content
stream, not approximately equal. A path that misses takes the general
route and is painted as written, which is the safe direction.

I could not make a rendered case that distinguishes the two: this
renderer paints a 10,000x cm as a solid page whatever the line width, so
there are no pixels to compare at the scale where the tolerance shows.
The change rests on the argument above, and the three existing tests
still pin the behaviour that is measurable.

Reported by CodeRabbit on #3327.
2026-10-20 23:27:49 +05:00
Hamza 9a997cf1b1 A rectangle is a traversal, not four corners visited
FredApps's note on #3327: five points that are all on the bounding box's
corners still need not trace the rectangle. Three sides and a step back
along the third --

    10 10 m 30 10 l 30 30 l 10 30 l 30 30 l S

-- visits every corner, so it was taken as the box and stroked with a
left side the path does not have.

path_as_rect now asks what a rectangle actually is: four distinct
points, each step from one to the next along a single axis, and the last
point back at the first (written as the fourth LineTo or as ClosePath).
That is a rectangle and nothing else is, so the corner test and its
off_corner flag are gone -- four points of an axis-aligned traversal are
necessarily the corners of their own bounding box. A degenerate path,
whose points are not distinct, now takes the general route as well,
where it painted a zero-width or zero-height rectangle before.

The vendored-patch table has the row this change was missing: without
it a re-vendor of hayro-interpret would drop the patch silently, which
is the other half of FredApps's review.
2026-10-20 21:28:05 +05:00
jpeskovandClaude 9175291a8f fix(render): draw missing spacing glyphs in Japanese CID substitutes as spaces (#3601)
UniJIS-UCS2-HW-H maps code 0x0020 to CID 231 / U+2002. The OpenType
substitute for a non-embedded Japanese CID font lacks that glyph, so
.notdef boxes were drawn. Type0Font::map_via_unicode now falls back to
the substitute's ordinary-space glyph for explicit spacing characters
its charmap lacks. Native and variation-selector glyphs keep priority;
PDF advances and Unicode text are unchanged.

Fixes #3601.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-20 17:33:37 +05:30
Ahmad Mujahid 3b3787078e Fix named properties in forms without resources 2026-10-20 15:39:35 +04:00
mtlcl410andClaude Opus 5.5 f6255c0ed6 M3: numbered glyph names read as nothing again, not as a StandardEncoding guess (#3169, #3170)
Review on #3239: a name that only numbers the glyph (C39, c251) says
nothing about the character. Reading it as the code's StandardEncoding
character invents text wherever the font's /Encoding puts the glyph
elsewhere: with /C1 at 233 it extracted O-slash for a glyph drawn as
e-acute. numbered_glyph_name and the fallback are removed; the AGL
specification's full mapping stays.

The test puts one glyph per line: the test font has none of these glyphs,
so they have no width, and on one line a later 'fi' sat on the first and
was dropped as its fake-bold copy once the guessed glyphs between them
were gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-20 15:54:51 +08:00
Tiago KielandClaude Opus 5.5 3b6d344eb5 fix(render): find comma-below letters by character (refs #1666)
A font naming a Latvian comma-below letter by its code point (/uni0136) or by
the glyph list's name for it (/Kcedilla, /Gcedilla...) still drew nothing:
the name resolves to the cedilla name the glyph list prefers, which the
composite table does not hold. The thirteen comma-below letters are now
looked up by character, which also replaces the special case for U+021A and
U+021B. T and t with a cedilla (U+0162, U+0163) are not in that table and
keep their cedilla.

Raised by CodeRabbit on the PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:51:29 -04:00
Tiago KielandClaude Opus 5.5 88735a7003 fix(render): draw comma-below letters in non-embedded standard fonts (refs #1666)
The stand-ins for the standard fonts (PDFium's Foxit fonts) have neither the
comma-below letters nor a commaaccent glyph, and the composite-letter patch
left comma accents out. A non-embedded font naming /scommaaccent therefore
drew nothing, which is the blank "si" left in Romanian XFA forms, and
/tcommaaccent drew a cedilla through the glyph list's alias to U+0163.

Romanian s/S/t/T and Latvian k/K/l/L/n/N/r/R/G with a comma below are now
built from the letter and the font's comma, scaled to three quarters and set
just under the baseline, moved back along the slant in italics. The uni021A and
uni021B names of T and t with a comma below, which the glyph list
does not name, resolve by code point. Lowercase g with its turned comma above
is still not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:25:14 -04:00
Claude b88c8d79ab M2: the two Identity-H ToUnicode rules point at each other
Review follow-up on #3497: the editing map in crates/fonts and the vendored
renderer each read an identity /ToUnicode name as code = Unicode without
controls or lone surrogates; each now names the other, so a change to one
is made to both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
2026-10-19 18:07:05 +00:00
Claude 7f9d86c915 M2: doc comments for the Identity-H ToUnicode paths
Type0Font::new and char_code_to_unicode get doc comments, and
unicode_map's names the identity ToUnicode name. The comments and
vendor/README.md cite Acrobat's observed behaviour only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
2026-10-19 17:07:36 +00:00
Claude 62f306ba4c Merge branch 'main' of https://github.com/storytold/pdfcraft into fix/identity-h-tounicode
# Conflicts:
#	contributors/people.toml
2026-10-19 17:05:37 +00:00
Earl Co 8b88a2d14b M2: fix shading bounds in Type 3 glyphs (#3178) (#3412) 2026-10-19 13:01:14 -04:00
Lcoding15andClaude Opus 5.5 96db0e3bb5 M2: Type 0 fonts with /ToUnicode /Identity-H read codes as Unicode
WSCAD (and other CAD exporters) write `/ToUnicode /Identity-H` — a name,
not a CMap stream — on composite fonts whose two-byte codes are the UTF-16
code units. Acrobat, MuPDF and pdf.js read each code as its Unicode value.
hayro resolved the name to the Identity CMap, which maps codes to CIDs only,
so such pages had no text at all: nothing could be selected, searched,
copied or edited (147 of 147 pages of a WSCAD schematic).

- vendored hayro-interpret patch 35: `Type0Font::identity_to_unicode`
- crates/fonts: the same reading for Edit text, find and redaction
  (controls and lone surrogates map to nothing; simple fonts unaffected)

Tests fail without the fix: render `identity_to_unicode_names_give_text`,
fonts `identity_to_unicode_names_read_codes_as_unicode`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:20:28 +00:00
mtlcl410andClaude Opus 5.5 bae6981959 fix(render): the device decides whether a knockout group's image clears its rectangle (#2971)
Review on #2974: the interpreter decided with a full-resolution decode
(`decoded_raster(None)`) before the renderer decoded at its draw size, a
second decode of every downscaled image under another cache key; and an
image a preview skips for its size still cleared what was under it.
`Device::knockout_image` now lets the renderer decide, with the same
eligibility and decode size as `draw_image` (shared `image_draw_size`), so
the draw reuses that decode and a skipped image clears nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:34:34 +08:00
mtlcl410andClaude Opus 5.5 3df8a6386d fix(render): a knockout clear follows a shading's /BBox and skips images that don't decode (#2971)
Review on #2974. In the erase pass `set_paint` returned no clip, so a
shading fill cleared its whole path while the redraw is clipped to the
shading's /BBox (ISO 32000-2 Table 77): red outside the box stayed erased.
It now returns the shading's clip there too. And an unmasked image cleared
its placement before decoding, so an image that doesn't decode (over
MAX_IMAGE_PIXELS, damaged data) erased the group's earlier artwork and drew
nothing; the clear now happens only when the image decodes, asked only in a
knockout group (Device::in_knockout_group). The stencil case in the test now
also checks that the stencil draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:03:57 +08:00
mtlcl410andClaude Opus 5.5 2627384757 fix(render): images and nested groups knock out in a knockout group too (#2971)
An image or a nested group is drawn into a layer of its own, which
covers the whole canvas (transparent where nothing was drawn), so it
can't be composited with Compose::Copy. What is under its shape is
now cleared first and the layer then composites over it:

- an unmasked image's shape is its placement rectangle: the
  interpreter hands it to the device (`Device::knockout_shape`), which
  clears it with a transparent Copy fill;
- a nested group's shape is the union of its elements' shapes: the
  interpreter draws its content once in the device's erase mode
  (`knockout_erase_wanted`, `begin_knockout_erase`,
  `end_knockout_erase`), where every paint clears under its shape and
  nothing is layered or drawn as an image, and then draws the group,
  always in a layer of its own (drawn straight onto the knockout
  group, its own elements knocked each other out).

A masked image's shape is its mask; it composites as before, which
matches MuPDF where the mask is opaque or transparent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:37:30 +08:00
mtlcl410andClaude Opus 5.5 d2bcda45fd fix(render): knockout transparency groups knock out (#2971)
In a knockout group (/K true) each element composites with the group's
initial backdrop, not with the elements drawn before it (ISO 32000-2,
knockout groups). The renderer read /K (hayro-interpret patch 28) but
drew such a group like any other, so overlapping semi-transparent
shapes in it blended with each other.

hayro-interpret patch (39) tells the device a group is a knockout
group (`Device::set_knockout`, default no-op). hayro patch (19) then
draws that group's fills, strokes, glyph outlines and shadings with
Compose::Copy, which vello_cpu weights by coverage, so each replaces
what the group drew under its shape. Images and nested groups are
drawn into a group layer of their own and still composite normally:
copying a whole layer would erase everything outside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:36:46 +08:00
mtlcl410andClaude Opus 5.5 49fa11942f M10: glyph names with lowercase hex digits map to nothing (#3170)
Review on #3239: the Adobe Glyph List Specification's `uni` and `u` forms
take uppercase hexadecimal digits only (section 2), and its examples map
`uni20ac` to an empty string. glyph_name_to_text accepted lowercase too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 12:22:00 +08:00
mtlcl410andClaude Opus 5.5 421a0dd9c9 fix(render): mesh triangles that share an edge leave no seam
The mesh sampler took each pixel's colour at its corner, and only when the
corner was strictly inside a triangle. A corner on an edge two triangles
share belonged to neither, so a Type 4-7 shading drew a white seam along
every shared edge that ran through the sample grid, and dropped pixels
along the mesh's own sides (a 30 x 30 square cut along its diagonal:
812 of 900 pixels; MuPDF 900).

Such a pixel now goes to the triangle that covers its centre, edges
included (Triangle::covers), and still takes the colour at its corner, so
every pixel that was drawn keeps its colour. Vendored hayro-interpret
patch 41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:16:05 +08:00
mtlcl410andClaude Opus 5.5 785d10724f fix(render): pattern-filled stencil masks take the fill paint at the opacity once (#2975)
Review follow-up. A pattern paint keeps the fill or stroke opacity it was
set with (`scn`), so resetting the graphics state's fill alpha did not stop
a pattern-filled stencil mask from being drawn at the opacity squared:
`Pattern::without_opacity` now drops it for the mask's paint (patch 35).
The renderer also painted a pattern-filled stencil mask with the stroke
paint, so an uncoloured tiling pattern drew in its stroke colour (black by
default); it now uses the fill paint.

The stencil test covers a colour fill, an uncoloured tiling pattern and a
shading pattern at ca 1, 0.5 and 0.25; each of the three changes, removed,
makes it fail. Rebased onto main; the vendored hayro-interpret patch is now
number 35 (36 was taken on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:07:46 +08:00
mtlcl410andClaude Opus 5.5 1cf6ca5579 fix(render): a stencil mask is drawn at the fill opacity once (#2975)
hayro-interpret draws every image XObject inside a transparency group
that applies the fill opacity (ca). A stencil mask (/ImageMask true)
also takes the fill colour, with that same alpha, as its paint, so the
opacity was applied twice: at ca 0.5 a mask was drawn at 25 %, at
ca 0.25 at about 6 %. Rectangles and raster images were right.

Vendored hayro-interpret patch (36) resets the fill alpha to 1 once
the image's group is pushed, before the stencil's paint is taken; the
saved state is restored after the image. Type 3 glyph and pattern
stencils take their paint from the glyph or pattern and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:07:09 +08:00
mtlcl410andClaude Opus 5.5 d0051fe68b M10: a visibility expression naming an object that isn't a listed group is malformed (#2986)
Review follow-ups on #2987; the patch (40) test reference moves into the Test column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 06:05:16 +08:00
mtlcl410andClaude Opus 5.5 39b96ccf3d fix(render): a visibility expression decides what an OCMD shows (#2986)
An optional content membership dictionary may carry a visibility
expression (/VE, PDF 1.6): [/And e...], [/Or e...] or [/Not e], each e an
optional content group or a nested expression. It takes precedence
over /OCGs and /P (ISO 32000-2 8.11.2.2). hayro-interpret ignored it, so
content under such a dictionary stayed visible when its layers were
hidden in the Layers panel.

Vendored hayro-interpret patch (40) evaluates the expression, nested
up to 32 deep. A malformed expression (unknown operator, /Not without
exactly one operand, /And or /Or without any, an operand that is
neither a group nor an expression, too deep) falls back to /OCGs and
/P as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:05:00 +08:00
Hamza 645a8ca205 Render: a five-point polyline is no longer painted as its bounding box
`path_as_rect` decided a path was a rectangle from its element count and from
the four corners of its bounding box being covered. Its comment names the shape
it means to match -- one MoveTo, three LineTo, one ClosePath -- but `len() != 5`
also admits one MoveTo and four LineTo, a five-point open polyline, and
covering the corners says nothing about the points between them. Both call
sites then paint `device.draw_rect(bbox)`, so such a path was drawn as its own
bounding box.

Rendered through `pdfcraft-cli render`, an open zigzag whose ends reach the
corners of its bounding box

  80 38 m  80 21.75 l  60.571 29.875 l  40 21.75 l  40 38 l  S

lost its middle peak and gained a straight edge the file does not contain: no
dark pixel near the peak, and 36 along the bounding box's top. A six-point
zigzag and a real rectangle both drew correctly, which is why this shows on
some art and not the rest -- and `s` escapes it because its ClosePath makes six
elements.

A point that is not a corner now rejects the path. A rectangle still matches
whether written `m l l l` and closed or `m l l l l` back to its first corner,
since every one of its points is a corner. The same mistake also discarded a
clip of that shape, through `push_clip_path`, which this fixes too.

Found from craft-libs' ATTRIBUTION note that the shared reader centralizes the
patched Hayro trees from PdfCraft: the same flaw is in craft-libs, sent there
as storytold/craft-libs#175.
2026-10-19 00:29:13 +05:00
mtlcl410andClaude Opus 5.5 ecb990c8ce M10: glyph names: a uni part with a surrogate group maps to nothing; numbered names read under a suffix (#3170)
Review follow-ups on #3239.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 01:57:14 +08:00
MarekSpodBiedry 853851075f Mark the alpha changes as PdfCraft patches 2026-10-18 18:37:46 +02:00
Oliver HagmannandClaude Fable 5.1 fe7169c8eb Renderer: sample a mesh shading over its triangles alone, not the whole path
A BricsCAD plan page (3370 × 1524 pt, 251,000 single-segment strokes and 310
one-triangle type 4 shadings painted with `sh` at 50% alpha) took 2 s per
1024 px tile at every zoom, and 7.9 s for a fit-to-width render; on a busy
machine the 20 s watchdog gave the page up ("page 57 took longer than 20 s and
was skipped"). A 16 px tile of the same page took 65 ms, so neither parsing nor
the strokes were the cost: `sh` fills the whole canvas with the shading, and
`set_paint` sampled the mesh texture over the whole path, one hash lookup and
one texel per canvas pixel, 310 times per tile.

hayro-interpret patch (35): `EncodedShadingPattern::extent`, the device-space
pixels a mesh's triangles touched when it was encoded (`None` for shadings that
paint everywhere, and for a mesh with a `/Background`). hayro patch (19):
`set_paint` returns `PaintSetup::Nothing` for a mesh with no triangle on the
canvas (an empty extent, tested before it is intersected: `Rect::ZERO` sits at
the canvas origin), which the fill, stroke, rect and stencil callers skip, and
otherwise intersects the texture with the extent and pads instead of repeating it.

Measured with `page_timing` on the page: fit 7864 → 880 ms, a 1024 px tile
2030 → 85 ms at 100% and 2030 → 70 ms at 400% (the 16 px tile's 65 ms is the
page's re-interpretation, paid per tile), with byte-identical output hashes.
The three `mesh_extent_tests` pin what the texture must and must not cover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 18:32:33 +02:00