Commit Graph
586 Commits
Author SHA1 Message Date
863af864a7 M1: respect generations in PDF object references and full saves (#1143)
* M1: respect generations in COS object references and full saves

Resolve stale references to null instead of replacement objects with the
same number. Validate complete identities in writer readers, indirect
stream lengths and catalog checks, and keep generation-aware length caches.

Cover cache reuse, edit overlays, snapshots, incremental and packed/classic
full saves, and stream lengths with synthetic regressions and independent
hayro checks.

Co-Authored-By: Codex <noreply@openai.com>

* M0: clarify refreshed CI roadmap note

Co-authored-by: OpenAI Codex <noreply@openai.com>

* M1: repair contradictory xref generation references

Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.

* M1: cover repaired generations and freed-slot round trips

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M1: document generation repair and bounded diagnostics

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M1: record generation repair regression evidence

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M1: document bounded COS generation repair

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M1: record generation repair parity evidence

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* fix(cos): preserve repaired generations through preflight and saves

Keep accepted xref/header aliases across repair-aware edits and snapshots, canonicalize full-save identities, and retain bounded generation diagnostics from older revisions. Add COS, writer/encryption, and real preflight regression coverage.

* test(preflight): assert repaired aliases share saved identity

Address the review nitpick by comparing reopened HeaderImage and RowImage references after damaged full saves, for classic/packed output and edited/restored snapshots. Focused preflight regression and rustfmt check pass.

* M14: update certificate dependencies for RustSec advisories

Bump rustls-webpki to0.103.16 and webpki-root-certs to1.0.10. Preserve all other locked packages and dependency lists, manifests, Git pins, and advisory policy.

---------

Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 09:28:43 +01:00
FredApps 15c505d1c6 Merge pull request #3858 from Kronbii/fix/scan-image-resampling
M2: scans stay sharp at screen size and smooth when zoomed (area-average shrink, bilinear enlarge)
2026-11-05 06:03:58 +01:00
FredApps 84e85c1ff3 Merge pull request #5758 from hasannadeem/feat/update-check-preference
M14: Preferences can turn off the update check at start (#5737, #5460)
2026-11-05 05:39:12 +01:00
FredApps f3dc867651 Merge remote-tracking branch 'origin/main' into w
# Conflicts:
#	vendor/README.md
2026-11-05 05:14:13 +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
kronbiiandClaude Opus 5.5 ff227b770c M2: area-average shrunk scans and smooth enlarged ones
Scanned pages looked thin and broken at screen size. An image that does not
ask for /Interpolate (most scans) was pre-shrunk with nearest-neighbour since
about one sample in ten and dropped most strokes of its text, and the nearest
sampler then read the shrunk raster again at a fractional offset.

- Shrinking (vendored hayro patch 21): non-interpolated images outside Type 3
  glyphs are area-averaged (area_shrink: each device pixel the
  coverage-weighted mean of the samples it covers, 16.16 fixed point whose
  weights sum to exactly one, premultiplied when masked) to their snapped
  device rectangle and drawn 1:1. /Interpolate governs smoothing when an image
  is enlarged (ISO 32000-2 8.9.5.3); averaging adds nothing between samples
  and drops none. PicScale's Box/Area filters were not usable: shrinking, they
  average a window four target pixels wide.
- Enlarging (patch 28): an image of page-scan size (512+ samples on its
  shorter side) is drawn bilinearly when enlarged, so a zoomed scan has no
  staircase edges; icons, QR codes and pixel art keep their hard pixels.
- A snapped image whose shrink failed keeps the full transform instead of
  being drawn at its source size (latent in the interpolated path too).

Measured on a 13-page 300 dpi scan at 96, 120 and 192 dpi: every rendered
sample is within 1 of the exact area average (it was up to 255 off, 17-23%
of samples by more than 1). Bilinear zoom costs nothing measurable (four
1024 px tiles at 400%: 50 ms, nearest 48 ms; bicubic was 267 ms).

Review fixes:

- a tile snaps a shrunk image's half-pixel edges where the whole page does
- a scan shrunk one way is smoothed the way it is stretched
- a smaller soft mask doesn't let transparent colour through the area shrink

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:32:52 +02:00
Hasan 36d24eae6e M14: Preferences can turn off the update check at start (#5737, #5460)
The desktop app asks for a newer release on its first frame, and nothing
could stop it, though the README says PdfCraft works offline. A preference,
on by default so nothing changes for anyone who leaves it, keeps the app off
the network until Help ▸ Check for updates is chosen. It has its own settings
key: check_updates_at_start was an opt-in, off unless chosen, so settings
written then would turn this off unasked.
2026-11-04 12:17:00 +05:00
9ce6d89f46 i18n: interface translations from craft-i18n (#6055)
* build(i18n): embed the craft-i18n catalogs from a pinned checkout

- `craft-i18n.rev` pins storytold/craft-i18n at e45ddf7 (the PdfCraft cutover sync of
  pdfcraft@ecdd1e6); `.gitattributes` keeps it LF.
- The `craft-i18n` crate (craft-libs 787a7a0, PdfCraft's existing pin) is a direct dependency
  of the UI crate; it was already in the tree through craft-ui.
- `crates/ui-egui/build.rs` writes `CRAFT_I18N` from `CRAFT_I18N_DIR`: one (locale, common.tsv,
  pdfcraft.tsv) entry per locale with a `pdfcraft.tsv`, paths written with `{:?}` and never
  canonicalized. Unset it is empty (English only); `CRAFT_I18N_REQUIRED=1` makes a missing or
  invalid checkout an error.
- Both flatpak manifests name the craft-i18n commit and `CRAFT_I18N_DIR`; a test keeps both
  commits equal to the trimmed pin.

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

* feat(i18n): look strings up through craft-i18n behind a thin Lang wrapper

`i18n.rs` keeps PdfCraft's API (tr, tr_ctx, tr_id, trn, key_name, fmt,
Lang, LANGUAGES, detection) and hands lookups to the craft-i18n crate:
`get` for key names, `trn_plain` for plurals (no isolates, no digit
grouping), PdfCraft's own `fmt` (no FSI isolates). `@id` rows and the
"Undo {action}" template are read only where PdfCraft had them, so menus
and history labels read exactly as before.

Detection is unchanged (LC_ALL/LC_MESSAGES/LANG, macOS defaults,
sys_locale); the crate's match_tag would drop nb for no/nn, so PdfCraft's
own tag mapping stays, with one parity test per path.

Completeness tests become reports; tests that need catalogs skip with a
note when the build has none. Font coverage tests read the embedded
catalogs (shared and PdfCraft rows).

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

* chore(i18n): remove the in-repo catalogs; craft-i18n holds them

The 34 `crates/ui-egui/src/i18n/*.tsv` catalogs were migrated into
craft-i18n at pdfcraft@ecdd1e6 and are now embedded from the pinned
checkout. ATTRIBUTION.toml drops their 34 entries for one craft-i18n
`[[build_input]]` (craft-i18n's ATTRIBUTION.md credits the translators);
AGENTS.md and docs/localization.md describe the new system and how to
add a language.

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

* test(i18n): skip catalog tests without craft-i18n; drop the catalog hash exemption

Tests that switch to a non-English language skip with a note when the
build embeds no catalogs (CRAFT_I18N_DIR unset), so the English-only
build tests green. `skip_without_catalogs` moves to the integration
test support module.

With no catalogs left in the repository, `cargo xtask assets` no longer
exempts `crates/ui-egui/src/i18n/*.tsv` from hashing: every `[[asset]]`
carries a sha256 again, and a `.tsv` added back would need one.

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

* build(i18n): watch the locales directory before reading it

A CRAFT_I18N_DIR that doesn't exist yet now reruns the build script once it appears (review).

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

* build(i18n): pin craft-i18n dfbe955, the merge of storytold/craft-i18n#129

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

* fix(i18n): skip an @id row that changes the label's placeholders or ellipsis

craft-i18n checks placeholders and the trailing ellipsis on every row whose
source is English text, but not on @id rows, whose source is the command id.
The in-repo catalog test used to fail on such a row; since the cutover it only
prints it, while tr_id and menu_label still showed it. Both now use PdfCraft's
own @id row only when it keeps the label's shape (same_shape, the rule
craft-i18n applies elsewhere), and otherwise show the label's translation, as
bundled_catalogs_are_consistent says.

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

* fix(i18n): reject an @id row with an open brace; list unused @id rows in the gaps report

same_shape skipped a `{` with no `}`, so an @id row such as "Öffnen {name…"
for "Open…" was still shown. It now counts as a changed shape.

bundled_catalogs_are_consistent printed the @id rows PdfCraft doesn't use, but
CI captures test output, so nobody saw them. translation_gaps_report, which
`cargo xtask i18n-report` runs with --nocapture, now lists them
(unused_id_rows).

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

* fix(i18n): reject an @id row with a `}` that closes no placeholder

same_shape counted only the `}` that close a `{`, so "Öffnen name}…" for
"Open…" or "{name}} öffnen" for "Open {name}" still passed. A row whose `}`
count differs from its placeholders now counts as a changed shape.

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

* fix(i18n): no braces in the assert messages of the brace test

assert! reads its message as a format string, so "a `}` without its `{`"
did not compile.

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 00:47:17 +01:00
AdrianTM 0d3f57e120 M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850) (#5954)
* M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850)

* M5: preserve typewriter whitespace through readers and edits (#5850)

* M5: retain direct-subtype comment listing for Fill & Sign fix (#5850)
2026-11-03 10:24:45 -05:00
ea353f7a59 fix(ui): do not open right-click context menu on image in crop mode (#5835) (#5900)
* fix(ui): do not open right-click context menu on image in crop mode (#5835)

* M7: preserve active crop context routing (#5835)

Prioritize active crops over overlapping images and share measured action-pill geometry with painting. Cover overlapping images, handle margins, page edges, and all view rotations with regression tests.

* fix(ui): test selected cropped image by quad in right-click loop (#5835)

* fix(ui): trim crop right-click routing to the crop box and handles (#5835)

The Done/Cancel/Remove Crop pills are widgets and already take their own
right-clicks, and the canvas context-menu guard already closes the image
menu, so drop the pill hit-test, the crop_pills layout extraction and the
extra secondary-click block. Move the session note to the Progress log.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 14:00:16 +01:00
Fatih KunakandClaude Opus 5.5 14ae839a28 i18n(tr): translate the 288 strings the gaps report lists (#5795)
* i18n(tr): translate the 288 strings the gaps report lists

The translation gaps report listed 288 English strings with no Turkish
translation, among them the Window menu ("Pencere"), the side-panel and
hotbar labels, the signature and timestamp validation messages, the
accessibility checker findings, the plurals and the calendar weekday heads.
The two English stand-ins at the top of the catalog are translated too.
`cargo xtask i18n-report` now lists 0 missing for tr.

Terms follow the existing catalog (annotation = açıklama, digital ID =
dijital kimlik, bookmark = yer işareti, redact = karart). Placeholders and
trailing ellipses match the English source. AI-assisted; not separately
reviewed line by line by a native speaker.

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

* i18n(tr): note the native review of the judgement calls

A native Turkish speaker reviewed the translations the PR flagged as
judgement calls ("of" as a slash, hotbar, navigation rail, the certificate
key-usage terms, the weekday heads) and kept them. The rest stay
AI-assisted, so full proofreading is still pending.

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

* docs(i18n): state the Turkish coverage by the gaps report, not the dated union

The 2,798-key union in localization-parity.md was measured on 2026-10-25 and
predates keys added since, so "every current key" against it did not add up
(CodeRabbit). The Turkish row now says what was measured: 2,867 entries and
0 missing in `cargo xtask i18n-report` on 2026-11-03.

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

* Revert the vendored Cargo.lock changes c700ce42 picked up by mistake

They came from running the vendored crates' tests locally and have nothing to
do with this PR.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:33:33 -05:00
Hasan N. bf0f3c9f77 M10: macOS print jobs name their title and owner (#5743) (#5757)
* M10: macOS print jobs name their title and owner (#5743)

lp sends no com.apple.print.JobInfo.* attributes, and Kyocera's macOS
filter crashes without PMJobName and PMJobOwner, so the job stops with
cups-filter-crashed and nothing prints. Both go to lp as job options, in
the form lp_args passes on: printable ASCII, up to 64 bytes.

* M10: keep macOS job metadata as the name CUPS should send (#5743)

Review of #5757, checked against this Mac's libcups in memory:

- `'` and `\` were consumed as quoting and `{` opened a collection, and a
  bare true/false became a boolean, so a filter reading PMJobName got
  nothing or the wrong type. They now become `_`, or the fallback.
- CUPS keeps the last of a repeated option, ignoring case, so the
  defaults replaced a value a doc_print caller had set. They are now
  added only for a key the job leaves out.
- Nothing proved the job info reached the spooler. A test now runs both
  submit paths against a stand-in `lp` on PATH.
2026-11-03 00:33:11 -05: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
d3384340a6 M7: make page-mark replacement transactional (#5657) (#5704)
* M7: make page-mark replacement transactional (#5657)

* ROADMAP: drop the header line (the log entry stays)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:49:18 -05:00
FredApps 0cba87ae6f Merge pull request #5677 from Arch-amon/arclight/freehand-hotbar
feat(ui,annot): customizable hotbar strip, freehand highlight, and moveable navigation rail
2026-11-01 23:34:38 +01: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 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
archamon 53c79ca9b0 feat(ui,annot): customizable hotbar strip, freehand highlight, and moveable navigation rail
- Add Freehand-Highlight tool with 100pt line width support, Lucide highlighter icon, and PDF Multiply blending.
- Add customizable floating and docked hotbar with command picker, size slider, full-side strip chrome integration, and settings gear icon anchored at the bar end.
- Add left/right docking options for the navigation rail with toggle button and preferences setting.
2026-11-01 20:23:12 +00:00
FredAppsandCodex GPT-6.1 a9de3db628 docs: record image editing review fixes and movement semantics
Validated with 3231 passing scoped tests (10 skipped), clippy, wasm checks, repository gates, and 42 CLI smoke steps including saved-document verification.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 17:59:35 +01:00
FredAppsandCodex GPT-6.1 2ab9c6992c docs(edit): record latest PR 2155 review fixes
Record delivered clipboard events, the mixed-size display correction, and explicit mixed-script normalization with Undo. Combined scoped Linux validation passed: 2785 tests, clippy, doctest checks, wasm, formatting, layers, assets, parity, and a 26-step CLI smoke test.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 16:38:24 +01:00
FredAppsandCodex GPT-6.1 0b0d39c661 docs(edit): record PR 2155 review follow-ups
Record image crop and stream ownership, visible orientation, clipboard and formatting fixes, Home controls, and reloadable font discovery as PR candidates. Estimates remain unchanged.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 15:01:00 +01:00
FredAppsandCodex GPT-6.1 1184bd02cb docs(edit): record safe crop and arrange semantics
Describe editor-owned crop removal, preserve authored clipping paths, and link the scope/state regressions in parity evidence. Record the maintainer changes without changing readiness estimates.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 13:00:26 +01:00
FredApps f2e1a8ce29 Merge branch 'main' into fix/1577-optional-page-edge-shadows-integration 2026-10-31 23:38:40 +01:00
FredApps a471cb1664 Merge pull request #1433 from srt8hellcat2-ui/feature/two-paged-cli-1354
M3: add optional --two-paged CLI flag (fixes #1354)
2026-10-31 23:35:31 +01:00
FredApps 50780519e3 Merge current main into PR2212 and reconcile license inventory and shared CI 2026-10-31 22:14:35 +01:00
FredApps bad4c81800 Merge current main into PR2088 and port shadow preference to split UI modules 2026-10-31 22:13:25 +01:00
FredApps 286a3c6eee Merge current main into PR1433 and preserve launch override across module splits 2026-10-31 22:12:05 +01:00
aiongg da4cc2c145 docs: tidy wording 2026-10-31 04:25:36 +00:00
aiongg 44ddb348a9 docs: tidy agent, contributor and asset docs 2026-10-31 04:17:20 +00:00
Grzegorz MartensandClaude Opus 5.5 39eb03a0b6 M11: Monochrome Images reads JBIG2 scans: averaged like any, and stored again as JBIG2
#5461 made Document::decoded_image read JBIG2, page exactly the image's size, and
kept the optimizer's monochrome step off it until it could weigh JBIG2 again. Now
one_bit reads JBIG2 last after any filters, within the raster's allowance, and mono
treats it as it does any black-and-white image:
- above the threshold it is averaged, and stored as the compression asks, the
  smallest of what that may store; under Retain it is stored as JBIG2 again, its kind
  of compression kept;
- not resampled, it is stored again as the compression asks when that is smaller: a
  generic region becomes symbols with JBIG2 asked for, and JBIG2 behind ASCIIHex is
  stored again alone (as Group 4 where that is smaller);
- a JBIG2 image stored again refers to no /JBIG2Globals; one whose page isn't its
  size isn't read, and stays.

jbig2_images_are_left_as_they_are becomes jbig2_scans_are_averaged_and_stored_again:
a JBIG2 scan at 600 ppi, alone, behind ASCIIHex and with globals, averages to the
same samples as its Flate copy under each compression (Retain keeping JBIG2); at 300
ppi a generic region of rendered text stays under Group 4 (larger) and Retain and
becomes symbols under JBIG2 (under half the size); behind ASCIIHex it is stored again
alone, as Group 4 where the hex is spaced out; a taller page stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:05:09 +01:00
songzhihui a57095c872 M2: assert exact GBK glyph rasters for installed Chinese families 2026-10-31 01:39:00 +08:00
songzhihui 9447cdc810 M2: merge main and resolve CJK viewer integration conflicts 2026-10-31 01:14:02 +08:00
Grzegorz MartensandClaude Opus 5.5 78fc1e5568 M8: redaction clears the covered pixels of JBIG2 images instead of removing them
Part of #283. A scan stored as JBIG2 (as the optimizer writes them) is decoded
through Document::decoded_image_within, its /JBIG2Globals included, and its covered
pixels cleared in a Flate copy that refers to no globals, as CCITT fax images are.
Before, any JBIG2 image under a mark was removed whole, blanking the page.

Still removed whole (fail-closed): a JBIG2 page of another size than the image,
which decoded_image refuses (the renderer draws the whole page into the image, so
the rows cleared wouldn't be the ones shown); globals that can't be read; damaged
data; an image claiming more than one bit or one component, even where its rows
take as many bytes; a page far past the image's raster, refused before it is
decoded.

Globals no other image uses leave the file with the original on the full rewrite
(tested); globals another image still uses stay, and the README and parity note say
they may hold the shapes of marks from under the area, not where they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:03:06 +01:00
FredApps d7f3e67141 fix: bound backdrop edits and withhold unsafe integration behavior 2026-10-30 14:45:50 +01:00
FredApps ee71348bc8 fix: bound soft-wrap metadata and preserve unknown backdrop content 2026-10-30 12:47:37 +01:00
FredApps 6d7f739e3c test: enforce page press routing and record integration regression evidence 2026-10-30 12:38:09 +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
Grzegorz MartensandClaude Opus 5.5 c7f39a4d9a M11: the parity table no longer lists JBIG2 symbol coding as missing
docs/target-app-parity.md's optimize row lists lossy JBIG2, refinement
coding and a symbol dictionary shared between images as missing, 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 22:37:25 +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 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 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 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