A stamp placed on a page with /Rotate was drawn in user space, so the
label or picture turned with the page. Counterrotate the appearance so
it reads upright as the page is displayed.
Co-authored-by: Brandon Thomas <[email protected]>
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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Muse Spark <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
* M9: visible digital signatures can show a drawn, typed or image graphic (#193)
A visible certificate signature can show a graphic where the signer's
name is large, as Acrobat's Configure Signature Appearance does: the
saved Fill & Sign signature (drawn, typed or image) or an imported
PNG/JPEG. Sign as previews it and remembers the choice; sign_document
takes `graphic` (image, text or strokes).
pdfcraft-sign gains `Graphic` and `SignOptions::graphic`. Graphics are
checked and size-capped before drawing; images keep alpha as a soft
mask and are encrypted with the rest of the update in encrypted files.
Refs #193
* Refresh catalog hashes
---------
Co-authored-by: Brandon Thomas <[email protected]>
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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* 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 <[email protected]>
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 <[email protected]>
---------
Co-authored-by: DWAA1660 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
A signature placed with Fill & Sign could only be moved with the Select
tool. While a Fill & Sign tool was active (Sign, Initials, Add text,
Check, Date...), hovering the signature showed the placement crosshair,
a drag did nothing and a click stamped another mark on top of it.
Now, as in Acrobat, a Fill & Sign tool picks up the Fill & Sign marks
already on the page: hovering one shows the move cursor, a click selects
it, a drag moves it and the selected mark's handles resize it. Away from
them the tool places as before, including on other comments. Image
signatures keep their live move/resize preview.
Comments now carry a fill_sign flag (pdfcraft_annot::is_fill_sign for
the engine's list, the same rules in the viewer's inspector), so this
also works on reopened files.
Co-authored-by: lloyd <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* M14: let neutral Windows locales fall through to display language
Co-authored-by: Codex <[email protected]>
* 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 <[email protected]>
* engine README: the execution plan is local-only, so don't link a path that doesn't exist
---------
Co-authored-by: Tanish Manku <[email protected]>
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: sanyamsingh001 <[email protected]>
Co-authored-by: Codex <[email protected]>
Co-authored-by: Streetblock <[email protected]>
Co-authored-by: lloyd <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* M5: flatten Fill & Sign marks when saving
Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.
* M4: zoom the Organize page grid
The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).
Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M4: zoomed grid pages are rendered at the size drawn
A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.
Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* View: the middle button pans instead of drawing with markup tools
egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.
Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M3: a short page gone to from the Pages panel stays current
The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M4: page operations from the Pages panel: context menu and drag to reorder
Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.
Parity: view.thumbnail-context-ops is shipped, with what it still lacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped
* M5: translate Fill & Sign flatten labels into Ukrainian
The coverage test failed because the new preference strings had no Ukrainian entries.
* Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591)
#516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one
rendering test. Both crates are layer 3, so `cargo xtask layers` (run in
CI) rejects the sideways edge and main went red. The test now lives in
crates/engine/tests/form_rotation.rs, where both crates are already
dependencies, and forms drops the dev-dependency. The parity evidence
points at the new location.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* uk: the author's wording for the flatten setting
---------
Co-authored-by: samehelhawary <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Cody Vogt <[email protected]>
Co-authored-by: William Max & Co. <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* 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 <[email protected]>
* Stamps and added images follow a JPEG's EXIF orientation (part of #434)
Phones store a portrait photo as landscape pixels plus an EXIF
Orientation tag. A JPEG went into the PDF as it was (DCTDecode), and PDF
image XObjects have no such tag, so a photo used as a custom stamp, an
added or replaced image, a watermark or background, or a button image
showed up turned 90 degrees.
Before these images are embedded, a JPEG whose Orientation is not 1 is
decoded, turned the way the tag says and re-encoded (quality 92). Its
JFIF resolution is kept, swapped for quarter turns, so the natural size
on the page stays right. Other images, JPEGs without the tag, JPEGs that
fail to decode and photos over 64 megapixels are embedded unchanged, as
before. Fill & Sign image signatures already applied the tag.
* Image signatures stay upright and anchored on rotated pages (part of #434)
On a page with /Rotate 90, 180 or 270 an image signature turned with the
page, because its picture was drawn along user-space axes while the click
or the "at" argument is a displayed point. It was also anchored at the
wrong point: its left edge and vertical centre were taken in user space,
so on a 90 degree page the image centre ended up elsewhere.
The signature's appearance now carries the counter-rotation as its form
/Matrix, which viewers fit to /Rect, so the picture reads upright as
displayed. The page's rotation is kept in /PCPictureRotate, so restyling
regenerates the same matrix and moving and resizing keep it. The box of the
signature runs along the displayed axes, so its left edge and vertical
centre are where they were asked for. The page view matrix in
pdfcraft-model is split out as view_matrix_for.
The Fill & Sign preview and the live move/resize preview draw the picture
the way the appearance does, and the resize handles keep the image's
proportions as displayed. comment_image_preview reports image_rotation,
how far the image is shown turned (0 for one placed upright).
Custom image stamps are left to #473.
* Use is_multiple_of for the quarter-turn checks (clippy) (#434)
* Probe the pointer preview away from egui_kittest's drawn cursor (#434)
The kittest render paints a mouse cursor (a 16 px triangle with a black outline) at the
hover point, so the 2 pt probe beside the pointer landed on its outline when zoom and view
rotation made 2 pt only a pixel or two. Put the stroke's block at the picture's right end and
probe there and at its far margin instead.
* 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 <[email protected]>
* Test the Ink stroke on its apex row: Ink is now a curve through its points (#374)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
#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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* M5.7: update image signatures live during resize and movement
* M5.7: preserve image signature proportions when resizing corners
---------
Co-authored-by: Brandon Thomas <[email protected]>
* M12: add calibrated 2D measurement tools
* M12: harden measurement tools for landing (#208)
Bounded snap targets (endpoints only for accepted segments, page-wide cap),
lenient extraction (deep q nesting and undecodable streams are skipped and
reported), per-page snap cache, measure::list reports unsupported entries
instead of failing, closed polygons with a repeated vertex, printable unit
labels (m²), cached panel listing, WinAnsi captions, indirect /VP kept,
measure.export skipped in the command test like other pickers, missing
translations, honest parity (scale and distance partial) and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: GLM <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
annot, compare, content, cos, export, geom, model, ocr, preflight, print and redact have no
production `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!`. The attribute
keeps it that way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stable clippy 1.99 denies chunks_exact/chunks_exact_mut with a constant
size (32 sites), which failed the test job on main and every open PR.
Behaviour unchanged; MSRV 1.90 has as_chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>