Some producers head a classic xref table "1 n" and then write object 0's
free entry first, so every row is one number off and object 1 reads as
free. The renderer coped, but cos did not: printing a scanned invoice
built from cos dropped object 1 (page 1's image) and printed the page
blank. Such a subsection is now read from 0 when its second row locates
an object that is numbered 1, in both xref readers, and the repair is
logged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The find-step shortcuts sat after the canvas's "egui wants keyboard
input" check, so they did nothing while the find field had the focus
(Enter worked). They now run before it, while a search is open, as in
Acrobat and Preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Remove Hidden Information and Sanitize took a link or comment off its
page's /Annots, but a tagged PDF's structure tree still referenced it
through an /OBJR, so the full rewrite kept the annotation, its URL or
text included, while link_list reported none. The annotations removed
from every page are now collected, and tags::drop_object_refs drops the
structure elements' object references to them (direct or indirect,
array or single kid), with the same seen-set bound as tags::clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Saves write a staging file and rename it over the target. On Unix the
rename ignores the target's permissions, so a read-only file was
overwritten without a word and came out writable. The shared atomic
writer now refuses a read-only target before staging anything, with an
error that points to Save As; the edits stay unsaved in the document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
A cover counted only when it hid 40% of the line's words or box, so a
black box over one number in a long line, and an unapplied redaction
mark over it, audited clean while the number was still extractable. A
cover that hides half of any one word now counts as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Explorer reads /select,"<path>". The path went in as one ordinary
argument, which Rust quotes whole when it contains a space
("/select,C:\a b.pdf"), and Explorer answers that by opening Documents.
A file saved to a folder or name with a space (often after Save As) was
therefore never shown. reveal_command now quotes the path after the
comma and the Windows runner passes the arguments as written; a path
containing a quote, which Windows doesn't allow, is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
the Revert prompt to zh-hant.tsv; merged together, the catalog held both
copies and `bundled_catalogs_are_consistent` (and the tests that load
zh-hant as a reference) failed on main. Keep #1750's wording (字型大小,
「」 quotes, as the rest of the catalog) and drop the other copy.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nine features carried `evidence`, `notes` (and once `tools`) twice, so the
file was no longer valid TOML and `cargo xtask parity` stopped with
"duplicate key". Each pair is merged into one: evidence and tools as the
union of both lists, notes keeping every fact either side added.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: arrow keys follow the Scroll speed preference (#985)
The preference set how far a wheel notch scrolls, but the arrow keys
always moved 40 points. They now move 40 points times the preference's
factor (half at Slow, double at Fast), Page Down / Page Up still move a
screen, and the preference is labelled for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(print): the Print dialog waits for the printer list instead of offering Save as PDF (#160)
The first time the dialog opens, the printer list is read in the
background and no printer is selected yet, so the dialog showed Save as
PDF and its button saved a PDF. It now says it is looking for printers
and the button waits until the list arrives or the user chooses Save as
PDF. An empty list says that no printers were found. The button's
Print / Save as PDF label is translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Four ui_group harnesses waited at most one wall-clock second (200 x 5 ms)
for the page to render, and stopped as soon as nothing was pending, which
can be before the text layer is even asked for. Under load the text
layer came later and column-select, identity and update-check tests
failed. They now wait through support::wait_until for what they use (a
glyph on screen, the highlight in the document), with its 30-second
deadline. The highlighter quick-click test of #1277 itself did not fail
on this base in 256 runs.
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: Save asks before writing over a changed file, one tab per file, locked autosaves (#2166)
Save compares the file on disk with the version the document came from
(its open, reload or last save) and asks before writing over another
one, with Save as... to keep it; the changed-on-disk notice says what
Reload and Save each do. A file that is open already comes to the front
instead of opening twice. A running instance holds an exclusive lock
file in the recovery folder and names it in its snapshots, so a second
instance neither offers nor discards what the first is autosaving.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M3: the pending save exists only where files do (#2166)
The web build has no file to compare against, so the struct and field
are desktop-only; the wasm check gained no warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
New files from Combine, Extract, Split and Create, signed, timestamped and
LTV-embedded files and the OCR batch output reopen with their pages before
anyone writes them, as saves already did. The desktop app, the automation
tools (with or without --root) and the CLI's edit, combine, extract and
split write through pdfcraft_platform::staging::write_atomically, so a
failed write or --out naming the input never leaves a half-written file.
Special files such as /dev/stdout are still written as given.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A rewritten resources dictionary, font, XObject or graphics state of a
signed page changes what the page shows. It was judged by the keys its
new version carried, so adding /FT or /Subtype /Widget made it a
permitted form fill. The roles now come from the signed revision's own
object graph, walked from each page's resources with bounds; a category
dictionary that only gained a name is judged as before.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Without the mark, 5.1 reads the file in the system code page and the
ellipsis on line 136 decodes into something that swallows a quote, so the
script does not parse (MissingEndCurlyBrace at line 133) and package.ps1,
which runs it with the shell it was started from, fails after building
the MSI. pwsh reads UTF-8 either way.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* M3: a document that asks to open in Full Screen mode asks first (/PageMode /FullScreen)
The Initial View option "Open in Full Screen mode" was written and read
but never acted on when opening. Now the app asks, as Acrobat's alert
does: Full Screen enters it, Not now leaves the window as it is. A
document cannot take the screen on its own.
Refs #379, #1459.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
pdfcraft_render::transition gives the picture of a page transition at a
moment of its run: which parts of the outgoing and incoming page to paint,
where, and how opaque, in page fractions, for all twelve styles and every
direction. Pure and tested; the full-screen presentation will paint from
it when it plays transitions.
Refs #379.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: set page transitions and display times (/Trans, /Dur) through the engine and the tools
organize::set_transition writes or removes a page's /Trans dictionary
(only the entries the style reads and that differ from the defaults) and
/Dur, with every value checked before any page changes; Edit::SetTransition
is one undo step ("Set page transitions", translated in every catalogue);
page_transition_set is the tool. The counterpart of reading them (#2074);
playing them and the Organize dialog stay planned.
Refs #379.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Each page's presentation settings of ISO 32000-2 s12.4.4 are read with the
spec's defaults for what is missing or out of range: the transition style,
duration, dimension, motion, direction, Fly scale and backdrop, and the
/Dur display time. doc_info lists them; pdfcraft-cli info counts them.
This is the data full screen needs to play transitions and auto-advance;
playing and editing them stay planned.
Refs #379, #2070.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Highlight appearances written by other editors are a transparency group whose
content is filled with `/BM /Multiply`. Every `/Group` form got a layer of its
own, so the Multiply blended with that layer's transparent backdrop and the
layer then covered the page as an opaque block.
Vendored hayro-interpret patch (28): a group that is neither isolated nor
knockout composites its elements with the backdrop (ISO 32000-2 §11.4.7), so
when it is entered at full opacity with no soft mask and Normal blending there
is nothing for a layer to add, and its content is drawn straight onto the page.
`/I` and `/K` are read from the group dictionary. An isolated group keeps its
blank backdrop; a knockout group or one entered at partial opacity, under a
soft mask or with a blend mode keeps its layer.
Test: `raster::tests::a_multiply_highlight_inside_a_transparency_group_keeps_the_text`
(the appearance drawn as an annotation and from page content, with `/I false
/K false` spelled out and with no group at all; an isolated group as a control;
a group entered at half opacity still composited as one). It fails before the
fix with the text pixel yellow instead of black. parity: view.render-transparency
cites it and says what is still approximated.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
`--pages` (extract), `--before` (split) and the edit page options accepted
comma lists only, so "1-3,5" was refused as a bad page number. A list now
takes single pages and inclusive ranges, in the order given, with the length
of a range bounded. The out-of-range message, which named the page one too
low in 0.7.1, is pinned by a test now that #1550 fixed it.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
To classify what later revisions changed, the signed bytes were reopened
with Document::open, without a password, so for a file opened with a
nonempty user password the earlier revision was unreadable and the earlier
signature became Invalid as soon as it was counter-signed. Document::
open_with_same_credentials opens such bytes with the current document's own
security handler when their /Encrypt and /ID are the same (no password is
needed again or kept), and the signature validator uses it.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The loopback listener spawned a thread per accepted connection and read its
first line with no deadline and no limit on how many could wait, so any
local process could hold threads for ever before knowing the token. Each
connection now has an absolute deadline to authenticate (10 s from
acceptance, re-armed per read so a byte at a time cannot extend it), at most
8 may wait at once (the rest are refused on the accepting thread with an
error and closed), writes time out, and a waiting place is given back on
authentication and on every exit.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A widget with no appearance to draw (none stored, or none for its selected
state) was removed from the page and the field tree with nothing in its
place, so the field's value was lost; a reply kept because it could not be
drawn was then deleted anyway as a reply of the flattened comment. Both now
stay as they are. A widget whose appearance draws nothing is still removed,
as is a reply note's empty appearance with its comment.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The page Copier recursed once per reference and once per nested array or
dictionary with no bound, so a source whose page reached a chain of thousands
of shallow indirect objects overflowed the stack in Insert Pages, Combine,
Extract, Split and Duplicate; the memo map only cut cycles. The copy now keeps
a depth of at most 256, refuses with an OrganizeError beyond it, and
import_pages_with frees the objects a refused copy added, so the destination
is as it was.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
add_field gave text and date fields a fixed /Helv 12 Tf, which is clipped in
a box shorter than 12 pt, and a script can't see the clipping. They now get
/Helv 0 Tf (Auto), as Acrobat's new text fields do: 12 pt where there is
room, smaller where there isn't. An explicit size set afterwards still wins;
choice fields keep 12 pt.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The phone pattern tried every position, so the last groups of a French
social security number, an IBAN or a SIRET matched as a trunk-0 or NANP
number once the groups before them were skipped, and the start of the
identifier stayed visible. A phone number is now a whole run of digit groups:
one reached through separators from a digit before it, or going on past the
28-character cap, is not one.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A relative path that doesn't exist below the root was refused with the path
as typed and the system's localised message, so a script written for
another working directory ("sub/a.pdf" with --root sub) gave nothing to
correct from. The refusal now names the resolved path and the root; paths
outside the root keep their uniform refusal.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A malformed EKU left extended_key_usage at None, which reads as "no
restriction", so an issuer-signed certificate with a broken EKU could reach a
Valid verdict; EKU being on the processed-critical list let a critical one
through as well. Like keyUsage, a present EKU now restricts the key even when
it doesn't parse: it must be a SEQUENCE whose members are all OIDs, otherwise
it allows no purpose.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The redaction audit turned an re rectangle into an unknown path on a
redundant h, and its fill branch left out the close-fill-stroke operators b
and b*, so those opaque covers over extractable text produced no finding.
Keep a closed rectangle closed under h and treat b/b* like the other fills.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
encrypt_dict multiplied any V4 StdCF /Length up to 32 by 8, so a large
negative value overflowed (a panic with overflow checks, a wrapped length
without). A zero or negative length is meaningless; read it as if absent, the
128-bit default, which also fixes small negatives that used to clamp to a
40-bit key and fail to open.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
clear_parent_tree walked /Nums with chunks_mut(2) and indexed pair[1], so a
trailing key without a value that matched the page's StructParents panicked
after the page content had already been replaced. Walk whole pairs only and
leave an unpaired key as it is: it maps nothing, so there is nothing to clear.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* i18n: Taiwan vocabulary and the missing entries in zh-hant
The Traditional Chinese catalog says it uses the vocabulary of Taiwan,
but 113 entries used mainland terms, often next to the Taiwan term for
the same word elsewhere in the file (對話框 and 對話方塊, 應用 and
套用 for Apply, 列表 and 清單). They now read 套用, 捨棄, 對話方塊,
另存新檔為, 自訂, 目前, 清單/清單方塊, 置中, 平面化, 選取, 送出 (submit),
浮水印, 醒目提示, 一般, 進階, 重設, 存取, 全螢幕, 範例, 標題列,
工具列, 收件者 and 字型大小, and quotations use 「」 instead of “”.
Application (應用程式) and Commit (提交) keep their words.
The 80 entries the Simplified Chinese catalog has and this one lacked
(About ▸ Contributors and Models, link and email confirmations,
preferences defaults, Create PDF from images, print scale notes, Fill &
Sign) are translated from the English, so those labels no longer show
in English. Reviewed by a native Taiwanese speaker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: zh-hant Grab bag reads 精簡
The contributor list's compact view keeps the chosen sort order, so it
is named for its look (精簡, against 表格), not as an unordered pile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Chinese next to Latin in one label sat a few percent of the font size
above or below the Latin, depending on the installed Chinese face. egui
centres a fallback face's line box on the primary face's instead of
lining up their baselines, so the fallback's baseline moves by half the
difference of ascent + descent (less half the difference of the line
gaps): +0.047 em for Microsoft JhengHei, +0.034 em for Microsoft YaHei,
-0.075 em for SimSun under Inter.
Fallback faces (the installed system face and the craft-fonts faces) now
get a FontTweak::y_offset_factor that cancels that shift, computed from
their own metrics with the skrifa egui uses, so any face lands on Inter's
baseline. skrifa moves from the native-only dependencies to all targets
(egui already brings it to wasm).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Carry the watched path and epoch through queued file reads, and reject stale results before changing the document. Preserve newer saved contents and undo history across Save and Save As while retaining ordinary external reloads.
Fixes#1802
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>