crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows, arrays sized 28.
crates/ui-egui/tests/fonts.rs: both catalog lists.
ATTRIBUTION.md: both sides' rows; count regenerated after all merges.
ROADMAP.md: main's summary with Romanian added (15 more catalogs); both log lines.
docs/localization-parity.md: main's rows plus the ro row; 28 interface languages; both revision rows.
docs/localization.md: main's text with Română, the ro locales and code, and the PR's catalog-list edits.
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows, arrays sized 27.
crates/ui-egui/tests/fonts.rs: both catalog lists; Catalan added to main's comment.
ATTRIBUTION.toml/.md: both sides' assets; hashes and counts regenerated after all merges.
ROADMAP.md: both log lines.
docs/localization-parity.md: main's rows plus the ca row; 27 interface languages; both revision rows.
docs/localization.md: main's text with Català, the ca locales, code and catalog paragraph added.
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows (vi, fi, sq), arrays sized 25.
crates/ui-egui/tests/fonts.rs: both catalog lists; Albanian added to main's comment.
ATTRIBUTION.toml/.md: both sides' assets; hashes and counts regenerated after all merges.
ROADMAP.md: both log lines.
docs/localization-parity.md: main's rows plus the sq row; 25 interface languages; both revision rows.
docs/localization.md: main's text with Shqip, the sq locales and code added.
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: summary names Vietnamese, Brazilian Portuguese and Spanish against the current union; both log lines.
docs/localization-parity.md: main's status line with the Spanish change added; the PR's denominator paragraph naming Brazilian Portuguese too; the PR's lt row plus main's fi row; revision rows newest first.
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: both progress-log lines, newest first.
docs/localization-parity.md: the PR's status line; both revision rows, newest first.
docs/roadmap.md: the PR's rows (main's rows plus the PR's wording).
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: both progress-log lines, newest first.
crates/ui-egui/src/i18n/cs.tsv: both sides' entries, no duplicate keys.
docs/localization.md, docs/roadmap.md: main's single paragraph and row, with the PR's Czech wording applied.
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: Prepare a form ▸ Calculation order: a page to reorder, check and fix the order fields calculate in
The calculated fields in the order they calculate (/CO), each with its
formula and the value it shows, moved with Up/Down arrows (as Acrobat's Set
Field Calculation Order), Alt+Up/Down or by dragging. A card says which
fields read one calculated later, which read each other in a loop and which
/CO leaves out, and holds Fix order; a loop's card opens one of its
formulas. On the page, arrows show what the field in focus reads and what
reads it, with each linked field's step. The order is a page of the Prepare
a form panel, opened from its Fields tools, with a back arrow to Prepare a
form; opening it ends Preview and closes the page grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form.calculation_order runs form_calculation_order headlessly
The command had no tool linked, so command_list showed none for it and
command_run refused it. form_calculation_order is now its tool: run without
a change, it reads the order.
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>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
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>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
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>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* fix(render): read each object stream's index once (vendored hayro-syntax patch 22)
`XRef::get_with` parsed an object stream's whole index of `objnum offset`
pairs again for every object it fetched from that stream, so reading all of a
stream of N objects took N² steps. Firefox keeps every object of a page in one
stream: the pdf.js corpus file bug1978317.pdf (65,542 objects in one stream)
took 37 s to draw and 40 s to extract its text, and was a known timeout in the
robustness baseline.
The parsed index is now kept per stream, beside the decoded stream data it
indexes (`ObjectStreamIndexes`), and never cleared, as that data isn't. A
malformed index is remembered as malformed; `/N` or `/First` that can't be
read (a reference that can't be followed while another stream loads) is not
remembered. An offset past `usize` is malformed instead of wrapping. A poisoned
lock is still used: entries are inserted whole.
Release build, Linux: bug1978317.pdf draws in 0.5 s (was 37 s) and extracts
its text in 1.9 s (was 40 s), and leaves the baseline's known timeouts;
40,000 pages in one stream open in 0.2 s (was 54 s).
`raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`
times out at 20 s with the fix reverted and passes in 0.2 s with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* test(render): an object stream offset past usize is malformed, not a panic
Review follow-up on #2427. Mutation testing by mtlcl410 found that replacing
patch (22)'s `checked_add` in `read_object_stream_index` with a plain `+`
survived every test, although in a debug build that `+` panics on the very
input it guards against (and wraps in a release build). Their test pins it:
it fails with "attempt to add with overflow" under that mutant and passes on
this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): PNG export writes opaque pages as RGB at deflate level 3
`encode_png` wrote every raster as RGBA at the png crate's default level
(zlib level 6). A rendered page is always opaque, so the alpha channel only
made the file bigger and slower to write. An opaque raster is now written as
RGB, at deflate level 3 with adaptive filtering; a raster with translucent
pixels is still written as RGBA with straight alpha.
The files come out the same size, 3-5x faster (release build, Linux):
300-dpi scan drawn at 220 dpi 805 -> 167 ms 4.46 -> 4.47 MB
text page at 220 dpi 70 -> 40 ms 0.85 -> 0.89 MB
annual-report page at 150 dpi 179 -> 64 ms 1.41 -> 1.38 MB
This is the encoder behind Export > Image (PNG), `pdfcraft-cli render` and
Print as Image; the automation tools (`page_render` and the other page
previews, MCP included) had a copy of it and now call it instead, so png is
only a dev-dependency of pdfcraft-automation. Two automation tests that read
`page_render` output as RGBA now accept the RGB a page renders as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* fix(engine): PNG export rejects a raster of the wrong length
encode_png now checks that the buffer is exactly width × height × 4 bytes
(checked arithmetic) before choosing RGB or RGBA. The opaque path's
as_chunks::<4>() dropped a partial trailing pixel, so encode_png(1, 1,
&[10, 20, 30, 255, 0]) wrote a valid 1×1 PNG where the old RGBA encoder
returned an error.
Regression test export::png_tests::rasters_of_the_wrong_length_are_rejected
covers opaque and translucent buffers with a partial pixel, a pixel short,
a pixel too many, an empty buffer and a size whose byte count overflows; it
fails on the previous head at the reviewer's case.
Review: https://github.com/storytold/pdfcraft/pull/2425#discussion_r4281603405
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adds a Finnish (Suomi, `fi`) catalog with 2,592 entries: every key of
the other bundled catalogs, written in sentence case from the English
labels. Finnish uses the one/other plural rule with the partitive
singular after any number but 1 (1 sivu, 2 sivua, 0 sivua).
Finnish is also a date language: calendar headings use the nominative
month (Lokakuu 2026) and dates the partitive (14. lokakuuta 2026).
Any fi-* locale selects it.
Tests cover registration, locale tags, persistence, plurals, calendar
wording, commands/tools, scanned UI literals, every bundled catalog key
and the Latin UI fonts. The catalog has its ATTRIBUTION.toml entry
(ATTRIBUTION.md regenerated with `cargo xtask assets --write`).
Clean-room, AI-assisted (Claude Code) translation; not yet proofread
line by line by a native speaker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cargo xtask assets` pins a SHA-256 for every catalog, so editing `es.tsv` left its
ATTRIBUTION.toml entry stale and `assets::tests::the_real_manifest_passes` failed on both
runners. Re-verified the file's provenance, updated the hash, recorded that the coverage
additions are AI-assisted and not separately native-reviewed -- as #2510 did for `lt` and
`vi` -- and regenerated ATTRIBUTION.md.
Review notes on docs/localization-parity.md:
- Lithuanian's `2,585 entries` was measured on 2026-10-13 and read as if `lt` were behind.
Re-measured today it covers the whole 2,674-key union, so it is restated like `es` and `vi`.
- *How this was measured* now says outright that the union is a moving target: every change
that adds a string widens it, a count only compares with others of the same date, and a
catalog at 100% falls behind without losing a translation. Earlier revisions quote the
union of their own date, not this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
es.tsv was missing 143 of the 2,674 (context, English) keys the other
bundled catalogs carry, so those strings fell back to English inside an
otherwise Spanish interface: every calendar month and weekday, the whole
certificate context of the signature inspector, the X.509 key usages, the
signature-revision and digital-ID lines, password validation, eight plural
messages, the combine-from-folder strings, the rendering and stale-file
notices and the tooltips that spell their shortcut out.
Spanish now covers the whole union, as lt and vi do: 2,531 -> 2,674 of
2,674, measured as docs/localization-parity.md describes. Counts there and
in ROADMAP.md re-measured against the current union; vi is restated at
2,674 for the same reason, still 100%.
Translations reuse the vocabulary already in the file (ID digital, Acercar
/ alejar, Portapapeles, A doble cara, Cortar y apilar) and were written
from the English labels, not from another implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* M5.6: preserve nested annotation values in FDF exports
Copy and renumber retained object graphs without expanding aliases or following
indirect cycles recursively. Refuse excessive work and propagate export errors
before the destination is written. Cover saved geometry, colour and borders,
shared graphs, streams, stale references and refusal through automation tools.
Co-Authored-By: Codex <noreply@openai.com>
* Bound FDF annotation copying before allocation
Keep selected aliases shared, budget retained keys and values before copying, and add direct-payload and alias-count regressions. Retain copied-graph limits and document source-memory scope.
* M5.6: bound FDF cache-eviction discovery and field values
Extend the concurrent annotation-budget correction while retaining its direct-payload and alias regressions. Record annotation positions instead of resolved bodies so cache eviction cannot pin repeated parsed copies; borrow direct values and budget retained entries before cloning. Count every emitted multi-select field value before allocating its output array.
Observed pre-fix allocation-cap and field-limit failures, then fixed tests passed. Final validation: 549 scoped XFDF/engine/automation tests passed, three existing ignored; strict all-targets Clippy, WASM, fmt, layers, assets, parity and diff checks passed.
AI assistance: OpenAI Codex for @MMexicann.
* M5.6: record bounded FDF export regression evidence
Record comment cache-eviction discovery and emitted-field-value budget regressions without changing feature scores or status. Describe copied-graph limits and existing source-parser limits precisely. Final 549 scoped tests, strict Clippy, WASM, fmt, layers, assets and parity passed.
AI assistance: OpenAI Codex for @MMexicann.
* M5.6: align FDF progress with budgeted discovery
Document lazy annotation positions, cache-eviction coverage and copied-graph limits while retaining the original nested-value progress entry and all upstream notes. Estimates unchanged.
AI assistance: OpenAI Codex for @MMexicann.
* M5.6: describe FDF discovery and copied-graph limits
Align exchange documentation with the tested lazy-position discovery implementation and its copied-graph limits. Source parsing/page discovery keep existing limits.
AI assistance: OpenAI Codex for @MMexicann.
* M5.6: align milestone notes with FDF discovery limits
Retain upstream milestone history and describe the tested discovery/copy budgets without increasing estimates or claiming a total source-memory bound.
AI assistance: OpenAI Codex for @MMexicann.
* M5.6: keep nested page links out of FDF exports
Preserve graph aliases and existing budgets; exclude direct and indirect Page/Pages type names. Add saved-PDF regression and bounded type-resolution controls. Both pre-fix failures observed; 46 scoped tests, strict Clippy, WASM and formatting passed.
* M5.6: document bounded nested page exclusion
Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.
* M5.6: record nested page-reference regression evidence
Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.
* M5.6: retain FDF limits in milestone evidence
Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.
* M5.6: cite nested page-reference exclusion tests
Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.
* M5.6: respect generations while identifying page references
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* XFDF: document stale Type preservation regression
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M5.6: record generation-aware page exclusion evidence
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M5.6: propagate FDF export errors in usage example
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
* M5.6: consolidate roadmap change and target fields
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
* M10: Office export reads forms as far as the renderer and says where it stops (#3120)
Export to Word, PowerPoint, HTML and RTF stopped at 12 nested forms and
4 096 forms a page and left the rest out silently. It now follows forms
50 deep, as the renderer does, and up to 50 000 a page (the renderer's
budget for nested paints), still bounded on hostile files.
doc_export_office lists pages past those limits in truncated_pages, with
a warning. New: reading_blocks_checked and export_office_checked;
reading_blocks and export_office keep their signatures.
Fixes#3120
Co-Authored-By: Claude <noreply@anthropic.com>
* Tests: pin the form depth limit to the renderer's 50 (#3120)
The depth test built its chain from MAX_FORM_DEPTH, so it moved with the
constant. Write the renderer's limit out: depth 50 is read, depth 51 is
left out and reported. Fails with the constant at 49 and at 51.
Co-Authored-By: Claude <noreply@anthropic.com>
* Export: say which forms count toward the page limit (#3120)
The page budget also counts forms the page draws itself, which the
renderer does not charge, so a page can be reported as cut although the
viewer shows it. The warning and the tool description now say so.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Text: tracked glyphs retain their word boundaries
* M7: cap inferred tracking at half an em so one-glyph words and cells keep their spaces
Adds the regression test from the #3477 review (fails without the cap).
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>
* M14: keep long menus usable in short windows
* Menus: scroll each top-level menu once, from its caller, so View's list isn't nested in a second scroll area
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>
Czech had every `tl!` literal but lacked ~220 keys the other catalogs
carry: the sentences the engine words (signature details, accessibility
findings and report, PDF/A problems, pages to print, history labels),
the date picker's weekday heads, the certificate viewer's fields and
shared buttons. All are now translated (2,674 entries).
"Tag" is "tag" throughout, as the catalog's own glossary says: the Tags
panel said "značky", which also names redaction marks, and "{n} tags"
read "1 značek".
Two new tests pin the engine wordings (with their holes), weekdays and
certificate fields; both fail on the previous catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: Ctrl-drag copies pages in the Organize grid (#2997)
Dragging thumbnails in the Organize page grid with Ctrl held (Option on
macOS) drops copies at the gap instead of moving the pages: a "+" badge
by the pointer, the copies land at the gap selected, and one undo step
takes them out. The insertion point joins the engine edit
(Edit::DuplicatePages { at }) so the copy is one undo step rather than a
duplicate plus a move.
page_duplicate takes a 1-based "at" (the first copy becomes page "at"),
so agents can drive the same thing, and the UI control channel's
ui.drag modifiers now reach the input state -- which gestures that read
it (this copy-drag, and Alt-drag column select) need.
Closes#2997.
* fix(ui): take the grid's copy modifier from the runtime OS (#3092 review)
Option-drag in the Organize grid moved the pages instead of copying them in a Mac
browser: `cfg!(target_os = "macos")` is false in the wasm build, and eframe sets the
context's OS from the browser. `copy_modifier` now takes the context and asks
`commands::mac_shortcuts`, as the shortcut labels do.
The drag helpers in `tests/editing.rs` and `tests/control.rs` picked the modifier the
same compile-time way, so they would have sent Ctrl on a Mac host; both read the
context's OS now. The new regression test
`option_copies_and_ctrl_moves_when_the_context_reports_macos` drives a Mac context —
Ctrl moves, Option copies — and fails without this change.
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
Fixes#1645. For a dynamic XFA form, `info` reported the placeholder
page and field counts, and `text` and `render` read the placeholder
pages, while the app and the automation tools lay the template out.
The CLI now opens dynamic XFA forms through the engine session and uses
its laid-out document for page and field metadata, text and pixels.
`info` keeps the input's file size and also reports the original counts
as `pdf_pages` and `acroform_fields`; `text` checks the copy permission
before extracting. The input file is never written.
Checks run (rustc 1.99.0): cargo fmt --check; cargo clippy -p
pdfcraft-cli --all-targets -- -D warnings; cargo test -p pdfcraft-cli;
cargo xtask layers; cargo xtask assets.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
In a region that mixes sizes (a 24 pt title over 12 pt lines), the
inline editor's caret on a small row was as tall as the region's first
size and reached into the row below. egui draws the caret at least one
row of the TextEdit's font tall, and that font had the paragraph's first
size. Each row already carries its source row's height, so the
TextEdit's font is now half the region's smallest size and only sets
that floor.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Clamp the surface device descriptor to the adapter's capabilities while
preserving its maximum 2D texture extent. Cover V3D shader variables,
lower GL capabilities, alignment ordering and capable-device defaults.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Add a clean-room Romanian translation of the UI catalog (2,670 entries,
every bundled key), the CLDR plural rule for Romanian (one / few /
other, "20 de pagini"), the ATTRIBUTION.toml entry, coverage and plural
tests, and the localization docs and roadmap entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk
crates/xfdf/README.md, parity/acrobat-features.toml: next's text, which has the same sentences without the formula-cell one the #2688 integration fix removed.
crates/xfdf/src/tests.rs: the XML comments test once, where next has it.
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
ROADMAP.md, docs/roadmap.md: union of both sides' log lines and milestone rows, each line once.
parity/acrobat-features.toml, crates/xfdf/README.md: next's text, a superset of main's (#1917, #2686 plus #2688).
crates/edit/src/tests.rs: both sides' new tests (#2558 and #3084).
crates/ui-egui/src/editing.rs: the picked picture is cleared on every edit, and a new drawing commits its raster (#3149).
Moving an image whose crop is its own (`q re W n cm Do Q`) as part of a
group put a fresh `cm` before the clip path on every move, so twenty
moves left twenty `cm` operators in front of one `re`. It never nested
deeper, but the content stream grew with every drag.
When a `cm` already sits right before that clip (one an earlier move
added, or the file's own), `translate_images` now replaces it with the
product of the new and old matrices. The CTM at the clip is
`c · prev · outer` either way (ISO 32000-1 8.3.4, 8.4.4). A previous
`cm` that is not finite, or a product larger than 1e12, keeps the old
behaviour and inserts a separate `cm`.
The new test moves an image cropped smaller than itself twenty times and
checks that the image and its visible crop both moved by the full offset,
that the operator count stays at its value after the first move, and that
the caption after it is untouched. It fails without this change
(32 operators against 13).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>