1065 Commits
Author SHA1 Message Date
songzhihui 9447cdc810 M2: merge main and resolve CJK viewer integration conflicts 2026-10-31 01:14:02 +08:00
FredApps 4791cacd2c test: qualify conservative tone retention and format review regressions 2026-10-30 14:55:44 +01:00
FredApps d7f3e67141 fix: bound backdrop edits and withhold unsafe integration behavior 2026-10-30 14:45:50 +01:00
FredApps 7d3d4830e5 Integrate remaining six PdfCraft parity PRs with conflict resolutions
Includes #4325 #1952 #4180 #2529 #4320 #4805, preserving their original histories.
Keep paragraph alignment and hard/soft wrap metadata together; retain CCITT/downsampling/RGB ownership guards with bounded JBIG2 export and exact-copy symbol encoding.
Combined interaction regressions added. Validation pending.
2026-10-30 11:02:07 +01:00
FredApps b21af3b969 Integrate 21 clean-merging PdfCraft parity PRs
Preserve each original PR head and its authorship as a merge parent.
Includes #4304 #4335 #4384 #4404 #4179 #5300 #4719 #4541 #2815 #4620 #3789 #3779 #2960 #5208 #4596 #3636 #4494 #4013 #5134 #2133 #2635.
Conflicting candidates skipped. Combined CI pending; see docs/integration-20261030.md.
2026-10-30 10:35:42 +01:00
FredApps ea66a642fe Reconcile Pages sidebar selection with current keyboard and panel modules
Preserve main's continuous text selection, page tools and module split. Carry only this branch's sidebar-focus feature and regression onto main, without resurrecting old common files from criss-cross merge bases. Hosted CI remains required.
2026-10-30 08:52:06 +01:00
FredApps 6b38a5ab47 Reconcile typed font sizes and Fill & Sign colours with current UI
Use the unified Format Text panel and current page-input context instead of restoring old monolithic UI code. Preserve main's pointer routing, editor accessibility and preferences; retain colour, save and input regressions. Hosted CI must validate this refreshed head.
2026-10-30 08:47:42 +01:00
Grzegorz MartensandClaude Opus 5.5 2d0a7b2241 M11: black-and-white RGB used beyond the page content read stays RGB
Packing makes an RGB image grey for every use at once, but /DefaultGray and
/DefaultRGB were checked only where the walk reads: page content and forms.
An annotation's appearance, a pattern, a Type3 glyph or a soft mask's group
drawing the same image can have other default colour spaces, so its colours
could change there.

The walk now records, by object number, every reference in the resources it
reads (each /XObject entry, drawn or not, an /XObject dictionary under its
resources, resources under a page, page-tree node or form), reading the
resources of every page and of every node above one, each page and node a
root. An image the walk reached stays covered unless an object reachable
from the trailer refers to it, or to an object on its way from a page, other
than through those references, or it is held by one that isn't covered; an
eight-bit DeviceRGB image that isn't covered stays RGB. So resources, an
/XObject dictionary or a form a page shares with an appearance keep it RGB,
as does a reference with another generation, while LibreOffice's layout (one
resources object for a page, its /Pages node, and a form inside it that
names it again) still packs. The scan runs only when packing is on and there
is such an image.

The images drawn or listed where the defaults are in force are kept by
object number too, so one named with another generation there stays RGB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:57:08 +01:00
Grzegorz MartensandClaude Opus 5.5 f3a547d717 M11: a test that Export ▸ Images reads symbol-coded JBIG2
The optimizer now writes JBIG2 images as a symbol dictionary and text
regions, and Export ▸ Images has to read them back. A page of three
repeated letters, symbol coded (smaller than the generic coding),
exports as its picture. The test is added to create.export-all-images'
parity evidence. Asked for in pre-push review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 22:37:25 +01:00
Grzegorz MartensandClaude Opus 5.5 deb70c92ca M11: JBIG2 symbol coding: at most 10,000 copies a text region and 32,767 symbols
A review found that a dense page came out as a stream PdfCraft's own
renderer refuses, so the page drew blank after optimizing: hayro-jbig2
decodes a text region of at most 10,000 symbol instances, and refuses
32,768 symbols or more. The stream was valid (Poppler decoded it), but
every mark went into one text region and up to 65,536 symbols were kept.
A 300 dpi letter page of the optimizer test's own text crossed it.

A page with more than 10,000 marks is now placed by several text
regions, each over a band of rows and all referring to the one symbol
dictionary, which each region but the last keeps retained (7.2.4). Past
32,767 distinct symbols the image is coded generically.

