A crypt filter whose /AuthEvent is /EFOpen needs its password only when
an embedded file is accessed (ISO 32000-2 7.6.6, Table 25). A document
whose strings and streams are in the clear (/StmF, /StrF /Identity) and
whose encrypting filters are all EFOpen ones, /EFF naming one, now opens
without a password in cos and in the renderer (hayro-syntax patch 36).
Without the password there is no file key: the embedded files' data is
held and saved as stored, and Document::embedded_files_locked and
reads_as_stored say so. Asking for such an attachment's bytes is an
error. A full save that would renumber data whose key mixes in the
object number (RC4, AES-128), or change the document's security, is
refused. The password still opens the document and its attachments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* A comment's text, border or line, and fill each at an opacity of its own
Comment Properties showed one opacity for a whole comment, so a text box's
see-through background took its text and border with it, and a rectangle
marking an area on a plan couldn't have an opaque line over a faint fill.
Now a text box, typewriter text or callout has a Text, Border and Fill
opacity, a rectangle, oval, polygon or cloud a Line and Fill opacity (a part
not drawn is greyed); everything else keeps one Opacity.
The line's stays /CA, the fill's is /ca (PDF 2.0's opacity for what is
filled) and the text's /PCTextOpacity, each only where it differs from the
one it would follow, and the appearance draws them with /GS0 (strokes at the
line's, fills at the fill's) and /GS1 for the text, so every viewer shows
them. The quick bar's Opacity and StyleAnnotation's set the whole comment's.
Engine: Style::{fill_opacity, text_opacity}, Edit::PartOpacity,
annot::{set_part_opacity, appearance::part_opacity}, Props::{fill_opacity,
text_opacity}. Make Properties Default and the settings carry the parts.
Headless: comment_add / comment_edit fill_opacity, text_opacity; comment_list
reports opacity, fill_opacity, text_opacity. Four labels in every catalog
(AI-assisted translations from English, noted in ATTRIBUTION.toml).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: the text's opacity slider shows what the text is drawn at; comment_list skips replies' properties; ui-parity's change record
A text box without fill and border whose text kept an opacity of its own showed the border's
in its Text opacity slider; it now shows the text's, and a change sets the whole comment's.
The opacity sliders are named by their labels. comment_list reads a comment's properties only
for comments, not for every reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: comment_list tells replies by is_reply, as main now threads them
main threads replies by their /IRT reference (#4061); the opacity fields skip a reply the
same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: a text-only text box made default gives the tool its text's opacity
With neither a border nor a fill, a text box's text is the whole comment:
Comment Properties shows its drawn opacity (the text's own, when set), and a
change there sets the whole comment's. Make Properties Default, and the
context menu's Make Current Properties Default, stored /CA instead, so a
text box whose text was at 50 % gave the tool 100 %. The default is now the
text's drawn opacity in that case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: the part opacities in main's annot layout (Style's fields, the text box drawing's parts, set_part_opacity exported)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rebase: main's new comment test builds a Style with the part opacities
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>
* → / ← 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>
* 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>
* M11: JBIG2 images share a symbol dictionary, up to 16 at a time
With Monochrome Images set to JBIG2, Optimize stored each image's
letters in a symbol dictionary of its own, so a document of N pages
carried its font's shapes N times. Now up to 16 images the run stores
as JBIG2 share one dictionary in a /JBIG2Globals stream, holding the
marks found in more than one of them; each image's text regions refer
to it first, then to a dictionary of its own for the marks only it
repeats. An image is stored that way when it comes out smaller, and a
group only when what its images save is more than the globals take.
- filters: encode_jbig2_shared codes a group: the globals are a symbol
dictionary segment numbered 0 and associated with no page (7.2.6);
each page numbers its segments from 1, as a segment refers only to
lower numbers (7.2.5), and the shared symbols' IDs come first, as the
dictionaries are referred to in that order (Table 31, SBSYMS). At
most 16,383 shared marks, those in the most images, so a page keeps
room for its own under the decoder's 32,767.
- optimize: the run holds the 1-bit samples of the images it stores as
JBIG2 (16 at most, 64 MiB at most) and stores them again sharing,
weighing each with the longest reference the globals could have.
Eight pages of 10 pt text rendered at 300 dpi take 41% less in
Helvetica and 42% in Times than coded alone, 10% in Courier, and 46%
in Helvetica at 600 dpi; groups of 2, 4, 16 and 32 save 19%, 33%, 45%
and 47%. Redacting one image stores the others of its group again
without the dictionary, so 16 keeps that to 15 images.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M11: test what the mutants found untested in shared JBIG2 dictionaries
- filters: the group goes through encode_jbig2_shared, which refuses it
for an image the encoder refuses; two marks in common are enough to
share; past the 65,536 distinct marks a group holds, a mark first seen
later isn't shared while those held count every image.
- optimize: optimizing again keeps both the shared pages and the pages
coded alone as they are; under Retain, averaged JBIG2 pages share
nothing; a last group of two shares.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M11: only images redaction can store again share a JBIG2 dictionary
Redacting one image of a group stores the others again without the
dictionary, which can hold the shapes of marks under the area, and
refuses the redaction for one it can't: it takes image masks and
images of one component as its own reading counts them (no DeviceN,
the family named directly), and at most 64 Mi pixels. Optimize took
DeviceN of one colourant and up to 80 million pixels, so such an image
in a group made any redaction of the others fail, where before it was
coded alone and the same redaction worked (the Consumers review).
Only an image mask or a DeviceGray image named directly, of at most
2^26 pixels, now joins a group; others are coded alone. An engine test
optimizes three pages, two DeviceGray and one DeviceN, and redacts the
first: the DeviceN page stays alone, the second is stored again, and
no image names the dictionary after a save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M11: optimize and redaction share one limit on the images redaction stores again
The second Consumers review: Optimize's limit on images that share a
JBIG2 dictionary copied redaction's private 64 Mi, with nothing tying
the two, so a lower one in redaction would bring back refused
redactions with no test going red. Both now read
pdfcraft_cos::MAX_REDACTED_IMAGE_PIXELS. The engine test adds an image
mask, the other kind that shares, so a redaction that stopped storing
either kind again fails it. doc_optimize's description, the README,
the parity note and ROADMAP say which images share (named directly).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M11: a JBIG2 group is coded before its samples would pass the limit
CodeRabbit on #5910: hold added an image to the group and then checked
SHARED_BYTES, so a group could pass the 64 MiB it is documented to stay
within by its last image, which can itself be up to 64 MiB (an image
that shares has at most 2^26 pixels, and as its rows are whole bytes,
at most that many bytes; a 1-pixel-wide one takes them all). hold_in
codes the group first when an image would take it past the limit, and
after when it has 16 images; with room for 10 bytes, images of 4 go two
to a group, where the old order made one of 12. CodeRabbit's follow-up
too: the 18-image test decodes every image and compares it with its
samples, across the boundary between the groups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* optimize: count the globals' object framing; filters: test the shared coder's size limit
- A JBIG2 group shares a dictionary only when it saves more than the
globals' dictionary and data plus NEW_OBJECT (96 bytes: the object's
framing, its xref entry, and an encrypted file's IV and padding), so a
group that saves less than that no longer makes the file larger.
- encode_jbig2_shared is Unsupported for an image taller than the
decoder takes and codes one at the bound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* filters: drop the unused rows binding in encode_shared
page() no longer takes the rows since #5511.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* filters: the shared coder's at-bound test says what it returns
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>