Commit Graph
4320 Commits
Author SHA1 Message Date
mtlcl410andClaude Opus 5.5 7ddb6226b8 M6: hovering a form field shows its tooltip (#3854)
The field's /TU is read into FormField::tooltip and shown in the Fields
panel, but resting the pointer on the field on the page showed nothing.
page_input now attaches it to the hover, as Acrobat does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 03:37:01 +08:00
Philip 22636e3a79 Merge remote-tracking branch 'origin/main' into feat/edit-scanned-page-offers-ocr 2026-10-21 20:31:03 +01:00
Philip 23ea087fa5 Merge remote-tracking branch 'origin/main' into feat/system-cjk-font-fallback 2026-10-21 20:31:02 +01:00
Brandon ThomasandClaude Opus 5.5 3e3064532f Release: PdfCraft v0.11.0 (#3893)
* Release: PdfCraft v0.11.0

- Bump the workspace to 0.11.0.
- Fix main's red CI (cargo-deny): update rustls-webpki 0.103.15 -> 0.103.16
  (RUSTSEC-2026-0382) and webpki-root-certs 1.0.9 -> 1.0.10 (RUSTSEC-2026-0383),
  both reached through winprint's sigstore build dependency.
- `cargo xtask version set` now moves the workspace crates' pins in
  [workspace.dependencies] with the version; before, `cargo update --workspace`
  refused the bump and the pins had to be edited by hand.

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

* Tests: a healthy private retry may answer during the watchdog pass that scheduled it

shared_timeouts_retry_healthy_pages_privately_before_blacklisting failed on
CI (PR #3893): the try_recv that runs the watchdog spawns the private retries,
and page 1's retry has no gate, so on a loaded runner its healthy result
arrived before that same call read the channel. Accept that early answer
(page 1, no error) and use it as the healthy result; an error still fails.
Reproduced by sleeping 300 ms after the watchdog in try_recv: the old test
fails every time, the new one passes.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.11.0
2026-10-21 15:27:33 -04:00
Philip 26dc2c390d Merge remote-tracking branch 'origin/main' into feat/system-cjk-font-fallback 2026-10-21 20:26:34 +01:00
PhilipandClaude Sonnet 5.5 3bdc4ddcc7 M10: review of #1609: test the Edit-triggered recognition rules without OCR models
FredApps: CI never fetches the OCR models, so the tests that recognize something
skip there and the recognition paths this PR adds went untested in CI.

The decision is split out of auto_ocr_scanned_page into scan_to_read, which
needs no models (the run itself judges the page's own text), with unit tests
that run everywhere: a picture page is read and a text or blank page is not;
only Edit being on starts it (its tab counts); Read mode and an open dialog
wait; a page already read is not read again and a page found no scan is judged
again once the document changes. Two mutants (no Read/modal guard; a no-scan
page never judged again) each fail one of them. The tests that recognize words
still need the models.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:26:24 +01:00
Philip 13605becce Merge remote-tracking branch 'origin/main' into feat/edit-scanned-page-offers-ocr 2026-10-21 20:17:50 +01:00
PhilipandClaude Sonnet 5.5 e42c21a3c8 docs: AGENTS.md §1.4 — the viewer may draw non-embedded CJK fonts with an installed face
Proposed for the maintainers to accept or drop (separate commit): the page
viewer, on screen only, may use one installed face for a CID font of an Adobe
CJK collection that a PDF does not embed. Exports, printing, OCR and the
automation tools never use it; nothing drawn with it is saved or printed; no
Noto CJK, Source Han or Adobe family is a candidate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:17:27 +01:00
PhilipandClaude Sonnet 5.5 57a16aea17 M2: review of #1567: draw installed CJK fonts on screen only
FredApps: AGENTS.md §1.4 allows reading an installed font at runtime only as
the interface's last fallback and says nothing from it may enter a PDF or
package, while the renderer fallback also served exports and printing.

RenderConfig gains system_fonts, off by default. The engine sets it only on the
pool that draws the viewer (display_config); exports, printing, OCR and the
automation tools render with the document's own configuration, so nothing drawn
with an installed font is saved or printed. Tests: the render of a non-embedded
CNS1, GB1 and Korea1 font is blank without the flag and drawn with it; the
viewer's pool draws such a page and an export of it does not (it fails if the
flag is on everywhere). No Adobe-named font can be a candidate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:17:27 +01:00
eliets2 4de9459f2f Give the print preview's grayscale tag a bit of its own
The preview was always monochrome, so the Print in grayscale checkbox appeared to do nothing but flicker between the colour thumbnail and the (wrongly) greyed sheet raster.

Move GRAYSCALE_TAG to a free tag bit (1 << 59) so it no longer overlaps PRINT_TAG, and let the colour and grayscale preview requests carry different tags so set_print_grayscale actually re-renders the right one.

Test: print_preview_tag_distinguishes_grayscale_from_colour failed before the change and passes after.
2026-10-21 21:48:08 +03:00
komo1iddinandClaude Opus 5.5 6495c08afd M7: a text editing session for one paragraph
A TextSession opens a paragraph once: it keeps the document as it was
and the page's lines then. Each draft is laid out on a copy of that
document, so no draft sees another's output, and the commit writes into
the document it was opened on, refusing ("the paragraph changed while it
was being edited") when that document has changed since.

text_edit_layout and rewrite_block now open a session and lay out or
commit once, so there is one path and their output is unchanged. No
caches yet: those come next, each under the same equivalence test.

First step of #2512.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 02:15:47 +08:00
komo1iddinandClaude Opus 5.5 f632c1b0ca M7: a resized page image stays blanked where the raster still has it
Two cases kept the old picture on screen after a move or resize:

- A page that had no tiles when the edit applied was taken as drawn
  again as soon as a first tile arrived, while its backdrop still had
  the picture. Out of date now means its whole-page raster or any tile
  still waits for a replacement.
- A second drag before the page was drawn again took the picture's
  pixels from its new frame, where the raster doesn't have it, and
  blanked only that. It now takes them from where the raster still has
  the picture and blanks that, and keeps it as the source on release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:26:39 +08:00
komo1iddinandClaude Opus 5.5 764439bbcc M7: hold an applied paragraph's look until every tile is drawn, and not after a turn
The look of an applied paragraph ended with the first whole-page raster.
On a tiled page the stale tiles are painted over that raster and can
still carry the old text. End it only once the page is no longer drawn
from before the edit: its whole-page raster (a tiled page's backdrop
too) and every one of its tiles has been replaced, whichever arrives
last.

Keep the page's turn on screen with the look, too: after the view is
turned, its position would be misplaced, so the raster shows instead,
as after a zoom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:26:22 +08:00
komo1iddinandClaude Opus 5.5 08513ad906 M7: keep a paragraph open with its typing when its edit fails
Applying a paragraph takes the editor away before the edit runs. When the
edit failed (a letter no font can draw, for one), the error was shown
and the typing was gone with the editor. A refused form field already
opens again with its value.

Keep the editor whose edit was queued (applying it, or clicking into
another paragraph). If that edit fails it opens again with what was
typed and takes the focus, instead of the box the click moved to; a
successful edit drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:13:49 +08:00
Adrian 946a77cf9b M4: a new bookmark goes to where the reader is (#3865)
A bookmark made in the app always went to the top of the page. It now goes to
the top-left of the selected text, or with nothing selected to the top of the
window, as /XYZ left top null: the reader's zoom is kept, and with only the top
pinned so is the horizontal position.

- render: PageInfo::xyz_for_view, the inverse of dest_fraction. It pins only
  the coordinates given; on a page turned a quarter the displayed y decides the
  user left and the displayed x the user top.
- organize: add_bookmark_at and OutlineError::BadPosition (a coordinate that is
  not finite is refused before anything changes).
- engine: Edit::AddBookmark takes left and top (None, None is the page top, as
  before).
- automation: bookmark_add takes top and left in points from the top-left of the
  displayed page, as bookmark_list reports them, bounded and refused when not a
  usable number.
- ui: DocView::bookmark_position. It follows the window's vertical when the view
  is turned a quarter (View > Rotate View), picks the selection's corner that
  shows at the top-left, and in a spread of unequal pages uses the page that
  crosses the window's top edge.
- parity: organize.bookmark-add evidence and what is still missing.
2026-10-21 12:12:03 -04:00
komo1iddinandClaude Opus 5.5 b5ba176023 test: the paragraph editor is drawn at full colour from the click's own frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:09:21 +08:00
komo1iddinandClaude Opus 5.5 1f335a2bad M7: an added picture resized by a corner shows only at its new size
The page without an added item is rendered once per gesture so the
item's pixels can follow a move (MoveBackground). A corner resize got
none of that: only the outline followed the corner, the picture stayed
at its old size under it for the whole drag, and showed there again
after the release until the page was rendered.

Give a picture's corner resize the same background, and stretch its
pixels from the box it had into the dragged box, then into the applied
box while the page settles. Text resized by a corner rewraps, so it
keeps the outline only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:45:00 +08:00
komo1iddinandClaude Opus 5.5 7a7d39f01f M7: keep a moved or resized page image in place until its page is drawn again
A dragged page image is drawn from the page raster, blanked where it was
and stretched into its new frame. On release the edit applied and the
drag state cleared in the same frame, but the page raster on screen
still had the picture at its old size and place until its replacement
arrived. For those frames the old picture showed again around the new
frame, then jumped.

Keep the old and new frames on the selection after a release, and draw
the picture the way the drag did while the page is out of date: its
raster, or on a tiled page any tile, still waits for a replacement. A
failed edit never makes the page stale, so nothing is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:15:28 +08:00
komo1iddinandClaude Opus 5.5 f7fc209498 M7: keep an applied paragraph's look until its page is drawn again
Applying a paragraph rewrites its page, but the page raster on screen
still has the old text until the page is rendered again. The textless
background and the editor went away on the frame the edit applied, so
for those frames the old text came back under the paragraph's new
outline, and then jumped to the new one.

The editor now remembers how it drew its text each frame. When the
edit that closes it is that paragraph's, the textless page and that
last look stay on screen until the page's new raster arrives, as
MoveBackground does for moved items. Any other edit, a zoom, or the
raster ends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:10:44 +08:00
Combiler01 bc12134b51 Keep marked-content sequences whole when arranging images (#3145) 2026-10-21 20:40:24 +05:30
komo1iddinandClaude Opus 5.5 68c85bce6e M7: open the paragraph editor without a fade-in
The paragraph editor is an egui Area, and Areas fade in over the style's
animation time (0.2 s) each time they appear. While the editor faded, so
did its cover and its frame: the page's own copy of the paragraph showed
through it under the editor's text, and the frame pulsed from dim to
full. Every click into a paragraph did this.

Show the in-place editors at once: the paragraph editor, the editor for
text added in this session, the Fill & Sign text box and the form field
editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:06:23 +08:00
Combiler01 ca20261b4f Broken Test Fixes 2026-10-21 20:26:47 +05:30
komo1iddinandClaude Opus 5.5 475f375ff4 M7: keep a word on its line when its spacing differs by less than a thousandth of an em
Joining runs into one line compared their character and word spacing to
1e-6. Word documents exported through macOS Quartz write 0.0001 Tc on
some words, so a line like "for company AEROSOL" split into "for
company A" and "EROSOL", two paragraphs that edit separately.

Spacing below a thousandth of an em, the unit glyph widths are given in,
can't be seen. Compare character and word spacing to that; scale and
rise still have to match exactly, and visible spacing still keeps runs
apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:41:17 +08:00
Hamza 755266cf9f cli: the argument bugs photocraft#7071 lists are live here too
Three of them, measured on main:

    pdfcraft-cli extract f.pdf --pages 1 --out --password
    (exit 0, and the PDF is in a file called `--password`)

    pdfcraft-cli check-one --edit f.pdf
    pdfcraft-cli: check-one: missing file

    pdfcraft-cli info /tmp/café.pdf          (that é as one Latin-1 byte)
    thread 'main' panicked at library/std/src/env.rs:878:51:
    called `Result::unwrap()` on an `Err` value

- An option whose value is a **file or folder** refuses a value that is another
  option, and names the way out for a file that really starts with `-`:
  `./--name`, as `--root` already offered. That also closes the gap the option
  walk left for those: an unknown option in a value position was skipped along
  with the option it followed, so `combine --out --nonsense` wrote `--nonsense`
  instead of saying what `--nonsense` was.

  Only those options. A password or a title can be any string, `--page`
  included — `an_option_shaped_password_is_not_parsed_as_a_page_flag` pins
  that, and a bad `--page` or `--dpi` is reported by the command in its own
  terms either way.

- `positional` asks the option table which tokens are values, instead of
  skipping the one after every option. The token after a flag that takes none
  is an argument: `check-one --edit f.pdf` read the file as `--edit`'s operand
  and then reported it missing.

- Arguments come through `args_os`, so one that is not UTF-8 is a usage error
  rather than a panic. `std::env::args` panics while collecting them, so even
  `--help` ended that way.

tests/options.rs covers each, and each fails without its fix (the file called
`--password` comes back; "check-one: missing file"; exit 101 with that report).
2026-10-21 18:45:04 +05:00
Combiler01 72028ba3cf fix: update rustls-webpki to patch RUSTSEC-2026-0382 2026-10-21 18:54:08 +05:30
Combiler01 96d738677b Update webpki-root-certs to 1.0.10 (RUSTSEC-2026-0383) 2026-10-21 18:44:57 +05:30
Combiler01 925241b502 Removed normalized-coordinate heuristic in crop_image_impl 2026-10-21 18:31:16 +05:30
Combiler01 cf336c2ff2 Stored the added-image rotation in the direction used by AddedImage.rotation 2026-10-21 17:49:31 +05:30
robertobulltech 1f78e2f37d M3: resize Pages inspector thumbnails (#3808) 2026-10-21 13:24:58 +02:00
johnpetersa19andCopilot App 8ea9049809 M14: update Brazilian Portuguese parity notes
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-21 08:19:51 -03:00
johnpetersa19andCopilot App 4eaba2bbcd M14: complete Brazilian Portuguese catalog
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-21 08:14:32 -03:00
Combiler01 b3b555ff5e Fixes the bug regarding duplicate images 2026-10-21 16:33:31 +05:30
Nikolai Nyegaard ca56271678 Preferences: list the languages alphabetically by their own name
- English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones
- The list used the registry order, which is the order the languages were added
2026-10-21 12:01:37 +02:00
Unhumanje cf565db3fe fix(annot): a comment listed many times gets one display appearance
with_missing_appearances_cached checked each /Annots entry against the source document, which never gets the appearance, so an annotation listed K times added K appearance streams to the display copy. The engine writes that copy on open and after every edit, so the display bytes grew with the number of listings: 20,000 listings of one Ink comment in a 136 KB file came to 428 MB.

Each annotation reference is now handled once per call, across all pages.
2026-10-21 11:24:48 +02:00
Unhumanje d335ff375d fix(xfdf): a field name full of periods no longer overflows the stack on export
The XFDF export nested one <field> element per period-separated part of a field's full name. A /T can hold periods of its own, so a name can have far more parts than the field tree has levels. Past 64 levels the import refuses the file the export wrote, and around 100,000 parts the recursive write overflows the main thread's stack and aborts the process.

The export now nests at most as many levels as the import reads back. The parts past the last level stay in the deepest element's name, and the import joins them back into the same full name.
2026-10-21 11:17:56 +02:00
Marco Arnulfo 8444a08c00 fix(cos): a repaired xref table fills only the numbers the damage leaves out (refs #1628) 2026-10-21 10:53:02 +02:00
Marco Arnulfo 57ab6fcb2a fix(cos): the renumbering check follows a shifted header (refs #1628)
With junk before %PDF the table's offsets are relative to the header, so the "1 G obj" check is also tried there, as load does; otherwise object 1 stayed free and a full save dropped the image.
2026-10-21 10:22:02 +02:00
Marco Arnulfo 062270da50 fix(cos): fill xref gaps on the compact cross-reference index (refs #1628)
main now keeps valid tables as a compact index; the gap filling reads entries through it and materializes them only when it repairs.
2026-10-21 10:18:13 +02:00
Marco Arnulfo 4e94aa1cbb fix(cos): find objects a broken cross-reference table leaves out (fixes #1628) 2026-10-21 10:18:13 +02:00
Dylan M. Taylor 4164951453 Merge pull request #2753 from dylanmtaylor/ci/shared-nix-workflow
ci: switch to shared nix workflow from craft-common-actions
2026-10-21 03:59:40 -04:00
Dylan M. Taylor d7481cca6e Merge remote-tracking branch 'origin/main' into pr-2753
# Conflicts:
#	.github/workflows/nix.yml
2026-10-21 03:41:36 -04:00
Gert Pellin 6beecdacf9 M7: Header & footer dialog: Shrink the page content checkbox (#2140)
The Header and Footer dialog (also opened by Bates numbering) gets a
checkbox that turns on the shrink, and its preview shows the band the
page content shrinks into. The label is translated in every catalog (the
catalog checksums in ATTRIBUTION.toml follow). The parity entry for the
shrink is shipped, with its limits: annotations and added text and images
are not moved yet.
2026-10-21 09:02:15 +02:00
Gert Pellin 5bf48e0f8c M7: doc_header_footer takes shrink (#2140)
The tool passes shrink through to the header and footer, with an end-to-end
test that reads the moved text back from a saved copy, refuses margins that
leave no room and checks that Remove brings the content back to full size.
2026-10-21 09:02:09 +02:00
Gert Pellin 01149dea77 M7: header and footer can shrink the page content out of their way (#2140)
HeaderFooter::shrink scales the original page content evenly into the
band the header and footer leave free, centred across the page, in
display space so rotated pages shrink the way they are shown. The scale
is a content stream first inside the q/Q wrapper, tagged as part of the
header and footer, so Remove and Update take it away again and
backgrounds behind the page stay full size. Margins that leave no room
are refused before anything is drawn.
2026-10-21 09:02:09 +02:00
Unhumanje cbcdca77f0 fix(organize): imports find the objects they added when new numbers fill gaps
import_pages_with and insert_file_pages took the objects numbered above the
destination's highest number for the ones the import added. When no number
is left above the highest (an exhausted /Size, or object 4294967295),
Document::add fills free numbers below it: the copies skipped resource
sharing, and a refused import left its objects in the destination. With
object 4294967295 the `n + 1` overflowed, a panic in debug builds and in
release a wrap to 0 that counted every object as added.

Record the object numbers before the import and take the ones present
afterwards but not before.
2026-10-21 08:31:49 +02:00
Oliver HagmannandClaude Opus 5.5 ea0c1ae106 Review: a selected line or polygon moved by its body is drawn where it is going
CodeRabbit on #3788: the handles and outline of a selected line, connected lines, polygon or
cloud followed a dragged end but not a drag of the whole comment, which kept its old handles
until the page was drawn again (the frame such comments had before showed the move). The
points now follow a body drag and the released move as well (changing_points, one rule for
reshaping and moving). Test: a_moved_or_reshaped_line_is_drawn_at_its_new_points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:26:54 +02:00
Janiz-zzzandClaude Opus 5.5 0a78f31fec Add Finnish interface and date language
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>
2026-10-21 09:10:16 +03:00
Lombardi2003andClaude Opus 5.5 14b3978344 fix(edit): a header reuses a font name only for its very own dictionary
From review of #3756: a /PCTimes naming Times-Roman with more to it (a
/ToUnicode map, /Widths) passed as the same face and was replaced, so
the page's own text could lose its map. A name is reused only when it
holds exactly the dictionary a header writes, and then it is left as it
is; anything else gets a free suffixed name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:56:53 +02:00
ecaa2980ba crypt: say how the quoted vectors reproduce, and name what nothing verifies
FredApps' approval on #2359 carried one non-blocking nit that is a real defect: the header told
contributors to "regenerate with make-oracle-fixtures.py in the workspace root" - a script that
exists only in my checkout, next to a venv (.dc-tools/) likewise outside the repository. A
contributor following it finds nothing, and this repository has no committed generator for these fixtures
(crates/oracle treats qpdf strictly as an external process, and CLAUDE.md keeps the product and build in
Rust).

The header now carries a self-contained recipe, measured on this host rather than asserted:
  * one blank 200x200 page, pikepdf 9.11 / qpdf core 12.2.0, Encryption(owner=OWNER,
    user=USER, allow=Permissions(extract=False), R=<revision>, aes=…, metadata=…),
    save(static_id=True), then read /Encrypt. At revisions 2-3 `aes=False, metadata=False` are
    FORCED - pikepdf refuses AES ("Cannot encrypt with AES when R < 4") and refuses encrypted
    metadata ("Cannot encrypt metadata when R < 4") - so leaving those flags to the reader makes
    the export-grade row unreproducible. My first draft of this comment made exactly that mistake.
  * Reproduction scope, by revision: each of the four deterministic rows quotes eight dictionary
    fields (32 per comparison leg). Revisions 2 and 3 reproduce as whole files - fresh writes today
    carry the same sha256 (63f2d95b…, 39afbdd5…) as the October originals these literals came
    from. Revision 4 reproduces all eight fields, but its file moves by exactly one ~32-byte
    stream because /CFM /AESV2 prepends a fresh random IV (ISO 32000-2 §7.6.4); the dictionary
    and /ID are unchanged. Revision 6 reproduces structurally only (/ID, R/V/Length/P and the
    metadata flag); its salts are new each run.

Running the generator at the reviewer's prompting exposed that it had not been executed since its
last edit: unless both aes=False and metadata=False are forced pikepdf refuses R<4, checking metadata first, so it wrote 4 of the 6 fixtures - revisions 2
and 3 missing, including this file's headline export-grade row - while printing a "skipped:" line
and exiting 0 regardless. It writes 6 of 6 now, and every quoted field reproduces as above.

The overstatements flagged during review are corrected in this file; they are quoted verbatim,
each with the basis for its new wording, in the PR description rather than here - commit messages
cannot be edited after a push, descriptions can. Two further fixes live in this message alone: an
ISO subsection written as §7.6.4.3, a form this repository does not cite (it uses bare §7.6.4 or the
deeper §7.6.4.x.y), and one clause duplicated mid-cycle.

What nothing here verifies, stated instead of implied - established by deliberately breaking the
table (4 mutations: 2 red, 2 green): authentication never reads /Perms (a corrupted one fails
only this file's own decrypt), and renaming a row's /StmF to Identity or replacing its
crypt-filter set leaves all three tests green, so the filter names are transcription held to
review. Pinning them, and re-deriving these rows through pdfcraft_oracle::qpdf() on the runner
that installs qpdf, belong in a follow-up. Also recorded honestly: the revision-6 fixtures these
rows were copied from are gone - my own regeneration overwrote them before I archived the folder -
so those two rows rest on internal consistency plus this record, not a surviving artifact. Revisions 2 and 3 carry their own proof, byte for byte (sha256 63f2d95b… and
39afbdd5…, October and today); revision 4 reproduces every quoted field, with only the one
IV-carrying stream moving.

Scope: comments plus one assertion message (a revision 3 /U description that wrongly claimed qpdf
leaves sixteen zero bytes; its condition, len == 64, is unchanged). No assertion logic, fixture,
dependency or product code touched. Parent remains the approved cdbce8ac, so this lands as a
fast-forward rather than a rewrite of the head FredApps reviewed. Gates at this tip
(upstream/main 22bedbf2; branch 326 behind, trial merge clean): cargo fmt --all -- --check rc=0;
clippy -p pdfcraft-crypt --all-targets -D warnings rc=0; cargo test -p pdfcraft-crypt
--no-fail-fast 28 passed / 0 failed (17 lib + the 8 from #2358 plus these 3); cargo xtask layers
OK, 37 crates; cargo xtask assets ok (272 repository, 6 bundled by dependencies, 22 fetched at
build time). CI had not run on this tip when this was written - the commit was local.

Co-authored-by: Qwen3.8-Flash-Next-oQ4e-WLSRefit-MTP <noreply@qwen.ai>
Co-authored-by: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-20 21:33:55 -07:00
Eric Kaden 526d87d85c M6: formattedValue formats only a value of the ISO date's own shape
2026-1-2 isn't an ISO date and stands as it is. A test also pins that
parentheses hold a picture's locale (date(en_US){YYYYMMDD} formats) and
that a clause without braces has no pattern.
2026-10-21 00:09:00 -04:00