New tests: 25,000 copies placed by three regions, which decode; the
symbol caps decoded, not just reached; and in the optimizer, a whole
letter page of rendered text that looks the same as JBIG2 symbols (it
drew blank before). The READMEs say "still looked at" for the caps, and
that the scans figure is the six pages' total (20% on one page).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 22:37:07 +01:00
Grzegorz MartensandClaude Opus 5.5 8eeafe3309 M11: JBIG2 symbol coding: tests for tall marks, the other corner and the symbol caps
A second review found two rules tested one way only: a mark 257 tall
(not just wide) is left to the generic region, which also bounds the
bitmap allocated for each mark, and runs joined at the lower-left corner
are one mark. The test now has a 1 x 257 column and an anti-diagonal.
The caps on distinct symbols (65,536) and their pixels (2^24) are tested
at and past them, as the run and row caps are.

The scans figure now names its noise: with 2% or more of the letters'
edge pixels flipped, symbols save at most 8% over the generic region.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 22:37:07 +01:00
Grzegorz MartensandClaude Opus 5.5 0d40594cf9 M11: JBIG2 symbol coding: tests for padding, the 32-bit range and the mark rules
Review found behaviour the tests let break unnoticed (each was a
surviving mutant, and each new test fails on it):
- black padding bits: a run reaching past the row would stop symbol
  coding on many scans;
- Annex A's 32-bit range and PREV at exactly 256, where a decision would
  go uncoded: coordinates past 4,435 on a 12,000-pixel-wide and a
  10,050-row page, and the contexts of 4436's coding;
- marks joined at a corner are one mark; 256 pixels a side is a symbol,
  257 is left to the generic region;
- the run and row caps, at and past them.

The roadmap's status line for this change now comes first, and the
"about a third smaller than Group 4 on scans" figure is replaced by what
was measured: 27-33% on the same pages with scan-like noise added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 22:37:07 +01:00
Grzegorz MartensandClaude Opus 5.5 9edd08ce07 M11: JBIG2 symbol coding for black-and-white images
pdfcraft_filters::encode_jbig2 stores each distinct mark of a 1-bit
image (8-connected black pixels, at most 256 a side) once, as a symbol
in a symbol dictionary, and places its exact copies with an immediate
lossless text region that refers to it. Larger marks, and in a second
plan marks that occur once, go to a generic region over their box. It
keeps whichever of that and the generic coding (encode_jbig2_generic,
unchanged) is smaller. Only exact copies share a symbol, so the stream
decodes to the same bits.

Written from the JBIG committee's ISO/IEC 14492 Final Committee Draft:
6.4 and 6.5 (text region and symbol dictionary decoding), 7.2, 7.4.2
and 7.4.3 (segment headers and syntax) and Annex A (the integer coders,
checked against its worked example and Table A.1).

On six pages of rendered 300 dpi text the output is 28% of the generic
coding's size and 17% of Group 4's, at about 100 ms a page; hayro-jbig2
and Poppler (pdfimages) both decode it to the original bits. Scans,
whose copies of a letter are seldom identical, gain little: that needs
refinement coding. A dictionary shared between pages (/JBIG2Globals)
is a further step.

