Commit Graph
59 Commits
Author SHA1 Message Date
mtlcl410andClaude Opus 5.5 2be6eb1577 M7: a moved text box's drag preview shows the box alone (#4464) (#4475)
* M7: a moved text box's drag preview shows the box alone (#4464)

While an added text box or picture was dragged, the pixels that followed
the pointer were cut from the page raster inside the item's box, so
whatever the box covered at its first place (a filled shape, a picture,
other text) was carried along with it.

The drag now renders the item alone too: a copy of the document whose
page keeps only the item's own content stream and no annotations, drawn
on a transparent page (`pdfcraft_edit::isolate_content`,
`Document::added_content_alone`). Those pixels follow the pointer over
the page rendered without the item, so what it covered stays put. The
preview shows once both rasters are in; until then the item stays where
it is, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

* M7: drop the move preview's earlier rasters when a re-render fails (#4464)

When the scale changed after both the page-without-item and item-alone
rasters were in, and a render at the new scale failed, only the renderers
were cleared. The old textures stayed, so the item kept following the
pointer from them although the preview had given up. A failure now clears
both texture slots too, so the item stays where it is while dragged.

The receive step moves into its own method so a test can drive it with a
pool whose renders always fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:02:46 -04:00
af4a307e99 M7: dotted day-first dates in header and footer (#4503)
Header and footer dates had US, ISO and day-first slash forms only. The
dotted forms written in German-speaking and much of Central and Eastern
Europe join them: <<dd.mm.yyyy>>, <<d.m.yyyy>>, <<dd.mm.yy>>, <<d.m.yy>>.
The dialog's date list offers the first three after the existing ones.

Generated with Claude Code

Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-25 10:00:06 -04:00
aiongg 54152aed87 fix(edit): refuse a mark on top of over-deep content before changing the page 2026-10-22 23:35:40 +00:00
aiongg b979d4d9c2 fix(audit): audit the glyphs on each page held, not their ActualText
The redaction audit read each page's text through text_lines, which walked
the whole page tree again for every page (quadratic in the page count) and
let /ActualText replace the glyphs, so an empty or substitute ActualText hid
covered text from the audit. Edit gains glyph_text_lines, which reads the
page dictionary it is given and ignores ActualText; the audit uses it.
2026-10-22 21:17:15 +00:00
aiongg c9090b00ef fix(edit): refuse to put content on top when marked-content depth is unknown
Past the marked-content depth budget the number of open sequences is unknown,
and the mark on top was appended with none of them closed, so the page's own
optional-content scopes (possibly hidden) could hide a redaction box, a
header or a watermark. Placing on top of such a page is now refused before
anything changes; behind it is still possible.
2026-10-22 21:16:27 +00:00
aiongg 6ca4431b50 fix(redact): check every input content stream, whatever it calls itself
Both checks for text left under applied redaction marks (redaction's own
verify pass and the engine's renderer cross-check) skipped any page
content stream whose dictionary said /PCMark /Redaction, so that the
overlay's label would not count. An input file could put that label on
its own content stream and keep it out of both checks. Applying
redactions now records the overlay streams it adds (stamp returns the
new stream), and only those are skipped.
2026-10-22 15:17:44 -04:00
aiongg 2c13fd9373 Merge r2 track t16: mixed: automation-mcp, crate:misc, crypt-security, export-convert, optimize, preflight-pdfa, save-io
# Conflicts:
#	ATTRIBUTION.toml
#	contributors/people.toml
#	crates/automation/src/comments.rs
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
#	parity/acrobat-features.toml
2026-10-22 02:23:58 +00:00
aiongg 3cf35888d7 Merge r2 track t12: i18n, i18n: new language catalogues
# Conflicts:
#	ATTRIBUTION.toml
2026-10-22 02:21:42 +00:00
aiongg 15f558aaa8 Merge r2 track t11: redaction, mixed: combine, compare, crate:edit, measure, ocr
# Conflicts:
#	ATTRIBUTION.toml
#	crates/automation/src/tools.rs
#	crates/edit/src/lib.rs
#	crates/ui-egui/tests/compare.rs
#	crates/ui-egui/tests/ui_group/marks.rs
2026-10-22 02:21:38 +00:00
aiongg 7bf122eea8 Merge r2 track t02: edit-content
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/edit/src/text.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/edit_text_ui.rs
2026-10-22 02:17:34 +00:00
aiongg 456b96d06b Merge PR #2454: M5: year-first slash dates (yyyy/mm/dd, yyyy/m/d) for Fill & Sign and headers 2026-10-22 01:49:17 +00:00
aiongg a456f516d9 Merge PR #2756: M7: a paragraph number keeps naming the paragraph text_paragraphs listed (#2516)
ROADMAP.md: kept both sides' progress-log lines.
crates/edit/src/lib.rs: re-export list is the union (TextSession from #3942; BlockKey, block_overlapping from #2756).
2026-10-22 01:47:18 +00:00
aiongg b8d5cdd603 Merge PR #3595: M7: paint a new background over older ones (#3591)
ROADMAP.md: progress-log lines from both sides, newest first.
2026-10-22 01:45:17 +00:00
aiongg 16af57acac Merge PR #3942: M7: a text editing session for one paragraph 2026-10-22 01:45:01 +00:00
aiongg d133dfbf75 Merge PR #1989: Edit images: rotate an image by any angle with a knob (#1940) 2026-10-22 01:44:58 +00:00
komo1iddinandClaude Opus 5.5 6495c08afd M7: a text editing session for one paragraph
A TextSession opens a paragraph once: it keeps the document as it was
and the page's lines then. Each draft is laid out on a copy of that
document, so no draft sees another's output, and the commit writes into
the document it was opened on, refusing ("the paragraph changed while it
was being edited") when that document has changed since.

text_edit_layout and rewrite_block now open a session and lay out or
commit once, so there is one path and their output is unchanged. No
caches yet: those come next, each under the same equivalence test.

First step of #2512.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 02:15:47 +08:00
Gert Pellin 01149dea77 M7: header and footer can shrink the page content out of their way (#2140)
HeaderFooter::shrink scales the original page content evenly into the
band the header and footer leave free, centred across the page, in
display space so rotated pages shrink the way they are shown. The scale
is a content stream first inside the q/Q wrapper, tagged as part of the
header and footer, so Remove and Update take it away again and
backgrounds behind the page stay full size. Margins that leave no room
are refused before anything is drawn.
2026-10-21 09:02:09 +02:00
Lombardi2003andClaude Opus 5.5 14b3978344 fix(edit): a header reuses a font name only for its very own dictionary
From review of #3756: a /PCTimes naming Times-Roman with more to it (a
/ToUnicode map, /Widths) passed as the same face and was replaced, so
the page's own text could lose its map. A name is reused only when it
holds exactly the dictionary a header writes, and then it is left as it
is; anything else gets a free suffixed name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:56:53 +02:00
Lombardi2003andClaude Opus 5.5 4e8e17c7e6 fix(edit): a header never takes over a font name the page already draws with
From review of #3756: add_resources set /PCTimes (or /PCCour, /PCHelv)
in the page's fonts whatever was there, so a page that already drew
another font by that name would have its own text change face. Headers
and footers now pick the usual name when it is free or already names the
same standard face, otherwise the first free name_1, name_2…, and add
only that face, never replacing an entry. An update finds its own name
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:13:00 +02:00
Lombardi2003andClaude Opus 5.5 fecfc774f2 feat(edit): Header & Footer can be drawn in Times or Courier (#3754)
Headers and footers were Helvetica only. HeaderFooter takes the same
standard-face Family as Add text: the content selects /PCHelv, /PCTimes
or /PCCour, the page gains that font, and centred and right-aligned text
is placed by the chosen face's standard-14 widths. doc_header_footer
takes font, and the dialog's fixed "Font: Helvetica" label becomes a
Helvetica / Times / Courier menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:48:48 +02:00
mtlcl410andClaude Opus 5.5 d85d49a4c6 M10: Export to Word keeps the page background colour (#2992)
A page coloured with Edit a PDF > Background exported to Word as a
white page: export pages carried text and images only, and the Word
section has no page colour.

- pdfcraft-edit `page_background`: the colour of the first thing a
  page paints, when that is an opaque fill (no soft mask, Normal blend)
  in a plain gray, RGB or CMYK colour, of a path with a rectangle
  covering the visible page, not clipped smaller; `None` otherwise.
- `export::Page::background` carries it; the Word writer sets it as
  the document background (`w:background`, plus the
  `w:displayBackgroundShape` setting Word needs to show it) when every
  page shares one non-white colour. Word has one background per
  document, so differing colours are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-20 16:55:20 +08:00
Hazel Granados e9c53ea718 M7: keep a behind watermark under one on top on an empty page (#3591)
On a page with no content of its own, an on-top watermark is the first
stream in /Contents, so it was counted with the behind-content marks and
a watermark added behind later was drawn over it. Nothing records
whether a watermark is behind, so a new behind mark now goes right after
the last background, which is always below any on-top watermark. A newer
behind watermark goes under older ones again, as before #3591.
2026-10-20 00:09:41 -05:00
Hazel Granados 5beaf88a2a M7: paint a new background over older ones (#3591)
Marks behind the page content were all inserted at the start of
/Contents, so a second background went under the first and the old
colour stayed. Backgrounds now come first, then behind-content
watermarks, each in the order added. A watermark added after a
background no longer disappears under it.
2026-10-19 23:42:25 -05:00
hcchung1 3909bebeec M7: a paragraph number keeps naming the paragraph text_paragraphs listed (#2516)
Paragraph numbers are positions in a list regrouped from geometry after
every edit, so an edit that merged a paragraph into its neighbour made the
next edit by number rewrite another paragraph and lose its text.

After text_paragraphs, text_edit and text_edit_layout find the listed
paragraph again by its text and box, and refuse one that no longer exists on
its own (list the page again). An edit's result names the numbers that are
gone in stale_paragraphs. The inline editor's switch-paragraph click holds
the same key instead of an index. Without a listing a number is a position,
as before. An identity that survives save and reopen is still #2508.
2026-10-20 10:02:06 +08:00
5602fbba12 M10: Office export follows forms 50 deep and up to 50 000 a page and says where it stops (#3120) (#3426)
* M10: Office export reads forms as far as the renderer and says where it stops (#3120)

Export to Word, PowerPoint, HTML and RTF stopped at 12 nested forms and
4 096 forms a page and left the rest out silently. It now follows forms
50 deep, as the renderer does, and up to 50 000 a page (the renderer's
budget for nested paints), still bounded on hostile files.
doc_export_office lists pages past those limits in truncated_pages, with
a warning. New: reading_blocks_checked and export_office_checked;
reading_blocks and export_office keep their signatures.

Fixes #3120

Co-Authored-By: Claude <noreply@anthropic.com>

* Tests: pin the form depth limit to the renderer's 50 (#3120)

The depth test built its chain from MAX_FORM_DEPTH, so it moved with the
constant. Write the renderer's limit out: depth 50 is read, depth 51 is
left out and reported. Fails with the constant at 49 and at 51.

Co-Authored-By: Claude <noreply@anthropic.com>

* Export: say which forms count toward the page limit (#3120)

The page budget also counts forms the page draws itself, which the
renderer does not charge, so a page can be reported as cut although the
viewer shows it. The warning and the tool description now say so.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:44 -04:00
vangel76andClaude Sonnet 5.5 f3b8654b23 Merge upstream/main into feat/image-rotate-snap
Resolve the ROADMAP.md progress-log conflict by keeping both sides' entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 02:08:53 +02:00
Ahmad MuandBrandon Thomas 38999b3e90 fix(edit): isolate added marks from unfinished marked content (#3279)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:05:44 -04:00
skylar taylor-barrick 43bb98eb88 fix(edit): isolate new marks from state left by content after an earlier wrapper (#3084)
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.
2026-10-17 21:50:58 +02:00
mtlcl410andClaude Opus 5.5 b97e59190f M5: year-first slash dates (yyyy/mm/dd, yyyy/m/d) for Fill & Sign and headers
Taiwan, Japan and China write dates year first with slashes (Windows'
zh-TW short date is yyyy/M/d), and neither the Fill & Sign presets nor
the header and footer date tokens offered it. Both now have yyyy/mm/dd
and yyyy/m/d. The header and footer list gains them at its end, since
its selection is an index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:48 +08:00
vangel76andClaude Sonnet 5.5 0871543fd8 M7: rotate an existing page image by any angle (#1940)
A selected image gets a rotate knob (Shift: 15 degree steps); image_edit
gains rotate_by (degrees). The turn is one extra cm about the visible centre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 23:14:59 +02:00
2a130abf82 Refuse header/footer Bates numbers that would overflow u64 (#2253)
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>
2026-10-15 02:52:17 -04:00
jonathanwxh-cellandClaude Opus 5.5 b655208aaf fix(ui): header, footer and watermark dialogs refuse undrawable text before OK (#2013) (#2107)
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>
2026-10-15 02:44:49 -04:00
b18639c242 M7: rewrap Chinese and Japanese text between characters (#1827)
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>
2026-10-15 02:25:42 -04:00
dbef872ed7 M2: preserve precision when writing page transforms (#946)
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-10-15 02:16:13 -04:00
MarekSpodBiedry 9215b6f3b4 Name the failing option and page count when a chained edit page is out of range (#1550) (#1575) 2026-10-13 00:10:05 -04:00
e33b66e9b5 Edit text: support per-font visual-order Indic text and nukta reordering (#1380)
* 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>
2026-10-12 08:18:37 -04:00
6d843a8e8b Preserve source fonts in text editing and resize clipped artwork (#1116)
* 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>
2026-10-11 20:27:55 -04:00
Zeyu LiandBrandon Thomas b91d38c073 M14: text the fonts can't draw is kept and reported, never written as ? silently (#1112)
* 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>
2026-10-11 17:53:55 -04:00
tzagimandBrandon Thomas e609722b36 M7: Hebrew in Edit text (reading order, writing order, Hebrew glyphs) (#521)
* 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>
2026-10-11 17:41:06 -04:00
42cfc6a8f2 M14: scan PdfCraft page-content markers once per document refresh (#1016)
* 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>
2026-10-11 14:08:27 -04:00
Shukat Ibrahim 2fbce7028c doc_create from text refuses characters Helvetica can't draw (#959) (#1030)
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.
2026-10-11 11:59:43 -04:00
AlexisandBrandon Thomas b752827162 M7: watermark offset from the page centre (edit.watermark-appearance) (#1076)
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>
2026-10-11 11:56:06 -04:00
82b1093b73 M7: select and move mixed PDF objects together (#844) (#907)
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>
2026-10-10 17:38:34 -04:00
muraa-pandBig Pickle f8334d0cd3 fix(edit): refuse a header start number that would wrap the next page (#804) (#834)
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>
2026-10-10 15:07:51 -04:00
BooksandBrandon Thomas e7a15783f3 Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes #314) (#406)
* 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>
2026-10-09 18:22:07 -04:00
8e3782d5ca Flatten Fill & Sign marks when saving (#464)
* 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>
2026-10-09 14:25:08 -04:00
f1af1e2e79 Issue #125: page text the standard fonts can't draw is refused, not written as ? (#510)
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>
2026-10-09 13:20:47 -04:00
Brandon ThomasandClaude Opus 5.5 d3db99818f Rename PrintCraft to PdfCraft (#174)
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>
2026-10-07 16:03:57 -04:00
Brandon ThomasandClaude Opus 5.5 ac222e818b Never crash: edit and xfdf have no expect calls
- 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>
2026-10-04 14:23:06 -04:00
Brandon ThomasandClaude Opus 5.5 385313e0bf M7: Format existing text: font family, bold/italic, size, colour and alignment for a paragraph (Format text panel live while editing; text_edit formatting arguments); centred and right-aligned paragraphs are detected
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:07:43 -04:00