Review on #2137 (nitpick): a SetAnnotationPopup edit marked its page
stale, so its tiles were rendered again, its text cache dropped and its
selection cleared, though nothing in the raster changes: the window is
drawn over the page and a /Popup annotation paints nothing. Edits that
also change the comment (a batch) still invalidate as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Review on #2137: the cited test opened, moved and closed a pop-up but
never looked at the connector line. The new test reads the frame's
shapes: a line in the comment's colour from the comment's edge to the
window's, nearest points at each end, shown with the pointer on the
comment or the window or with the comment selected, and not otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Review on #2137: when /Annots holds 5 0 R and object 5 is only a
reference to the comment 6 0 R, the pop-up's /Parent names 6 0 R, but
the ownership check compared it with 5 0 R. The comment's pop-up was
then missing from both the on-open listing (render inspect) and the
summaries after an edit (annot). Both now compare the references they
resolve to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Rebased onto main's reference-based threading (#4061, #4566). The pop-up
matched replies by /NM (in_reply_to == name), which need not be unique:
two notes sharing PyMuPDF's fitz-A0 both showed one note's replies, and a
reply to a reply was missing. It now files replies under a comment with
thread_tops over the settled parent links, as comments_panel does, and
has_popup asks Annotation::is_comment rather than in_reply_to (a reply
whose parent has no /NM read as a comment of its own).
Regression test a_pop_up_lists_its_replies_by_reference_when_names_repeat,
seen to fail with the /NM filter restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The pop-up's reply list kept every annotation in reply to the comment,
so a review status (/StateModel /Review) and the private checkmark reply
Mark writes (/StateModel /Marked) showed as replies with their generated
text. It now lists replies without a /State, as the Comments panel does
(comments_panel.rs).
Raised by CodeRabbit on #2137.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Review on #2137. set_popup accepted a pop-up whose /Parent was present but
not a reference, and the comment list (pdfcraft_annot::summaries and the
inspector) reported whatever /Popup named: another annotation's rectangle
and open state. One check, own_popup, now decides for both: /Subtype
/Popup, and /Parent absent or a reference to the comment (ISO 32000-2:2020
12.5.6.14, Table 186: "Optional; shall be an indirect reference").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A double-click on a comment opens its pop-up window as in Acrobat:
author, date, the text edited in place, the replies; the title bar
drags it, x closes it, each an undo step saved in /Popup (/Open, /Rect)
and a note's own /Open. A connector line in the comment's colour joins
the comment to its open window on roll-over or selection. comment_popup
does the same headlessly; comment_list reports the pop-up. Three undo
labels in 22 catalogues (Vietnamese included).
Ported onto main's split modules: set_popup lives in annot's modify.rs
and PopupInfo in info.rs; the pop-up windows are their own module,
ui-egui tools/comments/popups.rs, listed with the page modules that
read the pointer and set the cursor themselves; the hover tip filter is
in comments::describe. PageCx's pointer and redaction look, the
tests/it evidence paths and the Annotation test fixtures follow main
(formerly fcd33e131, c68e5d902, 0a7fb530b, 8af2c338e, 345c1573d), and
labels are logical text now that egui shapes bidi itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
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.
Use a named compact preset arrow beside the field, retain the row alignment/width assertions for the editable control, and open the current colour flyout in the saved-text regression. No accessibility or layout checks are disabled.
Preserve caption editing and current form/Prepare module boundaries. Carry export-only appearance preservation, validation and data round-trip tests to their current paths, and retain current attribution metadata. Hosted CI must validate this refreshed head.
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.
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.
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#b70228, its red, from the icon (#5245)). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).
craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/edit/src/images.rs
- crates/forms/src/appearance.rs
- `zip`: 9.1.0 → 9.1.3
Requirement `roxmltree` 0.20 → 0.21: `ParsingOptions` gained `entity_resolver` (initializers take `..Default::default()`, i.e. none, as before) and is no longer `Copy` (two functions build it with a closure, the pattern merge.rs already used).
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>
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
Review of the previous commit:
- Nothing checked the segment numbers the region split assigns: a
mutant numbering every region 2 decoded. The many-marks test now
reads each segment's number, and has a bar too tall to be a symbol,
so the generic region is numbered after the last text region.
- The decoder's 32,767-symbol limit was checked against all distinct
marks, before either plan, so an image with more distinct marks lost
the second plan, which codes only the repeated ones. The limit now
applies to each plan (MAX_CODED_SYMBOLS); finding marks keeps its
memory bound of 65,536 (MAX_SYMBOLS), as before the previous commit.
A test has 32,768 distinct marks above a page of repeated text, which
encode_jbig2 still symbol codes through the second plan.
- The dense-page optimizer test now checks that it takes several text
regions, and a compile-time assert keeps segment numbers within a
byte's reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
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
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
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