Found while testing crates/js/src/formcalc.rs:2169 (Str) and :2258 (Format number picture)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#2305 fixed WinAnsi 0xAD in `crates/fonts`, and closed with two loose ends it named: the
user-visible extraction path goes through hayro, not that table, so it still reported the
invisible character; and the same Annex D.2 note makes 0xA0 the `space` glyph. This finishes
both, and makes the two tables in the product agree about the same byte.
WinAnsiEncoding gives codes 0xA0 and 0xAD the glyph names `space` and `hyphen` -- the names it
also gives 0x20 and 0x2D -- and MacRomanEncoding gives 0xCA `space`. The tables called them
`nbspace` and `sfthyphen`, whose Adobe Glyph List values U+00A0 and U+00AD are invisible
formatting characters, so extraction hid ink the reader can see: "well-known" drawn with the
0xAD byte matched no search and no redaction pattern, and a non-breaking space did not match the
space a reader typed.
Evidence. Two sources transcribe the table the same way (an Annex D implementation's docs, and
the WinAnsi.enc encoding vector R ships, which gives `/space` at octal 240 and `/hyphen` at
255), and a black-box check settles the rest: on a synthetic fixture pdftotext reads the
base-encoded codes as `a-b a b`, and keeps U+00AD for a code an explicit `/Differences` names
`sfthyphen`. We now do both. Over the 989-file pdf.js corpus the soft hyphens we extract fall
from 117 to 13 and the non-breaking spaces from 30 to 26; everything left comes from `/ToUnicode`
maps, which say what they mean and are not touched.
Scope. The base table only, in the text direction only.
- A `/Differences` entry naming `sfthyphen` or `nbspace` is a deliberate statement about one
code and keeps its glyph list value, as pdftotext does.
- Glyph lookup is untouched, so a font program's own `sfthyphen` program is still found first.
In hayro that is why the alias sits in the Unicode path rather than in the generated encoding
table: `code_to_ps_name` is shared with lookup.
- Writing is unaffected: `encode` takes the lowest code showing a character, so ' ' and '-'
still write 0x20 and 0x2D, and a caller holding a literal U+00A0 still writes 0xA0.
Tests. `fonts` pins both tables and the whole decode/encode path including the `/Differences`
case; `render` pins the extraction a reader actually gets, over WinAnsi, MacRoman and
`/Differences`. Each was watched failing with each cell and each match arm reverted in turn, so
no assertion is passing by accident. fonts 70, render 296+4, redact 49, a11y 43 and edit 156 are
green, with fmt, clippy, `xtask layers` (37 crates) and `xtask assets` clean.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
CUPS drivers ship their names in other languages as PPD localization lines
(`*cupsLanguages`, `*de.Translation KMDuplex/Druckart: ""`, `*de.KMDuplex
Double/2-seitig: ""`), which the system print dialog uses. PdfCraft showed the
PPD's default (English) names in every language. `spool::printer_options_in`
now applies the lines for the UI language (`pt-br` → `pt_BR`, then `pt`;
`zh-hans` → `zh_CN`) to option, group and choice names. Each line is decoded as
UTF-8 on its own, as drivers write them in otherwise Latin-1 files; missing or
empty translations keep the driver's name. `printer_options` (automation) is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared dialog footer laid its buttons out right-to-left in all the
height left, centred. After a taller Document Properties tab the modal
kept that tab's height, so Close floated far below a shorter tab's last
row. The footer is now one row high.
Page hovers (form fields, comments, the link tool, image editing) set the
cursor from the raw pointer position, so a hand or text cursor showed
over fields under a dialog's backdrop, where nothing takes clicks. While
a modal is open the cursor starts from the arrow each frame and only the
modal's own widgets change it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Properties ▸ Space Audit shows the same bytes-by-kind table as the PDF
Optimizer's Audit Space Usage, which now shares its table code. The
audit walks the whole file, so it runs when the tab opens and again only
after an edit (keyed on the document and its edit generation). The tab
is also reachable as `--dialog space-audit`.
The audit table no longer overflows its dialog: egui's default minimum
column width stretched the narrow Bytes column past the computed widths,
widening the card by about 5 points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Description tab's plain values (File, Application, PDF producer) now
start where the text inside the Title/Author fields does, the fields use
the dialog's full width instead of a fixed 420 points, and Subject and
Keywords wrap onto more lines instead of clipping mid-word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar beside selected text closed after Highlight, Underline, Strikethrough,
Add comment and Replace text because those consume the selection. Copy kept the
selection, so the bar stayed open. Remember the copied selection and hide the
bar until the selection changes; the words stay selected for ⌘C or another
action.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 sidebar's expand button was labelled "Expand", the same as the Combine
grid's per-file Expand on its hover bar. With both on screen, the Combine
grid tests' lookup by that label found two buttons and 13 of them failed.
The buttons are now "Expand all tools" and "Collapse all tools", translated
in all 22 catalogues, and the labels are tl! literals the coverage scan sees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Combine grid stopped at 200%, too small to read a value off an invoice
while reordering the pages of a statement. The main viewer already reached
6400%, so only the card grid was capped.
- The range is 60–500%, and the buttons and keys step
60/80/100/125/150/175/200/250/300/400/500%.
- MAX_SIDE, the thumbnail texture cap, goes 768 → 1536, so a card near the top
re-renders sharper instead of upscaling a 768px texture. It only binds near the
top: a card below it asks for less and costs no more than it did. Up there one
thumbnail is a few megabytes, so TEXTURE_BUDGET and UPLOAD_BYTES_PER_FRAME keep
the cache at a screenful plus one frame's uploads — and a screenful at the top
of the range is a card or two, because a card is wider than most windows.
- The control channel's range message is derived from the constant rather than
repeating "60 and 200", so it cannot drift from the range again.
Found while testing crates/automation/src/lib.rs:1747
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The jump scrolls to MARGIN - GAP above the page. A page that fits has
nothing to scroll, and egui clamps the offset only after drawing the
frame, so the page showed 14 pt too high for a frame and then hopped
down. Clamp the jump to the scroll range before handing it to egui.
A text edit that needs glyphs the source font's embedding lacks writes
the face's full program once. It was written uncompressed, and the
document-wide search that lets another page reuse it compared the
stream's raw bytes, so it only matched an uncompressed copy.
Write the program with Flate, and compare its decoded bytes (after the
/Length1 check) both on the page and across the document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At Fit Page in single-page view, every wheel turn forward drew one
blank frame before the new page appeared.
- Lay out the shown row again after the wheel is read. The jump to the
new page was measured from the old row's place and scrolled both out
of view; going back it came out negative and was clamped, so only
forward turns flickered. The edge-turn deferral that worked around
this goes.
- Prefetch the rows either side of the shown one, so the next page is
rendered before the turn instead of showing blank paper.
AES-256 files (R5 and R6) store the permissions twice: in clear in /P,
and in /Perms, encrypted with the file key (ISO 32000-2, Algorithm 13).
We wrote /Perms but never read it, so a document opened with the user
password got whatever /P said, even when /P no longer matched /Perms.
permissions() now decrypts /Perms and, when it holds a valid block
(marked "adb"), counts a permission only if both /P and /Perms grant
it. Without a readable /Perms, /P applies as before. R2 to R4 need no
check: /P is part of their file key, so an edited /P does not open.
A turned paragraph's width came from its rows' upright bounding boxes,
turned into the baseline's frame. On a slant that box is wider than the
row by the row's height times |sin 2θ| (about 12 pt for 12 pt text at
30°), so a rewrite let a word that doesn't fit stay on its row.
Each run now also records its box along and across its baseline, from
the same corners as its upright box, and a line unions those. Grouping
and the rewrite's width read that box. Upright and quarter-turned text
are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register Catalan (`ca`) as an interface and date language with a
2,592-entry contributor-original catalog written from the meaning of the
English labels. Plurals use the one/other rule; every `ca-*` locale,
including Valencian (`ca_ES@valencia`), resolves to this catalog.
Tests cover registration and persistence, every command, menu, tool group
and section, every `tl!` literal in the UI source, and every key in the
other bundled catalogs. The Latin-script glyph test in tests/fonts.rs now
includes Catalan (the interpunct of l·l is covered by the bundled faces).
Catalan dates elide the preposition before a vowel (7 de març, 7 d'abril,
8 d'octubre), so the date language sets months_with_day, as Czech and Russian
do; a unit test pins it.
Update localization docs, the parity table and the roadmap log, and add the
ATTRIBUTION.toml entry (ATTRIBUTION.md regenerated). The contributor, a
native Catalan speaker, has reviewed the translation.
Validation: cargo fmt --check; clippy -D warnings and tests on
pdfcraft-ui-egui (i18n, fonts); pdfcraft-engine dates tests;
cargo xtask assets; cargo xtask layers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix main UI regressions in paging, fonts and test expectations
* Wait for asynchronous dropped-document preparation in regression
---------
Co-authored-by: Codex <codex@openai.com>
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>
signing_with_translated_labels_through_tools (from #1951) creates and uses
a digital ID with the password "secret1"; #2201 ("harden .p12 digital IDs")
now requires at least 8 characters. Each passed alone; together the test
fails on main, and so does every PR's test job. The password is "secret12"
in both places.
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
On Unix the staging file got the default mode for a new file (usually
0644) and was renamed over the original, so saving a private 0600 PDF
left it readable by other local users. The staging file is now created
owner-only and given the original's permissions before anything is
written. A new file still gets the usual mode. Desktop save, the CLI
and the automation tools all write through this function.
Fixes#1790.
Each edit that draws Japanese or Hebrew characters the page's fonts lack
generates new Type 3 fonts with the outlines of every character it draws.
Editing the paragraph again generated new ones and left the old ones in
the page's resources, so every edit added a set of glyph outlines to the
file.
After replace_line and rewrite_block, the page's generated fallback fonts
(Type 3, /Name /PCJapanese or /PCHebrew) that no Tf selects any more are
removed from its own font dictionary. The content read for this is the
page's streams, every form XObject it can reach that has no resources of
its own (those use the page's, ISO 32000-1 7.8.3), and the glyph
procedures of Type 3 fonts without resources. If any of those can't be
decoded strictly, nothing is removed. Registered fonts (PCFull) and the
file's own fonts are never touched.
Tests: a page with an unused generated font, one its content selects,
one only a resource-less form selects and a file's Type 3 font that only
shares the name prefix keeps all but the first after a rewrite, and keeps
everything when such a form can't be read. Rewriting Japanese text twice
(with craft-fonts) leaves only the second rewrite's fonts. The first test
fails without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shaped text (Arabic, Indic, combining marks) was written as one text
object per glyph: BT, the whole text state (Tf Tc Tw Tz TL Ts and the
colour), Tm, Tj and ET. A 15-glyph row took 15 text objects and 392
tokens (the whole stream), a long Arabic paragraph thousands of operators, and other
viewers saw every glyph as a separate piece of text.
Each run is now one text object: the state once, one Tm, and the glyphs
in TJ arrays. A glyph's adjustment moves it from where the glyph before
it ends (its width in the embedded font) to where shaping put it, and a
mark shaped above or below its letter is raised with Ts, then reset
(ISO 32000-1 9.4.3, 9.3.7). Character and word spacing are part of those
positions, so Tc and Tw are 0 in the run. The same row is now 1 text
object (90 tokens for the stream), and renders the same at 300 dpi (138 of 33
million bytes differ, by 1 level, from rounding).
The new test shapes marks beside and above their letters with Inter,
reads each glyph's origin back from the TJ displacements and checks it
against the shaped position. It fails without this change (15 text
objects in the run against 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>