The optimizer's Monochrome Images > JBIG2 (lossless) and doc_optimize
mono jbig2 use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 22:37:07 +01:00
Grzegorz MartensandClaude Opus 5.5 e18d1bfed3 M11: Export ▸ Images decodes JBIG2 globals within 32 MiB of their own
An image's /JBIG2Globals were decoded with the 1 GiB any stream may decode
to, before the JBIG2 page's size is known, so the page's pixel limit didn't
bound them: a few kilobytes of Flate could make export allocate up to 1 GiB.
They now decode within MAX_GLOBALS (32 MiB); globals past it skip the image,
with the limit in the reason. The test covers one byte over and exactly the
limit, and the image's /DecodeParms direct, by reference and in an array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:37:03 +01:00
Grzegorz MartensandClaude Opus 5.5 2b40413dc3 M6: calculation checks: an Acrobat widget name is a missing field, as nothing here resolves it
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>
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 feefeb8c95 M6: Calculation order: a formula that reads a missing field is marked, not only explained on hover
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
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 4cfd592730 M6: calculation checks: a formula that reads a field the form doesn't have, and a script that builds the names it reads
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
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 210ba20fe6 M11: the architecture doc says Export decodes CCITT and JPEG; the progress logs' wording
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 1ad951e25b M11: redaction removes Group 3 on bytes whole, which the renderer may draw as other rows
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 aaa3d815c1 M11: say where Group 3 with no code before the first row is written back as read
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 abae2472d3 M11: the crate docs, doc_optimize's mono and the M11 row name what the black-and-white step does now
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 740bb874a4 M11: say damaged Group 3 with end-of-line codes is stored as the decoder recovers it
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 d92ec2101a M11: monochrome downsampling's docs: halftones, the column cap, mask streams, what is left alone
- 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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 450c327c6b M11: black-and-white RGB stays RGB where grey would be drawn otherwise
- 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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 76236edf1e M11: the optimizer packs black-and-white RGB and indexed images to one bit
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 e221d552f9 M11: the optimizer stores Group 3 fax images as Group 4 when smaller
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 8bd156a4ac M11: Reduce File Size's descriptions say black-and-white images go to 300 ppi
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>
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 e3b9e175b5 M11: redaction clears pixels in CCITT fax images instead of removing them whole
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>
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 9fca1d44f0 M11: monochrome downsampling: say an indexed image's tie goes to entry 0, its palette unread
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>
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 151ca1d6ad M11: monochrome downsampling: a tie goes to the dark sample, after the decode array
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 907a9fa917 M11: monochrome downsampling: black-and-white images above 450 ppi are averaged down to 300
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
2026-10-29 20:48:03 +01:00
Grzegorz MartensandClaude Opus 5.5 8c720e4808 M11: Export reads byte-aligned Group 3 by the codes in its data
"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
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 ec4b265b57 M11: "the smaller wins" counts the soft mask in the tests and the docs
- 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
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 a4058c9242 M11: say redaction removes Create PDF's Group 4 pictures whole for now
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
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 f33227886d M11: eight-bit black-and-white grey takes the smaller of one bit and the grey settings
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
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 7ca7e01f92 M11: Export ▸ Images decodes CCITT fax images behind other filters
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
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 4712abaabb M11: Export ▸ Images decodes CCITT fax images
Group 4 images (now written by Create PDF and the optimizer) would
otherwise stop exporting: they were reported as "can't be exported
yet". A CCITT fax image (its only filter) is decoded through
hayro-ccitt with its /DecodeParms (K, Columns, Rows, EndOfBlock,
EndOfLine, EncodedByteAlign, BlackIs1), masks included; /Columns has to
be the image's width, so a hostile value can't size the buffers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:48:02 +01:00
Grzegorz MartensandClaude Opus 5.5 71875ce5a8 M11: Copy Image reads JBIG2; the parity table no longer lists lossless JBIG2 as missing
Copy Image and Copy Image in Original Format go through Export's
decoder, which reads JBIG2 now, so the view.copy-image parity note no
longer lists JBIG2 as missing. docs/target-app-parity.md's optimize row
lists JBIG2 symbol coding and lossy JBIG2 as missing instead of
monochrome JBIG2, with a status line and a revision row.

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
2026-10-29 20:47:54 +01:00
Grzegorz MartensandClaude Opus 5.5 d416d6a36e M11: say how export and redaction treat JBIG2 images they can't read
Export ▸ Images skips an image whose JBIG2 page has another width or
more pixels than an export takes; a JBIG2 stencil mask refused the same
way is left out and the image exported opaque, as JPX and CCITT masks
are, which the export test now checks. The README and parity note said
only "is skipped". And redaction doesn't decode JBIG2 or Group 4 yet,
so a mark over part of an image the optimizer stored either way removes
the whole image; the optimizer README and parity note now say so (#4384
adds Group 4). Found in pre-push review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:47:53 +01:00
Grzegorz MartensandClaude Opus 5.5 e6e34bfa8d M11: only Group 4 is limited to rows of 32,767 samples (README, parity)
The optimizer README's and optimize.mono-compression's "not yet" lists
said rows wider than 32,767 samples aren't stored. That is Group 4's
limit (MAX_G4_COLUMNS); JBIG2 takes rows of up to 1,048,576
(MAX_JBIG2_COLUMNS). Found by CodeRabbit on #4805.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:47:53 +01:00
Grzegorz MartensandClaude Opus 5.5 a069003782 M11: Export ▸ Images decodes JBIG2 images
JBIG2 images, and JBIG2 stencil masks, were reported as "can't be exported
yet", so an image the Optimizer stores as JBIG2 would stop exporting. They
are now decoded with hayro-jbig2 (already in the tree through hayro), with
their /JBIG2Globals from a DecodeParms dictionary or array, as JBIG2Decode
gives them: black as 0 (ISO 32000-2 §7.4.7). Unreadable globals, a JBIG2
page of another width, or fewer rows than the image skip the image; rows
past its height are dropped, so the output is bounded by the checked size.

docs/pdf-spec-parity.md §7.4 no longer says there is no JBIG2 encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:47:53 +01:00
Grzegorz MartensandClaude Opus 5.5 5c4cadce59 M11: lossless JBIG2 for black-and-white images
pdfcraft_filters::encode_jbig2_generic writes 1-bit rows as a JBIG2
stream for JBIG2Decode (embedded organisation: a page information
segment and an immediate lossless generic region, no globals): template
0 with its nominal AT pixels, typical prediction, and the MQ arithmetic
coder (mq.rs), written from the JBIG committee's ISO/IEC 14492 Final
Committee Draft. The coder reproduces its Annex H.2 test sequence byte
for byte; output round-trips through hayro-jbig2, and Poppler renders
it the same as the original.

