* fix(render): read each object stream's index once (vendored hayro-syntax patch 22)
`XRef::get_with` parsed an object stream's whole index of `objnum offset`
pairs again for every object it fetched from that stream, so reading all of a
stream of N objects took N² steps. Firefox keeps every object of a page in one
stream: the pdf.js corpus file bug1978317.pdf (65,542 objects in one stream)
took 37 s to draw and 40 s to extract its text, and was a known timeout in the
robustness baseline.
The parsed index is now kept per stream, beside the decoded stream data it
indexes (`ObjectStreamIndexes`), and never cleared, as that data isn't. A
malformed index is remembered as malformed; `/N` or `/First` that can't be
read (a reference that can't be followed while another stream loads) is not
remembered. An offset past `usize` is malformed instead of wrapping. A poisoned
lock is still used: entries are inserted whole.
Release build, Linux: bug1978317.pdf draws in 0.5 s (was 37 s) and extracts
its text in 1.9 s (was 40 s), and leaves the baseline's known timeouts;
40,000 pages in one stream open in 0.2 s (was 54 s).
`raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`
times out at 20 s with the fix reverted and passes in 0.2 s with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* test(render): an object stream offset past usize is malformed, not a panic
Review follow-up on #2427. Mutation testing by mtlcl410 found that replacing
patch (22)'s `checked_add` in `read_object_stream_index` with a plain `+`
survived every test, although in a debug build that `+` panics on the very
input it guards against (and wraps in a release build). Their test pins it:
it fails with "attempt to add with overflow" under that mutant and passes on
this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Release: PdfCraft v0.11.0
- Bump the workspace to 0.11.0.
- Fix main's red CI (cargo-deny): update rustls-webpki 0.103.15 -> 0.103.16
(RUSTSEC-2026-0382) and webpki-root-certs 1.0.9 -> 1.0.10 (RUSTSEC-2026-0383),
both reached through winprint's sigstore build dependency.
- `cargo xtask version set` now moves the workspace crates' pins in
[workspace.dependencies] with the version; before, `cargo update --workspace`
refused the bump and the pins had to be edited by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: a healthy private retry may answer during the watchdog pass that scheduled it
shared_timeouts_retry_healthy_pages_privately_before_blacklisting failed on
CI (PR #3893): the try_recv that runs the watchdog spawns the private retries,
and page 1's retry has no gate, so on a loaded runner its healthy result
arrived before that same call read the channel. Accept that early answer
(page 1, no error) and use it as the healthy result; an error still fails.
Reproduced by sleeping 300 ms after the watchdog in try_recv: the old test
fails every time, the new one passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
PageText::nearest measured the distance to each glyph's axis-aligned bounding
box. In sheared or turned text that box is wider than the glyph and overlaps its
neighbours, so a pointer on a letter was also inside the box of the letter before
it, at distance 0, and the earlier glyph won. A drag ending on the last letter of
a word or line then ended one or two letters short.
A glyph whose drawn em box (quad) holds the point now wins, the nearest centre if
several do. With no quad hit the bounding-box distance still decides, so pointers
past a line end or between lines behave as before; a tie there goes to the nearer
centre rather than the first glyph.
Tests: pdfcraft-render lib 312 -> 314. The sheared and 45-degree test fails on
the old hit-test (glyph "i" resolves to the glyph before it) and passes now.
pdfcraft-ui-egui text_select: 14 pass.
A glyph narrower than a twentieth of its height was matched as a fake-bold
redraw by a fifth of its height, so in OCR text layers squeezed to 20 Tz or
less the second l of "Hallo", and at 10 and 5 Tz the tt, ss, mm and ff and
letters repeated two or three places on, were dropped. Every glyph with a
width is now matched within half of it; only zero-width marks keep the
height-based distance.
waiting_worker_can_cancel_before_initializer_returns gave its threads
2 s to start, and a busy FreeBSD VM in CI took longer. Its waits only
guard against a hang: the initializer is held until the end of the
test, so a waiter that can't cancel never returns, however long the
guard is. They now allow 600 s, like the hang guard in
page_contents_and_nested_forms_share_one_budget (#2812), which already
covers the second test named in the issue. The test polls with a 1 ms
sleep instead of yield_now, and the held initializer no longer panics
a second time when the test fails.
Checked by hand: with a 3 s delay at both threads' start the old test
fails at its deadline and the new one passes; with the waiter's
condvar wait made untimed (a real stall) the new test fails at its
guard.
Fixes#1970
Co-authored-by: Claude <noreply@anthropic.com>
crates/fonts mapped codes U+007F and U+0080–U+009F to those controls while
the renderer's text layer (hayro patch 35) gives them no text. The generated
map now covers U+0020–U+007E and U+00A0–U+00FF in the first high byte, as
the text layer does; both tests check DEL and a C1 control.
(CodeRabbit review on #3222.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
* M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306)
The veraPDF fixture's JPX header names CIEJab (enumerated colour space 19),
which hayro-jpeg2000 refuses, so the image failed and the page drew blank.
When the decoder refuses the image and its colr boxes name such a space, it is
decoded again from a copy naming greyscale, sRGB or CMYK by component count,
as MuPDF and Poppler draw it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: copy the JP2 stream only when a colour enum needs replacing (#3306)
Review follow-up on #3363: a stream the decoder refused for another reason was
copied for nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* Renumber the JP2 colour-space patch (21): #3378 takes hayro-syntax (20)
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>
* fix(parse): accept spaces between stream and its line break
Microsoft Reporting Services writes `stream \r\n`, a space before the line break.
hayro-syntax and pdfcraft-cos took the stream data to start at the space, so the
content stream failed to inflate: the page rendered blank (white, or black with
inverted colours) and an edited save corrupted the stream. Spaces and tabs are now
skipped, but only when a line break follows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Number the stream end-of-line patch (20), as vendor/README does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M2: a tiling pattern drawn once shows its cell at full scale (#3349)
PowerPoint writes a picture fill as a tiling pattern with /XStep 2147480100.
Fitting that step into the 3000 px cell pixmap shrank the picture below a
pixel. When what is painted can only show the first cell, the cell's /BBox is
drawn at full scale with a transparent border and padded instead of tiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a tiling cell whose scale isn't finite keeps the old path (#3349)
Review follow-up on #3356: a /BBox 1e-40 wide passes the zero-area check and
gives an infinite cell scale, which would send non-finite geometry to the cell
renderer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a stroke's miter counts in the one-cell check (#3349)
Review follow-up on #3356: shows_one_cell took the line width as a stroke's
reach; a miter join reaches half the width times the miter limit. The reach
formula patch (9) uses is now one function, stroke_reach, used by both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a skewed stroke's reach uses the transform's largest stretch (#3349)
Review on #3356: under a skew, a direction between the axes stretches
further than either axis, so the larger axis stretch could place a miter
tip short of the next cell when it reached it, and the one-cell path left
that cell unpainted. The reach now uses the spectral norm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M4: custom document properties through the engine and the tools (#organize.custom-properties)
/Info entries beyond the ones the spec names are custom properties:
organize lists them (custom_info) and set_info accepts any such name
after Acrobat's checks (no standard name, no empty name, no spaces,
delimiters, / or #, at most 127 bytes); inspection lists them too
(DocInfo::custom); doc_info reports them under custom and doc_set_info
sets or clears them. The Custom tab in Document Properties and the XMP
mirror stay planned.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M4: custom document properties list the same before and after an edit
Review follow-up. The open-time reader (render/inspect.rs) now uses the
engine's rule (pdfcraft_organize::custom_info, trimmed, empty values
dropped): text strings only, NUL/whitespace trimmed, empty values left out,
and the 256 cap counting listed entries. Before, it also listed name
values (`/Dept /RnD`) and capped before filtering, so the list could change
after the first unrelated edit. An automation test opens a file with a name
value, blank, NUL-padded, empty and numeric entries and checks doc_info's
custom list is the same before and after setting the Title.
Drops the ROADMAP.md log line, which conflicted with main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M4: custom properties drop NUL/whitespace padding in one pass, before the cap
Review follow-up. custom_info now trims NULs and whitespace in any mix
(" \0Lab\0 " is "Lab"), leaves out values that are only padding, and does
both before taking 256, so blank entries can't crowd real ones out. It is
the one rule: the engine uses it as it is, and the open-time reader trims
the same way. Tests: 300 blank entries before a real one, and mixed
padding, in organize and through doc_info.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
* Keep a subscript or superscript in its line when extracting text (#3174)
* Text extraction: only a glyph right beside the previous one gets the sub/superscript centre limit
A large negative gap (a small glyph drawn back under a large one) kept the wider limit.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ROADMAP.md, docs/roadmap.md: union of both sides' log lines and milestone rows, each line once.
parity/acrobat-features.toml, crates/xfdf/README.md: next's text, a superset of main's (#1917, #2686 plus #2688).
crates/edit/src/tests.rs: both sides' new tests (#2558 and #3084).
crates/ui-egui/src/editing.rs: the picked picture is cleared on every edit, and a new drawing commits its raster (#3149).
Seven tests failed at random on slow or busy machines, main included
(FreeBSD run 38834256439 at 5fdd820b). They now count the work or wait
for the state they check; no assertion is loosened and nothing retries.
- spot_colour_images_run_their_tint_transform_once_per_sample_value
counts tint-transform runs (exactly 256, not one per pixel) through
vendored hayro-interpret patch (34), color::tint_transform_runs, a
thread-local counter, instead of a 10 s deadline.
- shadings_with_large_sampled_functions_paint_quickly: no counter tells
the flat table from the hash map (both read every sample per build),
so the build each paint does is timed against reading its samples on
the same thread, fastest of 8 rounds each, and must stay under 10x
(flat table 1.1x, hash map 109x), instead of a 10 s deadline.
- page_contents_and_nested_forms_share_one_budget: the budget is
checked by its warnings; the deadline is only a hang guard, now 600 s.
- The pen-stroke tests hold finished renders back from the view
(DocView::hold_rasters, for tests) while they look at the settling
stroke, then wait by state for its raster.
- files_and_quit_from_the_operating_system waits for the tab before
deleting the file the open worker reads.
- image_pixels_follow_the_frame_while_a_corner_is_dragged waits for the
page raster on screen before each drag.
Product fix (#1858): a released drawing stayed painted only until any
raster of its page arrived. In full screen its edit is applied a frame
later, so a raster already under way could end the preview first. A
settling drawing is now marked committed when its AddAnnotation lands,
only rasters after that end its preview, and a failed edit drops it.
* fix(render): sampled functions index a flat sample array (vendored hayro-interpret patch 31)
A Type 0 sampled function kept its samples in a HashMap keyed by each point of
its /Size grid (two SmallVecs per key, SipHash), and the renderer builds a
shading's functions again every time it is painted, so every paint hashed one
insert per sample. A 2,097,152-sample ramp painted 30 times took 38 s; the
pdf.js corpus's bug1721218 (InDesign) spent most of a 1.5 s page on it.
The samples now stay in stream order in one Vec, and a grid point is found by
strides (first input fastest). The interpolation keeps the same recursion and
arithmetic, so results are bit for bit the same, and a point the stream ended
before still gives no value. The repeated-paint page draws in well under a
second. The maximum sample value no longer computes 2^32 in a u32 (a panic with
overflow checks on at 32 bits per sample).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* fix(render): one decode of an image serves every size it is drawn at (vendored hayro-interpret patch 32)
The decoded-image cache (patch 24) keyed each decode by the size the image is
drawn at, but only the JPX filter decodes a reduced resolution for a size:
every other filter decodes the whole image. So a JPEG or Flate scan missed the
cache at every zoom step, was decoded again, and was kept once per zoom level.
The key now carries the target size only when JPX decodes the image, its
/SMask or its /Mask (read from /Filter or /F, erring towards keeping the
target). `InterpreterCache::decoded_image_decodes` counts the cache's decodes;
the new test draws a Flate image at five sizes and sees one decode, and a
copy whose soft mask is JPX-encoded still decodes per size. Both assertions
fail with the old keying.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* fix(render): patch meshes copy their vertices' colours less (vendored hayro-interpret patch 33)
A Coons or tensor-product patch is drawn as 722 triangles over a 20 x 20 grid
of points, and every vertex's colours (a SmallVec) were produced or copied up
to five times: each grid cell interpolated its four corners itself (1,444
interpolations a patch for 400 points), cloned all six vertices of its two
triangles, and the mesh sampler cloned each vertex again to move the triangle
into device space. Profiling page 8 of the annual report in #1584 put a
quarter of its time in those clones.
Each grid point's colours are now interpolated once, a cell's second triangle
takes the vertices its first copied, and the sampler moves a patch's freshly
made triangles instead of copying them (`TriangleSampler::sample_owned`; a
free-form triangle mesh, which the shading keeps, is still copied). The same
inputs give the same values: rendering the first three pages of every pdf.js
corpus file is byte for byte the same. Release build, Linux: report page 8
773 -> 696 ms, personwithdog.pdf 149 -> 129 ms, issue13520.pdf 104 -> 87 ms.
The new test pins the colours near each corner, at the centre and between them
for a Coons and a tensor-product patch (taken before the change); swapping two
vertices' grid points makes it fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* docs: roadmap entries for faster sampled functions, image decodes and patch meshes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#2305 fixed WinAnsi 0xAD in `crates/fonts`, and closed with two loose ends it named: the
user-visible extraction path goes through hayro, not that table, so it still reported the
invisible character; and the same Annex D.2 note makes 0xA0 the `space` glyph. This finishes
both, and makes the two tables in the product agree about the same byte.
WinAnsiEncoding gives codes 0xA0 and 0xAD the glyph names `space` and `hyphen` -- the names it
also gives 0x20 and 0x2D -- and MacRomanEncoding gives 0xCA `space`. The tables called them
`nbspace` and `sfthyphen`, whose Adobe Glyph List values U+00A0 and U+00AD are invisible
formatting characters, so extraction hid ink the reader can see: "well-known" drawn with the
0xAD byte matched no search and no redaction pattern, and a non-breaking space did not match the
space a reader typed.
Evidence. Two sources transcribe the table the same way (an Annex D implementation's docs, and
the WinAnsi.enc encoding vector R ships, which gives `/space` at octal 240 and `/hyphen` at
255), and a black-box check settles the rest: on a synthetic fixture pdftotext reads the
base-encoded codes as `a-b a b`, and keeps U+00AD for a code an explicit `/Differences` names
`sfthyphen`. We now do both. Over the 989-file pdf.js corpus the soft hyphens we extract fall
from 117 to 13 and the non-breaking spaces from 30 to 26; everything left comes from `/ToUnicode`
maps, which say what they mean and are not touched.
Scope. The base table only, in the text direction only.
- A `/Differences` entry naming `sfthyphen` or `nbspace` is a deliberate statement about one
code and keeps its glyph list value, as pdftotext does.
- Glyph lookup is untouched, so a font program's own `sfthyphen` program is still found first.
In hayro that is why the alias sits in the Unicode path rather than in the generated encoding
table: `code_to_ps_name` is shared with lookup.
- Writing is unaffected: `encode` takes the lowest code showing a character, so ' ' and '-'
still write 0x20 and 0x2D, and a caller holding a literal U+00A0 still writes 0xA0.
Tests. `fonts` pins both tables and the whole decode/encode path including the `/Differences`
case; `render` pins the extraction a reader actually gets, over WinAnsi, MacRoman and
`/Differences`. Each was watched failing with each cell and each match arm reverted in turn, so
no assertion is passing by accident. fonts 70, render 296+4, redact 49, a11y 43 and edit 156 are
green, with fmt, clippy, `xtask layers` (37 crates) and `xtask assets` clean.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(render): keep annotations readable under reading colours (#1548)
Reading colours remapped annotation paints as line art: the yellow
highlight fill is luminance-light, so it was mapped toward the dark
background, and Multiply then darkened the remapped-white words under
it into invisibility.
Two changes in hayro:
- The /Annots loop in interpret_page is bracketed by a new
begin_annotations/end_annotations Device signal (patch 26), and the
renderer's reading-colours mapping leaves annotation paints alone.
Reading colours replace document colours, never the colours a
comment was given — the Acrobat model.
- The page background is painted inside the page transparency group
(patch 27), as the initial backdrop, so a Multiply highlight blends
with the background too. With the background outside the group the
band kept its own flat colour and the words vanished into it. White
pages are unaffected: multiplying with white is the identity.
Regression test reading_colors_keep_highlight_annotations_readable
covers both annotation paths (/AP and the no-/AP /QuadPoints
fallback) under WHITE_ON_BLACK; parity entry a11y.replace-page-colors
updated with the behaviour and evidence.
Co-Authored-By: GLM <noreply@z.ai>
* contributors: credit @edurbs as Eduardo Soares
The contributor's own entry, chosen by them (real and display name).
Co-Authored-By: GLM <noreply@z.ai>
* #1557: record the vendored hayro patches in vendor/README.md
Main already uses hayro-interpret patches (26)-(28), so the annotation hooks become
hayro-interpret (29) and the renderer's parts hayro (17) and (18).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M0: update rustls to 0.23.46
Co-authored-by: Codex <codex@openai.com>
* M2: requeue render requests after parse cancellation
Retire the active request key after releasing cancelled parse ownership,
allowing current demand to be queued again. Cover renewal before and after
the abort with deterministic queue-admission and completion regressions.
Co-authored-by: Codex <codex@openai.com>
---------
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
pdfcraft_render::transition gives the picture of a page transition at a
moment of its run: which parts of the outgoing and incoming page to paint,
where, and how opaque, in page fractions, for all twelve styles and every
direction. Pure and tested; the full-screen presentation will paint from
it when it plays transitions.
Refs #379.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Each page's presentation settings of ISO 32000-2 s12.4.4 are read with the
spec's defaults for what is missing or out of range: the transition style,
duration, dimension, motion, direction, Fly scale and backdrop, and the
/Dur display time. doc_info lists them; pdfcraft-cli info counts them.
This is the data full screen needs to play transitions and auto-advance;
playing and editing them stay planned.
Refs #379, #2070.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* M2: heavy-sheet generator and page timing harness (item 8 of storytold/pdfcraft#623)
Adds two examples to pdfcraft-render. synth_sheet writes a synthetic A1 sheet whose content
follows the sample's layout, so timings on it can be compared with the sample. page_timing
replays the canvas's requests for page 1 and prints times, counts and an output hash. The
renderer is unchanged. Budgets are reported, not enforced.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* M2: sort sweep results by tile size too (item 8 of storytold/pdfcraft#623)
Tiles at the same origin but different sizes compared equal under the sort key, so the
sweep hash depended on completion order. Including the size makes the key unique.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Highlight appearances written by other editors are a transparency group whose
content is filled with `/BM /Multiply`. Every `/Group` form got a layer of its
own, so the Multiply blended with that layer's transparent backdrop and the
layer then covered the page as an opaque block.
Vendored hayro-interpret patch (28): a group that is neither isolated nor
knockout composites its elements with the backdrop (ISO 32000-2 §11.4.7), so
when it is entered at full opacity with no soft mask and Normal blending there
is nothing for a layer to add, and its content is drawn straight onto the page.
`/I` and `/K` are read from the group dictionary. An isolated group keeps its
blank backdrop; a knockout group or one entered at partial opacity, under a
soft mask or with a blend mode keeps its layer.
Test: `raster::tests::a_multiply_highlight_inside_a_transparency_group_keeps_the_text`
(the appearance drawn as an annotation and from page content, with `/I false
/K false` spelled out and with no group at all; an isolated group as a control;
a group entered at half opacity still composited as one). It fails before the
fix with the text pixel yellow instead of black. parity: view.render-transparency
cites it and says what is still approximated.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Apply the minimum after page-unit scaling without inflating small raw crops. Retain the existing unit policy and nearly-zero fallback, with regression coverage for origins, rotations and reciprocal rounding.
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The vendored hayro-syntax passed each filter an output limit, but
ASCII85 ignored it: every `z` expands one byte to four, so a Flate
stage at its limit could be grown another 4x past every budget.
ASCII85 now writes only what fits under the limit, stops with a
warning once it is reached (like RunLength, LZW and Flate), and caps
its starting capacity at the limit.
Test: decompression_bombs_stop_at_the_stream_limit gets a
[/FlateDecode /ASCII85Decode] bomb; it decoded 301,989,892 bytes
before and stops at MAX_DECODED_STREAM now.
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ISO 32000-2 Table 11 defines /Rows 0 (the default) as "height not
predetermined", but the vendored hayro-syntax decoder took it as zero
rows, so scanned pages written that way rendered blank. Treat 0 like an
absent /Rows and fall back to the image height.
Adds a synthetic regression test: an 8x4 all-black Group 4 stream,
hand-encoded from ITU-T T.6, rendered with /Rows absent, 4 and 0. It
fails without the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: Add word and character counts with optional content exclusions
Retain the complete reviewed change, including prior integration resolutions,
on current upstream.
M2: Add document word counts and semantic exclusions
* Refresh catalog hashes after merging main
---------
Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(render): convert spot-colour images once per pixel value (vendored hayro-interpret patch 28)
An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or
DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is now converted to RGB
through a table of every possible pixel value instead of pixel by pixel.
A Separation or DeviceN tint transform is a PDF function, often a PostScript
calculator program, and the image decoder ran it once per pixel. Fillable
character sheets draw their background art as an 8-bit DeviceN image: one with
a 3304 x 4105 background took 3.8 s and 430 MB to draw each page (Poppler: 0.27 s),
when opening it and again after every field was filled. It now draws in 0.11 s
and 100 MB, pixel for pixel the same.
The table decodes each sample through the same `decode_sample` as the per-pixel
path, keeps `fix_image_length`'s rules for short and overlong data (the pixels
past the data are converted as before), and is used only when the image has
more pixels than the table has entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8
* fix(render): place widgets without /P in one pass over the pages
Opening a document, the inspector finds the page of a form field whose widget
has no /P by searching every page's /Annots for it, once per field: O(fields x
annotations). A form of 8,000 such fields took 1.17 s to open instead of
0.27 s. The pages' /Annots are now read once, on the first such field, into a
map from annotation to page.
A widget that several pages list is now placed on the first of them, as
pdfcraft-forms places it; the search took whichever page a HashMap visited
first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8
* fix(ui): the Fields panel keeps up with large forms
Every frame the Fields panel searched the form for each row by name, once to
sort the rows into tab order and again to find what a click selects: O(fields²)
a frame. It also laid out, drew and built the tooltip of every row, scrolled
into view or not. With 8,000 fields the app used 13.4 s of CPU for 20 pointer
moves with the panel open (0.2 s with it closed).
The rows are now worked out by `field_rows`, which pairs each row with the
first form field of its name through one map a frame, as the engine pairs
them, and a row out of view is allocated and keeps its clicks, keyboard focus
and accessibility label but isn't drawn and builds no tooltip. The same 20
moves now take 0.86 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8
* docs: roadmap entries for faster form-heavy documents
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8
* fix(render): convert each lookup-table entry on its own (vendored hayro-interpret patch 28)
Patch 28 converted the whole table of possible pixel values in one batch.
Separation and DeviceN `convert_f32` concatenate each tint result, and a Type 4
program that returns the wrong number of components is only warned about, so a
malformed result for a value the image never uses shifted every later entry. A
32 x 32 image whose only sample (255) is red drew black because tint 0 returned
one component (PR #1691 review).
`convert_each` now converts the table one entry at a time, so a malformed entry
changes only itself. The largest table (65,536 entries, a two-ink DeviceN image
over DeviceCMYK) takes about 2 ms longer.
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>