A notation beside op and fields set the notation and dropped the rest, a
field name that wasn't a string was left out, a misspelt key was ignored,
and validate {"min": 0, "max": "100"} wrote a min-only range. Each is
now refused and nothing changes. A null bound, as form_fields shows the
one a range doesn't have, is no bound, so its output is taken back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
AFSimple_Calculate("SUM", "a, b", x) gave only a built-name site, so a
rename no longer rewrote a and b (main did) while parse_calculate still
read the sum of a and b: after a rename the sum read missing fields. The
string list now keeps its names when an argument follows, and what
follows is a name built at run time, as for an array (from the review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
The page's focus was a name, so with two fields called total (a
malformed form) hovering the second one's row showed the first one's
missing fields in the pill, and outlined and numbered both on the page.
A row and a widget under the pointer now carry their field object: the
pill says that field's reads and issues, and the page outlines it, its
step and the formulas that read it by object. The Prepare selection is
still only a name, so it stands for every field of that name and the
pill says all of their issues (from FredApps's review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
found accepted price.0 (up to the field's last widget), but links,
recalculate and the scripts' getField all take a name as the field or
the fields under it, so a formula reading price.0 got no value and no
order check while the page said nothing was wrong. found now looks
names up the same way; a widget name is flagged as missing until all of
them resolve it (from CodeRabbit's review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read like price.12 counted as found whenever price existed. A widget
number now finds something only below the field's widget count; a
field with one widget, or none, still answers to .0 (from CodeRabbit's
review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculate_json wrote the lowercased AFSimple code, so a product or an
average came back as {"op": "prd"} or {"op": "avg"}, which
form_set_props refused as an unknown op. One name per operation now
serves both: calculate_arg looks names up in it (still taking minimum
and maximum), and the listings write it (from CodeRabbit's review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculate_arg read the notation first, so {"notation": …, "script": …}
set the notation and dropped the script unnoticed; the script check now
comes first (from CodeRabbit's review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
- CalculatedField carries the field object: form_calculation_order and
the Calculation order page find each row's formula and value by it,
so two calculated fields sharing a name (a malformed form) each show
their own.
- An AFSimple_Calculate list counts as written out only when it ends
the call: "row" + suffix and [...].concat(more) are names built at
run time, and an array item counts only when it ends at a comma or
the list's end, so ["row" + i] no longer reads, or renames, "row".
- After a reorder, Undo is offered on the missing-field card instead of
bringing back the all-clear beside it.
From CodeRabbit's review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
The row gets a red step and a "Missing field" caption, as a loop does; a
red card names the formulas and links to the first, in place of the
all-clear; a script that builds names gets a grey "Unchecked reads"
caption and a note; the summary above the page lists a missing field
among the reads and says so, and says when a script names fields as it
runs. The name and formula lines of a row are sized by their own
right-hand text, so a wide caption no longer cuts the name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
calculation_order flags MissingField (a field deleted from under a sum, a
typo in notation, a script's getField; a group's name and a widget's
number count as there) and BuiltNames (a getField or AFSimple_Calculate
list built at run time, found as af::Site::Built in the same pass as the
literal names). form_calculation_order gives each field's formula and the
two new issues; form_fields shows a custom Calculate script; the
Calculation order page explains both in its row tooltips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
docs/architecture.md said every image codec decodes through the
bootstrap renderer; Export ▸ Images decodes CCITT (hayro-ccitt) and JPEG
itself. The progress logs' "(a dithered scan)" sat after "soft mask
counted", as if it described the mask, and so did the
optimize.mono-compression note's "(a dithered scan downsampled to JPEG)".
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
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
group_3_fax_images_become_group_4 coded its rows only off bytes. It now
also stores again rows on bytes: Modified Huffman (no end-of-line codes),
and codes whose fill ends each on a byte, as TIFF writes them, at /K 0 and
1 without /EndOfLine, which reading by that flag misread (the test fails
with that rule). Each becomes Group 4, smaller, decoding to the picture.
The page is compared with the picture in Flate rather than with itself
before: PdfCraft's bootstrap renderer draws TIFF's layout (the fill
ending each code on a byte) as other rows, at every /K and with
/EndOfLine or not (black box), so storing it as Group 4 corrects what
PdfCraft shows rather than keeping it. (Redaction removes such images
whole, for that reason, since #4384's "redaction removes Group 3 on
bytes whole".)
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
The optimize crate doc and MonoSettings named only 1-bit, CCITT and
eight-bit grey images; doc_optimize's mono said Group 4 only; the M11
milestone row stopped at Group 4 compression. They now include
downsampling (halftones aside), Group 3 to Group 4, 1-bit Flate, and
black-and-white RGB and indexed images that use only palette entries 0
and 1, packed to one bit. That last isn't "two-entry", as the roadmap and
the downsampling parity note had it: an indexed image packs whatever its
palette's size, when its samples are 0 and 1. The mono-compression note
says byte-aligned Group 3 with end-of-line codes but none before the
first row is stored again as PdfCraft draws it, which at /K 0 may be
other rows than were coded.
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
The rule came into decode_ccitt with the decoding it moved from Export ▸
Images (#4335's "Export reads byte-aligned Group 3 by the codes in its
data"), where the create test checks it through the export. Here it gets
tests of its own:
- 32 seeded text-like pictures, 8 to 207 samples wide, at k 0 and 1, in
each layout (no codes, with end_of_line and without; codes, each
starting on a byte or ending on one, with and without). Each decodes to
its picture and, a byte short, to fewer rows, not to other rows read
another way.
- Group 4 rows on bytes keep their aligned read, end_of_line or not.
- The any-input property varies end_of_line and encoded_byte_align too.
With the rule this replaced (rows on bytes read unaligned under
end_of_line, aligned otherwise) the test fails; cargo-mutants leaves no
mutant of the rule's lines alive. The filters README says how rows on
bytes are read.
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
- Packing compared an indexed image's decode array, and RGB's pairs, as
written: [0 255.0], an entry by reference or a real among RGB's pairs
stopped it, though they mean the same as integers. Each entry is
resolved and read as a number now, as is_identity_decode and
dark_is_one read them; RGB's pair is written back direct.
- A test holds that Retain stores nothing again it doesn't resample: a
Group 3 halftone above 450 ppi, which isn't averaged down, stays Group
3 byte for byte (the guard's || as && passed every test).
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
With end-of-line codes, the CCITT decoder picks a bad row up again at
the next code and reports no damage (an invalid code, a row of too many
pixels: probed as a black box), so such an image is stored again as
Group 4 the way it decodes. Without them, damage stops the decoding,
too few rows come out, and the image is left alone. Telling damage
apart would take a Group 3 parser of our own; the optimize README and
the mono-compression parity note say so instead.
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
- The optimize README, the crate doc and both parity notes say a
halftone or dither, and an image wider than a fax row, isn't averaged
down.
- The README's "Left alone" list is the colour and grey settings'; image
masks, decode arrays and CCITT go to Monochrome Images now. Its
monochrome "Not yet" names an image's own /SMask or /Mask image again
(only images drawn on a page are visited), as the parity note does;
the parity note's row limit is Group 4's.
- docs/architecture.md says CCITT decodes through pdfcraft-filters for
export, redaction and the optimizer, not only the renderer.
- docs/target-app-parity.md no longer lists monochrome downsampling as
missing.
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 three behaviours no test held:
- eight-bit black-and-white in calibrated grey, an ICC profile of one
component or a separation is packed, each keeping its colour space,
and calibrated RGB of only 0 and 1 isn't (taking every array colour
space for an indexed one passed every test);
- mixed Group 3 (/K 1, with end-of-line codes) is stored again as
Group 4 (taking /K above 0 for Group 4 passed);
- a resampled Group 4 image of vertical stripes is stored as 1-bit Flate,
which is smaller (ruling Flate out for any CCITT image passed).
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
- Redaction decodes an image no further than twice its raster plus a
byte a row, meant for ASCIIHex text under Flate; hex text broken into
lines is a little more, so such an image was removed whole instead of
cleared (already so on main). It is three bytes for each now, as the
optimizer allows.
- Tests: grey stored as hex text in lines under Flate, and RGB behind a
filter array (which a fax image's bound would refuse), are cleared;
decoded_image_within gives a fax image no more rows than its bound
holds.
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
- Packed to one bit, black-and-white DeviceRGB became DeviceGray. Where
/DefaultGray or /DefaultRGB is in force (ISO 32000-2 §8.6.5.6), that
drew it otherwise: through DefaultGray as grey, or no longer through
DefaultRGB. The page walk now notes the images drawn, on any use, where
a page's or form's resources (an enclosing one's included) name
either, and those stay RGB. When a use couldn't be followed, every
image counts as such.
- One whose soft mask has a /Matte stays RGB too: the matte has one
number for each of the parent's components, three, and grey has one.
- The optimize README, the mono-compression parity note and doc_reduce's
description said Group 3 to Group 4 wasn't done yet, that only images
packed from eight bits could become Flate, and that the colour space
and decode array always stay. They now match the code.
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
- Halftones and dithers: most of their new pixels cover both black and
white, and taking the sample that covers more turned their tones
solid (a 50% screen black, a 25% one white). An image whose new pixels
mostly cover both is now left at its resolution; text covers both only
along its edges and is still averaged down. downsample_bits says how
many new pixels cover both.
- A counter for each column: an image wider than a fax row
(MAX_CCITT_COLUMNS) isn't averaged, so a strip of 80 million columns
can't make the counters 640 MB.
- A fax image's decoding is bounded at about a byte a pixel, as
redaction's is (decoded_image_within), not 1 GiB; behind a Flate bomb
it is left alone.
- An indexed image's tie went to sample 0, which an inverting decode
array maps to entry 1, though the docs say entry 0. It goes to the
sample that decodes to entry 0 now (ISO 32000-2 §8.9.5.2).
Each has a test that fails without 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
Monochrome Images only took eight-bit grey of only black and white
besides 1-bit images, so the same scan written as RGB, or as an indexed
image of two palette entries, stayed at eight bits (and RGB went to the
colour settings, as JPEG). Now these are packed to one bit first:
- eight-bit DeviceRGB whose every pixel is black or white becomes
DeviceGray; a decode array the same for each component becomes that
pair, another stops it. Calibrated RGB and RGB with a profile stay.
- an indexed image of 2, 4 or 8 bits that uses only palette entries 0
and 1 keeps its palette, whatever the two colours; a decode array
other than the default stops it.
Colour-key masks still stop all of them, and Retain leaves them to the
grey and colour settings. They are decoded through /DecodeParms given
by reference too (Document::with_direct_parms), and when downsampled a
tie goes to the dark sample of the image as packed: packed RGB is grey,
with grey's decode pair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
Redaction clears an image mask's covered samples to the value that
paints nothing, which an inverting /Decode [1 0] flips. It read the
array only when direct, so given by reference (or with its first number
so) the cleared samples painted instead: the covered area came out
inked. That was already so on main, and this PR sends CCITT masks
through it. The array and its first number are resolved now, as export
and the optimizer read them; the fax test covers all three forms.
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
A CCITT fax image that wasn't resampled was left as it was, so Group 3
images (the 1-D and 2-D codings fax software writes) stayed Group 3.
Group 4 codes each row against the one above and is usually much
smaller. Now only Group 4 alone skips decoding; Group 3 is stored again
as Group 4 when that is smaller as stored, and Retain still keeps it.
It reads an image's CCITT parameters (its /DecodeParms entry with Table
11's defaults) through pdfcraft_cos::Document::fax_params, from #4384.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
doc_reduce, the engine's reduced_bytes doc, the optimize README's
presets and the optimize.reduce parity note still said every image
above 225 ppi goes to 150 ppi; black-and-white images go to 300 ppi
above 450 ppi, as CCITT Group 4 when smaller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
The batch step took Reduce File Size's monochrome settings unchanged, so
`ppi: 600` (or `--ppi 600`) kept colour and grey images at 600 ppi but
halved 600 ppi black-and-white scans to 300. Black-and-white images now
go to the larger of ppi and 300 ppi: a higher ppi keeps their
resolution, and a lower one doesn't take scanned text below 300. The
doc_batch description and docs/cli-batch.md say so.
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>
Stream::decoded_within reads /DecodeParms only when it is direct, so an
image whose PNG predictor was given by reference decoded with its row
tags still in the data. The cleared bytes then drifted one byte further
from the covered pixels each row, and the covered samples survived in
the redacted copy, recoverable by applying the predictor. Parameters
are now made direct first (Document::with_direct_parms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README said an indexed image's palette decides which entry is the
dark one on a tie, but dark_is_one never reads the palette: entry 0
wins whatever its colour. The README, the function's comment and the
parity note now say so (from FredApps' review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decoded_image paired the fax filter with the last /DecodeParms entry,
but Stream::filters leaves /Crypt out wherever it stands, so with
[/CCITTFaxDecode /Crypt] the fax filter got the Crypt entry and lost
its /K, /Columns and /BlackIs1. Its entry is now that of the last
filter but a /Crypt. /Crypt belongs first (ISO 32000-2 §7.4.10), so
only a malformed file had it (from CodeRabbit's review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 2 × 2 tie always gave sample 0, black in grey and the painted part of
an image mask with the default decode array. With an inverting one,
[1 0], 0 is white or unpainted, and in a separation 0 is no ink, so a
line half a new pixel wide could vanish. dark_is_one says which sample
is dark from the colour space and decode array: 0 in grey and of a
mask, 1 in a separation or a DeviceN of one colourant, each the other
way round when the decode array inverts; an indexed image keeps entry
0, as its palette, not the decode array, says which is darker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
The PDF Optimizer and Reduce File Size downsampled colour and grey images
but not black-and-white ones. Settings::mono is now MonoSettings {
downsample, target_ppi, above_ppi, compression }, 300 ppi above 450 ppi
and CCITT Group 4 by default (Monochrome Images ▸ Downsample to;
doc_optimize mono: { downsample, ppi, above_ppi, compression }).
A 1-bit image or image mask (no filter, general-purpose filters, or CCITT
fax after any of them) and eight-bit grey of only black and white are averaged by
area: each new pixel takes the sample covering more of it, a tie 0, so a
dark line half a new pixel wide stays. They are stored as Group 4, or as
1-bit Flate when that is smaller; with Retain a resampled image keeps its
kind of compression. Images with a soft mask keep their size.
CCITT decoding moves from Export ▸ Images into pdfcraft-filters
(decode_ccitt, CcittParams, through hayro-ccitt), and
pdfcraft_cos::Document::decoded_image reads a fax image's /DecodeParms
for it (undoing filters before CCITT, such as ASCII85Decode, with any
/DecodeParms given by reference made direct first), so export
and the optimizer share one reader, and Group 4 scans can be
downsampled. A 600 ppi text page went from 79 KB to 40 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
"Export ▸ Images reads byte-aligned Group 3 whichever way its fill
falls" read byte-aligned rows unaligned whenever /EndOfLine was set.
That broke byte-aligned Group 4 with /EndOfLine (refused); read Group 3
that says /EndOfLine but has no codes unaligned, and wrongly; and read
codes with the fill before them, but no /EndOfLine, on bytes: every row,
the wrong picture, nothing said. Now:
- Group 4, and rows not on bytes, read as the parameters say;
- Group 3 rows on bytes are read by the data: T.4 sends an end-of-line
code before the first row when it sends them at all, and no other code
starts with more than eight zeros. Data that starts with 11 zeros or
more (fill included) and a 1 has codes, and is read unaligned, where
the codes mark the rows whether the fill starts each code on a byte or
ends it on one; other data is read on bytes, whatever /EndOfLine says.
A reading that comes up short is reported rather than tried the other
way, where data cut short would read as other rows.
The Group 3 test exports nine pictures, stripes and eight of random
text-like runs (how a misreading comes out depends on the picture),
coded with T.4's terminating codes at /K 0 and 1: no codes, with
/EndOfLine and without; codes each starting on a byte or ending on one,
with /EndOfLine and without. Each exports as its picture, and each a
byte short is reported; at /K 1, codes with none before the first row
(not as T.4 sends them) export as their picture or are reported.
Byte-aligned Group 4 still reads with and without /EndOfLine. The
create.export-all-images parity note says what is still limited: such
codes may export wrong at /K 0, as the create README says. A rule tried
first, reading both ways and refusing an image that gave every row both
ways differently, failed on the stripes without codes (TIFF's Modified
Huffman layout): its unaligned reading gives every row too, wrongly. So
did one that read the other way when the first reading came up short: it
drew data cut short as other rows. cargo-mutants leaves no mutant of the
new lines alive.
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
- A noisy 640 × 800 soft mask under 1200 × 1500 text: resampled to the
image's new size it shrinks only a little, by less than the grey
settings' JPEG adds over the packed image, so packing wins; not
counting the new mask passed every test before.
- The hex-text half of the bomb test also stores eight-bit
black-and-white, which must pack, as its comment says.
- The optimize README, the mono-compression parity note and both
progress logs say the soft mask counts.
- The Copy Image parity note no longer lists CCITT as missing: it
copies through Export's decoder, which reads CCITT now.
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
With /EncodedByteAlign and end-of-line codes, the fill may start each
code on a byte or end it on one (T.4's fill, as TIFF writes it); the
second was misread, and the image refused as short. With end-of-line
codes the decoder now reads unaligned, as the codes mark the rows either
way (probed black-box for /K 0, 1 and 2); codes laid out so under
/EndOfLine false are still misread. The Group 3 test covers both
layouts, /EncodedByteAlign given by reference, and a /BlackIs1 that
inverts the picture given by reference (with_direct_parms resolves both
before the decoder reads them).
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
- Eight-bit black-and-white text under a noisy soft mask: the grey
settings win, as they shrink the mask far more than packing saves.
Counting the packed image without its mask passed every test before.
- The packing test checks the report's byte totals exactly.
- Export reads Group 3 with end-of-line codes, with rows on a byte
(EncodedByteAlign), with both, and mixed (/K 1, each row tagged
one-dimensional) with end-of-line codes and without. Decoding /K 1 as
one-dimensional, or ignoring EncodedByteAlign, now fails a test.
/EndOfLine is passed to the decoder, but no input here tells it apart:
the decoder accepts end-of-line codes either way, as ISO 32000-2
(Table 11) requires.
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
Redaction doesn't decode CCITT yet, so a mark over part of a picture
Create PDF stored as Group 4 removes the whole picture (nothing leaks);
before this PR such pictures were eight-bit Flate and had only the
covered pixels cleared. The create README and the image-to-PDF parity
note say so; #4384 adds CCITT to redaction. The workspace's comment on
hayro-ccitt says Export uses it, not only tests.
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
- An indexed image's lookup table given as a stream was decoded without
its /DecodeParms when they came by reference, so a PNG predictor's tag
bytes ran into the colours (red and blue came out [2,255,0] and black).
It decodes through with_direct_parms now, as the samples do. This was
already so on main; #4320 makes the same change.
- Tests for what this PR claims and no test held: Group 3 one-
dimensional data exports as its picture (mapping /K 0 to Group 4 gave
all white and passed); CCITT soft and stencil masks export as the same
masks in Flate (decoding them plainly dropped them and passed); a
Flate image's predictor given by reference reads as direct; and a
1-bit TIFF is stored as Group 4 (the TIFF test said "a 1-bit one" but
wrote eight bits; its comment now says what it writes).
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
An eight-bit grey image of only black and white was packed to one bit
and stored, and the grey settings never saw it. For a dithered scan that
is bigger than what Reduce File Size made of it before, downsampled to
JPEG: 1-bit Flate can't shrink the dots (a 1200 × 1500 dither at 300 ppi
was 157 KB packed, 67 KB as before). Both are made now, and the smaller
as stored wins, counting a soft mask resampled with the image. Clean
text still packs to one bit. The packing also decodes no further than
its samples can need, as the other image steps do now.
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
Packing eight-bit black-and-white grey to one bit decoded with
Stream::decoded, which ignores /DecodeParms given by reference. With PNG
predictor 10 each row then kept its tag byte, a 0, so the shifted rows
still passed as black and white and were packed and stored skewed. It
now decodes through Document::with_direct_parms (#4304), and the test
of images with their parameters by reference covers this case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
Stream::decoded reads only direct parameter dictionaries, so an image
behind [/FlateDecode /CCITTFaxDecode] whose Flate entry is a reference
lost its predictor, and the CCITT decoder got the predictor's bytes; a
plain Flate image with its predictor given by reference was misread the
same way. Export now decodes through
pdfcraft_cos::Document::with_direct_parms (#4304), as the optimizer
does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
A fax image with /Filter [/ASCII85Decode /CCITTFaxDecode], say, was
skipped: only a lone CCITT filter went to the CCITT decoder. Now the
filters before it are undone first, and its parameters are the
/DecodeParms entry at its own place in the array (ISO 32000-2 §7.4.1),
not the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
A CCITT image whose /K, /Columns or /Rows is an indirect integer was read
with the defaults (K 0, 1728 columns), and one whose /Rows is below its
/Height was cut short and skipped. The numbers are now resolved, and the
image is decoded up to its /Height rows whatever /Rows says, still bounded
by the size the export checked (review of #4335).
The doc comment of the optimizer's bilevel step now says what Retain
leaves to the grey settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z