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
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
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
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
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
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.
A partial rewrite put the paragraph marker around the shared ET, so the neighbour in that text object joined the paragraph and the next edit deleted it.
The join limit follows the text object's TL when it has one, checked against the measured baseline gap. A rewrite of every row is marked so a new row one leading above the next paragraph does not absorb it.
A gap of 2.4 times the font size was still under the 2.5 times join limit, so two paragraphs became one and editing the first deleted the second. Once a paragraph has two rows, a wider gap than its own leading stays the next paragraph.
Moving a rectangle, an oval or any other comment showed only its blue
frame going, the shape staying where it was until release. The comment is
now rendered alone once as the drag starts (Document::comment_layer: a
read-only copy with only that comment, no page content) and laid over the
page where it goes; the page beneath keeps showing it where it was until
the move is applied, as a CAD program's move does. Rendering the page
without it would cost a whole page's render at every drag on a heavy plan.
Stamps and Fill & Sign marks keep their own layers. annot: isolate_annotations
for several comments of a page, so a group can be drawn together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A scan drawn only through grouped Form artwork is reported in skipped
(once per form, with every page that draws it) instead of being counted
as no scan; a Form of vector artwork holding only a small picture
(under 36 dpi over its area) still counts as no scan.
- decode_samples checks the filter before the colour space and bit depth,
so a JPEG 2000 image without /ColorSpace or /BitsPerComponent is
reported as JPEG 2000, not as a colour-space or bit-depth problem.
- Optimize and Enhance scanned file no longer start while the other runs:
they share the progress card and capture the same document state.
- docs/gaps.md: background and edge-shadow removal apply to grey and RGB
scans, despeckle also to 1-bit ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main split comments.rs into tools/comments/: annotation_deleted,
after_delete and on_page go to state.rs, their test to tests.rs. main
pulled the post-edit selection into select_after_edit, which now takes the
deleted page's annotations from before the delete. ROADMAP.md and
docs/roadmap.md keep both entries, main's newer one first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scan & OCR ▸ Enhance scanned file (Acrobat's Enhance ▸ Scanned document)
now cleans up scanned pages: background removal (the camera clean-up's
level 0-100), edge shadow removal (book gutters, copier borders) and
despeckle, each its own switch.
The camera tool renders the page and replaces it with a picture, which
would drop a scan's invisible OCR text, comments and links. Instead each
scan image (pictures covering at least half the page, each at least a
tenth) is decoded, cleaned and written back into its own image object at
the same size and colour space: a JPEG stays a JPEG (quality 90), other
grey/RGB images are stored losslessly, a 1-bit image stays 1-bit with its
decode array, and every other dictionary entry (SMask, Interpolate, ...)
is kept. An image several pages share is cleaned once. What can't be
cleaned (CMYK, indexed, JPEG 2000, JBIG2, CCITT, colour-key masks, broken
data, oversized images) is reported, never skipped silently; pages that
are no scan are listed too.
The engine works as a job like Optimize (scan_clean_job, run on a worker
with progress and cancel, apply_scan_clean refuses a result for an older
document state) and applies one undoable step, "Enhance scanned file".
pdfcraft-optimize exposes decode_samples/encode_samples for in-place
pixel edits. camera_clean gains clean_samples for grey and RGB samples
with separate switches and an edge-shadow pass; the camera tool's output
is unchanged. page_enhance_scan drives it headlessly.
Not yet an option of Recognize text. Refs #4119.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the Comments panel a reply had no menu, and the card's Delete removes
the whole thread, so one wrong reply couldn't go without the comment and
every other reply. On the selected card each reply now has its own "…"
(Edit, Copy text, Delete), as Adobe's help describes. Delete takes that
reply with its pop-up and the replies to it, as comment_delete already
did headlessly. The comment and its other replies stay, in one undo step.
A locked reply's Delete is greyed out. Status badges and a Replace Text
caret get no menu.
Deleting moves what follows it up in /Annots. Before, any delete cleared
the selection, and the panel's inline editor stayed keyed to an index
that could now be another comment. CommentView::annotation_deleted now
finds both where they moved, from their place among what stays and what
they read, or clears them when they went too or the page changed
unexpectedly.
The new string is in every catalogue. Parity notes, the roadmap row and
comment_delete's description say a reply can be deleted on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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.