A schema's property or value type list, or a value type's field list,
written more than once is read each time; and a pdfaExtension:schemas
that isn't one rdf:Bag (TechNote 0009, 4.2) is reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A qualified value has exactly one rdf:value; a parse type other than
Resource, which XMP doesn't use (ISO 16684-1 7.9.2.5), breaks the value;
and an rdf:resource element holds no element content in any form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
All five extension schema container namespaces are written with their
own prefixes, pdfaExtension included (PDF Association TechNote 0009,
4.1). The module comment cites TechNote 0009 section 4 for the schemas,
entries and prefixes, and says where PDF/A-2 asks more than it does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Every pdfaExtension:schemas description needs its entries: a schema its
schema, namespaceURI and prefix; a property its name, valueType,
category and description; a value type its type, namespaceURI, prefix,
description and field; a field its name, valueType and description.
Each description namespace is written with its own prefix. One pass over
the metadata streams serves this check and the value types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Qualifiers beside an rdf:value that are predefined properties keep their
types; rdf:value may be an attribute of an empty property element; an
rdf:resource element is empty; and character data around array items or
in an rdf:Description structure breaks the value, as it already did in a
parseType="Resource" structure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The TIFF, EXIF and Additional EXIF namespaces come from the September
2005 XMP Specification that PDF/A-2 cites ("EXIF Schemas", pages 58-70),
with its EXIF value types (pages 83-87): Rational as numerator/
denominator, GPSCoordinate as DDD,MM,SSk or DDD,MM.mmk, and the Flash,
OECF/SFR, CFAPattern and DeviceSettings structures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A metadata stream is decoded by its byte order mark (UTF-16 big- or
little-endian, or UTF-8; ISO 16684-1 7.3) before it is parsed. A GUID
is a simple value but not a URI (ISO 16684-1 8.2.2.3), so the
document and instance IDs of xmpMM, stRef and stEvt don't take
rdf:resource.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* 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>
Every metadata stream is read, within one decode allowance, not only the
catalog's. Predefined properties inside a Pantry item keep their types;
rdf:value may be an attribute; rdf:resource makes a URI, which only a
text type takes; a value's text is all its text nodes; an array item is
rdf:li; a parseType="Resource" structure has no character data; a time
zone is Z, +hh:mm or -hh:mm; and a value nested past every type's depth
doesn't have its type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Properties of the Dublin Core, XMP Basic, Rights Management, Media
Management, Basic Job Ticket, Paged-Text, Dynamic Media, Adobe PDF,
Photoshop and Camera Raw namespaces must have the types ISO 16684-1
clause 8 and XMP Specification Part 2 give them: simple values by their
lexical form, arrays by kind and items, structures by their fields, in
every RDF/XML form a value can take. xmp::parse is shared with the
identification read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
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>