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
- 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
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
docs/pdf-spec-parity.md's §7.4 row still said there was no CCITT
encoding; it names the Optimizer's Group 4 encoder now, 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
- 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
- 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
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
* Record successful PdfCraft web application check
* E21: bound startup requests and validate font and license inputs
* E21: separate pending control completion and gesture regression phases
* M-egui037: enforce control admission and per-family font notices
* M-egui037: qualify capture fixes and document migration behavior
* egui-037: identify current and historical qualification results
* i18n(fa): add Persian interface translation catalog
Add crates/ui-egui/src/i18n/fa.tsv covering the current 2,776-key union, register fa (right-to-left, one/other plurals) in i18n and the date languages, and credit the catalog in ATTRIBUTION.
* i18n(fa): fix missing ZWNJ in compound suffixes
Join participles (شده/نشده) and productive suffixes (سازی, گذاری, گیری, بندی, پذیری, کننده, ...) and compound prefixes (هم, غیر, پیش, پس, چند, تک, باز) with U+200C, plus reviewer wording fixes.
* fix(i18n): Persian counts 0 as singular (CLDR fa) and writes weekday names with ZWNJ
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>
* M3: give the kept button release back before the pointer leaves (Wayland)
After a desktop grab the next input on Wayland often brings only
PointerGone. The synthetic release went after it, and a button event
sets egui's pointer position, so the gone pointer came back and hover
stuck at the old place until the pointer moved. The release now goes
before the first PointerGone as well as before the first press; a
release that follows PointerGone is still the kept one and not doubled.
Found by CodeRabbit in review of #3970.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs
* M3: move a release that follows the pointer leaving in front of it
A release the input brings after PointerGone is the one the desktop kept.
Left after PointerGone, it brought the gone pointer back at the release
position; it now moves in front, still not doubled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs
---------
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Report generators draw each picture as `q re W n cm Do Q` inside a clip the section shares with
its text. `image_transform_scope` only accepted a scope that contained every active clip, which
none does there, so it fell back to transforming the `Do` alone: the picture grew, but its own
clip stayed at the old size and the enlarged picture showed only through its old box.
When no scope contains every clip, the outermost scope holding only the image is used: it holds
all of the image's private clips, which now move with it, while the outer clip, shared with
other content, stays where it is. A clip shared with a caption inside the same scope still stays
fixed (`cropped_image_resizing_moves_private_clips_and_keeps_shared_ones`). Crop uses the same
scope, so cropping such a picture replaces its own clip instead of nesting a second one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- crates/render/src/inspect.rs: this branch's reply-linking methods and main's NoRotate
rectangles (#3016) both go in the Annotation impl; the annotation reader takes this
branch's loop with main's no_rotate field.
- crates/automation/src/comments.rs: this branch's HashMap import, main's import list.
- ATTRIBUTION.toml: each catalogue's source note keeps main's secured-document clause and
this branch's reply-count clause; ATTRIBUTION.md regenerated (cargo xtask assets --write).
- The 33 catalogues: main's new rows, then this branch's reply-count row.
main's #3016 tests build Annotation field by field; they now set parent and grouped.
* M14 #4253: remember the annotation tool pin across restarts
* M14 #4253: clarify roadmap timezone and Markdown formatting
* Keep main's roadmap status lines; only the progress-log entry is new
* Take main's forms.rs: main already gives each form test its own folder
* Roadmap: drop the form-test folder note (main already has its own)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: Document Properties has a Custom tab for custom properties
Document Properties has a new Custom tab between Fonts and Advanced. It
lists the document's custom properties, and Name and Value fields with
Add, Update and Delete edit the list. Clicking a listed property fills
the fields with it. OK writes the changes through SetInfo in the
existing "Change document properties" step, so one undo reverts them
with the other tabs' changes.
Add is off for an empty value, a name already listed, a name that
check_custom_key refuses (its message is the button's tooltip), and a
name the file already uses for an entry the list leaves out (not text,
only padding, or past the 256 listed). A property already in the file
under a refused name or a name that is not UTF-8 is listed, and Add,
Update and Delete stay off for it, so OK cannot fail on it or write a
different key.
Clearing an /Info entry now removes it when /Info is a dictionary
written directly in the trailer. set_info_entry returned without a
change there, so a delete in the Custom tab, a cleared Description field
and a doc_set_info clear left the entry in the file. It now writes a
copy without the entry as a new indirect /Info.
The engine re-exports check_custom_key and adds Document::has_info
(organize::has_info). set_option("dialog", "custom") opens the tab.
* Contributors: add dotCooCoo (Robert Lee)
* M4: Add in the Custom tab stops at the 256 properties the tab lists
Document Properties > Custom lists at most 256 properties, so a property
added past that was saved but left out of the list when the dialog opened
again. Add is now off once the tab's list or the file's own list holds 256;
a new name goes after the file's entries, so a file with more than 256 keeps
Add off after a Delete as well.
With only --automation-read-root / --automation-write-root set, a path
went straight to std::fs::canonicalize, so a UNC, WebDAV or device path
(`\host\share\x.pdf`, `\.\pipe\x`) was resolved, contacting the host,
before it was refused. `--root` mode already refuses those by name
(`foreign_share`).
`foreign_share` moves into file_access, and `AutomationRoots::check_path`
applies it to the path made absolute (by name, nothing read) before
following links, with the same refusal as any other path outside the
root. A root on a share still accepts that share. `resolve` is split
into `absolute` (empty path, device names, made absolute) and `resolve`
(links followed), so `out\NUL` keeps its own refusal.
Part of #4003 (the first of its three points).
main's #4284 stopped hashing translation catalogues in ATTRIBUTION.toml; the
conflict took main's side (no sha256 lines) and kept this branch's source notes.
Conflicts: docs/roadmap.md: all status lines kept, M10 row from main, M11 row with #3227's two sentences. parity/acrobat-features.toml: save-with-object-streams entry from #3227 with its test paths under tests/it/.
Conflicts:
- ATTRIBUTION.toml: fi.tsv source: main's text plus the PR's added sentence; the PR's sha256 line dropped (translation catalogs carry none on main).
- ATTRIBUTION.md: main's rows, fi row updated to the same source text.
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has the soft-mask sentence added to ours, M11 row from the earlier merge.
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has both sentences. parity/acrobat-features.toml: export.docx evidence has both tests, notes has both statements.
Conflicts: ROADMAP.md and docs/roadmap.md took both sides' status lines, progress-log and change-log rows; M10 table row from #4311, M11 row from #4310.
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.
pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.
The content_copy tool drives it headlessly, across pages and documents.
The desktop keys and menus come in follow-up PRs.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
* Assets: translation catalogs aren't hashed
The 33 UI translation catalogs are project files under the project
licence, and git history records their provenance. Their ATTRIBUTION.toml
entries keep title, author, source and licence but carry no sha256, so a
PR that changes UI text no longer has to edit ATTRIBUTION.toml.
cargo xtask assets accepts a kind = "translation" entry for a .tsv under
crates/ui-egui/src/i18n/ without a hash, rejects one that has a hash, and
still requires a hash for every other entry.
* Assets: a leftover hash on a translation catalog is a note, not an error
Delete the translation coverage tests and the tests of one language's
wording. Keep catalog validation (format, duplicates, placeholders, plural
forms, command ids) and the tests that use a language (registration,
plural rules, RTL order, glyphs, casing, font fallback).
cargo xtask i18n-report runs i18n::tests::translation_gaps_report, which
prints each language's missing count and first few keys and always
passes; xtask ci runs it after the tests.
* CI speed: line tables for workspace crates, no debug info for dependencies
* CI speed: xtask ci runs nextest and doctests, one wasm check, the CLI without default features
* Allow OFL typefaces whoever designed them (AGENTS.md §1.1)
Noto CJK and Source Han from their open-source releases are allowed like any
other OFL font. The fonts build no longer skips them, the asset check accepts
an OFL font credited to Adobe, and the docs describe the Chinese faces that
result. The ban on Adobe icons, artwork, UI graphics and the fonts Adobe
products install stays.
* Leave out whole CJK faces other than the Japanese ones for size
A Chinese or Korean face is embedded whole only under 4 MiB, so the 16.4 MB
Noto Sans CJK SC in craft-fonts stays out of the binary and the embedded
total against the pinned craft-fonts is unchanged (25,204,828 bytes).
* docs: replacement boxes for Chinese without craft-fonts only on the web or without an installed Chinese face
* xtask assets: the OFL font exemption needs at least one licence ID
* fonts build: count a CJK script tag with a region (Hant-HK) as CJK, shared with the embedding test
* AGENTS.md: the asset gate exempts OFL fonts from the Adobe-author check
* fonts build: count a Hangul-only face (Hang) as CJK for the size rule
* fonts: the CJK embedding test checks the size rule, sharing its constants with build.rs
* docs: the missing Hans face is a property of the pinned craft-fonts, not of every build