An image that shared a redacted image's /JBIG2Globals is now stored
again as the smallest of Flate, Group 4 or JBIG2 coded alone (smallest),
like a cleared copy, rather than always Flate, so the other pages of a
scan don't balloon either. The shared-globals test's second page is a
ragged scan and checks it is no longer Flate and is smaller.
A JBIG2 image's /JBIG2Globals hold the symbol dictionary of every image that
shares them (jbig2enc -s and most scanners give one to all of a scan's pages),
so the bitmaps of marks drawn only under a redaction stayed in the file while
another page's image used them. Every image using the globals of a removed or
replaced image (or of one in a removed or replaced form) is now decoded and
stored again in place as Flate without them; a full save then drops them. One
that can't be decoded for certain refuses the redaction
(RedactError::SharedJbig2Globals). Report::images_restored counts them.
Redaction wrote every cleared copy as Flate, so a redacted Group 4 or JBIG2 scan
page grew several times over until the optimizer ran. A cleared one-bit image of one
component is now stored as the smallest of Flate, CCITT Group 4 and, when it was
JBIG2, JBIG2 (PDF 1.4, which such a file already is), as stored, dictionary included:
a tiny image can lose by Group 4's parameters. Both are coded from the
cleared samples, so nothing of the original's code is carried over; rows Group 4 or
JBIG2 can't code (too wide) leave Flate. Images of more bits stay Flate.
Tests: a cleared one-bit Flate scan of ragged word blocks (where Group 4 is smaller
than Flate) comes out Group 4 and an eight-bit image Flate, each decoding to the
cleared samples (the whole buffer); a tiny image whose Group 4 code is shorter but whose
stream isn't, and rows wider than Group 4 codes, stay Flate and clear; the redacted
JBIG2 scan of #5461's test comes out JBIG2, smaller than
Flate, with no globals and no stream holding the original code. The tests' image
helpers decode through Document::decoded_image, as image codecs need.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #283. A scan stored as JBIG2 (as the optimizer writes them) is decoded
through Document::decoded_image_within, its /JBIG2Globals included, and its covered
pixels cleared in a Flate copy that refers to no globals, as CCITT fax images are.
Before, any JBIG2 image under a mark was removed whole, blanking the page.
Still removed whole (fail-closed): a JBIG2 page of another size than the image,
which decoded_image refuses (the renderer draws the whole page into the image, so
the rows cleared wouldn't be the ones shown); globals that can't be read; damaged
data; an image claiming more than one bit or one component, even where its rows
take as many bytes; a page far past the image's raster, refused before it is
decoded.
Globals no other image uses leave the file with the original on the full rewrite
(tested); globals another image still uses stay, and the README and parity note say
they may hold the shapes of marks from under the area, not where they were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PdfCraft's bootstrap renderer draws byte-aligned Group 3 whose fill ends
each end-of-line code on a byte (as TIFF writes it) as other rows than
decode_ccitt reads (black box, at every /K, with /EndOfLine or not:
mostly the first row, then blank). Redaction cleared pixels in the
picture decode_ccitt reads, so a mark over what PdfCraft showed needn't
cover what was cleared, and content shown blank stayed in the copy. Such
an image (/K 0 or more, /EncodedByteAlign) is now removed whole, as
every CCITT image was before this PR; Group 3 off bytes and Group 4 are
still cleared.
The image's parameters come from Document::fax_params, the reading of a
fax image's /DecodeParms entry that decoded_image already did, now on
its own (it was to come with #4404, which uses it too). The redaction
README, the module doc, the redaction and core.image-filters notes and
the spec parity table's status line and revision row say so, and the
core.image-filters note now records the renderer's misdrawing.
group_3_on_bytes_is_removed_whole_and_other_fax_images_cleared: a white
16 × 2 picture, checked to decode as such, is removed whole on bytes (no
codes; codes at /K 0; at /K 1 with /EncodedByteAlign by reference) and
cleared off bytes and as Group 4 on bytes. It fails without the guard;
cargo-mutants catches every viable mutant of it.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
decode_ccitt reads byte-aligned Group 3 whose data has end-of-line codes
but none before the first row (not as T.4 sends them) on bytes. At /K 0
with the fill ending each code on a byte, about one small picture in
five is then read as other rows; at fax width almost none are, as such
pages come out short and are refused. Export already said so. The
optimizer, when it resamples such an image or stores it again as Group 4
from behind another filter, and redaction, when it clears pixels in one,
write those rows back, and nothing said that.
Black-box, PdfCraft's renderer draws such an image exactly as
decode_ccitt reads it (all 42 misread of 200 seeded pictures, behind
ASCII85Decode too), so the written image is what PdfCraft showed before.
Refusing such data instead (a code after nearly every row but none
first) would also refuse some ordinary Modified Huffman images, whose
rows can meet 11 zeros by chance (0.5 to 5% of small ones in a
black-box probe), so the limit is stated rather than guarded: in
decode_ccitt's doc, the filters, optimize and redact READMEs, and the
core.image-filters and mono-compression notes. The optimize README's and
the mono-compression note's "not yet: Group 3 to Group 4 without
resampling" now say a lone Group 3 image: one behind another filter is
stored again as Group 4.
Also from the review: docs/roadmap.md keeps #4335's status line under
this PR's, rather than replacing it; the filters README's tests and the
core.image-filters evidence name the Group 3 layout test; the filters
crate's description names Group 3 and 4 decoding.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
Mutation testing found behaviour no test held, now covered:
- a Group 4 halftone above 450 ppi isn't resampled and stays byte for
byte, alone by default and behind ASCIIHexDecode with Retain;
- under Retain a resampled Flate scan stays Flate, though Group 4 would
be smaller (ragged bands);
- sixteen-bit grey of only 0x0000 and 0xFFFF keeps its bytes;
- a busy Group 4 image behind ASCIIHexDecode, whose code is over three
times its raster, decodes within the fax bound and is stored again as
Group 4 alone. Redaction's busy-fax test is that busy again: since the
bound for other images is three times the raster, the old checkerboard
fitted it, and dropping the fax bound passed.
The redaction README says three times, and doc_optimize's description
says what Retain keeps: CCITT stays CCITT, anything else becomes Flate.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
- Redacting a CCITT image went through decoded_image, which undoes the
filters before the fax decoder up to MAX_DECODED, so a Flate bomb in
front of a fax image cost a gigabyte while every other image stops at
twice its raster. Document::decoded_image_within takes a bound, and
redaction gives a fax image's code about a byte per pixel. A fax image
cut short, or behind a bomb, is removed whole; a busy one whose code
is bigger than twice its raster is still cleared.
- Every image is now decoded through decoded_image_within, which reads
/DecodeParms through Document::with_direct_parms: given by reference,
the numbers inside (/Predictor 7 0 R) included. That reading was first
written here and has moved down to #4304. The predictor test now covers
the dictionary, an array's entry and the number itself given by
reference; read without them, the cleared bytes drift off the covered
pixel and the covered sample stays in the copy.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Create PDF and the optimizer now store black-and-white scans as Group 4,
and redaction couldn't decode CCITT, so redacting one word on such a
scan removed the whole page image. A CCITT image (the last filter,
after any others) now decodes through Document::decoded_image and is
cleared in a Flate copy like any other image; one that can't be decoded
in full is still removed whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The apply section of the redact README described the fail-closed rules but not the new refusal of a Redact annotation with neither a usable QuadPoints array nor a usable Rect. Documented here with the error name and the page scope of the count. No behaviour change in this commit.
Redaction removed annotations and form fields under a mark from the pages
but left the structure tree's /OBJR references to them, so a full rewrite of
a tagged PDF kept the comment's /Contents and the field's /V. apply now
drops references to every annotation no page lists afterwards (widgets of a
removed field on other pages included), as Sanitize does since #2532, and
fails closed with RedactError::Tags when an element can't be edited.
* M8: redaction removes the replies of the annotations it removes (#2659)
A reply (/IRT) placed away from the mark survived, and its /IRT kept the
removed annotation - its comment text and the QuadPoints of the redacted
words - in the saved file. Replies to a removed annotation, and replies to
those, now go with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M8: test replies listed before their parent, with a pop-up on a reply (#2659)
Review of #2697: the replies precede the annotation they answer in
/Annots, deepest first, and a reply has a pop-up. A single pass in
/Annots order would keep them; the test fails with the loop cut to one
pass, which the first test does not notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Consolidate the reviewed change and its follow-ups on current upstream.
Original change notes:
M8: Bound and reuse repeated redaction overlays
Prepare appearances within a shared 8 MiB budget before changing the document. Reuse one measured and encoded row, reject excessive geometry, and preserve document and undo state on refusal.
Fixes#1326
M8: Preserve upstream font metrics in bounded redaction overlays
M8: reuse ordered redaction mark groups (#1326)
Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
* M8: Retire inherited redaction resources
* Redaction: pin inherited resources only on redacted pages, shared by reference
localize_resources rewrote every page in the document on every apply and copied a direct
inherited /Resources dictionary into each one, so a large document grew by N x the dict.
Now only redacted pages that inherit resources are pinned; a direct inherited dictionary is
made one indirect object and shared by reference. The parent's /Resources is retired when no
other page still inherits it (the leak fix), and kept (through the shared reference) when an
unredacted page still draws with it. A /Parent chain that can't be followed or disagrees with
the page tree retires nothing. Tests: an unredacted page is left untouched and the rewrite is
bounded; redacting every inheriting page retires the parent's resources. ROADMAP entry moved
into the revision-history table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text
Redaction properties offers "Custom text" or "Redaction code" under "Use overlay
text". Picking the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and
ticking codes makes new marks carry them as overlay text, in the set's order.
redact_mark takes code_set and codes and rejects unknown sets or codes with the
allowed list.
Also merges a duplicated evidence/notes pair in form.field-list that made
parity/acrobat-features.toml fail to parse on main.
* M8: say why the default redaction code set can't fail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: pin the clock in form_bulk (saves a second apart differed in /ModDate on Windows CI)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>