The optimizer's Monochrome Images gets JBIG2 (lossless) beside CCITT
Group 4 (still the default), doc_optimize mono jbig2; a document below
PDF 1.4 gets /Version /1.4 in its catalog. On 300 dpi text JBIG2 is
about a third smaller than Group 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:47:45 +01:00
Grzegorz MartensandClaude Opus 5.5 656c6ef0f4 M11: name every reader that doesn't take Group 4 yet; count the new partial in the parity tables
- Not only Export ▸ Images refuses a CCITT image until #4335: so does
  everything that exports an image through it. Save Image As refuses
  it, Copy Image fails, and Word, PowerPoint and HTML export leave the
  picture out, with the page not reported. The optimize README and the
  mono-compression parity note name them.
- optimize.mono-compression is partial now, so docs/target-app-parity.md
  counts it on the table's own measurement: K Optimize and standards 15
  shipped and 11 partial of 58 (35.3%), breadth 57.0% (423 + 75 of 808),
  mainstream 63.3% (292 + 45 of 497), with a status line and a revision
  row. ROADMAP.md's breadth rows and its K row say the same.

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
2026-10-29 20:47:39 +01:00
Grzegorz MartensandClaude Opus 5.5 4543bae3bf M11: say redaction and export don't read Group 4 yet; Reduce File Size's descriptions
- Redaction doesn't decode CCITT, so a mark over part of an image the
  optimizer stored as Group 4 removes the whole image (nothing leaks),
  and Export ▸ Images refuses it. The optimize README and the
  mono-compression parity note say so. Later PRs of this stack add both
  (#4335 export, #4384 redaction).
- doc_reduce's description, Engine::reduced_bytes and docs/cli-batch.md
  now list the black-and-white step; docs/target-app-parity.md's
  optimize row gives the measured counts and no longer lists CCITT as
  missing; the M11 milestone row lists it as done.

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
2026-10-29 20:47:39 +01:00
Grzegorz MartensandClaude Opus 5.5 f175beb25a M11: PDF Optimizer stores black-and-white images as CCITT Group 4 when smaller
pdfcraft_filters::encode_g4 encodes packed 1-bit rows as Group 4 (ITU-T
T.6, /K -1) through the fax crate (MIT, already in the tree via tiff),
with rows capped at 32,767 samples so its 16-bit positions can't
overflow; round trips are checked against hayro-ccitt.

The optimizer (Settings::mono, CCITT G4 by default, so Reduce File Size
too) re-encodes a 1-bit image of one colour component, or an image
mask, stored with no filter or general-purpose ones, when that is
smaller as stored. The samples decode to the same bits, so colour
space, decode array and masks stay. PDF Optimizer gets a Monochrome
Images row and doc_optimize a mono option. optimize.mono-compression
goes from planned to partial (JBIG2 and monochrome downsampling not
yet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:47:39 +01:00
alehelandClaude Opus 5.5 adeb9c1cf2 fix(comments): Undo and Redo close the panel's inline editor
A delete moves the panel's inline editor along with its comment or reply,
but Undo and Redo cleared only the selection. Either can put annotations
back or take them away, so the editor stayed keyed to an index that could
now be another reply: with B's editor open, delete reply A, Undo, and Save
wrote B's draft over A. history_step now closes the editor (and its
unsaved text) with the selection. Found by FredApps in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:22:11 +01:00
alehelandClaude Opus 5.5 acbc746a57 Merge origin/main into fix/delete-single-reply
No conflicts. main's changes to the files this branch touches (the page
cursor router in canvas.rs, hover tips in tools/comments, new tools and
document methods) don't meet this branch's hunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:10:59 +01:00
ihsan mokhlisse 55a933800b M7: keep a typed space run through a saved width change (#2517)
The wrap's space count has to survive an incremental save and a full rewrite, and a width-only text_edit has to reflow with that count. A recorded editor session narrows the box and widens it again.
2026-10-29 10:42:20 +01:00
ihsan mokhlisse b282ec26be M7: check a shared text object through the tools (#2517)
Editing either end of one text object, then saving and editing again, has to leave the other paragraph in place. The library tests did not drive text_edit and doc_save.
2026-10-29 10:11:19 +01:00
ihsan mokhlisse 59513aa887 M7: keep a preceding paragraph out of a shared text object's marker (#2517)
A partial rewrite of the last paragraph opened the marker at the shared BT, so the paragraph before it was inside the region.
2026-10-29 10:11:19 +01:00