Commit Graph
2571 Commits
Author SHA1 Message Date
aiongg eac3ad6e0f Merge PR #4068: i18n(fi): translate the 64 labels left in English after #2210
Conflicts:
- ATTRIBUTION.toml: fi.tsv source: main's text plus the PR's added sentence; the PR's sha256 line dropped (translation catalogs carry none on main).
- ATTRIBUTION.md: main's rows, fi row updated to the same source text.
2026-10-24 06:24:19 +00:00
aiongg 97a0b31c2c Merge PR #4058: M12: type or pick an architect's 1:N drawing scale in the Measure panel, applied at once
Conflicts:
- crates/ui-egui/src/i18n/mod.rs: kept main's side (main dropped the per-language translation tests; the PR's drawing_scale_labels_are_translated_in_every_catalog is not carried over).
- parity/acrobat-features.toml: measure.distance evidence: kept main's paths (tests/it/), appended the PR's two new entries.
2026-10-24 06:23:49 +00:00
aiongg 80f9555cf8 Merge PR #3786: M5: the arrow keys move the selected comment 2026-10-24 06:23:32 +00:00
aiongg a6351a5328 Merge PR #2195: M3: restore last view settings when reopening documents (#1767)
Conflicts:
- ROADMAP.md: progress log, kept both lines.
2026-10-24 06:23:31 +00:00
aiongg a7778f92f1 Merge PR #4362: M5: comments without an appearance draw their /Border dashes, Ink dashes and the caret's ¶ (ISO 32000-2 Tables 166, 168, 183) 2026-10-24 06:23:19 +00:00
aiongg 581faa876a Merge PR #4349: M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184) 2026-10-24 06:23:15 +00:00
aiongg 4b29f18bfb Merge PR #4303: M3: a link without a position keeps the reader's place (ISO 32000-2 §12.3.2.2) 2026-10-24 06:23:11 +00:00
aiongg 4e335dd6ef Merge PR #4096: feat(ui): the secured-document notice can be turned off for good (#4035)
Conflicts:
- ATTRIBUTION.toml: kept main's side; translation catalogs carry no sha256 on main, so the PR's sha256 lines for them are dropped.
2026-10-24 06:23:06 +00:00
aiongg 1b404e6956 Merge PR #4095: Comments: the composer hides while its comment is scrolled away (#4021)
Conflicts:
- crates/ui-egui/tests/it/comments.rs: kept both PRs' new tests (adjacent additions).
2026-10-24 06:22:46 +00:00
aiongg ad54ef3a5f Merge PR #3970: M3: resize and move the window again right after a resize or move (Wayland) 2026-10-24 06:22:33 +00:00
aiongg d626a4ae72 Merge PR #2190: M3: a fresh install follows the desktop's appearance (#1248) 2026-10-24 06:22:29 +00:00
aiongg ee989ab5ca Merge PR #3016: M5: sticky notes stay upright where they are placed on rotated pages (NoRotate, #2583)
Conflicts:
- parity/acrobat-features.toml: comment.note: kept main's icon notes text, added the NoRotate sentence and the PR's three evidence entries.
- vendor/README.md: kept main's rows; added the PR's hayro-interpret patch as (53) with its test (53).
2026-10-24 06:22:25 +00:00
bmh129andClaude Opus 5.5 185c2dbc36 M3: give the kept button release back before the next press (Wayland)
The release a desktop grab kept from egui was appended to the next input,
so when that input carried the press of a second resize or move the
button ended the frame up and the drag stopped. It now goes before the
input's first primary press; only a release ahead of any press counts as
the kept one, since a quick click's release belongs to its own press.
Found by CodeRabbit in review of #3970.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs
2026-10-24 00:58:06 -05:00
bmh129andClaude Opus 5.5 0b63a73c04 M3: progress docs for repeated window resizes and moves on Wayland
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
2026-10-24 00:46:28 -05:00
bmh129andClaude Opus 5.5 a9e6069f32 M3: resize and move the window again right after a resize or move (Wayland)
ViewportCommand::BeginResize and StartDrag hand the pointer to the
compositor, which keeps the button's release on Wayland; winit reports
only that the pointer left, and egui doesn't count that as a release. egui
then still thought the primary button held, so the next press on an edge
or the tab strip didn't start a drag until a click inside the window.

