Commit Graph
2577 Commits
Author SHA1 Message Date
aiongg bfe58a89ef Integration: #4266 char_spacing in #4230's AddedText literal 2026-10-24 06:25:59 +00:00
aiongg 880723e3af Merge PR #4266: M7: added text takes character spacing from Format text
crates/engine/src/lib.rs: union of the added:: re-exports (paste_place/paste_rect from #4230, CHAR_SPACING_LIMIT).
crates/ui-egui/src/content_ui.rs: editor keeps #4178's anchored width and horizontal_align and #4266's spacing layouter; spaced_job takes the alignment so the custom layouter keeps it.
crates/ui-egui/tests/it/content.rs: kept both tests.
2026-10-24 06:24:10 +00:00
aiongg f71a305915 Merge PR #4230: M7: paste added text boxes and images in the engine and through content_copy (#2636)
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
2026-10-24 06:23:24 +00:00
aiongg 8a64b65e4f Merge PR #4330: M8: redact labelled Italian international slash numbers
ROADMAP.md: kept both progress-log lines, newest first by position (HEAD's #4280 line, then this one).
2026-10-24 06:23:03 +00:00
aiongg 8dda3d50b9 Merge PR #4316: M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280) 2026-10-24 06:22:51 +00:00
aiongg f2c133b987 Merge PR #4258: M7: a batch of content edits refreshes the added content and page marks 2026-10-24 06:22:48 +00:00
aiongg 5cb74eeb69 Merge PR #4217: M7: edited text keeps every space typed (#4199, #3797)
crates/edit/src/tests.rs: both sides appended tests at the end; kept both.
Cargo.lock: unchanged from base (the PR's ctutils bump did not apply).
2026-10-24 06:22:45 +00:00
aiongg b9f5ef6662 Merge PR #4198: fix(audit): covered_text reports the words under the cover, not the whole line 2026-10-24 06:22:30 +00:00
aiongg a84a255e4e Integration: #4178 doc comment back above mode_tab 2026-10-24 06:22:30 +00:00
aiongg 321d59aec8 Merge PR #4178: fix(ui): the added-text editor keeps the box's alignment 2026-10-24 06:22:21 +00:00
aiongg f9f08017f0 Merge PR #4111: fix(edit): Delete removes the selected paragraph or objects in Edit text
crates/ui-egui/src/object_ui.rs: main removed the tests module's translation test; kept the PR's two
new delete tests and dropped the translation test.
2026-10-24 06:22:17 +00:00
aiongg 44831a6047 Merge PR #3670: Redaction: follow nested forms as deep as the readers do
crates/redact/src/interp.rs: kept both sides: main's per-page DecodeBudget and the PR's
visits counter (MAX_FORM_VISITS, same 50,000 as MAX_PAGE_FORMS; the visit check fires first).
2026-10-24 06:22:04 +00:00
aiongg 7c005aed5b Merge PR #3551: Edit text: keep Tr, stroke, line width and gs set inside BT when rewriting a paragraph (#3508, #3509) 2026-10-24 06:21:29 +00:00
aiongg d06bc3cb7c Merge PR #3546: Save commits the open Edit Text boxes before writing the file (#3496) 2026-10-24 06:21:25 +00:00
aiongg 6672e103a5 Merge PR #3009: fix(ui): the header, footer and watermark previews show the page under the marks (#2837) 2026-10-24 06:21:11 +00: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
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
nlevaandOpus 5.5 medium ultracode 5515f321c8 M7: copy added JPEG 2000 images as their file (#2636)
Review: copying an added JPEG 2000 image failed even within its own
document, because the exporter can't write JPEG 2000 yet. Add Image stores
such a picture as the file it was given, so that file is the copy: it
pastes into the same document as the same object and into another as one
embedded image. The notes now also say that a CMYK picture stored
losslessly turns to RGB in another document.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-24 01:17:34 +02:00
nlevaandOpus 5.5 medium ultracode 302ad68502 M7: paste added text boxes and images in the engine and through content_copy (#2636)
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.

pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.

The content_copy tool drives it headlessly, across pages and documents.

The desktop keys and menus come in follow-up PRs.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-24 01:17:34 +02:00
Kyle Kassen e373dffa20 M8: redact labelled Italian international slash numbers 2026-10-23 16:37:10 -05: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
Lcoding15 a4c5e5f9ee M7: the stand-in note names every standard family it can use (#4280)
CodeRabbit on #4316: the edit.font-reuse-substitution note said Helvetica
stands in for missing glyphs, but the face follows the source font's
family (Helvetica, Times or Courier) and its weight and slant.
2026-10-23 22:46:57 +02:00
Lcoding15 0b3b243e9a M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280)
Edit text set a paragraph whose font lacks a typed character (an accent
missing from a subset) in a standard font, always under the resource
name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph
after a regular one replaced PCEdHelv with Times-Bold, so every
paragraph set in the earlier stand-in turned bold, and the editor's
draft measurement saw those paragraphs change and grew its frame over
them.

The stand-in is now named after its face (PCEdTimesBold, ...), as the
line and region rewrites already name theirs.

Fixes #4280
2026-10-23 22:46:56 +02: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
Guillem Hermida 7668a2c378 tests: each forms test saves into a folder of its own
tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value (its
same-frame case) and typing_and_saving_in_the_same_frame_saves_the_last_keystrokes
both asked scratch() for "same-frame". The tests of the it binary share one
process id and run in parallel, so they got the same folder, and one could
clear it while the other was saving into it: the save then failed to read
back with NotFound. scratch() now adds a number taken per call.
2026-10-23 21:50:24 +02:00
Guillem Hermida bf81191122 tests: the pending-open control test waits for the worker by time, not by 30 frames 2026-10-23 21:50:24 +02: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
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
Marco Arnulfo 5af594e7df Added text editor: wrap width gives up the trailing character spacing (#4245)
The page counts the spacing after every character, the last of a line
included; epaint only puts it between characters, so a line could fit in
the editor and wrap in the saved text.
2026-10-23 19:27:10 +02: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
Marco Arnulfo cd6f031b82 M7: added text takes character spacing from Format text (fixes #4245) 2026-10-23 17:31:02 +02:00
nlevaandOpus 5.5 medium ultracode d8aff8bfc7 M7: a batch of content edits refreshes the added content and page marks
A content-only edit re-reads Document::added() and marks() only when the
edit itself is one that changes them. Edit::Batch has Scope::Content when
all its parts do, but content_refresh_lists never looked inside it, so a
batch of AddText/DeleteContent or AddWatermark edits left both lists stale
until the next full refresh or undo. It now answers for its parts.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-23 17:09:29 +02: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