place_tagged wrapped the page's content in q/Q only when no Wrap stream
existed yet. Content appended after an earlier wrapper (by another tool, or
an older OCR layer) that left a cm, a clip or text state set then moved,
hid or distorted every later header, Bates number, watermark, added text,
flattened annotation and redaction cover.
Placing a mark on top now reads the page's actual operators instead of the
/PCMark tags. When the content parses cleanly with balanced q/Q and closed
text objects and paths, the stretch from the first stream that leaves state
set is wrapped, once, at a stream boundary no comment runs into. Content
that is already self-contained gets no new wrapper, so repeated marks don't
pile up.
Content that can't be read that precisely (stray bytes, unbalanced q/Q, an
unfinished text object or path, too large) is wrapped whole as before,
rather than refusing: redaction stamps its covers after removing the text
and can't back out. A q that content leaves open is closed after it, where
closing it changes nothing already drawn.
The wrapper's close now starts with a newline, so a comment at the end of
the content before it can't swallow the Q. Removing marks strips only
wrapper pairs that end the list, keeping one that later content still
relies on, and recognises the older close bytes too.
Found while testing crates/edit/src/lib.rs:445
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The dialog previews in the interface font, which draws Cyrillic, while
applying draws with the standard fonts and refused it after OK.
HeaderFooter::first_undrawable and Watermark::first_undrawable are now
the one check: applying refuses through them, and the dialog shows the
Edit Text warning and disables OK on the same answer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Chinese and Japanese are written without spaces between words, so wrapping
only at spaces left a paragraph of them as a single overlong row. A row may
now also end between two ideographs or kana (UAX #14 class ID), except
before closing punctuation, closing brackets, small kana and similar marks,
or after opening brackets and currency signs (JIS X 4051 line-start and
line-end rules). Latin words inside such text stay whole; Korean and Thai
still break only at spaces.
Both the plain and the styled rewrite paths use the same rule.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Edit text: support per-font visual-order Indic text and nukta reordering
- Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with
Indic codepoints without OpenType GSUB tables, or legacy visual font names).
- Implement Indic visual-to-logical and logical-to-visual pre-base vowel
reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed
nuktas and conjuncts.
- Apply reordering during extraction and source font reuse only when visual_order
is set, leaving modern logical-order text untouched.
- Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs
and font visual_order tests in crates/edit/src/tests.rs.
* Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding
- A font is visual order only when it is a known legacy Indic family (subset tag
allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is
no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer
decodes every embedded font program on every call.
- Lines with /ActualText text are already logical: skip indic::to_logical, so
"बढ़िया" no longer becomes "बढ़यिा".
- reading_lines no longer runs bidi recovery on every line (it changed RTL
reading_blocks output for all documents); only the Indic recovery remains.
- indic.rs indexes through get().
- Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit
round-trip, logical ActualText in a legacy font, and a subset Noto font.
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>
* M0: remove upstream trademark artwork from contributor fork
Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: group compatible PDF words and hide source text during editing
Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.
Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: preserve rich PDF regions and preview text edits in place
Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.
Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: keep cropped artwork from blocking text editing
Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.
Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: resize cropped images with their clipping paths
Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.
Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: complete source font subsets and preserve clipped editing
Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.
Co-authored-by: OpenAI Codex <codex@openai.com>
* Revert "M0: remove upstream trademark artwork from contributor fork"
This reverts commit 3aa539b5b6.
* Test: give the text layer up to 4 s before copying (it flaked on a loaded machine)
---------
Co-authored-by: Kamoliddin Fatkhullaev <komoliddinfatxullayev@MacBook-Pro-Komoliddin.local>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
* Hebrew fix
* Tests: the faux-redaction audit gets its own work folder (it shared "audit" with the space audit, so the two raced)
* Arabic golden test: compare bytes only in a build without an Arabic face (CI's Linux job builds with craft-fonts)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14: Scan PdfCraft page-content markers once on open
Combine the engine-open marks and added-content discovery passes while keeping the existing public scans and document state unchanged. Add scanner equivalence and engine-open coverage, and retain the local Clippy binding needed for the workspace gate.
Co-authored-by: Codex <noreply@openai.com>
* M14: reuse the combined marker scan on full refresh
Keep full refresh from repeating the separate marks and added-content traversals, with a mark_saved regression against the independent scans.
Co-authored-by: Codex <noreply@openai.com>
* M14: scan marker metadata without cloning content containers
Borrow direct content objects and keep referenced objects alive during marker inspection. This avoids copying /Contents arrays and /PCAdded dictionaries for the combined read-only scan while preserving one-reference resolution and content-object identity.
Co-authored-by: Codex <noreply@openai.com>
* M14: record combined marker scan measurements
Update the #307 progress entry with full-refresh reuse, the borrowed scan implementation, and exact before/after timing and memory measurements.
Co-authored-by: Codex <noreply@openai.com>
* M14: align marker measurements with final benchmark run
Replace the preliminary values with the final exact-revision alternating-run medians and corresponding pair counts.
Co-authored-by: Codex <noreply@openai.com>
* marks_present and list_added read through scan_markers, so the parsing lives in one place
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
doc_create with from=text wrote every character outside WinAnsiEncoding
(Korean, Chinese, Japanese, emoji) as `?` with no error. It now refuses
such text with the same message page_add_text already gives, naming the
first undrawable character and its code point. Line breaks, page breaks
and tabs are layout and still pass. create_from_text itself is unchanged,
so opening .txt files, comment summaries and compare reports behave as
before.
Add Watermark / Update Watermark can now move the mark from the page
centre by a horizontal and a vertical offset in points (right and up).
- edit: add_watermark refuses a non-finite offset instead of writing it
into the content stream; tests for the placement and the refusal.
- automation: doc_watermark takes offset_x and offset_y (clamped to
+-20,000 points); a test saves, reopens and measures the text moving.
- ui-egui: an "Offset from centre" row with Horizontal and Vertical
fields; the preview follows it for text and for picture watermarks.
- i18n: "Offset from centre" in the catalogs that have a coverage test
(ar, bg, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans); hashes updated in
ATTRIBUTION.toml.
- parity and roadmap: edit.watermark-appearance stays partial (no offset
from the page edge, no print/screen-only variants, no repeat); image
and PDF watermarks no longer listed as missing; progress log line.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Preserve source glyphs and artwork, resolve the group atomically, and expose generation-scoped automation tools with selection and rendering regressions.
Co-authored-by: Tevinhead <173904641+Tevinhead@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Page numbering adds the start number to the position of the page in the run. At the top of the u32 range that addition wrapped, so a release build saved zero for the second page and still reported success, and a debug build hit a caught overflow error instead.
Check the whole run before drawing anything. When the last page number would pass the u32 limit the operation returns an actionable error and the document keeps no header, rather than a half applied run.
Fixes#804.
Co-authored-by: Big Pickle <noreply@opencode.ai>
* Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes#314)
The office export built its paragraphs from text_blocks and its images from
page_images. Both read only the page's own content streams (they back Edit
text and Edit image, which rewrite those streams), so a page drawn through
`/Fm1 Do`, as many print-to-PDF tools emit, exported as an empty document.
Add read-only reading_blocks and reading_images to pdfcraft-edit. They
follow Do into form XObjects (nested ones too), apply each form's /Matrix,
use its own /Resources (or the caller's when it has none), and start it
from the graphics state at the Do. Depth is capped at MAX_FORM_DEPTH and a
form already being read is skipped, so self-referencing and mutually
recursive forms are read once. export_pages uses them; editing is
unchanged and still offers only the page's own text and images.
* Export: a page's walk through form XObjects has a total visit budget, so forms that fan out can't hang it
* Export: keep the image walk within clippy's argument limit
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* uk: the author's wording for the flatten setting
---------
Co-authored-by: samehelhawary <basio20044@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Add text, headers/footers and watermarks wrote every character outside
WinAnsiEncoding as `?` and reported success. They now refuse it with a
specific error ("the standard fonts can't draw "中" (U+4E2D) ...") before
changing anything, as the maintainer asked on #327. Form fields and
text-box comments are unchanged (they keep the value).
- fonts: win_ansi_encodable / first_non_win_ansi, defined through
win_ansi itself so a fuller table widens them too; the edited-text
Type 3 decision uses the same check (same character set as before).
- edit: one drawable() check, fed the lines exactly as each tool draws
them, so a CR that would become `?` is refused and CRLF in headers is not.
- ui: the Add-text editor keeps such text open with an inline warning.
Done, a click elsewhere or a double-click on another item don't take
or replace it; it counts as unsaved work; Save stops and shows it; a
deleted page takes its draft with it.
- Arabic (#403): one public check, first_undrawable, follows the drawing:
Arabic shaped with the craft-fonts face is drawable, and only what's left
to the standard font counts. Without the face, #403 still decides for the
Arabic letters (new items refused with what's missing, existing ones stay
movable), but nothing else in the item may become `?`. The editor uses
the same check, cached per draft.
- #403's odd_arabic_text_never_panics: its U+2029/U+0085 input, which was
drawn as `?`, now expects a refusal naming the character.
- The warning is translated (fr, ru, zh-hans, zh-hant, es, ja, bg, de); the
catalogs' SHA-256 entries in ATTRIBUTION.toml are updated.
Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp
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>
- edit: the substitute font name is a `&str` constant, so there's no `from_utf8(..).expect`.
Paragraph grouping uses `let … else` / `if let` chains, and the reuse-font path takes the
metrics with `filter` instead of `expect("checked")`.
- xfdf: a comment key that vanished between listing and lookup is skipped.
Both crates now deny the panic lints. Adds the root clippy.toml (identical to #11) so tests may
still unwrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>