Commit Graph
238 Commits
Author SHA1 Message Date
bflatastic eb47b57b4f Merge #6610: M6: a field whose normal appearance gives no stream is drawn from its value, /MK and /BS (view.render-annotation-appearances)
# Conflicts:
#	parity/acrobat-features.toml
2026-11-09 08:19:52 +07:00
bflatastic 5e1d511216 Merge #6570: M6: a named kid is named up its own /Parent chain, and a radio option of any same-named field is chosen by the name (form.field-list)
# Conflicts:
#	crates/forms/src/read.rs
#	crates/render/src/inspect/annotations.rs
#	crates/render/src/inspect/tests.rs
#	parity/acrobat-features.toml
2026-11-09 08:19:22 +07:00
mtlcl410 d65ae4268e M6: a calculate script that sets its own field through the field object keeps that value (form.custom-scripts) (#6616) 2026-11-09 08:59:19 +08:00
mtlcl410 b9ffb68b12 M6: a product of empty fields is 0, not 1 (form.af-simple-calculate) (#6567) 2026-11-09 08:57:28 +08:00
mtlcl410 e83c200598 M6: a field whose /Kids loop back or name a root field keeps its other kids (form.field-list) (#6446) 2026-11-09 08:50:11 +08:00
mtlcl410 608ff62dae M6: a widget whose /Parent names a missing object is a field of its own (form.field-list) (#6445) 2026-11-09 08:49:56 +08:00
Harrison Booth 3b731f9458 Expose widget rectangles to field scripts (#6093) 2026-11-09 08:42:39 +08:00
b07a84f003 M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances) (#6503)
* M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances)

ISO 32000-2 Table 166 /C (and /IC, Tables 178, 180, 181; /MK /BC and /BG,
Table 192) is an array of 0, 1, 3 or 4 numbers whose length picks the
colour space. A four-entry array with a non-number in it, such as
[1 0 0 null] (PDFBOX-2699), made the /C parser give up, so a shape
without an appearance stream was not drawn at all. PDFium, MuPDF and
Poppler all read such an array as CMYK with the non-number as 0.

- annot: a four-entry /C or /IC reads non-numbers as 0; other lengths
  still need every entry to be a number.
- annot: the properties' fill uses the same parser (it read
  [r g b null] as RGB before), and /C no longer drops a non-number
  to make three numbers.
- forms: /MK /BC and /BG read a four-entry array the same way instead
  of dropping the non-number and drawing RGB.

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

* M5: comment properties report the stroke colour as drawn; a reference among four colour entries is not read as 0 (view.render-annotation-appearances)

props() reads a non-FreeText /C through the appearance parser, so a
[1 0 0 null] stroke reports CMYK cyan as drawn, and grey and CMYK
strokes report their converted colour. A reference among the four
entries of a /C array (ISO 32000-2 7.3.10) is left unreadable instead
of standing in as 0, so /C [1 0 0 6 0 R] is never drawn cyan.

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

* M5: re-run CI (fill_sign Escape test depends on the clock's second, passes locally)

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

* M5: the comment list reports a comment's colour as drawn (view.render-annotation-appearances)

summaries() accepted only a three-entry /C, so a Square with /C [1 0 0 null],
drawn cyan and reported cyan by props(), had no colour in the comment list;
grey and CMYK colours were dropped the same way. Non-FreeText comments now
read /C through drawn_color (the array itself followed when it is given by
reference), keeping the three-number fallback and the FreeText /DA handling.

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

* M5: the render inspector lists a comment's colour as drawn (view.render-annotation-appearances)

The inspector accepted only a three-entry /C, with a referenced entry read
as 0, so a /C [1 0 0 null], a grey or a CMYK colour listed none, unlike the
comment list and the drawing. Non-FreeText comments now read /C by the
appearance builder's rule (one entry grey, four CMYK with a non-number as 0
and a reference unreadable, finite and clamped), with the three-number
fallback reading referenced entries.

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

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:29:31 +08:00
BFlat 43f24aa169 Draw Thai in Fill & Sign, text boxes, form fields, headers, footers and watermarks (#6632)
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
2026-11-09 02:56:34 +08:00
mtlcl410andClaude Opus 5.5 ccc2cd0dee M6: a field whose normal appearance gives no stream is drawn from its value, /MK and /BS (view.render-annotation-appearances)
A widget's /AP /N that is neither a stream nor a state subdictionary with
a stream for /AS draws nothing (ISO 32000-2 12.5.5, Table 170), yet the
display copy treated any /N as the file's own appearance. Text, choice and
push-button widgets whose /N is null, a form XObject dictionary without its
stream data, or a subdictionary without /AS now get the drawn appearance a
widget without /N gets, and check boxes and radio buttons whose state entry
is not a stream are drawn from /MK. The test is the same one the renderer
uses to pick the stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 17:29:41 +08:00
mtlcl410andClaude Opus 5.5 8bb2d920a0 M6: a /Parent given through an object that is only a reference is followed in the field name (form.field-list)
qualified_name reads each ancestor through Document::resolve, which follows an object that is
nothing but a reference to another, as document inspection does, so both name the kid the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 12:49:40 +08:00
mtlcl410andClaude Opus 5.5 eee81da38b M6: a radio choice by the name is refused when any radio field of it is read-only, a claimed widget is read once, and inspection lists widgets beside named kids (form.field-list)
- fill: the first same-named field's read-only flag still applies when a later one draws the
  choice, and no radio field of the name is written when any of them is read-only.
- enumerate: a nameless /Fields widget its parent already claimed is not read again as a nameless
  field; one its parent's /Kids omits is still read on its own.
- walk: only kids whose /Subtype is /Widget replace a merged field's own widget.
- inspection: a kid whose /Parent is another field is named from that chain even under a direct
  listing field, and widget annotations beside named kids are listed as a field of the parent's
  name (ISO 32000-2 §12.7.4), as the form reader lists them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 11:21:59 +08:00
mtlcl410andClaude Opus 5.5 916ca60e80 M6: a field merged with a widget reads its widget kids as its buttons, and nameless widgets of an unlisted field are options of its name (form.field-list)
A field dictionary that also carries a zero-area widget and lists widget
kids now has the kids as its buttons (ISO 32000-2 §12.7.4: field and
widget merge only for a single widget). Widget annotations listed beside
named kids, and nameless /Fields entries whose /Parent /Fields doesn't
list, are read as buttons of that parent's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 06:33:46 +08:00
mtlcl410andClaude Opus 5.5 0ddcc60015 M6: every field sharing a radio group's name takes the chosen /V (form.field-list)
ISO 32000-2 §12.7.4.2: fields with the same fully qualified name have
the same value. Choosing a radio option by the name now writes the
choice as /V of a same-named twin that draws no button for it (its
buttons stay off), and of a field whose /Kids lists them without being
their /Parent, so it shares their name (issue15096.pdf's group, whose
/V was a stale Choice2). Exactly one button stays drawn on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 01:52:28 +08:00
mtlcl410andClaude Opus 5.5 401a64fd60 M6: a radio choice by the name leaves one button on in kids without /P saved both on (form.field-list)
Adds the layout issue15096.pdf's last revision has: the two same-named
radio kids carry no /P (their page comes from /Annots), and both are
drawn on with /V that disagree (Off with /AS /Choice1, Choice2 with
/AS /Choice2). Choosing Choice1, Choice2 and Choice1 again by the name
leaves exactly the chosen button on after each save and reopen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 01:43:01 +08:00
mtlcl410andClaude Opus 5.5 68dff686fe M6: a named kid is named up its own /Parent chain, and a radio option of any same-named field is chosen by the name (form.field-list)
A field's fully qualified name is built from the partial names of the field
and its ancestors, and a child names its parent with /Parent (ISO 32000-2
§12.7.4.2, Table 226). A /Kids entry with /T whose /Parent does not name the
field listing it is now named up its own /Parent chain, in the form reader
and in document inspection, as PDFium and MuPDF name it: pdf.js's
issue15096.pdf lists its two radio fields as Group1, not Group1.Group1.

Fields with the same fully qualified name are one field (§12.7.4.2), so a
radio option that a later one of them draws is now chosen by the name: it is
written through the first field that draws it and the others turn off.
Before, only the first field's options were accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 23:54:22 +08:00
6427357fcb Forms: date fields name months in the date language, not always English (#5303) (#5464)
* Forms: date fields name months in the date language, not always English (#5303)

`AFDate_FormatEx("dd-mmm-yy")` wrote "18-Oct-26" in an Italian interface,
where a localized Acrobat writes "18-ott-26". Form date formats,
`util.printd` and `util.scand` now take their month and weekday names
from the date language (Preferences > Date format > Language, which
follows the interface language until one is picked), and still read
English names typed in. The names are process-wide, so their test runs
in its own binary.

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

* Forms: dates read back in every date language (#5303)

Trying every date language showed what a field wrote not always reading
back: names holding digits ("10月"), spaces ("tháng 10"), dots ("ม.ค.")
or combining vowel signs (Thai, Hindi, Telugu) fell apart word by word,
and Italian, Spanish, Romanian and Albanian share some short weekday and
month names ("mar"). The language's names are now found whole in the
text first, the longest at each place, and read as their English names;
the format's order of `mmm` and `ddd` settles a shared name. Both
AFDate parsing and util.scand do this, on text up to 1 KB. A test reads
back every language, each name format, and hostile text.

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

* Forms: date names no field font can draw are drawn in English (#5303)

With the names in the date language, a Russian dd-mmm-yy field drawn in
Helvetica showed "18-????.-26": no /DR font held "окт." and the WinAnsi
stand-in drew each letter as ?. The same held for Japanese, Chinese,
Greek, Arabic, Thai and others, and for Latin names outside WinAnsi
(Czech "říj"). Before, the names were English and always drew. An
AFDate/AFTime field now formats its value again with English names for
a widget whose fonts can't draw the localized text; a custom Format
script is run again with util.printd writing English names for such a
widget, and its result kept only when it draws whole. A field whose
font holds the names keeps them, and scripts otherwise still print the
date language.

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

* Forms: an English weekday before the month keeps the month (#5303)

With Italian dates, "Tue 17 Mar 2026" in a "ddd d mmm yyyy" field read
as no date: the language's names were found in the text, but not
English ones, so "Tue" left the format's weekday unfilled and "Mar",
which Italian shares between Tuesday and March, was taken as the
weekday. Both AFDate parsing and util.scand lost the month, and
Spanish, Romanian and Albanian ("mar") did the same. English names are
now found too and fill their field of the format as localized ones do.
A test reads English and mixed weekdays before the month in those
languages.

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

* Forms: an English date run is drawn even when it prints the value (#5303)

A Helvetica field storing "18-Oct-26" whose Format script prints it
again as dd-mmm-yy still showed "18-???.-26" in Russian. The English
run printed "18-Oct-26", but it was dropped for equalling the stored
value, which was how a failed run was told apart: a failing script
handed back the value unchanged. The English run now says whether it
ran (`None` when the script failed or the runner can't write English
dates), and what it made is drawn whenever it draws whole. A test
reprints the stored value under Russian, and checks that a failing
script keeps its text and is reported undrawn.

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

* Forms: a short weekday typed without its dot is the weekday (#5303)

With French dates, "mar 6 janv 2026" in a "ddd d mmm yyyy" field read
as no date through util.scand, and "mar 06/01/2026" in "ddd dd/mm/yyyy"
through AFDate too. French writes Tuesday as "mar.", which a name
search only found with its dot; since English names are found as well,
the English "Mar" (March) was the only name of that length, so the
weekday became a month. Romanian "mar" did the same. A short name
ending in a dot is now also found typed without it, as the same kind of
name, so the format's order of name fields decides between it and an
equal name of the other kind. Both copies of the name search change. A
test reads French and Romanian weekdays typed without their dot.

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

* Forms: a shared name is the month when no weekday is typed (#5303)

With Italian dates, "17 mar 2026" in a "ddd d mmm yyyy" field read as
no date, nor did "mar 17, 2026" in "dddd, mmmm d, yyyy". AFDate leaves
weekdays out of what it reads (they are shown, not typed), but the name
search still expected the format's weekday first, so "mar", which
Italian shares between Tuesday and March, filled it and left no month.
Spanish and Albanian "mar" and Albanian "sht" (Saturday, September) did
the same, and the keystroke check refused those dates. Where such a
read finds no date, the names are now read again with no weekday
expected, so the shared name is the month; a typed weekday still comes
first ("mar 17 mar 2026"). util.scand reads its fields in order and
needs the weekday, so it is unchanged. A test types those dates with the
weekday left out.

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

* Forms: Turkish names typed in capitals are read (#5303)

With Turkish dates, "1 MAYIS 2026" and "1 EKİM 2026" in a d mmmm yyyy
field read as no date through AFDate and util.scand, and "SALI 6 OCAK
2026" through util.scand. Names were compared lower-cased, which turns
I into i, not the dotless ı of "Mayıs" and "Salı", and İ into i with a
combining dot above, which "Ekim" doesn't have. Names and typed text
are now folded alike, the capital I and the dotted and dotless i all as
i and the combining dot dropped, one piece for each char so positions
in the text stay aligned: in both copies of the name search, the AFDate
month reader and util.scand's name matcher. English reads as before. A
test types Turkish names in capitals and English ones as a check.

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

* Forms: Greek names typed in capitals are read again (#5303)

Folding names char by char turned a capital Σ into σ, so "ΙΑΝΟΥΆΡΙΟΣ"
no longer matched "Ιανουάριος", whose last letter is the final ς. The
fold now makes ς a σ on both sides, in the forms and JS parsers alike.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 16:12:04 +01:00
FredApps 9070431ed3 Merge pull request #5250 from KenM76/fix/cleared-max-len-stays-cleared
fix(forms): clearing a field's length limit leaves it with none, not its parent field's
2026-11-04 19:08:06 +01:00
c0cd40f2e1 M6: forms decodes field values, renamed calculations and detected pages within shared budgets (#6132)
* M6: forms decodes values, renamed calculations and detected pages within shared budgets

Part of #5755: the forms crate's four unbudgeted stream reads go through a
`DecodeBudget`, so an operation's decoding is bounded however often the file
names the same stream.

- A reading of the form decodes its fields' value streams (/V, /DV) within one
  64 MiB budget, as it already does their scripts. Each field got a fresh
  1 GiB allowance, so 300 fields naming one stream decoded it 300 times.
- Renaming a field rewrites the calculations that read it within one script
  budget; past it, a calculation is left as it is, like one that won't decode.
- Field detection reads a page's content within one page's allowance
  (`DecodeBudget::page`), as the content walks do, however often /Contents
  lists a stream.
- The PDF/A declaration is read from at most 8 MiB of XMP, as preflight's
  Standards panel reads it, and only once /NeedAppearances isn't already set.

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

* M6: the forms README says which metadata declares PDF/A

declares_pdfa reads pdfaid:part from XMP metadata that decodes within
8 MiB, as preflight's Standards panel reads it.

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

* M6: a test pins that metadata without pdfaid:part declares no PDF/A

cargo-mutants found `==` turned to `!=` in declares_pdfa's search unpinned:
no case had metadata within the cap that declares nothing.

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

* M6: field detection reads operators one at a time; the PDF/A check reads XMP as preflight does

From the Resources review of 66415d737:
- page_shapes collected every parsed operator, about 80 bytes for each
  decoded byte of dense content, so a page within its 256 MiB allowance
  could take some 20 GB. It now reads them one at a time
  (pdfcraft_content::ops) and drops the pieces once joined.
- declares_pdfa decoded through a DecodeBudget, which counts every filter
  stage against the 8 MiB cap, so XMP stored through a chain of filters
  stopped declaring PDF/A while preflight's Standards panel, which caps
  each stage, still read it. It reads the metadata as preflight does.

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

* M6: say what declares PDF/A to forms, and why its metadata read stays direct

From the Legal-forms review of c00e3d2fb: the README and doc comments said
forms reads the declaration "as preflight's Standards panel reads it". The
two share the 8 MiB cap, applied to each filter stage, but forms matches the
bytes pdfaid:part and preflight the identifier by its namespace, as on main.
The sweep's direct-/DecodeParms hit is answered at the site: preflight and
organize read metadata the same way, and the readers move together.

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

* forms: rename a shared calculation once; keep unread defaults on Clear form; cap q in detection

- A Calculate action that several fields share by reference is read and
  rewritten once, in place, so every calculation reads the new name and
  the script is decoded once, not skipped past the budget.
- Clear form leaves a field alone when its /DV is a stream the value
  budget didn't reach, instead of emptying it.
- Field detection keeps at most 256 saved graphics states; deeper q are
  counted so each Q still restores its own.
- The dense-content test checks that the budget is spent; exhausted() was
  true from the start with no form visits.

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

* forms: restore the matrix past the save cap; reset a value that wasn't read

- Field detection keeps the matrix of the first save past MAX_SAVES and
  restores it when the last of them closes. A shape painted after a Q
  that would have undone a cm inside those saves is dropped, as the
  matrix there isn't known, instead of being reported scaled.
- Clear form skips a field only when its value is known: one whose /V
  is a stream the value budget didn't reach is reset, not kept as if it
  equalled an empty default.

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

* forms: keep detection's saved states in a Saves type

shapes_within went past clippy's cognitive complexity limit (32/30) with
the save-cap tracking inline. The same logic now lives in Saves (save,
restore, moved, unknown).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-04 13:51:40 +01:00
FredApps 7d3d4830e5 Integrate remaining six PdfCraft parity PRs with conflict resolutions
Includes #4325 #1952 #4180 #2529 #4320 #4805, preserving their original histories.
Keep paragraph alignment and hard/soft wrap metadata together; retain CCITT/downsampling/RGB ownership guards with bounded JBIG2 export and exact-copy symbol encoding.
Combined interaction regressions added. Validation pending.
2026-10-30 11:02:07 +01:00
FredApps b07a012cd7 Reconcile button export values with current form authoring modules
Preserve caption editing and current form/Prepare module boundaries. Carry export-only appearance preservation, validation and data round-trip tests to their current paths, and retain current attribution metadata. Hosted CI must validate this refreshed head.
2026-10-30 08:56:21 +01:00
Dylan M. Taylor 3517d05b47 Update prompt shows the app icon and accent color instead of a letter (#5388)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#b70228, its red, from the icon (#5245)). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/edit/src/images.rs
- crates/forms/src/appearance.rs
2026-10-30 01:27:58 -04:00
Grzegorz MartensandClaude Opus 5.5 f64c71f65f M6: a test that set_props refuses an unstorable notation before writing anything
The tooltip given with it, which set_props writes before the calculation,
isn't set either; with the check moved to the calculation the test fails
(from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:48 +01:00
Grzegorz MartensandClaude Opus 5.5 46382092f1 M6: Field Properties refuses a notation it couldn't store, and stays open with what was typed
set_props refuses such a notation before writing anything, so the
Calculate tab can't store one either (the tool already refused it). A
refused edit used to close Field Properties and lose everything typed
into it; the dialog now stays open with its changes, as the dialog for
several fields does, and the notice says why (from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:48 +01:00
Grzegorz MartensandClaude Opus 5.5 d7c8e6ad1b M6: calculation checks: a notation's last name keeps the escaped space that ends it
parse_calculate trimmed the notation with str::trim, which also took an
escaped space ending its last name: "Qty * Total\ " read back as
"Qty * Total\", a field the form doesn't have, while the script's
JavaScript still read "Total ". It now trims to its first and last
token, as the field-reading scan does. notation_storable compares the
names and operators read back with those given, so a notation ending in
a lone \ (which escapes the space after it) is refused; a proptest reads
back notations of any names (from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:38 +01:00
Grzegorz MartensandClaude Opus 5.5 fd3634cc38 M6: form_set_props refuses a notation it couldn't store as written
A notation is kept in the script's /** BVCALC … EVCALC **/ comment and read
back from there. One holding EVCALC was stored cut short ("a EVCALC b"
read back as "a", and its JavaScript translation didn't parse), one
holding */ ended the comment, and an empty one became "event.value = ;".
form_set_props now refuses each (af::notation_storable: it has to read back
as itself). The test also pins a notation beside op alone and beside
fields alone (from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:38 +01:00
Grzegorz MartensandClaude Opus 5.5 50faef001a M6: calculation checks: CalculatedField::issues says it covers the formula too
MissingField and BuiltNames are about the formula, not its place in the
order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:38 +01:00
Grzegorz MartensandClaude Opus 5.5 1808720d75 M6: calculation checks: an AFSimple string list with an argument after it is read and renamed, as an array is
AFSimple_Calculate("SUM", "a, b", x) gave only a built-name site, so a
rename no longer rewrote a and b (main did) while parse_calculate still
read the sum of a and b: after a rename the sum read missing fields. The
string list now keeps its names when an argument follows, and what
follows is a name built at run time, as for an array (from the review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
2026-10-29 20:48:38 +01:00
Grzegorz MartensandClaude Opus 5.5 2b40413dc3 M6: calculation checks: an Acrobat widget name is a missing field, as nothing here resolves it
found accepted price.0 (up to the field's last widget), but links,
recalculate and the scripts' getField all take a name as the field or
the fields under it, so a formula reading price.0 got no value and no
order check while the page said nothing was wrong. found now looks
names up the same way; a widget name is flagged as missing until all of
them resolve it (from CodeRabbit's review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 91a4db27be M6: calculation checks: a widget number past a field's last widget is a missing field
A read like price.12 counted as found whenever price existed. A widget
number now finds something only below the field's widget count; a
field with one widget, or none, still answers to .0 (from CodeRabbit's
review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 6a5020760e M6: calculation checks: formulas by field object, whole AFSimple lists only, no all-clear beside a broken formula
- CalculatedField carries the field object: form_calculation_order and
  the Calculation order page find each row's formula and value by it,
  so two calculated fields sharing a name (a malformed form) each show
  their own.
- An AFSimple_Calculate list counts as written out only when it ends
  the call: "row" + suffix and [...].concat(more) are names built at
  run time, and an array item counts only when it ends at a comma or
  the list's end, so ["row" + i] no longer reads, or renames, "row".
- After a reorder, Undo is offered on the missing-field card instead of
  bringing back the all-clear beside it.

From CodeRabbit's review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:48:37 +01:00
Grzegorz MartensandClaude Opus 5.5 4cfd592730 M6: calculation checks: a formula that reads a field the form doesn't have, and a script that builds the names it reads
calculation_order flags MissingField (a field deleted from under a sum, a
typo in notation, a script's getField; a group's name and a widget's
number count as there) and BuiltNames (a getField or AFSimple_Calculate
list built at run time, found as af::Site::Built in the same pass as the
literal names). form_calculation_order gives each field's formula and the
two new issues; form_fields shows a custom Calculate script; the
Calculation order page explains both in its row tooltips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
2026-10-29 20:48:37 +01:00
KenM76andClaude Opus 5.5 412fbb397a fix(forms): clearing a field's length limit leaves it with none, not its parent field's
/MaxLen is inheritable (ISO 32000-2 12.7.5.3). Clearing a kid's limit removed its own key, so
the parent's limit came back. When an ancestor sets /MaxLen, write /MaxLen 0, which read.rs
already reads as no limit and which overrides the inherited one (#3323). Follow-up to #4991.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-29 00:50:10 -04:00
FredApps 97c062a14b Complete additive integration fields and test boundaries 2026-10-29 02:40:41 +01:00
FredApps e34cb1d1cc Integrate KenM76 PR #4991
# Conflicts:
#	crates/automation/tests/it/automation.rs
#	crates/forms/src/author/edit.rs
#	crates/forms/src/tests.rs
#	crates/forms/src/values.rs
#	parity/acrobat-features.toml
2026-10-29 02:38:25 +01:00
FredApps 07edd05aea Integrate KenM76 PR #4987
# Conflicts:
#	crates/forms/src/author/edit.rs
#	crates/forms/src/tests.rs
2026-10-29 02:38:03 +01:00
FredApps 9e30913d6e Integrate KenM76 PR #4977 2026-10-29 02:36:20 +01:00
FredApps a66686cb0d Integrate KenM76 PR #4862 2026-10-29 02:34:57 +01:00
KenM76andClaude Opus 5.5 3aeafeb09e Merge origin/main into fix/cleared-default-stays-cleared
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-28 19:18:37 -04:00
KenM76andClaude Opus 5.5 304f27fd0b Merge origin/main into fix/choice-index-follows-options
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-28 19:18:35 -04:00
KenM76andClaude Opus 5.5 2a26479cbd Merge origin/main into fix/radio-join-group-by-name
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-28 19:18:32 -04:00
aiongg f824475202 Merge pull request #4631 from Lcoding15/fix/delete-one-radio-button
M6: Delete removes one selected radio button, not its whole group (#4609)
2026-10-28 17:55:17 -04:00
.CooCoo 8f3a4f18bd M6: Organize pages ▸ Page properties sets the selected pages' tab order (#4516)
* M6: Organize pages ▸ Page properties sets the selected pages' tab order

Page properties in the Organize pages panel was listed as planned. In
the app, a page's tab order could only be set for every page at once,
with the form.tab_order commands, which offer no Unspecified. (The
form_tab_order tool already set chosen pages, Unspecified included.)

- Page properties (page.properties) opens a Tab Order dialog for the
  selected pages, with Use Row Order, Use Column Order, Use Document
  Structure and Unspecified. OK writes Edit::SetTabOrder for those pages
  as one undo step. The dialog opens on the pages' order, or with no
  choice made when they differ, and then OK is off until one is chosen.
  OK on the order the pages already have adds no undo step.
- Rows and columns run right to left in a document bound on the right,
  as Use Row Order and Use Column Order are described. PdfCraft reads a
  right-to-left binding from /ViewerPreferences /Direction /R2L
  (ISO 32000-1, 12.2, Table 150). Before, rank_tabs ranked them left to
  right.
- A page whose /Tabs is an indirect object now takes its row, column or
  structure order. Before, rank_tabs read only a direct name and ranked
  the page in annotation order.
- pdfcraft_forms::tab_orders and Document::tab_orders read each page's
  /Tabs. form_tab_order returns them as page_tabs, and reads without
  changing anything when it is given no order or field.
- set_option("dialog", "page-properties") opens the dialog.
- Parity: organize.page-tab-order is shipped, and its notes say the
  Actions tab of Page Properties (organize.page-actions) is still
  planned; the Organize pages grid notes list Page properties.

* Page properties: OK applies only to the document the dialog was opened on

The draft keeps the document's id. When another document is active by
OK, the dialog closes without changing either document.

* Menus: list Page properties… in the Pages menu

The menu bar now lists its commands explicitly, and every command with a menu group must appear in it.

* Page properties: apply the tab order only to an unchanged document

An edit, undo or redo made while the dialog is open can move the pages
at the stored indices. OK now closes the dialog without changing a page
when the document's edit generation differs from when it opened.

* Page properties: say when a document change cancels the tab order

OK shows a notice when the document changed or another document became
active while the dialog was open, so the user knows to open it again.
2026-10-28 17:48:29 -04:00
KenM76andClaude Opus 5.5 ac40f62294 C012 test: a cleared choice kid has no selected value, exactly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 22:54:50 -04:00
KenM76andClaude Opus 5.5 5716a95f8d C010: joining a group redraws only the buttons that moved
redraw_field after the join redrew every button of the target group, so a
button with its own appearance states lost them to the default marks. Only
the moved buttons are now redrawn under the group's name.

Test: joining_a_group_leaves_its_own_buttons_appearances_as_they_are
(fails before: the group's own button gets new /AP streams).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 22:53:55 -04:00
KenM76andClaude Opus 5.5 6e0eec60e9 C011: sorting keeps which of two items with one export value is selected
Sorting a field's own items carries its /I positions through the sort
(ISO 32000-2 12.7.5.4) instead of re-deriving them from /V, which names
only the export value. Values no carried position covers still take their
first position. items_to_sort and write_sorted_opt keep set_props under
the complexity limit.

Test: sorting_keeps_which_of_two_items_with_one_export_value_is_selected
(fails before: /I [0], Alpha, instead of [2], Zeta).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 22:53:37 -04:00
KenM76andClaude Opus 5.5 969cad099d Merge origin/main into fix/radio-join-group-by-name
Adopt upstream's split set_props: rename_field returns the group a renamed
radio button joins, and redraw_or_join keeps set_props under the new
cognitive_complexity limit. join.rs imports explicitly (wildcard_imports).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 21:57:42 -04:00
KenM76andClaude Opus 5.5 d95ea8710d Merge origin/main into fix/cleared-default-stays-cleared
Adopt upstream's explicit import lists in author/edit.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 21:35:31 -04:00
KenM76andClaude Opus 5.5 68e78e7886 Merge origin/main into fix/choice-index-follows-options
Adopt upstream's split set_props and explicit import lists. The list box
scroll after a property edit moves into scroll_list, keeping set_props under
the new cognitive_complexity limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-27 21:17:29 -04:00
Dylan M. Taylor 81c63c037a Group annotation, form and document operation parameters 2026-10-27 23:25:39 +00:00