window_controls::desktop_grab notes each hand-over, and
release_after_desktop_grab (called first in raw_input_hook) adds the
missed release to the next input, once, unless that input brings one.
Ported from WordCraft 1581c288, confirmed by hand there for repeated edge
resizes on GNOME/Wayland.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
2026-10-24 00:46:06 -05:00
aiongg 0b670caf4a fix(deps): bump crypto-bigint to 0.7.6 (RUSTSEC-2026-0387) (#4366)
cargo deny refuses crypto-bigint 0.7.5, which pdfcraft-sign uses through
rsa and crypto-primes. 0.7.6 is a semver-compatible patch release with the
fix.
2026-10-24 00:03:16 -04:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

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

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
mtlcl410andClaude Opus 5.5 001a900ac7 M5: comments without an appearance draw their /Border dashes, Ink dashes and the caret's ¶ (ISO 32000-2 Tables 166, 168, 183)
Comments that arrive without /AP are drawn from their dictionaries by
appearance::build, which lost three entries:

- /Border [hr vr w [dash]] (Table 166): the dash array made the whole
  array unreadable, so the width fell back to 1 and the border was solid.
  The width is now element 2 whatever follows, and without /BS the
  /Border dash array is drawn. /BS, when present, wins for both (Table
  166: "the Border entry is ignored"), so a /BS without /W means 1.
- Dash arrays are drawn only when finite, non-negative, not all zero
  (§8.4.3.6) and at most 64 long; anything else draws solid.
- Ink applies its /BS dash pattern (Table 168, Table 182), like the
  other stroked shapes.
- A caret with /Sy /P (Table 183) shows a ¶ (Helvetica, WinAnsi 0xB6) to
  its right in its colour, inside /Rect: in the room /RD leaves, or by
  narrowing the caret. /Sy /None draws as before.

The Helvetica font resource, written out three times, is one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 10:41:46 +08:00
aiongg 0c748e34a8 fix(render): bound xref streams, JBIG2 referred-to counts, saved graphics states and /W2 runs (#4350)
* fix(render): bound xref streams, JBIG2 referred-to counts, saved graphics states and /W2 runs

Input bounds and crash fixes in the vendored hayro crates, ported from
the snapshot craft-libs carries (storytold/craft-libs#176, #239, #241):

- hayro-syntax: an xref stream subsection must be backed by its decoded
  data. All-zero /W widths and a count longer than the data are refused
  before any entry is inserted; a tiny stream with a large /Size or
  /Index count inserted that many entries.
- hayro-syntax: a subsection running past object number u32::MAX is
  refused; start + i overflowed.
- hayro-syntax: the xref stream type field is read big-endian at its
  full /W[0] width, and /W[0] over 8 bytes is refused like /W[1].
- hayro-jbig2: the long-form referred-to segment count is capped at
  u16::MAX before its retention bytes are read; the 29-bit count went
  straight to Vec::with_capacity (about 2 GiB) and aborted the process.
- hayro-interpret: at most 4096 saved graphics states holding at most
  64 MiB together. A q past either limit is counted but not copied, and
  its Q still removes the clips added since.
- hayro-interpret: a /W2 run whose first CID is past MAX_CID stops there,
  as /W does; a first CID of 4294967295 discarded the whole /W2 table.

Each patch is marked in the source, has a regression test in its crate,
and is listed in vendor/README.md.

* fix(render): a binary usecmap counts against the CMap nesting limit; /W2 checks the CID first

- hayro-cmap: a binary CMap's usecmap passed its own depth to the nested
  parse, so a self-referencing binary CMap recursed until the stack
  overflowed. It now counts like a text usecmap (ledger hayro-cmap 2).
- hayro-interpret: the /W2 first-CID check runs before the run's metrics
  are read, so an incomplete run past MAX_CID no longer discards the rest
  of the table (ledger 52).

* fix(render): refuse xref-stream object numbers past i32::MAX

ObjectIdentifier stores an i32; a number between i32::MAX and u32::MAX
was cast to a negative object number (ledger hayro-syntax 26).
2026-10-23 22:17:12 -04:00
mtlcl410andClaude Opus 5.5 6506c8cc45 M3: a fractional zoom is remembered as it was, not rounded to a whole percent (#1767)
Review follow-up on #2195: the last view stored ViewDefaults::zoom_name(),
which rounds 62.5% to 63%. A fixed zoom is now written with its fraction
(with_zoom already reads it back); the fit tokens are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 10:09:48 +08:00
mtlcl410andClaude Opus 5.5 fb53ddf02f M3: full screen neither is remembered as the last view nor hides it (#1767)
CodeRabbit on #2195: a file opened while full screen was on entered the presentation
first, and the remembered view then replaced its one fitted page, while leaving full
screen went back to the display from before the restore; and a tab closed (or settings
saved) in full screen remembered full screen's temporary fit and display. The remembered
view is now applied before the presentation starts, and the view remembered is the
normal display (DocView::normal_display: the one presenting replaced) at the page
reached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:09:47 +08:00
mtlcl410andClaude Opus 5.5 7dc18ee131 M3: the restore-last-view preference in the catalogues added since (#1767)
Catalan, Finnish, Hindi, Indonesian, Korean, Romanian and Albanian have coverage
tests the new preference failed after rebasing onto main; Norwegian Bokmål and Thai
get it too. AI-assisted, marked as not reviewed by a native speaker. Lao and Central
Kurdish fall back to English for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:09:47 +08:00
mtlcl410andClaude Opus 5.5 5e69e620a3 M12: the last view remembers the two-page cover setting (#1767)
Review follow-up on #2195.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:09:47 +08:00
mtlcl410andClaude Opus 5.5 aa190d98b2 M3: Vietnamese for the restore-last-view preference (#1767)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:09:47 +08:00
mtlcl410andClaude Fable 5.1 946fb31eef M3: restore last view settings when reopening documents (#1767)
A file opened again shows the page, zoom and page display it was last
shown with, as Acrobat's "Restore last view settings when reopening
documents" does: remembered by path (the last 100 files) when its tab
closes or settings are saved with it open, applied over the file's own
initial view, on by default and switched off in Preferences ▸ Documents
and view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 10:09:47 +08:00
mtlcl410andClaude Opus 5.5 2f97fd2d59 M5: the placeholder for an unknown stamp name stays grey whatever its /C
An AP-less stamp whose /Name has no drawing got its placeholder frame in
the annotation's /C colour (/C [1 0 0] drew a red frame), while the doc
comment and parity note promise a neutral grey one. The frame now always
strokes PLACEHOLDER_GREY; the fourteen Table 184 names keep /C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 09:27:38 +08:00
mtlcl410andClaude Opus 5.5 3db28cc4e3 M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184)
A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:

- the fourteen standard names of Table 184 draw that stamp with
  PdfCraft's stamp drawing (eight from the palette, six with labels of
  their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
  is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
  frame with both diagonals, so the stamp stays visible and selectable.

The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 09:15:12 +08:00
Dylan M. Taylor a2ec27b477 ci(sonar): don't cancel superseded runs on main, so coverage is published (#4321) 2026-10-23 17:06:26 -04:00
aiongg c4a6dd6be3 Assets: don't hash translation catalogs in ATTRIBUTION.toml (#4284)
* Assets: translation catalogs aren't hashed

The 33 UI translation catalogs are project files under the project
licence, and git history records their provenance. Their ATTRIBUTION.toml
entries keep title, author, source and licence but carry no sha256, so a
PR that changes UI text no longer has to edit ATTRIBUTION.toml.

cargo xtask assets accepts a kind = "translation" entry for a .tsv under
crates/ui-egui/src/i18n/ without a hash, rejects one that has a hash, and
still requires a hash for every other entry.

* Assets: a leftover hash on a translation catalog is a note, not an error
2026-10-23 16:41:52 -04:00
aiongg 8d99a5ce1f Translations: a missing string never fails a test; xtask ci prints the gaps (#4302)
Delete the translation coverage tests and the tests of one language's
wording. Keep catalog validation (format, duplicates, placeholders, plural
forms, command ids) and the tests that use a language (registration,
plural rules, RTL order, glyphs, casing, font fallback).

cargo xtask i18n-report runs i18n::tests::translation_gaps_report, which
prints each language's missing count and first few keys and always
passes; xtask ci runs it after the tests.
2026-10-23 15:42:18 -04:00
Dylan M. Taylor aea5e78e66 release: the git-sources gate moves into craft-common-actions (storytold/craft-common-actions#78) (#4292)
The check is shared at release.yml@main now; keep only the reusable call.
2026-10-23 14:41:20 -04:00
Lcoding15andClaude Opus 5.5 21bbae39b9 M3: a link without a position keeps the reader's place (ISO 32000-2 §12.3.2.2)
A /XYZ destination with a null (or unusable) left or top went to the top of the
target page. ISO 32000-2 §12.3.2.2 says a null value keeps the current one, and
Acrobat Reader, observed on a synthetic two-page fixture at 400 %, keeps the
user-space point at the window's top-left on the new page at the same zoom.
CAD exports (WSCAD) write every cross-reference as [page /XYZ null null 0], so
following one zoomed in landed on the sheet's title row instead of where the
reader was looking.

go_to_dest now reads the place at the window's top-left before the jump and
uses it for the null coordinates, along each axis the page overflows; a page
that fits keeps the layout's own placement. Null /FitH top and /FitV left
still go to the page edge.

Tests: a unit test (zoomed, partly given, view turned a quarter, page that
fits) and an end-to-end click on such a link at 400 %, both seen failing with
the fix reverted. The malformed-bookmark case in
bookmark_destinations_scroll_to_their_position now expects the kept place, as
DestView documents unusable operands as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:41:06 +02:00
aiongg c9ac28a048 test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep (#4277)
* test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep

sign_id_create, earlier in the sweep, wrote a digital ID to the in.pdf
placeholder that sign_document then took as its id. Almost every
sign_document call failed to decrypt it and never reached the hostile
value under test, and the decrypts took most of the test's time.

Each tool now starts from an empty root, and sign_document gets a test
ID with its password and a fresh blank document per call. 370 of its
440 calls get past the ID (1 before) and 340 past the document (0
before); the sweep takes 7 s instead of 89 s.

* test(automation): empty the sweep's root through its canonical path

form_merge_data, given the hostile name " ", writes a file of that name
into the root through the canonical \\?\ path, where Windows takes it
literally. Emptying the root through the plain path dropped the trailing
space, so the entry named the root itself and removing it deleted the
root. Entries are now reached through the canonical path, their type is
read from the directory entry, and a failed removal is reported.

* test(automation): fail the sweep when the root can't be emptied

Listing, typing or removing an entry left in the root now fails the test
with the entry's path, so no tool runs against an earlier tool's files.
2026-10-23 14:22:19 -04:00
aiongg d1756e487d CI: shared gate workflow; nightly fuzzing in its own workflow (#4270)
* CI: the shared gate workflow; nightly fuzzing in fuzz.yml

* CI: run the gate now that the shared workflow is on craft-common-actions main

* CI: adding full-ci to an open PR runs the gate; fuzz checkout keeps no token
2026-10-23 14:02:43 -04:00
aiongg 045c8b39a7 CI: nextest, line tables only, one wasm check (#4269)
* CI speed: line tables for workspace crates, no debug info for dependencies

* CI speed: xtask ci runs nextest and doctests, one wasm check, the CLI without default features
2026-10-23 13:34:21 -04:00
aiongg cf0f80604f Allow OFL typefaces, Noto CJK and Source Han included (AGENTS.md §1.1) (#3908)
* Allow OFL typefaces whoever designed them (AGENTS.md §1.1)

Noto CJK and Source Han from their open-source releases are allowed like any
other OFL font. The fonts build no longer skips them, the asset check accepts
an OFL font credited to Adobe, and the docs describe the Chinese faces that
result. The ban on Adobe icons, artwork, UI graphics and the fonts Adobe
products install stays.

* Leave out whole CJK faces other than the Japanese ones for size

A Chinese or Korean face is embedded whole only under 4 MiB, so the 16.4 MB
Noto Sans CJK SC in craft-fonts stays out of the binary and the embedded
total against the pinned craft-fonts is unchanged (25,204,828 bytes).

* docs: replacement boxes for Chinese without craft-fonts only on the web or without an installed Chinese face

* xtask assets: the OFL font exemption needs at least one licence ID

* fonts build: count a CJK script tag with a region (Hant-HK) as CJK, shared with the embedding test

* AGENTS.md: the asset gate exempts OFL fonts from the Adobe-author check

* fonts build: count a Hangul-only face (Hang) as CJK for the size rule

* fonts: the CJK embedding test checks the size rule, sharing its constants with build.rs

* docs: the missing Hans face is a property of the pinned craft-fonts, not of every build
2026-10-23 13:02:56 -04:00
aiongg 487907adfb fix(links): resolve remote document paths with craft-docpath's resolve_absolute (#3730)
* fix(links): use resolve_absolute for remote document paths

* fix(ui): refuse output file names that end in a dot or a space

* ci: check that git dependencies come from storytold

* ci: release only with git dependencies from storytold

* ci: check git dependency sources on a locked, parsed Cargo.lock

The git-sources step in ci.yml and release.yml fails when Cargo.lock is
missing, when .cargo/config.toml replaces or patches a source, when
Cargo.lock is out of date with the manifests (cargo metadata --locked),
and when any package source parsed from Cargo.lock as TOML is a git
source outside github.com/storytold/. A grep on Cargo's usual spelling
missed single-quoted strings and stale lockfiles.

* fix(links): a Windows path on Unix is refused, not reported as needing a source

resolve_remote_document_path reported MissingSourcePath for any path that
resolved but not to an absolute one. With an absolute source, such a path
is one this platform cannot open (C:\x.pdf on Unix): it is now
NotLocal(Invalid). A relative target from a missing or relative source
still gives MissingSourcePath.

* ci: check the legacy .cargo/config for source replacement too

Cargo reads .cargo/config as well as .cargo/config.toml, so the
git-sources step greps both; grep -s keeps a missing file from
failing the step.

* ci: parse cargo config for source replacement, patches and path overrides
2026-10-23 13:02:41 -04:00
aiongg 2cf91e3c09 Packaging: pin the signing client and appimagetool, and check their hashes (#3731)
* Packaging: pin the Windows signing client and check its hash

* Packaging: pin appimagetool and check its hash

* Packaging: fail clearly when no pinned appimagetool exists for the architecture

riscv64 has no pinned appimagetool, so building its AppImage without APPIMAGETOOL or a tool
on PATH stopped on an unbound APPIMAGETOOL_SHA256 after a download attempt.

* Packaging: pin the AppImage runtime and check its hash

appimagetool otherwise downloads the type2-runtime continuous build and embeds it unchecked.
The script fetches release 20251108 by version, checks its SHA-256 on download and on every
run, and passes it with --runtime-file. An architecture with no pinned runtime exits with a
clear message.
2026-10-23 13:00:09 -04:00
57e531ccca G14: Romanian: translate the 64 catalog rows still in English (#4016)
* G14: Romanian: translate the 65 catalog rows still in English

The Romanian catalog landed in main through the integration branch
(bf3a199f, PR #2505); the keys main gained since were filled with English.
This translates them: calculation order, the zoom box, rich-text formatting,
print options, Pages thumbnail size, Reshape comment, the CMS signed-attribute
and short-RSA-key warnings, and the page-numbering Start and page Subset
labels. Romanian translations main already has (the text field table) are
kept as they are. ATTRIBUTION.toml carries the new SHA-256.

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

* G14: Romanian: quote „data” in the CMS warning; Subset reads Selecție

Review of #4016: the content type "data" is quoted so it isn't read as "the
date", and the page Subset label and the booklet subset label both read
"Selecție" instead of "Filtru" / the anglicism "Subset broșură".
ATTRIBUTION.toml carries the new SHA-256.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:43:59 -04:00
f46268fbe4 M9: visible signatures read upright on rotated pages (#4067)
* M9: visible signatures read upright on rotated pages

A visible digital signature placed on a page with /Rotate was drawn in
user space, so its graphic and details turned with the page: on scans
stored landscape with /Rotate 270 the signature came out sideways.
Stamps (#1398) and Fill & Sign marks (#2392) were already corrected;
signatures were not.

A new signature widget on a turned page gets /MK /R equal to the page
rotation (inherited rotation included, via pdfcraft_model::pages), and
its appearance is laid out in the box as displayed (sides swapped for a
quarter turn) with a /Matrix from pdfcraft_model::view_matrix_for that
maps it exactly onto /Rect. Signing an existing field follows that
field's own /MK /R, so a field without one is drawn as before. Pages
that are not turned produce the same bytes as before.

Tests: sign crate (layout equals the upright box's, /MK /R, /Matrix,
inherited /Rotate, existing fields with and without /MK /R, malformed
/MK /R) and an engine render test on 0/90/180/270 pages with the
quadrant image. Both fail with the fix reverted.

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

* Keep one evidence/notes pair in the sign.digital parity entry

---------

Co-authored-by: vjeko2404 <vjekoslav@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:43:54 -04:00
aalbertiarchitecteandClaude Sonnet 5.5 9206254142 Open Save As and export dialogs in the document's own folder (#4012)
* Save As and exports open in the document's own folder

Save As, saving a document that needs a location, the Office and
comment/form data exports, Optimize and Sign now start the file dialog in
the folder of the open PDF instead of the system's default. A new
document and an email attachment's temporary copy keep the old behaviour.

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

* Match the full Outlook temporary-copy layout before skipping the document folder

Anchor is_mail_temp_copy to the cache layout (one random folder, then the file) and
to the Packages\Microsoft.OutlookForWindows_<id> folder, so ordinary paths that merely
contain those names keep the document's own folder. Regression cases added.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:43:20 -04:00
Ridwan 857c491360 fix(app): pdfcraft --help prints usage instead of opening the app (#4076) (#4093)
The desktop app's argument loop took `--help` as a view setting named
`help` (consuming the next argument as its value) and `-h` as a file to
open, so both started the window and a scripted call never returned.

`-h` and `--help` now print a short usage, which points scripts to
pdfcraft-cli, and exit 0 before any window or log file is created.
`--version` and `--help` write through a plain `writeln!` instead of
`println!`, which panicked (exit 101) when standard output was full or
closed, e.g. `pdfcraft --version > /dev/full`.

Release builds on Windows are GUI-subsystem programs, so, as with
`--version` already, the text shows when the output is redirected or
piped but not in a plain console window.

Fixes #4076
2026-10-23 10:43:14 -04:00
Jayesh Prajapati 70704ff260 M2: text extraction finishes a column before the next one across a paragraph gap (#3658) (#4214)
* M2: text extraction finishes a column before the next one across a paragraph gap (#3658)

The block order took the top-most unread block next, so after the first
paragraph of a left column it jumped to the top of the right column and
read the rest of the left column last. Now the nearest block below the
one just read, overlapping it horizontally and at most 2.5 heights of
its last line below it, comes next, unless an unread block sits above it
in its own span (a column above a full-width heading or footnote),
beside it on its left, or shares its first line (the first lines overlap
by at least 30% of the taller one: a table or form row, which still
reads across, also with labels and values in different sizes or on
shifted baselines; a line that only touches it or a small footnote
marker is not a row). Footers, footnotes and captions further away keep
their old place. Extraction, Find and copy share this order.

Known limit: when a paragraph in the next column starts on the same line
as the continuation (text on a baseline grid), it counts as a row and
the top-most rule applies, so the order of #3658 comes back there.
Telling prose from a row in that case needs more evidence.

Fixes #3658

* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385

The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.

(cherry picked from commit 1f8811761a)
2026-10-23 10:43:10 -04:00
Ahmad Mu 44e8eeb289 Cancel stale Organize page drags after document changes (#4039)
* M4: Cancel stale Organize page drags after document changes

* M4: Record Organize drag cancellation coverage
2026-10-23 10:43:05 -04:00
mtlcl410andClaude Opus 5.5 e6b9792539 M4: bookmarks show and set their title colour and style (ISO 32000-2 Tables 151, 152) (#4171)
* M4: the Bookmarks panel shows each bookmark's colour and style (#3193)

ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers
in the range 0.0 to 1.0, representing the components in the DeviceRGB
colour space of the colour that shall be used for the outline entry's
text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2)
(Table 152). Both were ignored: every title was drawn in the theme's
text colour and face.

The inspector (OutlineItem) and the bookmark list (organize::Bookmark,
used once a document is edited) now read them: each component is kept
within 0-1, and black (the default) or a /C that isn't three numbers
reads as no colour. The panel draws bold titles in the semibold face,
italic ones slanted, and the colour as given, lightened towards the text
colour only as far as it needs to read on the panel (3:1), so dark blue
stays visible in the dark theme. bookmark_list reports color, bold and
italic where they differ from the defaults.

Mutants, each failing a test: italic or bold bit ignored (both readers,
the panel), black kept as a colour, no clamping, a four-number /C
accepted, colour ignored or truncated in the panel, the contrast floor
removed, always jumping to the text colour. Checks for non-finite
numbers were equivalent (clamping already covers any size), so they
are gone.

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

* M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193)

#3193 asks for a way to mark bookmarks by colour, for example chapters
that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a
title colour (/C, Table 151, DeviceRGB, default black) and style flags
(/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous
commit shows them, and this one lets the user change them.

- organize: set_bookmark_color writes /C, removing it for black (the
  default) and refusing a component outside 0-1; set_bookmark_style
  sets or clears the bold and italic bits, keeps any bit the table
  doesn't define, and removes /F when no bit is left.
- engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under
  the same assemble permission as the other bookmark edits. Their undo
  labels are the menus' own names, already translated in every
  catalogue.
- ui-egui: the bookmark menu gains Style (Bold and Italic check boxes)
  and Colour (the comment swatch grid, with Other colour...). Only
  existing UI strings are used.
- automation: bookmark_set_style takes any of color ([r, g, b], 0-1),
  bold and italic, and changes only what it is given.

Mutants, each failing a test: no range check (library and tool), black
kept as /C, a bit not set, a bit not cleared, an untouched bit cleared,
/F 0 written, the bits swapped (library and menu), the tool accepting
nothing to change or ignoring bold/italic, the menu's colour ignored.

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

* M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193)

Review on #4171 (CodeRabbit). Given both, the tool applied two edits,
so undo took back only the style and left the colour, and a failing
second edit left the first applied. They now go in one Edit::Batch
named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS,
and "Style" BATCH_LABELS, so the history can translate them.

The test sets colour and style together and undoes once; with the two
edits applied separately it fails.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:59 -04:00
pat26-dotcomandClaude Opus 5.5 7322091d95 Accessibility: set a table's Summary and a header cell's Scope (#4103, #4104) (#4130)
* M12: set a table's Summary and a header cell's Scope (#4103, #4104)

accessibility_set_tag_property takes `summary` (Table tags) and `scope`
(TH tags: Row, Column or Both). Both are Table attributes, written into
the tag's attribute object with /O /Table in /A: an existing one is
edited in place, keeping other attribute objects and their revision
numbers; one reached by reference is copied into the tag's own /A first,
so tags sharing it keep their value; a new one is added when there is
none. Clearing drops the key, an attribute object left with only /O,
and an empty /A. The tag type is checked after the role map.

accessibility_tags lists both. Setting a summary clears the
table-summary finding.

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

* M12: edit the Table attribute object that holds the value (#4103, #4104)

With more than one /O /Table attribute object in a tag's /A, setting a
summary or scope edited the first one even when a later one held the
key, and clearing left that later value in place (both still read back
as the old value). Setting now edits the object holding the key (else
the first Table object, else a new one), and clearing removes the key
from every Table object holding it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:54 -04:00
PiohrunandClaude Opus 5.5 ba01244987 M6: calculations read their notation only from a comment, and a regex after a condition as a regex (#3993)
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex

Two findings from CodeRabbit's review of #3550, both reproduced first:

- parse_calculate and calculation_sites found BVCALC and EVCALC anywhere
  in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`:
  renaming `a` rewrote the string, and the form calculated `a` instead of
  running the script. The lexer now keeps the text of each comment, and
  the notation is read from the first comment that holds BVCALC and the
  EVCALC after it (notation_range), by both the Calculate tab and a
  rename. A line comment counts, and a comment that never ends runs to
  the end of the script, as before.
- A `/` after any `)` divided, so after the condition of `if`, `while`,
  `for` or `with` a regular expression was read as code: renaming `a`
  rewrote `if (flag) /getField("a")/.test(x)`, and in
  `if (flag) /"/.test(x)` the quote started a string that hid the
  script's real getField calls. The lexer now notes which `(` opened
  such a condition (not a method of that name, `o.if(x)`), and a `/`
  after its `)` starts a regular expression.

af::tests::notation_is_read_only_from_a_comment,
af::tests::a_regex_after_a_condition_is_not_read_as_code and
tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script
fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated.

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

* M6: form.props-calculate notes that a regex right after a brace is still read as code

From CodeRabbit's review of this PR. The lexer still divides after `}`,
which ends a block as often as an object literal, so in
`if (x) { } /"/.test(s); event.value = getField("a").value;` the quote
starts a string and the getField is hidden from a rename and from the
calculation order. Checked with a scratch test before writing it down.

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

* M6: a line comment ends at any line terminator, so code after `\r` is still read

From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`,
so with `\r` line endings it hid the rest of the script: a rename missed
the getField calls after it (since #3550), and a string after it that
spells BVCALC ... EVCALC read as the notation. A `/` taken for the start
of a regular expression likewise looked past `\r` for its end, and could
swallow the next line's code.

Both now stop at any of JavaScript's line terminators (`\n`, `\r`,
U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are
skipped rather than read as the start of a word.

af::tests::a_line_comment_ends_at_any_line_terminator fails without each
of the three changes: with `\r`, the string read as notation `a`; with
the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the
getField after it; without skipping U+2028, the getField after it was
not a call.

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

* M6: a word, number or regex's flags end at U+2028 and U+2029

From CodeRabbit's review of this PR. The scans for an identifier, a
number and a regular expression's flags took every byte past ASCII as
part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")`
the getField was part of one word and not a call: a rename missed it, and
the calculation order didn't read it.

`word` now takes a position and is false where line_ends_at is true, so
the three scans share one rule. The arm that skips U+2028 and U+2029
between tokens stays: without it, the punctuation arm would step one byte
into the character.

af::tests::a_token_ends_at_a_line_separator fails without the fix, for a
word, a number and a regex's flags alike; it and
a_line_comment_ends_at_any_line_terminator fail without the skipping arm.

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

* M6: the `while` that ends a `do` loop counts as a condition, said and tested

A `/` after the `)` of a do-while starts the next statement, so it starts a
regular expression there too. The comment on BEFORE_CONDITION says so, and
a_regex_after_a_condition_is_not_read_as_code covers it (from review).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:48 -04:00
Andrew GentryandClaude Opus 5.5 7169160df9 fix(ui): keep colour popups open when "+" opens the custom colour picker (#4066)
* fix(ui): keep colour popups open when "+" opens the custom colour picker

The colour popups used egui's default menu close behaviour (CloseOnClick),
which closes on any click, including the "+" swatch inside the popup. The
picker's open state was remembered, so it appeared only on the next open.

Close these popups only on clicks outside them: the quick bar's colour and
fill popups, the comment context menu's Colour submenu and Format text's
colour popup. Picking a swatch or pressing Apply still closes them.

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

* contributors: credit agent-reed by GitHub profile name

* test(ui): pin the fill popup and context-menu Colour submenu keeping their picker open

The quick bar's Fill colour popup and the comment context menu's Colour
submenu each get a kittest test: "+" shows the picker without closing the
popup, and choosing a colour still closes it. Both fail with the fix reverted.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:43 -04:00
Piyush Goel 30bafb4992 ui: add vertical spacing around panel header separators to prevent clipping (#4073) 2026-10-23 10:42:38 -04:00
mtlcl410andClaude Opus 5.5 ac41758cf3 M3: a Launch link that names a PDF opens it in PdfCraft (ISO 32000-2 §12.6.4.6) (#4185)
* M3: a Launch link that names a PDF opens it in PdfCraft (#4168)

Documents that cross-reference each other often use Launch actions to
open another PDF next to them (#4168: 268 links such as
..\se\se_ST.pdf). PdfCraft follows no Launch action, so clicking them
did nothing, while the same link written as a GoToR opens.

ISO 32000-2 §12.6.4.6: a Launch action "launches an application or
opens or prints a document". Starting programs is not something a
document should be able to do, but opening a PDF is what a remote
Go-To already does. So a Launch action whose /F names a .pdf file, and
that has no /Win launch parameters (application, operation,
arguments), is read as the remote Go-To to that file's first page
(LinkTarget::RemoteGoTo with launch: true). It then goes through the
same path as one: PDF only, --root confinement, confirmation for
absolute paths, opened as a document and never handed to the OS.
Windows writers separate such paths with \, where §7.11.2.1 uses /, so
a path without any / has its \ read as /. doc_info's link target says
"action": "Launch" or "GoToR".

Tests: launch_actions_that_name_a_pdf_open_it_like_a_remote_link
(relative with \, a file specification, an executable, /Win, a bare
.pdf, a mixed path, a URL file system) and
a_launch_link_to_a_pdf_opens_it_like_a_remote_link (link_open opens
the PDF in the root; a program link opens nothing). Mutants, each
failing: the /Win check dropped, \ always or never read as /, any
extension, a bare ".pdf", case-sensitive ".pdf", Launch not read.

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

* M3: a Launch link with a stray /D still opens its PDF (#4168)

Launch (ISO 32000-2 Table 207) has no /D, but launch_pdf went through
remote_go_to, which rejects the action when /D isn't a valid GoToR
destination. A Launch naming c.pdf with /D 0 stayed Other(Launch).
The /F parsing is now shared (action_file) and Launch ignores /D.

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-23 10:42:32 -04:00