Commit Graph
138 Commits
Author SHA1 Message Date
bflatastic 74b3452328 Fit the landed changes together: the print-settings discard is its own function 2026-11-09 18:37:28 +07:00
bflatastic 0bfef25cce Merge #4440: M11: PDF Optimizer can discard an embedded search index (optimize.discard-search-index)
# Conflicts:
#	crates/automation/src/tools.rs
2026-11-09 18:27:14 +07:00
bflatastic 9b57d7edd1 Merge #4434: M11: PDF Optimizer can discard links to other documents (optimize.discard-external-refs) 2026-11-09 18:26:16 +07:00
bflatastic a4a600573e Merge #1998: Print production: Convert colours (grayscale, RGB, CMYK by the device formulas)
# Conflicts:
#	crates/automation/tests/it/automation.rs
#	crates/edit/src/lib.rs
#	crates/edit/src/tests.rs
#	crates/engine/src/commands.rs
#	crates/engine/src/lib.rs
#	crates/optimize/tests/optimize.rs
#	crates/ui-egui/src/app.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/state.rs
#	crates/ui-egui/tests/it/editing.rs
2026-11-09 18:17:38 +07:00
dotCooCoo b575f8536c Merge main into feat/discard-external-refs 2026-11-09 03:03:59 -08:00
dotCooCoo 74a820856d Merge main into feat/discard-search-index
# Conflicts:
#	crates/automation/src/tools.rs
2026-11-09 03:01:49 -08:00
.CooCoo e78102c907 M11: PDF Optimizer can discard form submission, import and reset actions (optimize.discard-form-actions) (#4416) 2026-11-09 18:35:01 +08:00
jonathanwxh-cell 008b035326 fix(optimize): validate named destination targets and ignore orphaned refs (#5362) 2026-11-09 18:30:27 +08:00
jonathanwxh-cell b04823573b fix(optimize): merge side-by-side and 2D grid image fragments (#4818) 2026-11-09 18:26:53 +08:00
dotCooCoo 571f1c622d Merge main into feat/discard-search-index
# Conflicts:
#	crates/automation/src/tools.rs
2026-11-09 00:09:11 -08:00
dotCooCoo 5b5378efd7 Merge main into feat/discard-external-refs
# Conflicts:
#	crates/automation/src/tools.rs
2026-11-09 00:08:30 -08:00
Osvaldo Duran MelendezandClaude Sonnet 5.5 1b3e0e5630 Merge upstream/main into pr/convert-colours
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 22:07:06 -05:00
jonathanwxh-cell 9011276f2f fix(optimize): size objects in compressed object streams by their compressed share (#4951) 2026-11-09 10:10:25 +08:00
jayzed80 9c9b65437f fix(optimize): downsampling never enlarges; a huge target_ppi aborted on a 20 TB allocation (#5425) 2026-11-09 10:05:33 +08:00
quincy-jc d0b2a32744 perf(optimize): work out pictures on threads and skip needless page reads (#6230) 2026-11-09 08:53:27 +08:00
FredApps 0a7798707b Merge pull request #5825 from Piohrun/fix/flate-input-work
fix(filters): inflate with zlib-rs everywhere, and charge the image allowance for what Flate reads
2026-11-08 13:11:25 +01:00
Osvaldo Duran MelendezandClaude Sonnet 5.5 0376bdfe16 Merge upstream main into convert-colours
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 06:43:13 -05:00
Grzegorz MartensandClaude Opus 5.5 69e26167e4 M2.6: a page's content is read with its /DecodeParms given by reference
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.

- `decode_with_damage` takes the document and reads the parameters through
  it, and so does `decode_contents_with_damage`, and `decode_contents` with
  it: printing, accessibility's reading of a page and a page copied as a form
  read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
  of the stream holds only its `/Filter` and the parameters, its data shared,
  so a stream a page's `/Contents` lists again and again isn't copied whole
  for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 480fdb114a M2.6: Optimize leaves a page damaged partway as it is stored
Optimize read a page's content up to any damage in its streams as the whole
of it. Merging a page's image strips then wrote the page's content again as
one stream, without what followed the damage, and the image pass measured
each image by its uses before the damage only, so one drawn larger past it
could be resampled below that use.

- `DecodeBudget::decode_contents_with_damage` reads a page's `/Contents` as
  `decode_contents` does, each stream also saying whether it is damaged
  partway; `decode_contents` is it without the flag.
- `content_bytes` gives `None` for a page with a damaged stream, as for one
  that doesn't decode: the content is not all known. Its strips aren't merged
  (`merge_page` already leaves such a page alone), and no image is resampled
  (`effective_resolutions` gives every image `INFINITY`).
- A form damaged partway leaves the measuring incomplete in the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
FredApps 9cc1736dc9 Enhance scanned file: decode bi-level images within the budget, DecodeParms resolved 2026-11-05 12:56:10 +01:00
FredApps 2f2d69be17 M10: merge main into feat/scan-clean; classify mixed scan pages in one pass
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.
2026-11-05 12:14:01 +01:00
dotCooCoo 35da1c59f0 Merge main into feat/discard-external-refs 2026-11-04 11:07:49 -08:00
dotCooCoo b944c134d4 Merge main into feat/discard-search-index 2026-11-04 11:07:48 -08:00
FredAppsandClaude Opus 5.5 01751aeb7e Merge main into fix/flate-input-work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:37:20 +01:00
d579f26de3 M11: JBIG2 images share a symbol dictionary, up to 16 at a time (optimize.mono-compression) (#5910)
* 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>
2026-11-04 13:34:08 +01:00
FredAppsandClaude Opus 5.5 3ed799b6f8 Merge main into fix/flate-input-work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:53:59 +01:00
Osvaldo Duran MelendezandClaude Sonnet 5.5 a94808ffe6 Convert colours: pass the image budget to pixels after the merge with upstream
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 00:40:54 -05:00
Osvaldo Duran MelendezandClaude Opus 5.5 43c40582e7 Merge upstream main 14ae839a into pr/convert-colours: code conflicts resolved by hand, translation catalogs kept from both sides, ATTRIBUTION hashes recomputed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:39:57 -05:00
cb405f207e fix(optimize): make pictures under a stencil mask smaller (#5847)
* fix(optimize): make pictures under a stencil mask smaller

An image with any /Mask was skipped by the colour and grey settings. That is
right for a colour-key mask, which names sample values that resampling and JPEG
would change. A stencil mask is an image of its own, with its own resolution
(ISO 32000-2 8.9.6.3), so the picture can change under it and the mask stays as
it is. Some exporters put one on every picture, even where it hides nothing, so
whole drawing sets of pictures were left at full size.

A mask that can't be read as a stencil, such as a reference to nothing, still
leaves its picture alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(optimize): only a mask that says /ImageMask true counts as a stencil

A stream /Mask without /ImageMask true let its picture be replaced, although a
mask that isn't a stencil is meant to leave its picture alone. It now does.

The stencil test also checks that the hole still shows what's behind it. The first
pixel all round is left out, because the coarser picture blends into it a little
(at most 26 of 255 in this test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(optimize): draw a coloured page behind the stencil test's picture

On a white page, a hole painted white looks the same as an open hole, so the check
on the hole couldn't tell them apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: quincy-jc <184324679+quincy-jc@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:32:58 -05:00
dotCooCoo f9ce38e1b5 Merge main and preserve indirect PieceInfo dictionaries 2026-11-02 20:31:29 -08:00
dotCooCoo 3ef82b55eb Preserve action chains when discarding external references 2026-11-02 20:15:58 -08:00
dotCooCoo c3749e6a98 Merge commit 'refs/check/4434-main' into feat/discard-external-refs
# Conflicts:
#	crates/automation/src/tools.rs
2026-11-02 19:00:33 -08:00
Grzegorz MartensandClaude Opus 5.5 10172d7a94 M2.6: a Flate stage pays for what it reads
The image allowance charged a Flate stage the bytes it made, and its input
only as what the stage before it made. Inflating does work for every block it
reads, whether or not the block makes anything: a stream of empty
fixed-Huffman blocks, ten bits each, took 1,383 ns a byte to make nothing
(miniz_oxide 0.9.1, release), one of a literal each 768 ns a byte, empty
dynamic blocks 106, against the 3 ns a step budget.rs states. A 250-byte
stream of four Flates over empty blocks took 47 s and was charged about
34 million steps, so a review of #5698 worked out about 7 hours before a
240 KB file of them was refused, through clearing, Optimize, export, compare
and redaction's images and dictionaries alike.

Where decoding is paid for (decode_tolerant_within, decode_within), a Flate
stage now pays FLATE_READ_WORK, 128 steps, for each byte it reads, before it
runs: a stage the allowance can't pay for doesn't start. Per byte read, the
worst of the shapes measured comes to about 11 ns a step at that price, inside
what a JBIG2 step takes. The raw retry of a zlib header that only looked valid reads the
bytes again, so it pays again, makes no more than is left once it has, and is
refused when it can't be paid for. Byte counts for a byte allowance
(decode_counted, decode_tolerant_counted, as a page's content uses) are
unchanged.

With a tiny file's allowance (2^34 steps), the 250-byte stream is decoded
three times in 141 s, and the fourth stops before its last stage reads the
empty blocks (8.2 ns a step, release). Data an encoder makes took 0.4 to 3.2 ns a byte it
made, reading included, and each byte of a file adds 64 times the price to an
operation's allowance.

Tests: a_flate_stage_pays_for_what_it_reads (empty blocks: 129 steps a byte,
refused a step short with nothing inflated) and a_second_reading_is_paid_for;
the exact costs in filters, cos and optimize tests add what Flate reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
FredApps ec25e7d327 M2.6: images decode outside the renderer only through an ImageBudget
`ImageBudget::decode` is now the one public way to decode an image's
CCITT fax or JBIG2 data: `Document::decoded_image` and
`decoded_image_within`, which decoded with no limit on the work, are
test-only in pdfcraft-cos, and pdfcraft-filters exports only
`decode_ccitt_within` and `decode_jbig2_within`, which spend from a
caller's allowance (`decode_jbig2` is gone, `decode_ccitt` private).
So an operation over many images can't decode them without sharing an
allowance; the compiler holds new callers to it. Doctests that must not
compile check both crates; with the old entry points public they fail.

Tests outside the crates decode through an unlimited ImageBudget.
Streams in general (`Stream::decoded`) are left for a follow-up.
2026-11-02 14:24:21 +01:00
dotCooCoo f13d6db75a Merge main into discard-search-index 2026-11-01 18:09:23 -08:00
dotCooCoo 40d7c6cbd7 Merge main into discard-external-refs 2026-11-01 17:58:56 -08:00
Osvaldo DuranandClaude Opus 5.5 d06154ac80 Print production: Convert colours (grayscale, RGB, CMYK by the device formulas)
Every colour operator in the pages' content and the Form XObjects it draws is
rewritten in the target space by the standard device formulas, source colours
read through the space in force (edit/src/colour.rs: device, ICCBased, CalRGB/
CalGray, Lab, Indexed, Separation/DeviceN through a Type 2 tint). Preserve black,
pictures too (optimize::recolour_image: 8-bit gray/RGB re-encoded, JPEG stays
JPEG, CMYK lossless), comments' and fields' appearances optionally.
Edit::ConvertColours (one undo step; refuses when nothing changes, so
Session::count_colours counts first), the Convert colours dialog, command
prepress.convert_colors, tool doc_convert_colours, rows in every language
(RGB and CMYK allowed unchanged in the Ukrainian and Kazakh checks), parity
print.convert-colors shipped.

Carries the review fixes:
- ATTRIBUTION.toml: the language files' checksums after the new rows
- Convert colours: keep RGB and CMYK as-is in the Czech and Turkish menu-label checks
- Convert colours: CMYK black start, shared objects copied, streams kept apart, guarded count
- Convert colours tests: drop an unused helper
- Format with rustfmt
- i18n(ui): carry this PR's new wordings in the catalogues that must cover them
- Parity: evidence paths follow the tests moved into tests/it
- fix(colours): comment permission for appearances; page range per document
- fix(convert-colours): say what it is not, and what it leaves behind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:53:50 -05:00
FredApps 4d8eede62a Merge pull request #5472 from Piohrun/feat/mono-jbig2
M11: Monochrome Images reads JBIG2 scans: averaged like any, and stored again as JBIG2 (optimize.mono-compression)
2026-11-01 22:44:16 +01:00
FredApps 2850d52825 Merge pull request #5471 from Piohrun/feat/mono-indexed-polarity
M11: Monochrome Images averages indexed black-and-white images, the palette saying which sample is dark (optimize.downsample-images)
2026-11-01 22:43:06 +01:00
FredApps 99e63d4238 Merge pull request #5461 from Piohrun/feat/redact-jbig2
M8: redaction clears the covered pixels of JBIG2 scans instead of removing them (protect.redact-image-pixels)
2026-11-01 22:39:07 +01:00
FredApps cc986fa289 Merge origin/main into feat/jbig2-run-allowance 2026-11-01 21:06:29 +01:00
Grzegorz MartensandClaude Opus 5.5 db4162d2ef test(create,optimize): the other way round, the same size and larger
The shapes in these tests all ran one way. The way round depends on the shapes: 1 × 100,000 to
100,000 × 1 asks for 10 GB if it goes across first, or 30 GB at three components, so that is held to
as well. A mask of its image's size comes through byte for byte, and a larger one scales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 09:31:58 +01:00
Grzegorz MartensandClaude Opus 5.5 640e7acb15 fix(create,optimize): a mask of other proportions doesn't take the process down
Scaling goes down first, through a buffer of the source's width by the target's
height at 16 bytes a pixel, and neither image's size limit bounds that product.
A soft mask of 60,000 × 1 on an image of 1 × 60,000, about a kilobyte of PDF,
asked for 57 GB and aborted the process, in image export, Office export, Copy
Image and Optimize at its default settings. The same shapes in smaller sizes
held gigabytes.

Such a picture is scaled across first, which holds no more pixels than one of
the two has, so the size limits bound it after all. Proportions that aren't
far apart scale as they did, to the byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 02:28:29 +01:00
Grzegorz MartensandClaude Opus 5.5 2fb88feaf7 M2.6: a JPEG pays for being read before its header is
Reading a JPEG's header was free, and it can be most of the file: a mask of
1 MiB of tables that disagreed with its dictionary, shared by 4,000 images,
took 101 s with none refused, and that time grows as the square of the file.
A JPEG now pays 4 steps a byte before its tables are parsed, on every path, and
a mask's header is read once. That is a bound and not a speed-up: the same
file of 4,000 images fits the allowance and still takes 97 s, and 12,000 stop
after 10,233 in 250 s.

A chain that can't run pays nothing for its pixels, and the docs say what past
the allowance isn't counted: LZW streams the image pass didn't read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 01:57:39 +01:00
Grzegorz MartensandClaude Opus 5.5 ade8ebbdb4 M2.6: a run pays for what a stream costs, not what it holds
Four ways round the allowance, each measured by a review of hostile input:

A Flate stream read as zlib that fails at once, and then as raw deflate that
decodes everything before failing, gave nothing back and so was charged
nothing, here and in DecodeBudget: 110 s for a 387 KB file, all decoded. What
the retry decoded now counts, though the data is still dropped as it was.

Fax and 1-bit Flate images paid by the byte for work done by the pixel: 19
bytes can be a blank page of 100 million pixels, about a second each. Every
image decoded here now pays for its pixels first, as JBIG2 already did.

A JPEG paid nothing, and what one asks for doesn't grow with its length: 156
bytes declared 80 megapixels, 72 s for an 8 KB file. A JPEG pays for its
samples before it is decoded, in Optimize and in an export that has to decode
one for its mask.

Merging fragments paid for reading strips and not for the picture it built:
393 s for 1,154 bytes. It pays for the picture before allocating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 00:20:43 +01:00
Grzegorz MartensandClaude Opus 5.5 696728686f M2.6: Optimize's LZW pass spends the run's allowance, and Cancel reaches it
Storing LZW streams with Flate decoded image streams outside the allowance,
with a limit for each stream and no total, and nothing asked whether to go
on: a 593 KB file of 100 images took 113 s with none refused, and images
refused by the image pass were still rewritten. The pass now pays for what
it decodes and for storing and checking it, at 8 steps a byte since Flate
takes up to 78 ns a byte, and streams past that stay LZW.

An image is counted as refused only when it stayed as it was: one packed
before the allowance ran out on its second read was rewritten. doc_reduce
reports images_refused as doc_optimize does, and the app's action says so
in its log. A refused image no longer reads as an unsupported filter:
FilterError::Spent says what happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 23:26:25 +01:00
Grzegorz MartensandClaude Opus 5.5 083191f970 M2.6: every filter of a run's images pays, whether or not it works
Filters were charged only for what they handed a JBIG2 decoder, once they
had handed it over: a chain that failed, or went past its image's limit,
cost its time and nothing else, and images that weren't JBIG2 weren't
charged at all. A 734 KB file of 200 images of Flate inside Flate took an
image export 111 s, and that grew with the file. It now takes 12 s.

Each byte read and each byte a stage decodes is now a step of the same
allowance, paid as it happens, so no stage runs past what is left. That
holds for every image read through ImageBudget, and a fax image pays for
its rows and its code. JBIG2 globals pay when they have to be read, and
kept ones don't. Optimize's two reads that went round the budget go
through it, and merging fragments shares it.

Optimize reads an image twice, to pack it and to resample it, so a refused
image is now counted once a run rather than once a read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 22:23:06 +01:00
Grzegorz MartensandClaude Opus 5.5 621f17f3b5 M2.6: an image already at the target resolution isn't decoded to find that out
Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 18:07:06 +01:00
Grzegorz MartensandClaude Opus 5.5 a6944cbbdf M2.6: the widest image that can be averaged is decoded
The exclusion's test only looked past the limit, so a mutant moving it by one survived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:43:46 +01:00
Grzegorz MartensandClaude Opus 5.5 199354246b M2.6: redaction reads past /Crypt, and what can't be averaged spends nothing
Redaction checked the raw /Filter array, so /Crypt in a chain removed an image it could have cleared,
though decoding leaves /Crypt out. It leaves it out too.

Decoded only to be resampled, an image that can't be averaged came to None after the decode, having
spent the run's allowance and perhaps been counted as refused. Each such case now stops before it.

Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:36:44 +01:00