Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
426 commits
Select commit Hold shift + click to select a range
52d49c3
fix(audit): charge a line's runs to the comparison bound
mercuryVM Oct 23, 2026
12b5e67
feat(ui): add 'Split document...' to page thumbnail context menus
so-sai Oct 22, 2026
833d654
optimize: the JPEG header scan stops at the first stray byte (#1201 r…
guillehr2 Oct 23, 2026
35b4677
M11: LZW to Flate leaves streams whose decode parameters aren't direc…
marcoarnulfo Oct 23, 2026
d09ccb2
i18n(ui): carry this PR's new wordings in the catalogues that must co…
regendorf-ux Oct 23, 2026
c9c6c85
Merge remote-tracking branch 'origin/main' into fix/1201-optimize-kee…
guillehr2 Oct 23, 2026
5514756
Merge remote-tracking branch 'origin/main' into fix/back-arrows-leave…
guillehr2 Oct 23, 2026
b9db0fd
Merge remote-tracking branch 'origin/main' into fix/2837-preview-page…
guillehr2 Oct 23, 2026
4bd1e72
Refuse an OCR batch whose inputs would write one output file
eliets2 Oct 20, 2026
39918c4
Describe the shared-output refusal in the batch OCR tool description
eliets2 Oct 20, 2026
558fa0c
Merge remote-tracking branch 'origin/main' into fix/2583-note-icon-no…
guillehr2 Oct 23, 2026
d9115bb
M7: edited text keeps every space typed (#4199, #3797)
Jayesh-Prajapati9 Oct 23, 2026
6d767cc
Edit text: keep Tr, stroke, line width and gs set inside BT when rewr…
cadelass Oct 19, 2026
9163f29
Edit text: per-run q/Q, /LW from gs, and a Tr partway through a line …
Oct 22, 2026
60985e1
Edit text: a dash or line cap partway through an outlined line starts…
Oct 23, 2026
3e99889
M7: CR and CRLF break rows on the styled rewrite path too (#4199)
Jayesh-Prajapati9 Oct 23, 2026
33aea96
style: rustfmt the cancellation test
guillehr2 Oct 23, 2026
64d8c20
Leave the page grid when the Crop tool is chosen
eliets2 Oct 22, 2026
cefbefb
deps: ctutils 0.4.5 for RUSTSEC-2026-0385
guillehr2 Oct 23, 2026
e1b6401
test(ui): the attachments panel snapshot lists the Attach and Delete …
regendorf-ux Oct 23, 2026
eed2afa
fix(forms): an inherited signature value and a direct /AcroForm are h…
mtlcl410 Oct 23, 2026
07f0faf
tests: the NoRotate test's annotation has main's points field too
guillehr2 Oct 23, 2026
bde5c37
deps: ctutils 0.4.5 for RUSTSEC-2026-0385
guillehr2 Oct 23, 2026
afb2586
deps: ctutils 0.4.5 for RUSTSEC-2026-0385
guillehr2 Oct 23, 2026
3225b03
M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
Jayesh-Prajapati9 Oct 23, 2026
9efc172
M6: default names skip empty parents; a full name under the same pare…
mtlcl410 Oct 23, 2026
3b9cfe1
Tests: UI tests wait for background work by time, not frame count (#4…
Jayesh-Prajapati9 Oct 23, 2026
866a28a
M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
Jayesh-Prajapati9 Oct 23, 2026
cc6fa04
deps: ctutils 0.4.5 for RUSTSEC-2026-0385
guillehr2 Oct 23, 2026
1481eb1
Update ctutils to fix RUSTSEC-2026-0385
eliets2 Oct 23, 2026
4d45183
Update ctutils to fix RUSTSEC-2026-0385
eliets2 Oct 23, 2026
9333baa
ui: the Edit panel's back arrow, under All tools since main's new lay…
guillehr2 Oct 23, 2026
e76ebb9
M6: a document script that throws no longer stops the form's other sc…
mtlcl410 Oct 22, 2026
cc02912
M6: js_run reports the document scripts that failed (#3940)
mtlcl410 Oct 22, 2026
77db1bd
M6: parity note for document scripts that throw (#3940)
mtlcl410 Oct 22, 2026
a8beb90
M6: a document script refused before running is skipped and reported …
mtlcl410 Oct 22, 2026
a7afaac
M6: merged script output reports a document script's error once too (…
mtlcl410 Oct 22, 2026
994ce63
M6: a document script's error is told apart by where it came from, no…
mtlcl410 Oct 23, 2026
c141c94
Merge remote-tracking branch 'upstream/main' into fix/util-scand
EricKaden Oct 23, 2026
44b7e0a
Merge remote-tracking branch 'upstream/main' into fix/exec-menu-attac…
EricKaden Oct 23, 2026
d8aff8b
M7: a batch of content edits refreshes the added content and page marks
lev-n Oct 23, 2026
9ee24d7
Merge remote-tracking branch 'origin/main' into feat/edit-scanned-pag…
philip-tam Oct 23, 2026
dd396c2
Merge upstream main 57e531cc into pr/attachments
Oct 23, 2026
cd6f031
M7: added text takes character spacing from Format text (fixes #4245)
marcoarnulfo Oct 23, 2026
5051a16
M10: review of #1609: drop an automatic run's result once the documen…
philip-tam Oct 23, 2026
9eff0d2
Parity: evidence paths follow the tests moved into tests/it
regendorf-ux Oct 23, 2026
81e0542
organize: format field widget splitting changes
midasmatterhorn Oct 23, 2026
5af594e
Added text editor: wrap width gives up the trailing character spacing…
marcoarnulfo Oct 23, 2026
8b9d501
Check boxes and radio buttons draw their mark in the field's text col…
marcoarnulfo Oct 23, 2026
6062535
Test: the check strokes and the circle fills in the field's colour (#…
marcoarnulfo Oct 23, 2026
441d900
M12: XFA text is measured at its standard-14 font's own widths
EricKaden Oct 23, 2026
61fe4b5
M6: Field Properties ▸ Actions: Edit opens an existing script (#4006)
marcoarnulfo Oct 23, 2026
1f41c0c
organize: preserve widget actions and inherited field state
midasmatterhorn Oct 23, 2026
21bbae3
M3: a link without a position keeps the reader's place (ISO 32000-2 §…
Lcoding15 Oct 23, 2026
afcc54f
Omit minus sign when FormCalc Format() rounds to zero
Silvertree2010 Oct 23, 2026
c5b0e6a
M6: Actions tab: replacing a trigger's first action keeps its chain, …
marcoarnulfo Oct 23, 2026
bf81191
tests: the pending-open control test waits for the worker by time, no…
guillehr2 Oct 23, 2026
7668a2c
tests: each forms test saves into a folder of its own
guillehr2 Oct 23, 2026
a1f6a64
tests: the pending-open control test waits for the worker by time, no…
guillehr2 Oct 23, 2026
0b8758e
M12: links in XFA rich text become clickable links
EricKaden Oct 23, 2026
1c4a481
M12: round XFA check buttons draw a round background and border
EricKaden Oct 23, 2026
0008a18
M12: XFA borders and rectangles with rounded corners are drawn rounded
EricKaden Oct 23, 2026
21f3241
M12: a round border as wide as its check button fills it as a disc
EricKaden Oct 23, 2026
0b3b243
M7: a bold paragraph's stand-in font no longer turns earlier edits bo…
Lcoding15 Oct 23, 2026
a4c5e5f
M7: the stand-in note names every standard family it can use (#4280)
Lcoding15 Oct 23, 2026
d91028a
M12: a link's address is shared by its runs, words and spans, not copied
EricKaden Oct 23, 2026
ba37d41
test(ui): recovery idle tests make folders that never share a name
EricKaden Oct 23, 2026
ac18147
M12: a round check button's underline border stays an underline
EricKaden Oct 23, 2026
3cf6e5f
M12: a link's pieces don't join across a rule, picture or field betwe…
EricKaden Oct 23, 2026
3355eda
M12: only a link's own underline joins its pieces, and web links need…
EricKaden Oct 23, 2026
352ebb1
M12: a backslash ends a web link's host, as web addresses read it
EricKaden Oct 23, 2026
bf86689
M11: measure optimizer image resolution in physical page units
KyleKassen Oct 23, 2026
a8839bc
M10: preserve page artwork when printing annotations
KyleKassen Oct 23, 2026
12d1956
M11: keep default GitHub username contributor credit
KyleKassen Oct 23, 2026
30d85bb
M10: write DOCX cell widths from their grid columns
KyleKassen Oct 23, 2026
1a69e50
M10: document annotation resource collision regression
KyleKassen Oct 23, 2026
9dc4bd5
M10: preserve table text from covered export slots
KyleKassen Oct 23, 2026
50b5e90
M10: apply soft-mask Decode intervals to image exports
KyleKassen Oct 23, 2026
e373dff
M8: redact labelled Italian international slash numbers
KyleKassen Oct 23, 2026
8b2cefc
Merge upstream main a2ec27b4 into pr/attachments: translation catalog…
Oct 23, 2026
0ad7396
Merge remote-tracking branch 'origin/main' into feat/edit-scanned-pag…
philip-tam Oct 23, 2026
302ad68
M7: paste added text boxes and images in the engine and through conte…
lev-n Oct 23, 2026
5515f32
M7: copy added JPEG 2000 images as their file (#2636)
lev-n Oct 23, 2026
64bdea4
M6: fields that don't scroll stop taking text when full
EricKaden Oct 23, 2026
f5e7499
M12: flowed XFA subforms are drawn with their border and fill
EricKaden Oct 23, 2026
b12965e
M6: the date picker has a Today button (#4274)
mtlcl410 Oct 23, 2026
3db28cc
M5: stamps without an appearance draw the stamp they name (ISO 32000-…
mtlcl410 Oct 24, 2026
184e9f3
M10: a booklet decides two-sided itself, as Acrobat does (#2707)
mtlcl410 Oct 16, 2026
2f97fd2
M5: the placeholder for an unknown stamp name stays grey whatever its /C
mtlcl410 Oct 24, 2026
d64a698
fix(parity): cite the booklet print test at its path under tests/it (…
mtlcl410 Oct 24, 2026
d7da5df
M12: a flowed XFA subform a page break cuts gets a closed border on e…
EricKaden Oct 24, 2026
42910de
Reject open-ended page range on a document with no pages
Silvertree2010 Oct 24, 2026
6d7e887
M6: a paste into a full field keeps the longest part that fits, acros…
EricKaden Oct 24, 2026
a7d86de
M12: a flowed subform's last part keeps its bottom margin inside the …
EricKaden Oct 24, 2026
946fb31
M3: restore last view settings when reopening documents (#1767)
mtlcl410 Oct 17, 2026
aa190d9
M3: Vietnamese for the restore-last-view preference (#1767)
mtlcl410 Oct 17, 2026
5e69e62
M12: the last view remembers the two-page cover setting (#1767)
mtlcl410 Oct 18, 2026
7dc18ee
M3: the restore-last-view preference in the catalogues added since (#…
mtlcl410 Oct 22, 2026
fb53ddf
M3: full screen neither is remembered as the last view nor hides it (…
mtlcl410 Oct 22, 2026
6506c8c
M3: a fractional zoom is remembered as it was, not rounded to a whole…
mtlcl410 Oct 23, 2026
cc6b042
M12: a flowed subform's repeated top margins give way to a child that…
EricKaden Oct 24, 2026
3cf6b0e
M12: docstrings for the layout helpers and test the flowed-border cha…
EricKaden Oct 24, 2026
35716c6
M12: a flowed table a page break cuts between its rows keeps its bord…
EricKaden Oct 24, 2026
001a900
M5: comments without an appearance draw their /Border dashes, Ink das…
mtlcl410 Oct 24, 2026
cafd507
M12: page breaks of a section's own keep the border around it, a lead…
EricKaden Oct 24, 2026
4d190aa
M12: the parity note and test say which sections lose their border
EricKaden Oct 24, 2026
63510e2
test(ui): docstrings for the recovery tests' folder helper
EricKaden Oct 24, 2026
2fc2511
M12: a test that a section whose first child moves to the next page t…
EricKaden Oct 24, 2026
0142a96
M2: a DeviceN colour with twelve inks paints, and a colour that can't…
mtlcl410 Oct 23, 2026
098f221
M2: the operand-cap patch is hayro-syntax patch 28 (#4260)
mtlcl410 Oct 24, 2026
5233e12
M6: check boxes, radio buttons and push buttons without an appearance…
mtlcl410 Oct 24, 2026
dd1a894
M6: leave the combo box drop-down arrow to #3567
mtlcl410 Oct 24, 2026
a6df1d3
M2: Invisible annotations of unknown types and popups don't paint the…
mtlcl410 Oct 24, 2026
4287340
M2: a stand-in for a non-embedded font keeps its glyph shapes at wide…
mtlcl410 Oct 24, 2026
827cec3
M2: resolve an indirect annotation /Subtype before the Invisible prin…
mtlcl410 Oct 24, 2026
f2e222e
M2: a non-embedded CIDFont's fallback request carries its descendant'…
mtlcl410 Oct 24, 2026
d96427f
M2: vendor/README.md: one Test cell per row in the hayro-interpret an…
mtlcl410 Oct 24, 2026
a9e6069
M3: resize and move the window again right after a resize or move (Wa…
bmh129 Oct 21, 2026
0b63a73
M3: progress docs for repeated window resizes and moves on Wayland
bmh129 Oct 22, 2026
185c2db
M3: give the kept button release back before the next press (Wayland)
bmh129 Oct 24, 2026
ba85d2a
Merge PR #2957: M6: dotted field names make parent fields (#2696)
aiongg Oct 24, 2026
6672e10
Merge PR #3009: fix(ui): the header, footer and watermark previews sh…
aiongg Oct 24, 2026
fa3a022
Merge PR #3000: fix(edit): batch styled paragraph fallback glyphs
aiongg Oct 24, 2026
afba441
Merge PR #2289: organize: split a combined field/widget so every copi…
aiongg Oct 24, 2026
4fedfe7
Merge PR #2648: fix(xfa): skip PDF fallback scripts while XFA layout …
aiongg Oct 24, 2026
aa78832
Integration: #3000 drop unrelated wait_for_file hunk in forms.rs test
aiongg Oct 24, 2026
ce20005
Merge PR #4155: M4: a save prompt that opens in the background asks f…
aiongg Oct 24, 2026
d34476d
Merge PR #4157: M2: a symbolic TrueType font reads its (3, 0) cmap be…
aiongg Oct 24, 2026
0cfde22
Merge PR #3488: fix(forms): write each /DR font as its own object, so…
aiongg Oct 24, 2026
4167ac6
Merge PR #4310: M11: honor page UserUnit when measuring image resolution
aiongg Oct 24, 2026
d06bc3c
Merge PR #3546: Save commits the open Edit Text boxes before writing …
aiongg Oct 24, 2026
7c005ae
Merge PR #3551: Edit text: keep Tr, stroke, line width and gs set ins…
aiongg Oct 24, 2026
b6b6e6e
Merge PR #3943: M6: values typed into Password fields are written to …
aiongg Oct 24, 2026
70d92a0
Integration: #2289 keep Contents, BM, Lang and AF on the split widget
aiongg Oct 24, 2026
01dc49c
Merge PR #4053: fix: Issue #4018, form_fill warns when a value does n…
aiongg Oct 24, 2026
4eb7cf4
Merge PR #4295: Check boxes and radio buttons draw their mark in the …
aiongg Oct 24, 2026
339d8f4
Merge PR #4307: M6: Field Properties > Actions: Edit opens an existin…
aiongg Oct 24, 2026
66c8db0
Merge PR #3849: cli: the argument bugs photocraft#7071 lists are live…
aiongg Oct 24, 2026
8b1f21f
Merge PR #4311: M10: write DOCX cell widths from their grid columns
aiongg Oct 24, 2026
c77906b
Merge PR #3974: Leave the page grid when the Crop tool is chosen
aiongg Oct 24, 2026
1fd3759
Merge PR #3401: M12: laying out a dynamic XFA form takes off the usag…
aiongg Oct 24, 2026
19b36de
Merge PR #3997: fix(automation): guard MCP dispatch and bound line an…
aiongg Oct 24, 2026
c0687ed
Merge PR #3552: M12: a date typed by another of an XFA field's edit p…
aiongg Oct 24, 2026
2a1ceeb
Merge PR #4339: M6: fields that don't scroll stop taking text when full
aiongg Oct 24, 2026
40258a5
Merge PR #4132: fix(sign): judge a page's indirect /Annots array by t…
aiongg Oct 24, 2026
423e98c
Merge PR #4343: M6: the date picker has a Today button (#4274)
aiongg Oct 24, 2026
75f3a26
Merge PR #4133: fix(sign): let an LTV update declare the ESIC extensi…
aiongg Oct 24, 2026
44831a6
Merge PR #3670: Redaction: follow nested forms as deep as the readers do
aiongg Oct 24, 2026
0041a3b
Merge PR #4161: M10: every paper size reaches the printer, from the d…
aiongg Oct 24, 2026
c24e90b
Merge PR #3985: M6: a document script that throws no longer stops the…
aiongg Oct 24, 2026
f9f0801
Merge PR #4111: fix(edit): Delete removes the selected paragraph or o…
aiongg Oct 24, 2026
496e3b3
Merge PR #4312: M10: preserve page artwork when printing annotations
aiongg Oct 24, 2026
9230cb8
Merge PR #4352: Reject open-ended page range on a document with no pages
aiongg Oct 24, 2026
321d59a
Merge PR #4178: fix(ui): the added-text editor keeps the box's alignment
aiongg Oct 24, 2026
ee989ab
Merge PR #3016: M5: sticky notes stay upright where they are placed o…
aiongg Oct 24, 2026
d626a4a
Merge PR #2190: M3: a fresh install follows the desktop's appearance …
aiongg Oct 24, 2026
a84a255
Integration: #4178 doc comment back above mode_tab
aiongg Oct 24, 2026
b9f5ef6
Merge PR #4198: fix(audit): covered_text reports the words under the …
aiongg Oct 24, 2026
c097653
Merge PR #4329: M10: preserve table text from covered export slots
aiongg Oct 24, 2026
8a0319a
Merge PR #4368: M6: check boxes, radio buttons and push buttons witho…
aiongg Oct 24, 2026
ad54ef3
Merge PR #3970: M3: resize and move the window again right after a re…
aiongg Oct 24, 2026
8ff6c8a
Merge PR #2748: M10: a booklet decides two-sided itself, as Acrobat d…
aiongg Oct 24, 2026
2d8e21e
Merge PR #3750: feat(organize): Extract pages takes typed page ranges…
aiongg Oct 24, 2026
6ed0082
Merge PR #4211: feat(ui): add 'Split document...' to page thumbnail c…
aiongg Oct 24, 2026
5ee6544
Merge PR #4242: Tests: UI tests wait for background work by time, not…
aiongg Oct 24, 2026
481b91f
Merge PR #4323: test(ui): recovery idle tests make folders that never…
aiongg Oct 24, 2026
5cb74ee
Merge PR #4217: M7: edited text keeps every space typed (#4199, #3797)
aiongg Oct 24, 2026
1b404e6
Merge PR #4095: Comments: the composer hides while its comment is scr…
aiongg Oct 24, 2026
f2c133b
Merge PR #4258: M7: a batch of content edits refreshes the added cont…
aiongg Oct 24, 2026
9c47dd3
Merge PR #4331: M10: apply soft-mask Decode intervals to image exports
aiongg Oct 24, 2026
8dda3d5
Merge PR #4316: M7: a bold paragraph's stand-in font no longer turns …
aiongg Oct 24, 2026
0844974
Merge PR #4345: M2: a DeviceN colour with twelve inks paints, and a c…
aiongg Oct 24, 2026
bcbd9cb
Merge PR #4287: M12: XFA text is measured at its standard-14 font's o…
aiongg Oct 24, 2026
3f62296
Merge PR #4299: Omit minus sign when FormCalc Format() rounds to zero
aiongg Oct 24, 2026
5990381
Merge PR #3549: M6: util.scand reads dates as another viewer does
aiongg Oct 24, 2026
8a64b65
Merge PR #4330: M8: redact labelled Italian international slash numbers
aiongg Oct 24, 2026
4e335dd
Merge PR #4096: feat(ui): the secured-document notice can be turned o…
aiongg Oct 24, 2026
4b29f18
Merge PR #4303: M3: a link without a position keeps the reader's plac…
aiongg Oct 24, 2026
581faa8
Merge PR #4349: M5: stamps without an appearance draw the stamp they …
aiongg Oct 24, 2026
a7778f9
Merge PR #4362: M5: comments without an appearance draw their /Border…
aiongg Oct 24, 2026
f71a305
Merge PR #4230: M7: paste added text boxes and images in the engine a…
aiongg Oct 24, 2026
a6351a5
Merge PR #2195: M3: restore last view settings when reopening documen…
aiongg Oct 24, 2026
80f9555
Merge PR #3786: M5: the arrow keys move the selected comment
aiongg Oct 24, 2026
f017ca3
Merge PR #3580: M6: form buttons show the side panels (app.execMenuItem)
aiongg Oct 24, 2026
7613537
Merge PR #4317: M12: round XFA check buttons draw a round background …
aiongg Oct 24, 2026
97a0b31
Merge PR #4058: M12: type or pick an architect's 1:N drawing scale in…
aiongg Oct 24, 2026
0a20a18
Merge PR #4371: M2: Invisible annotations of unknown types and popups…
aiongg Oct 24, 2026
541c82d
Merge PR #4318: M12: links in XFA rich text become clickable links
aiongg Oct 24, 2026
880723e
Merge PR #4266: M7: added text takes character spacing from Format text
aiongg Oct 24, 2026
eac3ad6
Merge PR #4068: i18n(fi): translate the 64 labels left in English aft…
aiongg Oct 24, 2026
014d9f4
Merge PR #4147: Flatten comments from the page's right-click menu and…
aiongg Oct 24, 2026
a637c39
Merge PR #4319: M12: XFA borders and rectangles with rounded corners …
aiongg Oct 24, 2026
46772a2
Merge PR #4374: M2: a substitute for a non-embedded font keeps its gl…
aiongg Oct 24, 2026
c537579
Merge PR #4338: M12: flowed XFA subforms are drawn with their border …
aiongg Oct 24, 2026
4ad99af
Merge PR #4380: M2: a non-embedded CIDFont's fallback request carries…
aiongg Oct 24, 2026
1d27020
Merge PR #3227: M11: target PDF version and object streams option in …
aiongg Oct 24, 2026
bfe58a8
Integration: #4266 char_spacing in #4230's AddedText literal
aiongg Oct 24, 2026
aedf339
Merge PR #4181: M11: PDF Optimizer Clean Up stores LZW streams with F…
aiongg Oct 24, 2026
15a9f29
Integration: #4266 restore the color field in #4230's AddedText literal
aiongg Oct 24, 2026
52f41f5
Integration: cargo fmt
aiongg Oct 24, 2026
c79c157
Integration: #4147 test literals take Annotation::no_rotate (#3016) a…
aiongg Oct 24, 2026
b3e3a19
Integration: #3227 parity evidence paths under tests/it
aiongg Oct 24, 2026
4754024
Merge track r3-t01
aiongg Oct 24, 2026
a05dbdc
Merge track r3-t02
aiongg Oct 24, 2026
9705e7a
Merge track r3-t03
aiongg Oct 24, 2026
1732f8e
M12 #2589: preserve XFA script policy with current JavaScript context
PerchAnton Oct 24, 2026
692f34f
Merge track r3-t04
aiongg Oct 24, 2026
70e3a10
M12 #2589: integrate current upstream UI test support
PerchAnton Oct 24, 2026
6a206a0
Merge track r3-t05
aiongg Oct 24, 2026
ce684cd
Merge track r3-t06
aiongg Oct 24, 2026
b3efc42
Integration: comments tests use support::settle (#r3-t06 shared helpe…
aiongg Oct 24, 2026
b4691a3
Integration: cargo fmt
aiongg Oct 24, 2026
1a21789
Integration: the page's decode budget owns the form count in redactio…
aiongg Oct 24, 2026
21eed36
Merge PR #1609: M10: recognize a scanned page when Edit is turned on …
aiongg Oct 24, 2026
9f56d9d
Merge PR #4196: OCR: a recognizer panic or a dead worker ends the run…
aiongg Oct 24, 2026
9b986f8
Integration: drop the doc comment of the removed visit cap (#3670 × m…
aiongg Oct 24, 2026
431d1c2
Merge PR #3012: M11: Optimize keeps a JPEG's colour transform (#1201)
aiongg Oct 24, 2026
a954a96
Merge PR #4041: fix: Issue #4020, Extract keeps the bookmarks of the …
aiongg Oct 24, 2026
5fae5fa
Merge PR #3056: fix(ui): the back arrows leave Edit as the Edit tab does
aiongg Oct 24, 2026
23d2e1c
Merge PR #3765: Refuse an OCR batch whose inputs would write one outp…
aiongg Oct 24, 2026
464dbd4
Integration: #3765 name the case-only comparison in the doc comments …
aiongg Oct 24, 2026
f053da3
Merge PR #1960: Attachments: attach a file and delete one (panel, edi…
aiongg Oct 24, 2026
cededfa
Integration: #1960 put the attachment_action doc line back above atta…
aiongg Oct 24, 2026
b232ed2
Merge PR #2588: M3: Document Properties shows a title and author stat…
aiongg Oct 24, 2026
91b6f7f
Integration: #3670's fan-out test allows for a loaded machine
aiongg Oct 24, 2026
deffc37
Integration: #2588 keep XMP-only document information across full ref…
aiongg Oct 24, 2026
2ffcd0f
Merge PR #3694: Select All takes the whole document's text while scro…
aiongg Oct 24, 2026
e05dc23
Merge upstream main into the r3 batch
aiongg Oct 24, 2026
2a91f1d
Integration: the page's decode budget owns the form count in redactio…
aiongg Oct 24, 2026
a9fe3d4
Integration: drop the doc comment of the removed visit cap (#3670 × m…
aiongg Oct 24, 2026
7733b61
Integration: #3670's fan-out test allows for a loaded machine
aiongg Oct 24, 2026
95ec634
Merge upstream main into integration batch A
aiongg Oct 24, 2026
2e561bb
Merge integration batch B (tracks r3-t05, r3-t06)
aiongg Oct 24, 2026
35f3fe0
Integration: #3694 a press beneath a menu or dialog does not end Sele…
aiongg Oct 24, 2026
315454d
Merge PR #2648 at its updated head
aiongg Oct 24, 2026
8ce5a3e
Merge integration batch A at #2648's updated head
aiongg Oct 24, 2026
7685882
Merge integration batch C (track r3-t07)
aiongg Oct 24, 2026
2208dc1
Integration: cargo fmt
aiongg Oct 24, 2026
8021aee
Integration: the edit composer stays visible while part of its commen…
aiongg Oct 24, 2026
b04257d
Integration: a typed Password-field value stays in the open document …
aiongg Oct 24, 2026
06959df
Integration: execMenuItem requests go through the request cap (#3580)
aiongg Oct 24, 2026
be826a3
Integration: a short multi-line field accepts the line it draws (#4339)
aiongg Oct 24, 2026
1fb1877
Integration: attachment_add reads its file through the size-capped re…
aiongg Oct 24, 2026
29889fc
Integration: list the attachment edit labels for translation (#1960)
aiongg Oct 24, 2026
82eedb6
Integration: a retained-version optimize reports the later of header …
aiongg Oct 24, 2026
9595f59
Integration: a script-opened panel reveals a background Inspector tab…
aiongg Oct 24, 2026
70404ac
Integration: format the review-fix tests (#4339)
aiongg Oct 24, 2026
afa14c3
Merge integration batch A's review fixes
aiongg Oct 24, 2026
f9a179f
Merge integration batch B's review fixes
aiongg Oct 24, 2026
2ae1e91
fix(ui): comment composer visibility uses the shown rectangle
aiongg Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 33 additions & 33 deletions ATTRIBUTION.md

Large diffs are not rendered by default.

66 changes: 33 additions & 33 deletions ATTRIBUTION.toml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 48 additions & 1 deletion ROADMAP.md

Large diffs are not rendered by default.

120 changes: 92 additions & 28 deletions apps/pdfcraft-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,24 @@ help and feedback: https://discord.gg/artcraft"
);

fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
// `std::env::args` panics on an argument that is not UTF-8 ("called `Result::unwrap()` on an
// `Err` value"), which a Latin-1 file name is, and it panics while collecting them: even
// `--help` ended that way. Such a name cannot be passed on either, since the paths here are
// `str`, so it is a usage error (photocraft#1108).
let mut args: Vec<String> = Vec::new();
for arg in std::env::args_os().skip(1) {
match arg.into_string() {
Ok(a) => args.push(a),
Err(bad) => {
let _ = writeln!(
std::io::stderr().lock(),
"pdfcraft-cli: `{}` is not valid UTF-8, so it cannot be used as an argument",
bad.to_string_lossy()
);
return ExitCode::FAILURE;
}
}
}
// `<command> --help` too; only right after the command, so a later `-h` (a value) still reaches it.
let command = match args.get(1).map(String::as_str) {
Some("--help" | "-h") => Some("--help"),
Expand Down Expand Up @@ -169,7 +186,7 @@ fn version() -> Result<(), CliError> {
/// found so scripts and CI can gate on it, and when the audit itself cannot run.
fn audit(args: &[String]) -> Result<(), CliError> {
validate_options("audit", args, AUDIT_OPTIONS)?;
let path = *positional(args).first().ok_or("audit: missing file")?;
let path = *positional(args, AUDIT_OPTIONS).first().ok_or("audit: missing file")?;
let bytes = read(path)?;
let password = flag(args, "--password");
let doc = pdfcraft_cos::Document::open_with_password(bytes.clone(), password).map_err(|e| format!("{path}: {e}"))?;
Expand Down Expand Up @@ -208,6 +225,28 @@ struct OptionSpec {
takes_value: bool,
}

/// The options whose value is a file or folder. Only these refuse a value that is another
/// option: a file named `--password` is never what was meant, while a password can be anything
/// (`--password --page` is a password of `--page`, which tests/text.rs pins), and so can a title.
/// The way to name a file that does start with `-` is `./--name`, as `--root` already offered.
const PATH_VALUE_OPTIONS: [&str; 6] = ["--out", "--out-dir", "--json", "--root", "--control", "--script"];

/// Whether a token can be the value of a path option. Another option cannot: `--out --password`
/// used to write the PDF to a file called `--password` and exit 0 (photocraft#6944).
fn can_be_path_value(name: &str, value: Option<&str>) -> bool {
!PATH_VALUE_OPTIONS.contains(&name) || value.is_some_and(|v| !v.is_empty() && !v.starts_with("--"))
}

/// The complaint about a path option whose value is missing or is another option.
fn needs_value(command: &str, name: &str, value: Option<&str>) -> String {
match value {
Some(v) if !v.is_empty() => {
format!("{command}: {name} needs a value and `{v}` is another option; write `./{v}` for a file of that name")
}
_ => format!("{command}: {name} needs a value"),
}
}

/// Refuse unknown options and malformed confinement flags before any file is read.
fn validate_options(command: &str, args: &[String], specs: &[OptionSpec]) -> Result<(), CliError> {
if specs.iter().any(|spec| spec.name == "--root") {
Expand All @@ -230,9 +269,19 @@ fn validate_options(command: &str, args: &[String], specs: &[OptionSpec]) -> Res
let Some(spec) = specs.iter().find(|spec| spec.name == arg) else {
return Err(format!("{command}: unknown option --{name}").into());
};
// Other missing or malformed values are left to the command, which reports them in its own
// terms (`text` and `extract` explain what a page value must be).
i += if spec.takes_value { 2 } else { 1 };
// A file's name is not another option, which also closes the gap the walk left for
// those: an unknown option in that value position was skipped along with it. Other
// values, missing or malformed, are left to the command, which reports them in its
// own terms (`text` and `extract` explain what a page value must be).
i += if spec.takes_value {
let value = args.get(i + 1).map(String::as_str);
if !can_be_path_value(spec.name, value) {
return Err(needs_value(command, spec.name, value).into());
}
2
} else {
1
};
} else {
i += 1;
}
Expand Down Expand Up @@ -278,19 +327,24 @@ const UI_OPTIONS: &[OptionSpec] = &[OptionSpec { name: "--control", takes_value:
#[cfg(feature = "mcp")]
const MCP_OPTIONS: &[OptionSpec] = &[OptionSpec { name: "--root", takes_value: true }, OptionSpec { name: "--compact", takes_value: false }];

fn positional(args: &[String]) -> Vec<&str> {
/// The arguments that are not options or their values. Which tokens are values is the spec's
/// answer: skipping the one after every option made `check-one --edit file.pdf` report "missing
/// file", the file having been read as `--edit`'s value.
fn positional<'a>(args: &'a [String], specs: &[OptionSpec]) -> Vec<&'a str> {
let mut out = Vec::new();
let mut skip = false;
for a in args {
if skip {
skip = false;
continue;
}
if a.starts_with("--") {
skip = true;
continue;
let mut i = 0;
while let Some(a) = args.get(i) {
match specs.iter().find(|spec| spec.name == a.as_str()) {
Some(spec) if spec.takes_value => i += 2,
// An option that takes no value, and an unknown one, which `validate_options` has
// already refused by the time this runs.
Some(_) => i += 1,
None if a.starts_with("--") => i += 1,
None => {
out.push(a.as_str());
i += 1;
}
}
out.push(a.as_str());
}
out
}
Expand All @@ -301,7 +355,7 @@ fn read(path: &str) -> Result<Arc<Vec<u8>>, String> {

fn info(args: &[String]) -> Result<(), CliError> {
validate_options("info", args, INFO_OPTIONS)?;
let path = *positional(args).first().ok_or("info: missing file")?;
let path = *positional(args, INFO_OPTIONS).first().ok_or("info: missing file")?;
let bytes = read(path)?;
let password = flag(args, "--password");
// The renderer takes only the user password of R2–R4 files; `Session::open` takes either.
Expand Down Expand Up @@ -354,7 +408,7 @@ fn info(args: &[String]) -> Result<(), CliError> {

fn text(args: &[String]) -> Result<(), CliError> {
validate_options("text", args, TEXT_OPTIONS)?;
let path = *positional(args).first().ok_or("text: missing file")?;
let path = *positional(args, TEXT_OPTIONS).first().ok_or("text: missing file")?;
let mut selected_page = None;
// Validate every supplied value before reading, retaining the first valid selection.
let mut options = args.iter();
Expand Down Expand Up @@ -482,7 +536,13 @@ fn edit(args: &[String]) -> Result<(), CliError> {
continue;
}
}
let value = args.get(i + 1).map(String::as_str).ok_or_else(|| format!("edit: {arg} needs a value"))?;
// `edit` parses its own options (they run in order), so `--out` is checked here in the
// same way: it wrote a PDF to a file called `--full`.
let value = args.get(i + 1).map(String::as_str);
if !can_be_path_value(arg, value) {
return Err(needs_value("edit", arg, value).into());
}
let value = value.ok_or_else(|| format!("edit: {arg} needs a value"))?;
match arg {
"--out" => out = Some(value),
"--password" => password = Some(value),
Expand Down Expand Up @@ -569,7 +629,7 @@ fn file_stem(path: &str) -> String {
fn combine(args: &[String]) -> Result<(), CliError> {
validate_options("combine", args, COMBINE_OPTIONS)?;
let out = flag(args, "--out").ok_or("combine: missing --out")?;
let inputs = positional(args);
let inputs = positional(args, COMBINE_OPTIONS);
if inputs.len() < 2 {
return Err("combine: give at least two input files".into());
}
Expand All @@ -580,7 +640,7 @@ fn combine(args: &[String]) -> Result<(), CliError> {

fn extract(args: &[String]) -> Result<(), CliError> {
validate_options("extract", args, EXTRACT_OPTIONS)?;
let path = *positional(args).first().ok_or("extract: missing file")?;
let path = *positional(args, EXTRACT_OPTIONS).first().ok_or("extract: missing file")?;
let out = flag(args, "--out").ok_or("extract: missing --out")?;
let pages = page_list(flag(args, "--pages").ok_or("extract: missing --pages")?)?;
let mut session = pdfcraft_engine::Session::new();
Expand All @@ -592,7 +652,7 @@ fn extract(args: &[String]) -> Result<(), CliError> {
fn split(args: &[String]) -> Result<(), CliError> {
validate_options("split", args, SPLIT_OPTIONS)?;
use pdfcraft_engine::SplitBy;
let path = *positional(args).first().ok_or("split: missing file")?;
let path = *positional(args, SPLIT_OPTIONS).first().ok_or("split: missing file")?;
let by = match (flag(args, "--every"), flag(args, "--before")) {
(Some(n), None) => {
// A count the splitter cannot hold is refused here, before the file is opened,
Expand Down Expand Up @@ -637,7 +697,7 @@ fn parse_dpi(value: &str) -> Result<f32, String> {

fn render(args: &[String]) -> Result<(), CliError> {
validate_options("render", args, RENDER_OPTIONS)?;
let path = *positional(args).first().ok_or("render: missing file")?;
let path = *positional(args, RENDER_OPTIONS).first().ok_or("render: missing file")?;
let page: usize =
flag(args, "--page").unwrap_or("1").parse().ok().filter(|page| *page > 0).ok_or("bad --page: expected a positive page number")?;
let page_index = page - 1;
Expand Down Expand Up @@ -688,6 +748,10 @@ fn render(args: &[String]) -> Result<(), CliError> {
pdfcraft_render::RenderWarning::ContentTruncated => {
let _ = writeln!(std::io::stderr().lock(), "warning: page {page}: content past the page's safety budget was skipped");
}
pdfcraft_render::RenderWarning::ColourFallback => {
let _ =
writeln!(std::io::stderr().lock(), "warning: page {page}: a colour that could not be computed was painted in a fallback colour");
}
}
}
Ok(())
Expand All @@ -696,7 +760,7 @@ fn render(args: &[String]) -> Result<(), CliError> {
/// Child-process body for `check`: prints one JSON line.
fn check_one(args: &[String]) -> Result<(), CliError> {
validate_options("check-one", args, CHECK_ONE_OPTIONS)?;
let path = *positional(args).first().ok_or("check-one: missing file")?;
let path = *positional(args, CHECK_ONE_OPTIONS).first().ok_or("check-one: missing file")?;
let dpi = parse_dpi(flag(args, "--dpi").unwrap_or("36"))?;
let start = Instant::now();
let bytes = read(path)?;
Expand Down Expand Up @@ -802,7 +866,7 @@ fn check(args: &[String]) -> Result<(), CliError> {
);
let dpi = flag(args, "--dpi").unwrap_or("36").to_string();
parse_dpi(&dpi)?;
let files = collect(&positional(args));
let files = collect(&positional(args, CHECK_OPTIONS));
if files.is_empty() {
return Err("check: no PDF files found".into());
}
Expand Down Expand Up @@ -985,9 +1049,9 @@ fn run(args: &[String]) -> Result<(), CliError> {
}
return Ok(());
}
let tool = *positional(args).first().ok_or("run: missing tool name (see `pdfcraft-cli tools`)")?;
let tool = *positional(args, RUN_OPTIONS).first().ok_or("run: missing tool name (see `pdfcraft-cli tools`)")?;
let mut obj = serde_json::Map::new();
for kv in positional(args).iter().skip(1) {
for kv in positional(args, RUN_OPTIONS).iter().skip(1) {
let (k, v) = kv.split_once('=').ok_or(format!("run: expected key=value, got {kv:?}"))?;
let value = serde_json::from_str(v).unwrap_or_else(|_| serde_json::Value::String(v.to_string()));
obj.insert(k.to_string(), value);
Expand Down Expand Up @@ -1053,7 +1117,7 @@ fn ui(args: &[String]) -> Result<(), CliError> {
// Refused rather than truncated, which would quietly reach some other port.
let port = u16::try_from(port).map_err(|_| format!("{file}: {port} is not a port number"))?;
let token = info["token"].as_str().ok_or(format!("{file}: no token"))?;
let pos = positional(args);
let pos = positional(args, UI_OPTIONS);
let method = *pos.first().ok_or("ui: missing method (state, inspect, click, drag, type, key, command, commands, set, open, screenshot)")?;
let method = if method.starts_with("ui.") { method.to_string() } else { format!("ui.{method}") };
let mut params = serde_json::Map::new();
Expand Down
86 changes: 86 additions & 0 deletions apps/pdfcraft-cli/tests/it/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,89 @@ fn non_edit_commands_reject_unknown_options_before_io() {
assert!(stderr.contains("unknown option") && stderr.contains(unknown), "{args:?}: {stderr}");
}
}

/// photocraft#6944: an option took the next token whatever it was, so a line with a value missing
/// in the middle of it did something else entirely — `extract in.pdf --pages 1 --out --password`
/// wrote the PDF to a file called `--password` and exited 0. The unknown option hidden in a value
/// position passed the check above too, since the walk skipped it along with the option.
///
/// Only the options whose value is a file or folder refuse one: a password or a title can be any
/// string, `--page` included, which `text`'s own tests pin and this does not change.
#[test]
fn a_file_option_refuses_a_value_that_is_another_option() {
let dir = std::env::temp_dir().join(format!("pdfcraft-cli-values-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
for (option, args) in [
("--out", vec!["extract", "missing.pdf", "--pages", "1", "--out", "--password"]),
("--out", vec!["combine", "a.pdf", "b.pdf", "--out", "--nonsense"]),
("--out", vec!["render", "missing.pdf", "--out", "--dpi", "96"]),
("--out-dir", vec!["split", "missing.pdf", "--every", "2", "--out-dir", "--password"]),
("--json", vec!["check", "missing.pdf", "--json", "--dpi"]),
("--control", vec!["ui", "--control", "--out", "state"]),
// `edit` parses its own options, in the order given, and had the same gap.
("--out", vec!["edit", "missing.pdf", "--out", "--full"]),
] {
let out = Command::new(BIN).current_dir(&dir).args(&args).output().expect("run cli");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{args:?} unexpectedly succeeded");
assert!(stderr.contains(&format!("{option} needs a value")), "{args:?}: {stderr}");
assert!(stderr.contains("another option"), "{args:?}: {stderr}");
}
assert_eq!(std::fs::read_dir(&dir).expect("read dir").count(), 0, "nothing was written");
let _ = std::fs::remove_dir_all(&dir);
}

/// And a file whose name really does start with `-` is still reachable, the way the shell reaches
/// one and `--root` already documented: `./--name`. The error is then about the input file, which
/// is as far as these lines get here.
#[test]
fn a_file_whose_name_starts_with_dashes_is_still_reachable() {
// The error is about the file the command could not open, so the test asks which file it
// names -- not how the OS words "not found", which differs on Windows (#3849 review).
for (names, args) in [
("missing.pdf", vec!["extract", "missing.pdf", "--pages", "1", "--out", "./--dash.pdf"]),
("missing.pdf", vec!["edit", "missing.pdf", "--out", "./--dash.pdf"]),
("./--dash.pdf", vec!["info", "./--dash.pdf"]),
] {
let out = Command::new(BIN).args(&args).output().expect("run cli");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{args:?}: no such file either way");
assert!(stderr.contains(names), "{args:?}: the error is about {names}: {stderr}");
// Not a usage error: the `./--dash.pdf` reached the command as a file name.
assert!(!stderr.contains("needs a value") && !stderr.contains("unknown option"), "{args:?}: {stderr}");
}
}

/// The token after an option that takes no value is not its value: `check-one --edit file.pdf`
/// reported "check-one: missing file", the file having been read as `--edit`'s operand.
#[test]
fn a_flag_that_takes_no_value_does_not_swallow_the_file() {
for args in [vec!["check-one", "--edit", "missing.pdf"], vec!["check-one", "missing.pdf", "--edit"]] {
let out = Command::new(BIN).args(&args).output().expect("run cli");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{args:?}");
// Which file the error is about, not how the OS says it is missing (#3849 review).
assert!(stderr.contains("missing.pdf"), "{args:?}: the error is about the file: {stderr}");
assert!(!stderr.contains("missing file"), "the file was found, and then not found: {stderr}");
assert!(!stderr.contains("needs a value"), "{args:?}: {stderr}");
}
}

/// photocraft#1108: an argument that is not UTF-8 panicked in `std::env::args` ("called
/// `Result::unwrap()` on an `Err` value"), while collecting the arguments — so it panicked even
/// for `--help`. Such a name cannot be used here, where paths are `str`, so it is a usage error.
#[cfg(unix)]
#[test]
fn an_argument_that_is_not_utf8_is_an_error_not_a_panic() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

for args in [vec![OsStr::new("info"), OsStr::from_bytes(b"/tmp/caf\xe9.pdf")], vec![OsStr::new("--help"), OsStr::from_bytes(b"caf\xe9")]] {
let out = Command::new(BIN).args(&args).output().expect("run cli");
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code(), Some(1), "an error, not a panic (101): {stderr}");
assert!(stderr.contains("not valid UTF-8"), "{stderr}");
assert!(!stderr.contains("panicked"), "{stderr}");
}
}
5 changes: 5 additions & 0 deletions crates/annot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ deterministic; the engine supplies them.
signature) and PdfCraft's rubber stamps counterrotate their appearance on rotated pages, so
the picture or label reads upright as the page is displayed. Restyling a picture stamp keeps
that appearance matrix. The rotation is the page's `/Rotate` at placement.
- Sticky notes and file attachments are not counterrotated: they carry the NoRotate flag and
their icon is drawn in an unturned 20 × 20 box. A viewer keeps them upright on a turned page,
about the upper-left corner of `/Rect` (ISO 32000-2 §12.5.3). PdfCraft's renderer does so
(vendored hayro-interpret patch 37), and `Summary::no_rotate` tells the UI where the icon is
drawn.

## Not yet

Expand Down
Loading
Loading