Review follow-ups for hayro patch (30):
- The stencil coverage that cuts the pattern is drawn with in_type3_glyph
cleared, as the sub-render it replaced was: inside a glyph a shrunk
stencil was nearest-sampled and lost strokes thinner than the shrink.
- The bordered cell of a pattern whose step shows one cell (patch (20))
goes through the same cache (Renderer::tiling_cell, keyed apart by
one_cell), so its fills no longer draw and keep a cell each.
- A test pins a patterned stencil under a soft mask to the soft mask's
opacity once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* → / ← scroll a page wider than the window sideways before turning it
On a plan zoomed in, the arrow keys turned the page (#185) and never showed
its other side: the view moved sideways only by the scroll bar, the wheel or
the Hand. Now → / ← scroll a line that way while there is more of the page,
and turn the page at its edge, as ↓ / ↑ do in single-page view. ⌘→ / ⌘←
turn it at once; a page that fits across turns as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: → / ← read the current page's or spread's edges, not the widest page's; ui-parity's change record
In continuous view the content is as wide as the widest page, so on a narrower current page
that shows whole → scrolled toward the wide page's edge instead of turning. The edges are now
the current page's (or spread's) as drawn, or the content's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: a spread's second page off screen still counts for → / ←
The row's edges came from the pages' screen transforms, and a page gets one
only while it meets the window. With a zoomed two-page spread scrolled so its
second page was still wholly to the right of the window, the row ended with
the first page, so the next → turned the spread instead of scrolling to the
second page. The edges are now taken from the layout rectangles of every page
in the row, placed as the drawn pages are.
A TwoUp case in the viewer tests zooms a spread until its page gap sits at the
window's right edge and checks that → scrolls on rather than turning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: → / ← read the row after continuous view has moved the current page
In continuous view the current page is updated while the pages are laid
out, after the row's extent was taken, so the edges could describe the
previous row and the next → or ← turn the page instead of scrolling the
page that had just become current. The row is now read after that update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: a page turned by → / ← at its edge starts at the edge the reader comes from
Turning at a wide page's right edge with →, the next page kept the
horizontal scroll (the same column, as other jumps keep it), so an equally
wide page opened at its right edge and the next → turned again without
showing its left side; ← had the same in reverse. When plain → or ← turns
the page, a page or spread wider than the window now starts at its left
(after →) or right (after ←). ⌘→ / ⌘←, Page Down / Page Up and every other
jump keep the column.
The sideways-arrow test now goes across two wide pages both ways: → scrolls
the next page from its left edge, ← turns back to the previous page shown
from its right edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: the sideways arrow keys through main's PageEnv, PageOut and GlideCx, and shown_row with its layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
A stencil mask (/ImageMask) painted with a pattern was drawn through a
luminance mask, which vello_cpu only takes at canvas size: every draw
rendered, converted and composited a full-page mask. And a tiling
pattern's cell pixmap was drawn again for every fill, each copy kept
until the page was composited. pdf.js issue 8078 paints 73,047 16 x 16
stencils with one pattern whose cell is 3.5 MB at 36 dpi: the render
ran for 5 minutes and then failed to allocate.
hayro patch (30): the stencil is drawn under a clip of its device
rectangle (padded by two pixels for a stencil snapped to whole pixels),
the pattern in an isolated layer cut to the stencil by a layer holding
it composited with Compose::DestIn (ISO 32000-2 8.9.6.2: the pattern
shows where the mask's samples paint). A coloured tiling pattern's cell
is drawn once per setting of the pattern and cell scale (TilingCells,
eight entries per renderer). hayro-interpret patch (78) numbers each
setting of a tiling pattern (TilingPattern::instance).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: Shift holds lines level or plumb while drawing (ortho)
With Shift held, a new line or arrow runs level or plumb from where it
starts, whichever is nearer to the pointer (a CAD drawing's ortho mode),
and each new side of connected lines, a polygon or a cloud is held the
same way from the corner before, rubber band included. Dragging an end
or corner of a drawn one with Shift is held the same way; it turned to
45° steps before. The measuring tools keep their 45° steps.
measure::snap::ortho next to constrain; comment_add takes `ortho` for
line, arrow, polygon, cloud and polyline. Parity comment.draw-ortho.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ortho: over the first corner the rubber band shows the closing side, Shift or not
From review: a click there closes the polygon or cloud to its first
corner, while the preview with Shift held showed a level or plumb side.
The click and the preview now share one rule (closes_at).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: Shift is read where main's split put the drawing input, and the ortho helpers are imported there
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: Shift holds a drawn line, not a freehand stroke (a hunk had landed in the wrong branch)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ortho: the parity note names the closing side's exception; the test's doc comment is back above its test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* A double-click on a comment card's text edits it
On the selected card in the Comments panel only the header took the
double-click: the text is a selectable label, so a double-click there
selected a word and the editor stayed closed (a tester's report). It now
opens the editor too; dragging over the text still selects it for copying.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: the double-clicked card's editor is drawn too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: the double-click test also starts from a card that isn't selected
It selected the card before any input, so it never covered what the change
is for in the Comments panel as one uses it: a first click on the text of an
unselected card selects it, the second edits it. The test now runs both ways.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Escape leaves a Fill & Sign tool, as it leaves a comment tool
The first Escape closes the open type box, the next one goes back to
Select; Fill & Sign's Text tool otherwise opened a box at every click with
no way out by the keyboard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fill & Sign: a new text box deselects, so two presses of Escape close it and leave the tool
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merge origin/main. Conflicts: docs/changelog headers keep both histories;
edit/run.rs keeps EnhanceScan beside main's run_edit_page_image; optimize
images.rs keeps main's scaled() and the crate-visible jpeg(); the German
strings in ui-egui/src/i18n/de.tsv go with that file, which main moved to
craft-i18n. decode_samples now takes main's ImageBudget; a scan clean-up
run shares one sized by the file.
Review (CodeRabbit, scan_clean.rs): a page whose own images and a
scan-holding Form only together cover it now cleans the images and reports
the Form; a page whose images alone cover it reports its Form too. New
regression fails on the old classification.
The transports, the message types and the origin checks come from the shared crate (storytold/craft-libs#420); PdfCraft keeps the save and dirty hooks, the open size limit and the author.
data-host="page" on the canvas opts a page that loads the module into its own document into the host bridge. The ?host=parent messages travel as non-bubbling CustomEvents on the canvas; host::Bridge holds a Transport (Parent or Page) with one send, and save, dirty, ready and failure reporting are shared. ?host=parent is checked first and unchanged. Hosting guide documents it.
* M1: respect generations in COS object references and full saves
Resolve stale references to null instead of replacement objects with the
same number. Validate complete identities in writer readers, indirect
stream lengths and catalog checks, and keep generation-aware length caches.
Cover cache reuse, edit overlays, snapshots, incremental and packed/classic
full saves, and stream lengths with synthetic regressions and independent
hayro checks.
Co-Authored-By: Codex <noreply@openai.com>
* M0: clarify refreshed CI roadmap note
Co-authored-by: OpenAI Codex <noreply@openai.com>
* M1: repair contradictory xref generation references
Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.
* M1: cover repaired generations and freed-slot round trips
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M1: document generation repair and bounded diagnostics
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M1: record generation repair regression evidence
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M1: document bounded COS generation repair
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M1: record generation repair parity evidence
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* fix(cos): preserve repaired generations through preflight and saves
Keep accepted xref/header aliases across repair-aware edits and snapshots, canonicalize full-save identities, and retain bounded generation diagnostics from older revisions. Add COS, writer/encryption, and real preflight regression coverage.
* test(preflight): assert repaired aliases share saved identity
Address the review nitpick by comparing reopened HeaderImage and RowImage references after damaged full saves, for classic/packed output and edited/restored snapshots. Focused preflight regression and rustfmt check pass.
* M14: update certificate dependencies for RustSec advisories
Bump rustls-webpki to0.103.16 and webpki-root-certs to1.0.10. Preserve all other locked packages and dependency lists, manifests, Git pins, and advisory policy.
---------
Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* fix(ui): About credits tables fit without side-scrolling and their columns resize
The Contributors table and the Models table were egui Grids inside a
two-way scroll area: they opened wider than the dialog, so they scrolled
sideways, and Company and Model sat almost touching. Both are now
egui_extras tables. Each column opens at the width of its header and widest
cell, the spare width goes to the text columns (Name; Company and Model),
and every column edge can be dragged. Widening a column grows the table
like a spreadsheet, and only then does it scroll sideways; the header stays
put while the rows scroll. Counts and dates are right-aligned and every
cell is vertically centred.
The sorted column stays highlighted and its direction shows on the
toolbar's sort-order button, so headers no longer reserve room for an
arrow. The opening widths come from fit_widths, a pure function with unit
tests that a fitted table fills its room exactly and that only the text
columns give way when room is short.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let every credits column be dragged, and refit changed tables
The last column was egui's remainder column, which has no drag handle, so
it could only be resized from the edge before it. It is now an ordinary
column with its own handle.
A table keeps its column widths, so switching to a longer name left the
Name column at its old width and clipped the text. The table now goes
back to the opening fit whenever that fit changes: a longer widest cell
or a resized window. Dragged widths are dropped at that point.
fit_widths could overflow the room when one column hit the minimum,
because the other columns did not take up its share. Columns now give
way in proportion to their width above the minimum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refit credits tables on what they measure, not the fitted widths
Comparing the fitted widths missed the case it was meant for. Name is the
only growing column, so a longer name takes its extra width from the
spare room and the fit comes out the same: a dragged Name width stayed,
clipping the longer name.
It could also drop dragged widths by accident. The fit comes from the
available width inside the scroll area, which scrolling can nudge, so
scrolling a widened table could reset it.
The table now compares the natural widths and the clip rect's width,
which scrolling does not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
main's 3D-embed change (#5808) added crates/annot/src/threed.rs with its own
pub MAX_MODEL_BYTES (same 256 MiB value), so both were re-exported from the
crate root and pdfcraft-annot no longer compiled after merging main. model3d
now uses threed's constant.
A refinement whose reference is offset right of it (GRREFERENCEDX above
0, which a refined text instance has from a wider bitmap, ISO/IEC 14492
Table 12, and a refined symbol from an RDX above 0, Table 18) read the
reference's first 32 columns as 0. hayro-jbig2 emptied its buffers of
the reference left of it and, at its first column, didn't load them
again, so the refinement decoded to other pixels than were coded, or
failed and lost the page.
Vendored patch 15 (13 is #5999, 14 #6016): left of the reference the
buffers keep the words of its first column each row starts with. With
an offset of 0 or less nothing changes.
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M6: forms decodes values, renamed calculations and detected pages within shared budgets
Part of #5755: the forms crate's four unbudgeted stream reads go through a
`DecodeBudget`, so an operation's decoding is bounded however often the file
names the same stream.
- A reading of the form decodes its fields' value streams (/V, /DV) within one
64 MiB budget, as it already does their scripts. Each field got a fresh
1 GiB allowance, so 300 fields naming one stream decoded it 300 times.
- Renaming a field rewrites the calculations that read it within one script
budget; past it, a calculation is left as it is, like one that won't decode.
- Field detection reads a page's content within one page's allowance
(`DecodeBudget::page`), as the content walks do, however often /Contents
lists a stream.
- The PDF/A declaration is read from at most 8 MiB of XMP, as preflight's
Standards panel reads it, and only once /NeedAppearances isn't already set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: the forms README says which metadata declares PDF/A
declares_pdfa reads pdfaid:part from XMP metadata that decodes within
8 MiB, as preflight's Standards panel reads it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: a test pins that metadata without pdfaid:part declares no PDF/A
cargo-mutants found `==` turned to `!=` in declares_pdfa's search unpinned:
no case had metadata within the cap that declares nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: field detection reads operators one at a time; the PDF/A check reads XMP as preflight does
From the Resources review of 66415d737:
- page_shapes collected every parsed operator, about 80 bytes for each
decoded byte of dense content, so a page within its 256 MiB allowance
could take some 20 GB. It now reads them one at a time
(pdfcraft_content::ops) and drops the pieces once joined.
- declares_pdfa decoded through a DecodeBudget, which counts every filter
stage against the 8 MiB cap, so XMP stored through a chain of filters
stopped declaring PDF/A while preflight's Standards panel, which caps
each stage, still read it. It reads the metadata as preflight does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M6: say what declares PDF/A to forms, and why its metadata read stays direct
From the Legal-forms review of c00e3d2fb: the README and doc comments said
forms reads the declaration "as preflight's Standards panel reads it". The
two share the 8 MiB cap, applied to each filter stage, but forms matches the
bytes pdfaid:part and preflight the identifier by its namespace, as on main.
The sweep's direct-/DecodeParms hit is answered at the site: preflight and
organize read metadata the same way, and the readers move together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* forms: rename a shared calculation once; keep unread defaults on Clear form; cap q in detection
- A Calculate action that several fields share by reference is read and
rewritten once, in place, so every calculation reads the new name and
the script is decoded once, not skipped past the budget.
- Clear form leaves a field alone when its /DV is a stream the value
budget didn't reach, instead of emptying it.
- Field detection keeps at most 256 saved graphics states; deeper q are
counted so each Q still restores its own.
- The dense-content test checks that the budget is spent; exhausted() was
true from the start with no form visits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* forms: restore the matrix past the save cap; reset a value that wasn't read
- Field detection keeps the matrix of the first save past MAX_SAVES and
restores it when the last of them closes. A shape painted after a Q
that would have undone a cm inside those saves is dropped, as the
matrix there isn't known, instead of being reported scaled.
- Clear form skips a field only when its value is known: one whose /V
is a stream the value budget didn't reach is reset, not kept as if it
equalled an empty default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* forms: keep detection's saved states in a Saves type
shapes_within went past clippy's cognitive complexity limit (32/30) with
the save-cap tracking inline. The same logic now lives in Saves (save,
restore, moved, unknown).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>