Commit Graph

885 Commits

Author SHA1 Message Date
Pratham Bhanushali eab641ac8f
feat(ui): add command palette arrow-key navigation (#1419) 2026-10-12 08:38:47 -04:00
Marco Arnulfo d7590ebfbe
fix(content): count an unreadable inline image with no EI as skipped (#1318) (#1418)
Follow-up to #1404. An inline image whose parameters can't be read and that has no EI after them ran to the end of the stream as a BI op with no data, and those bytes were not counted. It is now skipped like one with an EI, its bytes counted in Parsed::skipped. A stream that ends inside the parameters (BI /W 1) still keeps its BI op.
2026-10-12 08:38:01 -04:00
LabDennis 291d738160
M10: Test current-page print previews and saved output (#1415)
Extend the coverage of #1411 with preview raster selection and a dialog-driven PDF save test. Reopen and render the saved PDF to verify the viewed physical page when labels restart. Cover duplicate labels and selection guards without changing production code.

Refs #1308, #1411

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
2026-10-12 08:37:11 -04:00
Earl Co 2839152af1
M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging

* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check

The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).

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

* M12: preview text structure proposals for auto-tagging

* Keep read-only auto-tag preview separate from unavailable command

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:20:13 -04:00
krinry 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
Tangu Achilis 5cce1b247f
M10: edit OCR text on scanned pages so the change shows (#1397)
* M10: edit OCR text on scanned pages so the change shows

Editing a recognised word on a scan rewrote it as invisible OCR text, so
the page still showed the old word, and the whole OCR layer grouped into
one paragraph that rewrapped when one word changed.

- An edited invisible paragraph is drawn visibly, over a cover in the
  paper colour sampled around it, in the scan's sampled ink colour
  (or the chosen colour).
- OCR words group into phrases: a table cell or a tab stop splits them
  and each scanned line stays apart; a phrase grows rightwards.
- Styled rewrites measure with the text matrix's horizontal scale, so
  stretched OCR words keep their widths instead of overlapping.
- The inline editor covers the scanned word and grows like the result.

* M10: size OCR text from each word's letters

A word box from recognition is tight around the ink, but the text layer
assumed every box spans Helvetica's ascender-descender height. A
capitals-only word such as "DT" spans only the cap height, so its text
was set about 23% too small and its baseline floated up; editing it
then drew the new word visibly smaller than the scanned one.

The em and baseline now come from the word's letters: cap height or
x-height on top, descender or baseline at the bottom. On a real scanned
form, a rewritten "DT" -> "DSI" now matches the scanned word's height
and baseline. The engine OCR-edit tests build their words the way a scan
would box them.

* Edit OCR text: sample colours only for invisible blocks, from the current bytes

Session::apply looked up every edited block with a fresh text_blocks pass and
re-serialized the whole document to render the sampling tile. Text blocks are
now cached per page on the editor (keyed on the unmodified document's source
identity, so any edit, undo or save invalidates them), visible blocks skip
sampling entirely, and the tile renders from the document's current bytes.
Adds a visible stretched-text regression test and documents that editing any
invisible text makes it visible over a painted cover.

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

* Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt

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:16:42 -04:00
TheLastRoadRunner a259af95c3
fix(render): draw Turkish Ğ İ Ş ğ ş in non-embedded fonts (vendored hayro-interpret patch 26) (#1409)
A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins
for the standard 14 fonts. Those carry only the 230-odd glyphs of the
Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond
them in its /Differences mapped to .notdef and drew nothing. iText 2
writes Turkish exactly this way (Times New Roman left unembedded,
/Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such
letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew.

StandardKind now builds a Latin Extended-A letter the stand-in lacks from
the base letter and accent it does have, as Type 1 seac composites and
the standard-14 AFM composites are built: the accent is centred on the
letter, raised over capitals and letters with ascenders (and moved along
the slant in italics), cedillas hang below and ogoneks from the right
edge. Composite glyph ids sit above any font's glyph count, one per
character code; their width comes from the standard metrics of the name.

Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts
(fails without the patch: Gbreve drew no pixel).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:57:03 -04:00
Marco Arnulfo a4c53f706f
fix(ui): Current page prints the viewed page, not a page labelled with its number (#1308) (#1411)
The Print dialog turned Current page into the range "N" for the page on
screen, and ranges read numbers as page labels first. In a document
labelled i, ii, iii, 1, 2, viewing the first page printed the fourth,
whose label is "1".

Current page is now passed as a position, the way Selected pages is,
through print::select_listed. A typed range still reads labels first.

Adds print_ui::tests::current_page_is_the_viewed_page_not_a_label,
which fails before this change.
2026-10-12 07:55:39 -04:00
umesh429 a9a8ac63b7
fix(audit): an identity cm between re and fill no longer hides a cover (#1333) (#1410)
Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:54:52 -04:00
Marco Arnulfo 77c3f2003a
fix(automation): doc_save reports a required full rewrite as not incremental (#1329) (#1405)
doc_save reported "incremental": !full, from the arguments alone. When
an in-place save had to be a full rewrite (after doc_protect adds or
removes encryption, or after Sanitize), write_incremental fell back to
write_full, but the result still said the save was incremental.

The result now says whether the written file starts with the file it
was saved over, read from the new Document::saved_bytes. An ordinary
in-place save still reports true and an explicit full save false.

Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental,
which fails before this change for both doc_protect and
doc_remove_hidden.
2026-10-12 07:54:17 -04:00
Marco Arnulfo 240ea53b8b
fix(content): skip an inline image with an unreadable key to its EI (#1318) (#1404)
When a key in an inline image dictionary was not a name (`BI /W 4 H 4
ID ... EI`), the parser ended the image at the end of the stream and
counted nothing as skipped, so every operator after the image was lost:
text_lines found no text on a page whose text follows such an image.

An image whose parameters can't be read is now skipped up to the EI
that ends it, the skipped bytes are counted, and parsing resumes after
it. Without an EI the image still runs to the end of the stream, as
before.

Adds tests::an_unreadable_inline_image_is_skipped_to_its_end, which
fails before this change.
2026-10-12 07:53:44 -04:00
Marco Arnulfo 2ee8ce1ac1
fix(forms): keep a whitespace-only value in a text field without a format (#1323) (#1403)
The commit-time Keystroke check returned an empty value for any
whitespace-only text before looking at the field's format, so setting a
space, a tab or a non-breaking space in a plain text field reported
success and removed the field's value. Text and XFDF imports go through
the same setter and lost the value the same way.

Blank input now clears only a field with a format (number, percent,
date, time, special or mask), as before; a field without one stores the
text as given. An empty value still clears any field.

Adds tests::whitespace_only_text_is_kept_in_a_field_without_a_format
and a keystroke assertion in af::tests::numbers_format_like_acrobat,
both of which fail before this change.
2026-10-12 07:53:10 -04:00
Marco Arnulfo 452ab51ab8
fix(sign): refuse to sign a field whose /V is not a signature (#1328) (#1402)
Follow-up to #1374. Signing an existing field refused a field whose /V
is a signature dictionary and signed one whose /V is missing or null,
but a /V that is neither (an empty string, a number, a name, an array)
was treated as unsigned and overwritten with the new signature.

Signing such a field by name now fails with "the value of the field
... is not a signature" and leaves the document as it is. A missing or
null /V, direct or indirect, still signs, and a signed field is still
refused as already signed. The signature list is unchanged: it still
shows such a field as unsigned, since it carries no signature.

Adds a_field_whose_value_is_not_a_signature_is_refused_by_name, which
fails before this change.
2026-10-12 07:52:34 -04:00
umesh429 6f49967459
fix(cli): edit refuses move/insert destinations past the end (#1330) (#1400)
Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:51:55 -04:00
Francesco Clemeno 0cb311d21c
M3: select an image on the page and copy it (#1363)
* M3: select an image on the page and copy it

Select tool: a click on a raster image selects it (also one drawn through Form artwork, or with
text over it such as an OCR layer); ⌘C copies it as a file in its original format (a JPEG as
stored in the PDF, otherwise a lossless PNG). Right-click ▸ Copy Image copies its pixels at full
resolution, and Copy Image in Original Format the file; Edit text & images has both for the
selected image. Both respect the document's copy permission.

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

* M3: translate the copy-image strings into Czech, Kazakh, Slovene and Turkish

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

* Copy image: keep the clipboard file in a private per-user folder

Copy Image in Original Format wrote into the shared temporary folder
(/tmp/pdfcraft-clipboard) and deleted everything in it first, so another
user on the machine could plant a link or folder there and have PdfCraft
delete or overwrite their target. The file now goes in PdfCraft's
per-user cache folder (or the portable data folder), which is checked to
be a real folder and set to 0700 on Unix (refused when it isn't ours).
Only earlier copies (regular files named '<stem> p<N> img<M>.jpg|png')
are removed, links are never followed, and the new file is created with
create_new so it can't be redirected.

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

* Tests: escape Windows paths in synthetic GoToR file specifications

canonicalize() returns \\?\C:\... on Windows; unescaped backslashes in a PDF literal string become control characters, so link_open refused the target and main's Windows CI failed. Taken from #1359's helper fix.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:47:35 -04:00
LabDennis efd68b377e
M7: Preserve explicit page ranges across dialog redraws (#1304) (#1382)
* M7: Preserve explicit page ranges across dialog redraws

The shared marks/export range widget treated 1..=1 as uninitialized and
expanded it to the document length on every redraw. A first-page-only
request consequently exported or modified additional pages.

Separate all-pages default handling from the endpoint widgets. Populate
the full document bounds only in all-pages mode; preserve custom ranges
and their odd/even filters.

Add synthetic UI regressions for stable ranges, first-page-only image,
text and embedded-image exports, and pixel comparisons of added/updated
header-footers, watermarks and backgrounds. All three regressions fail
before the fix and pass afterward. Update milestone progress.

Fixes #1304

* M7: Limit page-range documentation to the progress log

---------

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:24:18 -04:00
Ridwan 5bf6d17538
Issue #1368: keep text a dragged paragraph passed over (#1381)
Rewriting a paragraph also removed every line overlapping its box by more
than half, to clear fake-bold and repeated copies. Each drag is its own
edit, so after dropping one paragraph on another and dragging it on, the
covered paragraph was taken for a copy and deleted from the content
stream. A copy now also has to show the same text.

The same check is used when replacing a line, so a distinct line drawn
close enough to overlap (#1316) is no longer removed either.
2026-10-12 07:21:23 -04:00
Marco Arnulfo a0575f2478
fix(fonts): a /Differences code past 255 no longer encodes as its last byte (#1314) (#1394)
A simple font whose /Differences puts Euro at code 300 encoded "€" as
the single byte 0x2C, which the same font decodes as a comma. Simple
fonts show one-byte codes, so a larger code now has no glyph to encode
with: encode returns None and the caller falls back to another font.
The code stays in the decoding table as before.

Adds pdf::tests::differences_codes_past_one_byte_are_not_encoded,
which fails before this change.
2026-10-12 07:20:48 -04:00
Marco Arnulfo 24ccb5f3ae
fix(forms): a literal Off value reads as Off outside buttons (#1320) (#1393)
The field reader dropped the value "Off" for every field kind, so a
text field, combo box or single-select list set to "Off" read as empty
and exported an empty value to CSV, text, XML, XFDF and FDF, although
the document stored it. "Off" is now treated as no value only for
check boxes and radio buttons, where it is the off state.

Adds tests::a_literal_off_value_reads_as_off_outside_buttons, which
fails before this change.
2026-10-12 07:20:15 -04:00
Marco Arnulfo 3e2877cbbb
fix(forms): a whole look keeps the rest of an indirect /MK (#1322) (#1392)
set_props with a complete look read the widget's /MK without resolving
it, so an indirect one started from an empty dictionary: a check box
lost its Cross mark, its 90-degree rotation and any unknown entry, and
the loss survived a save. The /MK is now resolved first, as the
check_style branch already does; a direct /MK behaves as before.

Adds tests::a_whole_look_keeps_the_rest_of_an_indirect_mk, which fails
before this change.
2026-10-12 07:19:42 -04:00
Marco Arnulfo 0584425e67
fix(automation): content_update refuses text styling on an image (#1331) (#1391)
font, size, bold, italic, color and align were never read for an added
image, so content_update reported success, wrote the same image back,
marked the document dirty and added an "Edit content" undo step. They
are now refused with an error, as rotate, flip, crop and image already
are on text, before anything is applied.

Adds automation::text_styling_on_an_image_is_refused_without_an_edit,
which fails before this change.
2026-10-12 07:19:06 -04:00
Marco Arnulfo b6a1402bf3
fix(ui): the Fields panel lists a field on a missing page as unplaced (#1307) (#1390)
A widget whose /P is a page number past the end of the document (here
/P 99 in a one-page file) kept that page, and the Fields panel indexed
the page list with it to label the heading, which panicked. A page the
document doesn't have now gets the existing "Unplaced" heading.

Adds forms::the_fields_panel_lists_a_field_on_a_missing_page_as_unplaced,
which fails before this change.
2026-10-12 07:18:32 -04:00
Marco Arnulfo af1c997aff
fix(fonts): a composite /W run past the last CID stops there (#1313) (#1389)
A /W entry such as [4294967295 [100 200]] added the run index to its
first CID unchecked, so the second width overflowed: a panic with
overflow checks on, and with them off a width given to CID 0, which the
entry does not name. The run now ends at the last CID it can address;
/W [0 [100 200]] reads as before.

Adds pdf::tests::composite_width_run_past_the_last_cid_is_dropped,
which fails before this change.
2026-10-12 07:17:59 -04:00
LabDennis 2a004529dd
M14: Preserve recovery snapshots without readable metadata (#1378)
* M14: Preserve recovery snapshots without readable metadata

Recovery writes the PDF before its metadata sidecar. A failed sidecar write
or interrupted save could leave a usable snapshot that the next listing
silently deleted.

List PDF snapshots without deleting recovery files. When metadata is
missing, unreadable, corrupt or refers to another key, retain the snapshot
and recover it without an original path so saving requires a destination.
Keep the existing password flow for encrypted snapshots.

Add regression coverage for failed metadata writes, repeated listing,
missing/corrupt/mismatched metadata and encrypted recovery. Update the M14
progress documentation.

Validation: the failed-write regression fails before the fix; all 11
recovery tests pass after it. Formatting and the 31-crate WASM check pass.
The full workspace and strict Clippy gates remain red; see the PR body
for the failing tests and diagnostics.

* Recovery: replace damaged metadata once its snapshot is recovered; one progress-log entry

Recovering a snapshot whose sidecar was missing, unreadable or for another
key now writes fresh metadata for it (recording whether it is encrypted), so
the damaged file doesn't linger. clear() also removes sidecars whose PDF is
gone; snapshots themselves are never removed by listing. The header and M14
milestone-row edits are replaced by a single progress-log entry.

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

---------

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:13:35 -04:00
philip-tam 88d96c93a8
M2: snap shrunk interpolated images to whole pixels so scans stay sharp (#1377)
* M2: snap shrunk interpolated images to whole pixels so scans stay sharp

A scanned page (one 300 dpi image with /Interpolate true) was shrunk to a
ceil-sized raster and then sampled a second time at a fractional scale and
offset, which softened every edge next to Acrobat. An axis-aligned shrunk
interpolated image is now shrunk once to its rounded device rectangle and
drawn 1:1 at the rounded corner. Rotated, skewed, flipped, Type 3 and
non-interpolated images are unchanged.

On a synthetic scan page the mean error against an area-average reference
falls from 3.4 to 1.6 grey levels at 96 dpi (3.0 to 1.5 at 144 dpi).

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

* vendor: list the hayro snap patch as (14) in the Patches and Tests columns; correct gaps.md patch counts

The patch description had been appended to the Tests column of one of the
two hayro rows. gaps.md now counts the distinct patches vendor/README.md
lists (26 hayro-interpret, 15 hayro, 3 hayro-jbig2, 17 hayro-syntax).

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 07:12:02 -04:00
Yousef Mojahid 186c1dc767
M7: preserve Arabic text order in copy, search and editing (#1369)
* M7: preserve Arabic text order when copying and editing

Record per-line ActualText for shaped added text and preserve its logical order in Edit text. Cover mixed scripts, marks, copy/search, retyping and save/reopen; retain the drawing golden.

Assisted-by: Codex

* Edit text: keep only right-to-left ActualText out of line joining and bidi recovery

Word and Chrome wrap Latin ligatures in /ActualText spans mid-word; splitting
lines at every span broke such words into pieces in Edit Text. Only spans
whose ActualText has strong right-to-left characters keep their own line and
typing order now. Regression: a Latin fi ligature span stays on its line.

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 07:11:23 -04:00
Marco Arnulfo 2796f3de0d
fix(print): refuse huge posters at the sheet limit instead of overflowing (#1335) (#1376)
A poster scale large enough to make both tile counts saturate at
usize::MAX overflowed when they were multiplied for the 1024-sheet
check: a panic with overflow checks, and a product that wraps to 1
without them, which let the sheet loops run. The headless doc_print
tool accepts any finite positive scale, so a script could reach it.

Multiply with checked_mul and treat an overflow as over the limit, so
the request gets the existing "more than 1024 sheets" error.

Adds tests::huge_posters_are_refused_at_the_sheet_limit, which panics
before this change and passes with and without overflow checks after it.
2026-10-12 06:27:50 -04:00
Marco Arnulfo 9593ff7419
fix(redact): removing comments keeps non-comment annotations (#1325) (#1375)
Remove Hidden Information counted and removed as a comment every
annotation that was not a link, widget, pop-up, file attachment or
redaction mark, so a Comments-only cleanup also deleted Watermark,
PrinterMark, TrapNet, Screen, Movie, RichMedia and 3D annotations.

Use pdfcraft_annot::is_comment_subtype, which the comments list already
uses, and keep excluding file attachments (removed with Attachments)
and redaction marks as before.

Adds tests::removing_comments_keeps_annotations_that_are_not_comments,
which fails before this change.
2026-10-12 06:27:04 -04:00
Marco Arnulfo f97f24cfbc
fix(sign): a signature field with a null /V can be signed by name (#1328) (#1374)
The signature list counts a field as signed only when its /V resolves
to a dictionary, but signing an existing field refused any field that
had a /V key. A field with `/V null`, or a /V pointing at a null object,
was listed as unsigned and then refused with "the field is already
signed".

Both now ask the field for its signature dictionary through one
predicate. A field with a signature is still refused, and a field
without /V still signs as before.

Adds a_field_with_a_null_value_is_unsigned_and_can_be_signed, which
fails before this change.
2026-10-12 06:26:18 -04:00
Marco Arnulfo 0108a5b07b
fix(preflight): find colour operators after any PDF whitespace (#1334) (#1373)
The PDF/A device-colour scan split content streams only on spaces and
line feeds, so `0 0 0 1 k` written with carriage returns, tabs, form
feeds or NULs between the tokens went unseen: verify reported no
DeviceCMYK, and convert gave the CMYK page an sRGB output intent.

Split on all six whitespace characters of ISO 32000-2 §7.2.3, using the
lexer's own `is_whitespace` (now exported from pdfcraft-cos) instead of
a second list.

Adds tests::device_colour_is_found_after_any_whitespace, which fails
before this change; its gray and RGB controls check that those pages
still get the sRGB output intent.
2026-10-12 06:25:42 -04:00
umesh429 27ce6f7584
CLI: audit rejects unknown options before reading the file (fixes #1332) (#1372)
`pdfcraft-cli audit` skipped option validation, so a misspelled
`--passwrod` fell through to a file-read error instead of naming the
bad option. Audit now runs `validate_options` first, like `info`, and
the options test covers it.

Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:25:06 -04:00
Marco Arnulfo ec5d9da36c
fix(xfdf): text data from a one-field form keeps its commas (#1321) (#1371)
The tab-delimited export of a single field has no tab, so import read it
as CSV and kept only the text before the first comma. CSV quotes a value
that holds a comma, so a single name over several unquoted cells is now
read as that tab-delimited text. Exported files are unchanged, and CSV
(quoted, multi-line, one or more columns) reads as before.
2026-10-12 06:24:27 -04:00
Earl Co d2d1b16b21
M5: adjust Fill and Sign text size (#1285)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:13:48 -04:00
Omar Elders d57f740003
Refine search match selection and text failure jitter handling (#1291)
* Fix search match selection order and text reload failure recovery

1. rerun_find: Accumulate and sort matches across all loaded pages and pick the first match on or after the currently viewed page before flashing once, avoiding arbitrary viewport jumps to page 0 when viewing subsequent pages. 2. receive_result: Gracefully fall back to the next match at or after the viewed page without flashing when page text extraction fails, avoiding latent None state that caused viewport jitter on subsequent page reloads. 3. Add tests verifying match selection on viewed page, continuous multi-page drawing with active search, and text failure recovery.

* rustfmt

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

* Clippy: RenderRequest is Copy

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 05:52:36 -04:00
Master f81e23bd4e
test(cli): reject unknown MCP options (#1342)
* test(cli): reject unknown MCP options

* Tests: wait longer for background text and rendering in viewer tests (FreeBSD CI flake); rustfmt

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 05:46:41 -04:00
cyfung1031 bf4b6799f6
ci: keep line tables for workspace debug info (#1298) 2026-10-12 05:45:51 -04:00
cyfung1031 6d9eb9c3ca
ci: run dependency layering once (#1286) 2026-10-12 05:45:13 -04:00
Brandon Thomas 78f00e45fe Release prep: 0.7.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:44:35 -04:00
cyfung1031 ef844cefb4
CI: keep craft-fonts configuration stable across Linux gates (#1284) 2026-10-12 05:44:33 -04:00
Paul Spooren 9a8517599b
M12: accept literal digits in date formats (dd.mm.26) (#1352)
A date field formatted "dd.mm.26" (a leave form fixing the year) rejected
"15.05.26": parse_date read three numbers but the format only asked for a
day and a month. Digit runs in the format are now literal tokens that may
be typed or left out; a different number in their place is still rejected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:43:55 -04:00
Jules Kreuer 47d612817a
M8: Clean indirect and form tag references (#1351) 2026-10-12 05:43:12 -04:00
nevicia2010-hub 51982db121
M3: match Greek final sigma in case-insensitive search (#1349)
* M3: use the macOS Finder label in the tab-menu regression

The UI already uses Reveal in Finder on macOS, but the regression test
queried Reveal in File Explorer on every platform. Select the platform's
label while preserving the disabled-action and inactive-tab assertions.

Co-Authored-By: Codex <noreply@openai.com>

* M3: match Greek final sigma in case-insensitive search

Fold capital, normal and final sigma to the same character so Greek words
match in either case. Cover exact search, whole words, glyph ranges and
bounded matches without changing the existing Turkish-I behavior.

Record the search fix and the macOS test portability follow-up in the
roadmap. Workspace tests, all-target Clippy, formatting, wasm, layering
and a synthetic CLI before/after comparison pass on macOS.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: nevicia2010-hub <259836477+nevicia2010-hub@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 05:42:27 -04:00
Joel Tony 9d6552291b
Issue #1296: hayro-syntax applies SASLprep to AES-256 passwords (PdfCraft patch 16) (#1345)
Revisions 5-6 of the standard security handler key the file on the
password processed with SASLprep (ISO 32000-2 7.6.4.3.3, Algorithm 2.A
step a). pdfcraft-crypt writes and reads it so, but the vendored
hayro-syntax skipped the step ("TODO: Do the above.") and used the UTF-8
bytes as typed. A password that SASLprep changes (a decomposed "cafe" +
U+0301, a no-break space, fullwidth letters typed by an IME in full-width
mode) was wrong for the renderer: Protect with such an open password was
rolled back ("the password is incorrect"), and an AES-256 file with such a
password did not open in a session with it, although pdfcraft-cos
accepted it.

- hayro-syntax (vendored patch 16) tries an R5/R6 password in its SASLprep
  form (as pdfcraft-crypt computes it: as given if SASLprep refuses it,
  UTF-8, at most 127 bytes), then, when that differs, as given.
  stringprep, already in the tree through pdfcraft-crypt, is an optional
  dependency enabled by the std feature; without std nothing changes.
- Tests: hayro-syntax crypto::pdfcraft_password_tests (candidate list and
  R5/R6 dictionaries built from prepared passwords) and two engine tests
  (Protect and reopen, and pdfcraft-cos-encrypted files) for the three
  cases, with precomposed "caf\u{E9}" as the control. The dictionary test
  and both engine tests fail without the fix.

Fixes #1296.
2026-10-12 05:41:38 -04:00
MarekSpodBiedry 549fa7ae04
xtask: stop headless Chrome leaving macOS code-sign clones (#1196) (#1292) 2026-10-12 05:40:50 -04:00
ysrdevs 4b45fc9cb4
XFA: one script thread per event, and the live form kept up to date in place (#1355)
* XFA: one script thread per event, and the live form kept up to date in place

A form with hundreds of calculate scripts used to spend its open-time
budget on set-up: every script got a thread of its own, and the engine
dropped its live form after every value a script settled, rebuilding
the whole tree for the next script.

The js crate gains xfa_session::XfaSession: the scripts of one event
run on one thread (with the script stack), each in a fresh JavaScript
context or FormCalc interpreter on a copy of the form as the engine
holds it then, so scripts share nothing, a script that fails halfway
leaves nothing behind, and what a script changed reaches the next one
only through the engine. A script past its time limit is abandoned on
the thread and the session is dead from then on; a fault in the engine
(a panic) loses only that script, the thread is replaced. On wasm the
scripts run inline, as before.

The engine's runner keeps one session per event and, for a value, now
refreshes the one field in its live form in place (xfa::refresh_value,
computing the value as the tree builder would: saved values, template
defaults) instead of rebuilding the tree; rows, presence, access,
resets, relayouts and data rewrites still rebuild it. Under test, the
live form is compared with a fresh build before every script and at
the end of every event, and a difference ends the test binary (a panic
would be caught by the engine's guard and turned into a script error).

210 calculate scripts (one per row of 10 sections x 20 rows, plus a sum
per section), debug build, instrumented inside the engine: the script
event on open 495 ms -> 204 ms (of which live-form rebuilds 217 ms ->
0), on a change 300 ms -> 172 ms; through the tools, layout included,
open 1.27 s -> 0.97 s and a change 0.44 s -> 0.22 s. The rebuild cost
removed grew with form size times script count.

* CI: the GoToR tests' case lists are only grown on Unix, so Windows must not warn on their mut
2026-10-12 05:39:49 -04:00
Bryan Mark 0e55d07b5f
M12: say when a form's scripts were turned off (#1251)
* M12: say when a form's scripts were turned off

When an XFA script runs past its time limit it is abandoned and the
form's scripts are turned off for the document. The only trace was an
error line in the JavaScript console, so calculations and buttons just
stopped working.

- Engine: Document::xfa_scripts_off() reports the state (set on open and
  on an edit), so a viewer shows it without reading error text. doc_info
  reports it to agents as xfa_scripts_off.
- App: when it happens (at open or after a click) a toast says so once,
  and the notice bar says so from then on, ahead of its other notices,
  with a JavaScript Console button. A notice bar the user had closed
  comes back for it; closed again, it stays closed.
- The sentence is translated in the 17 catalogs that are complete
  (hashes in ATTRIBUTION.toml updated).

Tests: two kittest UI tests (runaway at open; runaway click after the
bar was closed), which fail without the app change; engine assertions
for both states; a doc_info tool test.

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

* Catalogs: the scripts-off notice in Greek, Polish and Telugu; refresh hashes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 03:42:56 -04:00
Krzysztof Cieślik 7d419a6c73
feat(edit): remove all links command and context menu (#539) (#1213)
* feat(edit): remove all links command and context menu (#539)

- Expose 'edit.remove_links' and 'edit.remove_all_links' commands to remove all links across the document or on specified pages.
- Add right-click context menu options for links: Properties, Delete, and Remove all links in both Link tool and Select tool modes.
- Extend automation tool 'links_remove' with optional 'pages' parameter and removal count.
- Add notification feedback in UI for link deletion.
- Mark 'edit.remove-all-links' as shipped in Parity with evidence tests.
- Add unit and integration tests covering automation, engine, and UI undo/redo.

* Remove all links: one command id, catalog rows, link menu only on a link

- Drop the duplicate edit.remove_all_links command (same label as the
  existing edit.remove_links, so it showed twice); edit.remove_links now
  runs remove_all_links with its toast.
- Add the toast strings (No links to remove, Removed 1 link, Removed {n}
  links) to all 20 catalogs.
- The canvas context menu shows the link items only when the right-click
  hit a link (LinkView::context, set per right-click), so a link left
  selected no longer takes over the comments menu, and the Link tool no
  longer replaces the comments menu away from links. Regression test.

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

* Restore the PR's links_remove pages test (lost with the workdir conflict)

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 03:31:39 -04:00
git-monchiant e8e275cbd4
Text extraction: honour /ActualText, so Thai from Chrome PDFs is searchable (#1217)
* Text extraction: honour /ActualText, so Thai from Chrome PDFs is searchable

Chrome (Skia) writes /ActualText around each Thai, Indic or Arabic cluster
whose shaped glyphs (a tone mark moved clear of a vowel, a ligature) have no
Unicode of their own. Text extraction ignored it, so those marks were lost:
"น้ำหนัก" came out as "นาหนัก", "เป็น" as "เปน", and searching for any
such word found nothing.

- hayro-interpret patch (25): Device::actual_text hands the device a
  marked content sequence's /ActualText, inline or named in /Properties.
- TextDevice turns the glyphs of a replacement into one glyph carrying the
  replacement text, boxed around them. The outermost replacement wins, an
  empty one stands for no text, and one spanning several lines or writing
  directions keeps per-glyph text so selection and lines still work.
- Canva draws text twice (outline or shadow) and marks only one drawing:
  the unmarked redraw at the same places takes the same replacement, so the
  existing redraw detection still drops the copy.

Checked on 1,114 real-world PDFs against the previous build, with pdftotext
as the reference: 957 extract byte-identical text, 157 change and all of
them read closer to the reference (exact matches 3 -> 122), none got worse,
none newly fail, and total extraction time is unchanged.

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

* Text extraction: bound the work of the ActualText redraw dedupe

`TextDevice::finish()` compared every replacement with every unmatched glyph
in up to 4096 grid cells, so a hostile page stacking ~10^5 glyphs and ~10^5
replacements at one spot meant ~10^10 comparisons (a hang). It now charges
each visited cell and each candidate run against a per-page budget
(`MAX_REDRAW_WORK`, 2^22) and stops deduplicating once it is spent; redraws
found so far are still merged. A regression test feeds 100,000 replacements and
100,000 non-matching glyphs at one place.

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

---------

Co-authored-by: git-monchiant <4667243+git-monchiant@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 03:28:27 -04:00
cyfung1031 fcf6e39d46
M14: tune Windows low-DPI JetBrains Mono hinting (#1264)
* M14: tune Windows low-DPI mono hinting

* Theme: ignore the raster-sharpness probe test

`windows_hinting_improves_low_dpi_mono_raster_without_changing_layout_or_coverage`
asserts exact egui rasterisation deltas that move with egui upgrades; it is now
`#[ignore]` (run with --ignored when retuning). The tweak and its policy test
stay.

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 03:25:25 -04:00
srt8hellcat2-ui 06480e395d
M3: add PDF tab context menu (fixes #1233) (#1266)
* feat(ui): add PDF tab context menu (#1233)

* Tab menu: reveal through D-Bus off the UI thread

`dbus-send --print-reply --reply-timeout=2000` (and the xdg-open fallback)
could block the frame for 2 s or more; on Linux and FreeBSD the reveal now runs
on a `pdfcraft-reveal` thread and a failure is logged. The existence check stays
on the UI thread and is still reported; Windows and macOS launches are
unchanged. Refreshes the catalog hashes after the main merge.

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

---------

Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:15:02 -04:00