add_text_replacement built the StrikeOut and the Caret from one Meta, and
add_annotation stamps /NM from meta.id, so the pair shared a single name. XFDF
names a reply by the /NM of what it replies to, so the exporter wrote the
caret's inreplyto as the caret's own name. Importing that file found a comment
replying to itself, and the Replace Text comment did not arrive: the reporter's
second document listed none. FDF was unaffected because it keeps the pair by
object structure rather than by name.
Take a second Meta for the caret. The ids stay supplied by the caller, as the
rest of the crate does so edits remain deterministic, and the one production
caller already has a generator and calls it twice. /IRT still points at the
strikeout, so the group still moves, lists and deletes as one.
Tests: the annotation test now asserts the two carry different names and that
the caret's /IRT resolves to the strikeout's. A new XFDF test exports a Replace
Text pair and imports it into another document, asserting both comments arrive
with their thread intact; it fails if the two are given one id again.
Regression test for the /URI percent-encoding fix: round-trips a non-ASCII URL and an already-escaped URL through save/reload and asserts the stored URIs are pure ASCII with existing escapes preserved.
Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
* Comments: font size for text boxes in Comment Properties (#1908)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Flattening deleted a cloudy comment it could not draw, and a refused text-box restyle or resize left the new properties in the file. Arrow defaults were stored on plain lines. A custom match ending on a separator included the next glyph, and the vertical-text redaction error claimed an overlap it had not checked.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14: text the fonts can't draw is kept and reported, never written as ? silently
Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX].
* Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation
Every content edit (drag, resize, retype) on a large document ran a full
synchronous refresh on the UI thread: a whole-file re-inspection plus a
redraw of every missing annotation appearance, then dropped all page
rasters. On a 72 MB / 202-page CAD file with 27k appearance-less
annotations each commit cost ~1.5 s before anything re-rendered. Now ~0.3 s.
- New Scope::Content for edits that touch only page content, resources and
added content/marks (move/resize/retype/images/added items/marks): the
page tree, annotations, fields, links, outline and fonts are unchanged,
so refresh_content rebuilds working bytes, display bytes and renderer and
validates with a light check_pages (parse plus page count, keeping the
roll-back guarantee) instead of a full inspect. added/marks are re-read
only when the edit can change them; undo/redo re-read both.
- AppearanceCache in pdfcraft-annot: built display appearances are reused
while no annotation edit runs (content edits leave every annotation
dictionary alone); cleared on comment, structural and full refreshes.
set_appearance is split into build/apply halves so a hit skips the draw.
- check_pages in pdfcraft-render: light post-edit validation.
- Viewer: single-page content edits invalidate only their page
(page_changed); other pages keep tiles, text and thumbnails.
- XFA dataset sync now also runs for content edits.
Co-Authored-By: Muse Spark <noreply@meta.com>
* Fonts added by a content edit show in Properties and doc_info (read again on demand); form edits clear cached appearances too
* M14: DATE_LANGUAGES is a static (21 entries trip clippy's large_const_arrays); rustfmt the Greek entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Preferences scroll in a short window; tests scroll to controls below the fold (#487's roomier rows); the highlighter test waits for its mark under load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Select tool: a multi-click selects only on the frame egui reports it, so a cleared selection (or the highlighter's marked word) isn't selected again
The highlighter test now clicks at a human pace, with frames in between, which is what exposed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Muse Spark <noreply@meta.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Acrobat saves the line breaks in a comment's /Contents as CR, e.g.
"first line\r\rsecond paragraph". egui draws CR as nothing and only breaks
lines at LF, so the comments panel, the hover popup and the edit box ran
the paragraphs together ("first linesecond paragraph").
Both readers of comment text (pdfcraft_render::inspect on open and
pdfcraft_annot::summaries after an edit) now turn CRLF and CR into LF.
Regression tests: carriage_returns_in_comment_text_become_line_breaks in
annot and render (both fail before this change).
* fix(annot): validate finite link areas before add or edit
* Test: find the edited link by its /Annots index, not its position in the list
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A drawn Fill & Sign signature is an Ink annotation, and Ink couldn't be
resized: the Select tool showed no handles and the engine refused
ResizeAnnotation ("Ink comments can't be resized"). Typed and image
signatures (stamps) already resize (#335, #355), so drawn ones were the
gap left in #331.
- annot: set_rect scales an Ink annotation's strokes into the new
rectangle, keeping each side's margin between the strokes and /Rect
so the line isn't clipped, and regenerates the appearance. A straight
stroke with no extent on one axis is centred on it.
- ui-egui: Ink is resizable with the 8 handles. A drawn signature's
corners keep its shape, as image signatures' do; other drawings resize
freely like shapes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: fill colour for rectangle, oval and polygon comments (#686)
Rectangles, ovals, polygons and clouds could only be given a border
colour. They can now be filled, or have their fill removed:
- the comment quick bar has a Fill colour control for those tools
("No fill" or a swatch), kept with the other per-tool styles;
- Comment Properties > Appearance has a Fill colour row, applied in the
same undo step as the other changes and carried over by Make Current
Properties Default;
- comment_edit takes `fill` (a colour, or "none").
pdfcraft-annot gains `set_fill` (writes or removes /IC and redraws the
appearance; lines, ink and locked comments refuse it) and reports
`fillable`/`fill` in the comment properties. The engine adds
Edit::FillAnnotation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XAJjko29kzKj7ULXoXSnN
* M5: update ATTRIBUTION.toml hashes for the edited i18n catalogs (#686)
The fill-colour strings changed 13 translation catalogs, so
xtask's the_real_manifest_passes failed on ubuntu and windows CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
find_urls case-folded a copy of the page text and then sliced the original
characters with offsets taken from that copy. Case folding can change a
string's length — U+0130 (İ) lowercases to "i" plus a combining dot — so the
offsets drifted past the end of the source and `chars[i..j]` panicked. The CLI
exited 101 before it could create the link; MCP reported an internal tool error
but kept serving.
Match the ASCII prefixes case-insensitively against the original characters
instead, so every offset stays in the source's own index space, and use get()
so no lookup can index past the end.
A new test reproduces the reported panic on the old code (range start index 41
out of range for slice of length 39) and passes with the fix. pdfcraft-annot's
tests, clippy and rustfmt are green.
Fixes#816.
Co-authored-by: Big Pickle <noreply@opencode.ai>
LaTeX `animate` PDFs store each frame as a read-only push button and drive
them from a Screen annotation. PdfCraft showed "This document contains N
interactive form fields." because the bar counted any field, and listed
the Screen annotation in the Comments panel because only Link, Widget and
Popup were filtered out.
- The form-fields bar now needs at least one field that isn't a push
button (something to fill in).
- Non-markup annotations (ISO 32000-2 §12.5.6: Screen, Movie, RichMedia,
3D, PrinterMark, TrapNet, Watermark) are no longer comments, both when
a document is inspected and when the comment list is rebuilt after an
edit (new pdfcraft_annot::is_comment_subtype).
Playing the animation itself (page actions, timers) is out of scope here.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* D5: README for cos and engine, and an honest cos status line
Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.
Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.
Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Preserve alternate appearances and indirect push-button values
Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.
Co-authored-by: Codex <codex@openai.com>
* M14: let neutral Windows locales fall through to display language
Co-authored-by: Codex <codex@openai.com>
* pdfcraft-cli --help lists `mcp` only when the build has it
Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".
USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.
- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
mcp_without_the_feature_says_it_was_left_out (without the feature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine README: the execution plan is local-only, so don't link a path that doesn't exist
---------
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: draw all ten PDF line-ending styles
Imported lines, polylines and callouts kept Square, Circle, Diamond, Butt, Slash and reverse arrows in /LE but would not redraw them. Those styles now draw, and comment_add and Comment properties can set them.
* hu, uk: Line ending
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: comments without an appearance stream are drawn (display only, never saved)
#260 (page 7): a FreeText box with /DA and an orange /C background, a
Text note and an Ink stroke, all without /AP, were not drawn; Acrobat,
MuPDF, PDFium and Poppler draw them. The renderer only draws /AP (plus
the Highlight fallback of #274), and pdfcraft-annot, which already
draws all of these for PdfCraft's own comments, can't be used from the
renderer (both are L3).
pdfcraft_annot::with_missing_appearances returns a copy of the document
in which every annotation without /AP /N that appearance::build can
draw (FreeText, Ink, Text, FileAttachment, stamps, shapes, lines,
markup, …) has the appearance build draws from its dictionary; links,
form fields and pop-ups are left alone, as is anything build declines.
The engine keeps that copy, written as an in-memory incremental update,
in a new Document::display: the page view's render pool, page_render
and page images (export_source) read it. Document::bytes, what Save,
signatures and the inspector use, is unchanged. display is rebuilt
wherever the renderer was (open, refresh, scoped refresh) and is bytes
itself when nothing is missing or the document can't be edited.
Tests: annot tests::comments_without_appearances_get_one_for_display_only
(FreeText, Ink and Text get /AP /N in the copy, an existing /AP is kept,
Link, Widget and an unknown subtype aren't touched, the original is
untouched, none means no copy) and engine
tests::comments_without_appearances_are_drawn_but_not_saved (the page
view draws the FreeText background and the Ink stroke, before and after
an edit; the working bytes stay the opened file; Save writes no /AP).
On main the page view shows white where the FreeText box is. Parity:
view.render-annotation-appearances cites the test and notes what's left
(Link borders without /AP, printing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the Ink stroke on its apex row: Ink is now a curve through its points (#374)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#260 (page 7) compares PdfCraft with Acrobat, which draws an Ink
annotation's /InkList as a smooth curve through the points;
pdfcraft-annot drew straight segments between them (as MuPDF, Poppler
and PDFium do). A stroke of three or more points is now drawn as a
Catmull-Rom spline through every point, written as cubic Béziers: the
curve leaves each point parallel to the chord between its neighbours,
and an end uses itself as the missing neighbour. Two points stay a
line and one point a dot. This is the appearance PdfCraft writes for
its own ink, signatures drawn with Fill & Sign included, and, with the
display-only appearances for comments that have none, how ink without
/AP is shown.
A curve can swing beyond its points, so a new ink annotation's /Rect
now also covers the curve's control points (the curve stays within
them); before, only the points plus the line-width margin.
Test: annot tests::ink_strokes_are_smooth_curves_through_their_points
checks the exact curve for a three-point stroke, a line for two points,
a dot for one, and a /Rect that holds a stroke whose curve rises above
its highest point (22 on main, 23.667 now). The comment.pencil parity
entry cites it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Comments: a text box's rectangle follows its edited text
Editing a text box (or typewriter or callout) rewrote /Contents but
kept the old /Rect: the appearance clipped the new text to the old box
and silently dropped lines that no longer fit, while shorter text left
an oversized box. set_contents now re-measures the text with the same
Helvetica widths as at creation and re-fits the box: the wrap width and
top edge stay, the height fits the wrapped lines. A callout's leader
line keeps its place and re-attaches to the moved box (that geometry
moved from set_rect into a shared apply_text_box). A locked box keeps
its text editable, so it re-fits too.
Covered by annot tests (grow and shrink with nothing clipped, callout
leader re-attach, locked box) and a comment_edit round trip through the
automation tools; parity entries cite them.
Co-Authored-By: GLM <noreply@z.ai>
* Polish edit text and images tool
* feat(edit-text): select source font style
* fix(edit-text): preserve detected PDF font styles
* feat(edit-text): support Japanese replacements
* fix(forms): escape non-ASCII format script strings
* Forms: unwrap_or instead of map_or with identity (clippy map_or_identity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>