CodeRabbit caught that the macOS oracle asserted unconditionally that
/Library/Fonts/Arial Unicode.ttf resolves and loads under MAX_BYTES. On a Mac
without that font the assertion fails while Devanagari coverage is perfectly
fine — the loader treats candidates as optional, and so should this test.
Hardened beyond an is_file() guard: is_file() follows symlinks, so a link
whose target Apple has moved would read as "not installed" and skip — the very
dead-path defect this branch exists to catch. Gate on symlink_metadata so a
present-but-unresolved entry fails loudly, keep the resolution and MAX_BYTES
assertions where the face exists, report otherwise. The path is taken from the
list instead of repeated here, so it cannot drift from what ships.
Also pin three load-bearing constants in the CI-visible list test, because each
could otherwise be retuned silently (verified: all three were green before):
MAX_BYTES 32 MiB — the cap that decides whether a 23 MB Unicode face is
accepted, raised or lowered both now fail; DEVANAGARI_PROBE U+0915 — what makes
a Hindi check a Hindi check, since Arial covers both scripts and was masking a
probe swap; MAX_FACES 16 — the depth into .ttc collections the loader trusts.
Verified on macOS 26.6 (Apple M5 Max): cargo fmt --all clean; clippy
--all-targets -D warnings quiet; system_fonts 12 passed in normal mode and under
PDFCRAFT_SYSTEM_FONTS=0; whole crate 1340 passed / 0 failed. Mutations: cap
lowered (2 tests red), cap raised to 512 MiB, probe swapped to Arabic alef and
MAX_FACES cut to 1 all red via the pins. One limit stays open, stated rather
than glossed: nothing verifies 16 faces is deep enough for real collections —
that needs a synthetic .ttc with the covering glyph past index 16, and here every
needed face sits at index 0.
es.tsv was missing 143 of the 2,674 (context, English) keys the other
bundled catalogs carry, so those strings fell back to English inside an
otherwise Spanish interface: every calendar month and weekday, the whole
certificate context of the signature inspector, the X.509 key usages, the
signature-revision and digital-ID lines, password validation, eight plural
messages, the combine-from-folder strings, the rendering and stale-file
notices and the tooltips that spell their shortcut out.
Spanish now covers the whole union, as lt and vi do: 2,531 -> 2,674 of
2,674, measured as docs/localization-parity.md describes. Counts there and
in ROADMAP.md re-measured against the current union; vi is restated at
2,674 for the same reason, still 100%.
Translations reuse the vocabulary already in the file (ID digital, Acercar
/ alejar, Portapapeles, A doble cara, Cortar y apilar) and were written
from the English labels, not from another implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scrolling continuously, Edit > Select All (Cmd+A) selects the text of
every page of the document; one page at a time it still selects the page
shown, as does a document of one page. Shown pages highlight as their
text arrives, and Copy gets all of it in page order, reading the text of
pages not read for display (text far away is let go to bound memory, so
it isn't kept for this). A press on a page or Escape ends it. Select All
still returns whether text is selected.
The Select All parity entry covers both modes; the viewer and copy
security tests run one page at a time, which is what they check.
`a_script_fallback_is_the_first_candidate_with_the_probe` calls
script_fallback, which returns nothing when PDFCRAFT_SYSTEM_FONTS=0 — the
switch cargo xtask screenshots sets (AGENTS.md §1.4) — so it failed in that
mode on main, independently of the Devanagari fix. Skip both loader tests with
a printed reason instead, matching crates/ui-egui/tests/fonts.rs.
Split out from the previous commit so it can be dropped without touching the fix:
PDFCRAFT_SYSTEM_FONTS=0 cargo test -p pdfcraft-ui-egui --lib system_fonts now
reports 12 passed rather than one failure.
Two of the five paths in the macOS Devanagari list do not exist —
`Supplemental/DevanagariSangamMN.ttc`, where Apple spells the family
`Devanagari Sangam MN.ttc` with spaces as this file already does for its
Telugu and Tamil siblings, and `Supplemental/Kohinoor.ttc`, one level above
where Kohinoor.ttc actually lives — so the family documented as second choice
has never been reachable. Both arrived together in #1033 and are present from
v0.7.1 through main; no CI could notice, since ci.yml has no macOS runner and
read() logs only on a hit.
List the families best-first, correctly spelled, and look each up in both
macOS system font folders. Preference is unchanged: all of Devanagari MT's
folders are tried before Sangam MN starts, Arial Unicode stays last, and at
most one installed face per script is still loaded (§1.4).
Devanagari MT wins on the machine tested, so Hindi renders identically here;
the change is that a Mac without it lands on the intended fallback instead of
skipping past it. That missing-Mac case is read off the code path, not
observed — no screenshot or log claims otherwise.
Tests: an exact assertion of the whole list (spellings, folders, order) that
runs on every OS and also pins devanagari_candidates() to it; a loader test
proving the first readable candidate wins, using two bundled faces of
different byte length; a spaced-name resolution test against a temp
macOS-shaped tree that panics rather than passing silently when the temp dir
is unusable; and a macOS-only disk oracle requiring every family folder that
exists here to be reachable, the list to cover Devanagari, Arial Unicode to
load under MAX_BYTES, and devanagari_fallback() to land on the same bytes and
face as this list. Families Apple stops shipping are reported, not failed —
that absence is what the list exists for.
Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy --all-targets
-D warnings quiet; system_fonts 12 passed; whole crate 1340 passed / 0 failed;
xtask layers (37 crates) and assets (272/6/22). Eight of ten deliberate
breakages turn a test red; MAX_FACES depth and the PDFCRAFT_SYSTEM_FONTS=0
guarantee do not, both disclosed rather than left implicit.
On a page with no content of its own, an on-top watermark is the first
stream in /Contents, so it was counted with the behind-content marks and
a watermark added behind later was drawn over it. Nothing records
whether a watermark is behind, so a new behind mark now goes right after
the last background, which is always below any on-top watermark. A newer
behind watermark goes under older ones again, as before #3591.
Choosing Label under Page Sizing & Handling swaps the paper to the
4 x 6 in label stock. Leaving Label for Size, Poster, Multiple or
Booklet now gives back the paper chosen before, unless another paper
was picked while in Label.
Fixes#3564.
Co-Authored-By: Claude <noreply@anthropic.com>
Marks behind the page content were all inserted at the start of
/Contents, so a second background went under the first and the old
colour stayed. Backgrounds now come first, then behind-content
watermarks, each in the order added. A watermark added after a
background no longer disappears under it.
page_render's hide_text_block used the paragraph number as a position in the page's current
paragraphs, so after an edit regrouped the page it hid a different paragraph from the one
text_edit would rewrite. It now resolves the number against the text_paragraphs listing and is
refused when the paragraph is gone.
text_edit also accepted any overlapping paragraph that contained the requested text as the
edited one. When the edit merged the paragraph into a neighbour, the merged paragraph contains
that text too, so the number followed it and a second edit by that number dropped the
neighbour's text. The edited paragraph must now show exactly the requested text; a merged one
leaves the number stale.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Paragraph numbers are positions in a list regrouped from geometry after
every edit, so an edit that merged a paragraph into its neighbour made the
next edit by number rewrite another paragraph and lose its text.
After text_paragraphs, text_edit and text_edit_layout find the listed
paragraph again by its text and box, and refuse one that no longer exists on
its own (list the page again). An edit's result names the numbers that are
gone in stale_paragraphs. The inline editor's switch-paragraph click holds
the same key instead of an index. Without a listing a number is a position,
as before. An identity that survives save and reopen is still #2508.
attachment_add compared every attachment name, including files attached to pages as comments, so a page comment with the same file name made the reported key empty. It now compares only the document-level name-tree keys. Deleting the last document attachment when the catalog's /Names is an indirect dictionary left an empty /Names behind; it is now removed from the catalog, as the direct case already did.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A font naming a Latvian comma-below letter by its code point (/uni0136) or by
the glyph list's name for it (/Kcedilla, /Gcedilla...) still drew nothing:
the name resolves to the cedilla name the glyph list prefers, which the
composite table does not hold. The thirteen comma-below letters are now
looked up by character, which also replaces the special case for U+021A and
U+021B. T and t with a cedilla (U+0162, U+0163) are not in that table and
keep their cedilla.
Raised by CodeRabbit on the PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`[(FIRSTARR)] [(LASTARR)] TJ` is malformed (TJ takes one array
operand), but the operator still shows the one immediately before it,
as the renderer, MuPDF and pdftotext agree. Three readers here counted
operands from the front instead and showed the discarded first array:
- crates/edit/src/text.rs (text_lines/text_paragraphs, and the Office
export through reading_lines)
- crates/redact/src/interp.rs (search-based redaction's content
interpreter)
- crates/a11y/src/headings.rs (autotagged heading detection)
So the editor could offer a word the page never draws, a redaction
search could miss it, and a heading could read as text nobody sees.
Take the last array for TJ in all three, matching the `_ =>
op.operands.last()` arm already used for Tj/'/" alongside it.
Added a regression test per crate (`a_tj_with_a_surplus_array_reads_
the_last_one`, `a_tj_surplus_arrays_undrawn_glyphs_are_not_seen_
under_a_mark`, `a_heading_with_a_tj_surplus_array_reads_the_one_the_
page_draws`), confirmed each fails without its fix and passes with it.
cargo test -p pdfcraft-edit (196 passed) -p pdfcraft-redact (81
passed) -p pdfcraft-a11y (44 passed), fmt, clippy -D warnings, xtask
layers and xtask assets all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
load_certificates now reads PKCS #7 certificate bundles (degenerate
SignedData, RFC 5652 §5.2) in DER and as PEM PKCS7/CMS blocks, the way
Windows, Acrobat and certificate authorities export chains. PEM END
lines must match their BEGIN label.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stand-ins for the standard fonts (PDFium's Foxit fonts) have neither the
comma-below letters nor a commaaccent glyph, and the composite-letter patch
left comma accents out. A non-embedded font naming /scommaaccent therefore
drew nothing, which is the blank "si" left in Romanian XFA forms, and
/tcommaaccent drew a cedilla through the glyph list's alias to U+0163.
Romanian s/S/t/T and Latvian k/K/l/L/n/N/r/R/G with a comma below are now
built from the letter and the font's comma, scaled to three quarters and set
just under the baseline, moved back along the slant in italics. The uni021A and
uni021B names of T and t with a comma below, which the glyph list
does not name, resolve by code point. Lowercase g with its turned comma above
is still not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New settings start with Highlight fields on. A stored choice is kept; a missing or
malformed value falls back to on. Preferences > Appearance gets the checkbox.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Byte-only drops (no filesystem path) have nothing for tab_showing to
match and open another tab, pinned by
opening_documents_without_a_path_still_open_twice; the drag-drop-open
note claimed focus for "a dropped document that is already open"
without that limit.