A pre-publish review caught both comment errors, and Apple's own documentation settles the
rest of it:
- installed_faces_switched_off said "both loader tests". It now guards three sites — two
loader tests plus the macOS oracle — ever since 21dc64e added the third call without
touching this sentence.
- MACOS_FONT_DIRS claimed the Telugu, Sinhala and Tamil lists "still name one folder
each". They do not: Telugu names /System/Library/Fonts, Supplemental/ and /Library/Fonts
(four paths), Tamil three, Sinhala two. The accurate statement is that no family in
those lists is looked up in both system folders, which is the property Devanagari now
has and they do not. Their first choices all exist on macOS 26.6, verified again here.
- The oracle's doc comment now records the authority for treating listed faces as optional:
Apple's "Fonts included with macOS Tahoe" page (support.apple.com/122869) is titled
"Installed or downloadable fonts" and states that most install automatically while others
download via Font Book — without marking which entries are which. Absence of a face is
therefore expected on some Macs, which is exactly what the reported-not-failed behaviour
accommodates.
Comment-only change: fmt --all clean, clippy -D warnings quiet, system_fonts 13 passed with
and without PDFCRAFT_SYSTEM_FONTS=0.
Found while testing crates/forms/src/af.rs:166 (Parser::string)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A content stream that uses l, c, v, y or h before any m or re has no
subpath to extend. The edit-side image scan (crates/edit/src/images.rs)
handed the operators straight to BezPath's line_to/curve_to/close_path,
which panic on an empty path by contract, so object_list, page_images,
image_hit and the Office export's image walk all crashed on such a page,
while the renderer, text extraction, MuPDF and pdftotext skip the
malformed segment and render the rest.
The scan now skips the segment when the path has no elements, mirroring
the v arm's existing guard and kurbo's own condition (debug_assert on
!is_empty). A path started with m or re extends and closes exactly as
before. A clip set by a stray segment (W n on an empty path) takes a
zero bounding box, which kurbo defines as Rect::default, not a panic.
Regression test: each of the five segment operators before any m, then
a well-formed m l h and a re W n clip, all before an image Do - the
image is still listed in every case.
Verified against kurbo 0.13.1's source (line_to/curve_to/close_path
debug_assert on empty; bounding_box unwrap_or_default) and the issue's
repro; no Rust toolchain on this machine, so cargo test was not run
here - saying so plainly for the reviewer.
Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
Round-6 review caught that this branch rejected is_file() for one path and kept using it
forty lines above. The family loop filtered candidates with is_file(), which follows symlinks,
so an entry that exists but no longer resolves was classified "not installed" and skipped —
exactly the defect this branch fixes elsewhere.
Now both blocks share one rule: presence via symlink_metadata, then a hard requirement that
the entry resolves. Absence is still tolerated and reported; a broken entry fails loudly with
the family named, because that is the failure we are trying to catch rather than a machine
difference. Checked against this Mac first: no family or last-resort path is present-but-dangling
here, so the stricter rule cannot produce a spurious red on current macOS.
Also corrected two stale comments — the tripwire test is separate from the layout test *below*
it, and the oracle's doc comment now states the exception the PR description already does: a Mac
where nothing in the list covers Devanagari fails, since such a machine cannot validate anything.
Verified solo on macOS 26.6 (Apple M5 Max), no concurrent writers this time: fmt --all clean;
clippy -D warnings all-targets quiet; system_fonts 13 passed with and without
PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed / 0 failed / 5 ignored; xtask layers (37 crates)
and assets (272/6/22). Mutation evidence for the change, via font folders redirected to /tmp so
system paths were untouched: with a dangling DevanagariMT.ttc in the system folder the current
code fails at "exists but does not resolve", while reverting to is_file() filtering makes the
identical defect pass silently. Re-ran the pinned regressions too: cap raised to 512 MiB,
MAX_FACES cut to 1, probe swapped to Arabic alef, last_resort derived from first(), misspelling
restored — all red.
Review on #2994: fill rectangles and the clip were kept as their page-space
bounding boxes, so a rectangle turned or sheared by the CTM could pass as
covering the page while leaving its corners out, and a following fill became
Word's page colour. Each rectangle is now kept as its four page-space
corners, the clip as the rectangles it intersects, and the crop box counts as
covered when its corners lie inside each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review follow-ups on #2994: under even-odd any second rectangle, under nonzero
one wound the other way, can leave a hole the Word page colour would cover.
The parity note says the page colour hasn't been opened in Word yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A page coloured with Edit a PDF > Background exported to Word as a
white page: export pages carried text and images only, and the Word
section has no page colour.
- pdfcraft-edit `page_background`: the colour of the first thing a
page paints, when that is an opaque fill (no soft mask, Normal blend)
in a plain gray, RGB or CMYK colour, of a path with a rectangle
covering the visible page, not clipped smaller; `None` otherwise.
- `export::Page::background` carries it; the Word writer sets it as
the document background (`w:background`, plus the
`w:displayBackgroundShape` setting Word needs to show it) when every
page shares one non-white colour. Word has one background per
document, so differing colours are left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The root's MediaBox and the page's own both have four numbers, so the
length check passed even if the page lost its own value. Compare the
values instead.
Follow-up to review of this branch, no production change.
The oracle now asserts the list still ends at Arial Unicode before checking it. Without
that, deriving the path from list.first() instead of last() stayed green on this machine and
printed "not installed" — the check disappeared because /System/Library/Fonts/DevanagariMT.ttc
is absent here. Deriving the path from the list is still better than repeating a literal, but
only if something verifies it points where the test thinks.
The three retune tripwires (MAX_BYTES, DEVANAGARI_PROBE, MAX_FACES) move into their own test,
so the layout test stays about Apple's directory layout and these are labelled for what they
are: constants nothing else pins, each confirmed to be the only guard by deleting it and seeing
the matching regression go green again. Comments corrected accordingly, including not claiming
Apple moved a target when a user could equally have removed it.
Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy -D warnings all-targets quiet;
system_fonts 13 passed with and without PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed /
0 failed / 5 ignored; xtask layers (37 crates) and assets (272/6/22). Mutations re-run at this
commit: folder-major expansion, folder-order swap, typo restore, loader reversal, cap raised to
512 MiB, probe swapped to Arabic alef and MAX_FACES cut to 1 are all red; deleting the
PDFCRAFT_SYSTEM_FONTS=0 early return leaves all 1341 green, which is stated rather than hidden.
Review on #3239: a name that only numbers the glyph (C39, c251) says
nothing about the character. Reading it as the code's StandardEncoding
character invents text wherever the font's /Encoding puts the glyph
elsewhere: with /C1 at 233 it extracted O-slash for a glyph drawn as
e-acute. numbered_glyph_name and the fallback are removed; the AGL
specification's full mapping stays.
The test puts one glyph per line: the test font has none of these glyphs,
so they have no width, and on one line a later 'fi' sat on the first and
was dropped as its fake-bold copy once the guessed glyphs between them
were gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
annot::page_refs, the forms page_refs and organize's walk queued every
/Kids entry before the seen check, and organize cloned the inherited
attributes, Resources included, for each queued entry. A page tree whose
/Kids lists the same child many times, or whose nodes share one /Kids
array, held all of those entries at once before they were dropped as
already seen.
Each walk is now a small iterator with one cursor per open /Kids array,
the way pdfcraft_model::pages has walked since #1926. organize carries
inherited attributes as the nodes that set them and copies them only for
the pages it returns. Page order, the pages found, the inherited values
and the million-node cap are unchanged.
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.