A font the document does not embed was replaced by whatever the caller
happened to pick, and nothing told the user. `doc_info` said only
`embedded: false`, which is why "my Chinese PDF shows nothing" has been
a mystery rather than a sentence.
`crates/fonts/src/fallback.rs` applies one chain, in a documented order,
and returns what it chose with a reason:
1. embedded -> nothing substituted;
2. /BaseFont names a standard-14 face or a known alias (exact metrics);
3. craft-fonts holds that exact family;
4. /CIDSystemInfo /Ordering names a script we have a face for;
5. the ordering names a script we have no licensed face for -- report
none, so nothing is drawn rather than the wrong glyphs;
6. otherwise the nearest of the fourteen by descriptor flags.
It delegates to the existing per-script selectors instead of restating
their order, so each keeps one source of truth. It answers the
dictionary question only; "which face has a glyph for this character?"
depends on the string being drawn and stays where the drawing happens.
`doc_info` gains `substitute` and `substitution` per font. Both null
means the document's own glyphs are shown; a null `substitute` with a
reason means no face was available at all. That is Acrobat's Actual
Font, minus the interface, which `parity/` now records as still missing.
Two findings on the way:
`Std14::from_base_font` matches loosely, so "SomeHouseSerif" resolved to
Times by its name containing "serif". The face is a fair guess but
reporting it as "names one of the standard 14" would have been untrue of
a font nobody has heard of. `Std14::recognise` now says whether a name
matched an alias or only a family hint, and the two report differently.
`from_base_font` is unchanged.
The generated table moved to `crates/fonts/src/std14_metrics.rs`, data
alone, and the ATTRIBUTION entry and AGENTS.md 1.1 line follow it. F1
put the hash on a file holding both the table and the code reading it,
so this commit's ordinary edits tripped the gate -- and a hash that
churns on unrelated changes gets re-attested reflexively, which is the
protection gone. Verified both directions after the split: editing
std14.rs passes the gate, changing one width in std14_metrics.rs fails
it.
Every new test was watched failing first: removing the script step
breaks the two unit tests and the doc_info integration test.
Co-Authored-By: Claude <noreply@anthropic.com>
Rebase onto 7163ba3 (396 commits on). Three adaptations, none of them
behavioural:
- `crates/fonts` renamed its `arabic` module to `shaping` and added
Thai, so `Family::width`'s callers moved; the italic argument is
re-applied to all six, including one new call site main added in
`draw_laid_shaped`.
- `Metrics::text_of` now returns `Cow<str>`, so the F2 tests compare
through `as_deref()`.
- `unicode_map` writes into a struct field now, so `/Differences`
inserts go to `out.encoding`.
`SMALL_ITEM_FINGERPRINT` in added/arabic_tests.rs is re-pinned, and this
is the part worth reviewing. That golden is the appearance-stream pin
F1's acceptance criteria asked for, which landed from the Arabic work
after F1 was written; it caught the change correctly. The item's Latin
run is "Hi " in Helvetica 12pt. The old character-class guess called H
680, i 260 and space 260 (14.4pt); the published metrics are 722, 222
and 278 (14.664pt), verified against the table. The Arabic clusters that
follow start 0.264pt further right, which is the intended effect: the
glyphs now land where the font says they do. The Arabic path itself is
untouched.
Also dropped from the rebase: a re-added ATTRIBUTION entry for
vendor/hayro-interpret/assets/LAB.icc, which main deleted in favour of
CGATS001Compat-v2-micro.icc. Carrying it would have attributed a file
that no longer exists.
Co-Authored-By: Claude <noreply@anthropic.com>
Two gaps in code-to-Unicode, both found by probing the current
behaviour rather than by reading the code.
A font with no /Widths and no /ToUnicode fell back to StandardEncoding
whatever it was, so /Symbol code 0x61 -- `alpha` in Symbol's own
built-in encoding -- decoded as "a". ZapfDingbats and MacExpert were
read as Latin the same way. That map is what `redact` matches patterns
against, so a wrong character there is a silent redaction failure, and
a wrong character is worse than no character. Symbol, ZapfDingbats and
MacExpert now yield no mapping at all unless the font names a base
encoding, which is always honoured as the deliberate statement it is.
This keys on the base font *name*, not the /FontDescriptor symbolic
flag. A great many ordinary Latin TrueType subsets set that flag and
still rely on a Latin encoding, so suppressing on the flag would lose
real text across a large share of real documents. A test pins that
restraint so it is not "fixed" into a regression later.
Glyph names now follow the published naming algorithm instead of two
of its cases: a name is cut at its first `.` (`a.sc` is `a`), split on
`_` into components (`f_i` is "fi"), and `uniXXXX` carries any number
of four-digit values (`uni00660069` is "fi"). Names are resolved whole
or not at all, so half a ligature is never returned. `glyph_text`
returns a String for this; `glyph_unicode` stays as the single-character
view. Hex parsing rejects the leading `+` that `from_str_radix` would
otherwise accept, and a lone surrogate half.
Both halves were verified by reverting each fix and watching the
matching tests fail. The parity note on protect.redact-patterns now
says Symbol-encoded text is skipped rather than mis-decoded.
Still open, and an owner decision: Symbol and ZapfDingbats could be
mapped properly with a glyph-name-to-Unicode table, but the vendored
Adobe Glyph List is not on the AGENTS.md 1.1 closed list.
Co-Authored-By: Claude <noreply@anthropic.com>
pdfcraft_fonts::helvetica_width was a fifteen-line guess by character class,
and seven crates generated appearance streams through it: annot, edit, forms,
ocr, redact, sign and xfa. Times was Helvetica x 0.9, bold was x 1.05, Courier
a flat 0.6. Against the published metrics a Times capital A is 722, so that
estimate said 600 -- a 17% error on a letter, compounding across a line.
Std14 carries the real advance widths for all fourteen faces, indexed by
character code: WinAnsiEncoding for the twelve text fonts, which is what we
write into the appearance streams we generate, and their own built-in
encodings for Symbol and ZapfDingbats. The tables are generated by joining the
published encodings with the per-glyph widths.
helvetica_width now delegates, so every call site gained exact Helvetica with
no change. The three places that faked another face by scaling it -- Family
in edit::added, the paragraph measure in edit::text, and the redaction overlay
fonts -- now select the right face instead. So does Metrics::width in
fonts::pdf, which measures existing document text when a font carries no
/Widths; that path feeds redaction's glyph removal, where a wrong width means
cutting the wrong glyphs.
Tests spot-check the table against values typed in from the specification by
hand, so it is verified against the spec rather than against whatever produced
it; plus Courier being fixed pitch everywhere, full printable-ASCII and Latin-1
coverage, Symbol and ZapfDingbats not being WinAnsi, /BaseFont aliases and
subset prefixes resolving, and hostile text staying finite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* M12: preview text structure proposals for auto-tagging
* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check
The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12: preview text structure proposals for auto-tagging
* Keep read-only auto-tag preview separate from unavailable command
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: support per-font visual-order Indic text and nukta reordering
- Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with
Indic codepoints without OpenType GSUB tables, or legacy visual font names).
- Implement Indic visual-to-logical and logical-to-visual pre-base vowel
reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed
nuktas and conjuncts.
- Apply reordering during extraction and source font reuse only when visual_order
is set, leaving modern logical-order text untouched.
- Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs
and font visual_order tests in crates/edit/src/tests.rs.
* Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding
- A font is visual order only when it is a known legacy Indic family (subset tag
allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is
no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer
decodes every embedded font program on every call.
- Lines with /ActualText text are already logical: skip indic::to_logical, so
"बढ़िया" no longer becomes "बढ़यिा".
- reading_lines no longer runs bidi recovery on every line (it changed RTL
reading_blocks output for all documents); only the Indic recovery remains.
- indic.rs indexes through get().
- Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit
round-trip, logical ActualText in a legacy font, and a subset Noto font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A simple font whose /Differences puts Euro at code 300 encoded "€" as
the single byte 0x2C, which the same font decodes as a comma. Simple
fonts show one-byte codes, so a larger code now has no glyph to encode
with: encode returns None and the caller falls back to another font.
The code stays in the decoding table as before.
Adds pdf::tests::differences_codes_past_one_byte_are_not_encoded,
which fails before this change.
A /W entry such as [4294967295 [100 200]] added the run index to its
first CID unchecked, so the second width overflowed: a panic with
overflow checks on, and with them off a width given to CID 0, which the
entry does not name. The run now ends at the last CID it can address;
/W [0 [100 200]] reads as before.
Adds pdf::tests::composite_width_run_past_the_last_cid_is_dropped,
which fails before this change.
* M0: remove upstream trademark artwork from contributor fork
Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: group compatible PDF words and hide source text during editing
Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.
Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: preserve rich PDF regions and preview text edits in place
Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.
Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: keep cropped artwork from blocking text editing
Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.
Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: resize cropped images with their clipping paths
Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.
Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: complete source font subsets and preserve clipped editing
Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.
Co-authored-by: OpenAI Codex <codex@openai.com>
* Revert "M0: remove upstream trademark artwork from contributor fork"
This reverts commit 3aa539b5b6.
* Test: give the text layer up to 4 s before copying (it flaked on a loaded machine)
---------
Co-authored-by: Kamoliddin Fatkhullaev <komoliddinfatxullayev@MacBook-Pro-Komoliddin.local>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M7: support shaped Thai text in Add Text and UI fallback fonts
Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations.
Constraint: Thai font assets remain in the companion craft-fonts repository.
Confidence: high
Scope-risk: moderate
Co-authored-by: Codex <noreply@openai.com>
* M7: show Thai Add Text before and after the fix
Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures.
Constraint: Synthetic content and licensed fonts only; system font fallback disabled.
Co-authored-by: Codex <noreply@openai.com>
* M7: demonstrate Thai stress cases in the comparison screenshot
Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler.
Co-authored-by: Codex <noreply@openai.com>
* CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai)
ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move
from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so
Thai Add Text is built in and tested.
* ATTRIBUTION: drop the screenshot entry again (the image isn't committed)
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14: text the fonts can't draw is kept and reported, never written as ? silently
Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX].
* Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Hebrew fix
* Tests: the faux-redaction audit gets its own work folder (it shared "audit" with the space audit, so the two raced)
* Arabic golden test: compare bytes only in a build without an Arabic face (CI's Linux job builds with craft-fonts)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Dancing Script, the typed-signature face, is Latin and Vietnamese only, so
script_outline skipped every Cyrillic or Greek character and a Serbian
Cyrillic name came out empty. Characters the script lacks are now drawn from
the bundled Inter Regular, slanted about 12 degrees and scaled to the
script's capital height. Characters neither face has are still skipped.
Inter's bytes are now exported as pdfcraft_fonts::INTER_REGULAR and the UI
uses that static, so the font is still embedded once.
Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M7: keep embedded CMap ranges compact instead of truncating expansion
A ToUnicode CMap's sequential bfrange used to expand into one HashMap
entry per code, and the parser stopped at 2^20 expanded codes. A valid
CMap with more codes than that lost every mapping past the cap.
Each sequential range is now one record. Codes below 65 536 find their
covering record in a table, higher codes by binary search over a cover
index; the later declaration wins where ranges and explicit entries
overlap. Explicit entries keep their strings. The budget counts an
explicit entry as one and a range as two, since a range covers at least
two codes, so every CMap with at most 2^20 codes is kept whole. encode
finds the lowest code per character from the same records.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* ToUnicode: size the low-code tables to the codes a font covers
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14: Arabic interface language
A 2,136-entry Arabic catalog (six CLDR plural forms, Arabic month and
weekday names), selected by `ar` and every `ar-*` locale. egui joins
Arabic letters but places words left to right, so the catalog is put
into display order when it loads (bidi::display_rtl); placeholders,
keyboard shortcuts and caption colons stay in place. The command
search matches a multi-word Arabic query word by word.
The craft-fonts pin moves to the revision with Noto Sans Arabic.
crates/fonts/build.rs now refuses to embed Adobe type designs, so the
Noto Sans CJK SC in that revision is never embedded (AGENTS.md §1.1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Arabic catalog follows the {key} shortcut labels and has Redaction code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Add text, headers/footers and watermarks wrote every character outside
WinAnsiEncoding as `?` and reported success. They now refuse it with a
specific error ("the standard fonts can't draw "中" (U+4E2D) ...") before
changing anything, as the maintainer asked on #327. Form fields and
text-box comments are unchanged (they keep the value).
- fonts: win_ansi_encodable / first_non_win_ansi, defined through
win_ansi itself so a fuller table widens them too; the edited-text
Type 3 decision uses the same check (same character set as before).
- edit: one drawable() check, fed the lines exactly as each tool draws
them, so a CR that would become `?` is refused and CRLF in headers is not.
- ui: the Add-text editor keeps such text open with an inline warning.
Done, a click elsewhere or a double-click on another item don't take
or replace it; it counts as unsaved work; Save stops and shows it; a
deleted page takes its draft with it.
- Arabic (#403): one public check, first_undrawable, follows the drawing:
Arabic shaped with the craft-fonts face is drawable, and only what's left
to the standard font counts. Without the face, #403 still decides for the
Arabic letters (new items refused with what's missing, existing ones stay
movable), but nothing else in the item may become `?`. The editor uses
the same check, cached per draft.
- #403's odd_arabic_text_never_panics: its U+2029/U+0085 input, which was
drawn as `?`, now expects a refusal naming the character.
- The warning is translated (fr, ru, zh-hans, zh-hant, es, ja, bg, de); the
catalogs' SHA-256 entries in ATTRIBUTION.toml are updated.
Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6.2: form fields in Unicode CID fonts show Japanese instead of "?"
Japanese forms often give their fields a /DA font that /DR defines as a Type0
font with a predefined Unicode CMap, such as a non-embedded HeiseiMin-W3 with
/UniJIS-UCS2-H. Field appearances accepted only simple fonts, so they fell back
to Helvetica and WinAnsi, and every Japanese character was drawn as "?".
Appearances now use such fonts and encode the text in the CMap's form (UCS-2,
UTF-16, UTF-8 or UTF-32), measuring full-width characters as one em so centring,
auto size, comb cells and multiline wrapping fit. Identity-H and embedded CMaps
still fall back to Helvetica.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6.2: vertical Unicode CMaps fall back to Helvetica, since field appearances are horizontal
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
pdfcraft_fonts::win_ansi, edit's is_win_ansi_char and the xtask demo
encoder each carried a hand-written subset of WinAnsiEncoding's 0x80-0x9F
row (quotes, dashes, euro, trademark) and left out Š š Ž ž Œ œ Ÿ ƒ † ‡ ˆ
˜ ‰ ‹ ›. Text editing therefore treated š/ž as non-WinAnsi and sent them
to the Type3 fallback font (or refused them without craft-fonts), and
every other generator (forms, annotations, OCR layer, redaction overlay,
signatures, create) wrote them as '?'.
win_ansi now goes through a new win_ansi_byte, which reads the 0x80-0x9F
codes from the same ISO 32000-2 Annex D table the text extractor decodes
with, and is_win_ansi_char reuses it. xtask doesn't depend on
pdfcraft-fonts, so its demo encoder gets the full row with a pointer to
the shared table. č/ć are not in WinAnsi and still use the fallback.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Add text wrote Arabic as "?" (standard-14 fonts, WinAnsi). Items with
Arabic are now shaped with the craft-fonts Arab face through harfrust
(already in the tree via egui) and laid out line by line in display
order with unicode-bidi (UAX #9), so Arabic mixed with Latin, digits and
brackets reads correctly. Each cluster (a letter with its dots or marks)
is one glyph of a Type 3 font drawn from the face's outlines, with a
ToUnicode entry, so text extraction, copy and search get every letter
once and no font file is embedded.
- fonts: shape_arabic / arabic_glyph / arabic_has / document_arabic_font;
japanese_glyph's bounded outline code is shared as bounded_outline.
- edit: draw_arabic and type3_arabic; wrapping measures shaped words.
Fonts get names free on the page (a full save renumbers objects, not
resource names) and are removed when the item is updated or deleted.
Outlines are read before anything is written.
- Without the face, adding Arabic is a clear error that changes nothing;
an existing item holding Arabic still moves, drawn as "?" as before.
- Non-Arabic items are drawn exactly as before.
Refs #250, #280, #369.
Co-authored-by: fares <aw674860@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The font-metrics CMap parser (ToUnicode and embedded CID CMaps) split the
stream on whitespace and sliced hex tokens as UTF-8 text:
- a non-ASCII byte inside `<...>` sliced a character in half and panicked
- `bfrange` arrays and `/Differences` incremented code counters past
u32::MAX, and CID lookup added `c + (code - lo)` unchecked
- 4 KB of overlapping `bfrange`s could ask for millions of map entries
- legal CMaps with no space between tokens (`<0001><0070>`) mapped
nothing, so their text was not found for editing
A byte-level lexer now reads hex strings (whitespace inside allowed),
brackets, names, literal strings and comments. Counters use checked or
saturating arithmetic, array entries stop at the range's `hi`, and a CMap
adds at most 1 Mi ToUnicode entries and 64 Ki codespace/CID ranges. A bad
array entry still stands for its code, as before.
Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Replacing text in a serif line (Times New Roman, say) with Cyrillic failed
with "Japanese fallback font has no glyph for U+0444". The Type 3 fallback
took only the style's first face, Shippori Mincho, which has no Cyrillic,
although BIZ UDMincho and BIZ UDPGothic ship alongside it.
`document_japanese_fonts_for_style` lists every Japanese face in the
existing order of preference, and `type3_font` uses the first one that has
a glyph for every character. If none has them all, the preferred face still
names the missing character, as before.
* M7: select real Japanese fallback faces by family and weight
Refs #184. Keep Type 3 font-name reporting separate in #387; missing weights and the small web font input remain explicit limitations.
Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)
* Test the Japanese fallback name for a sans source: the Gothic face since #394 matches the source style
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Telugu interface font
- The interface takes `Telu` faces from craft-fonts after the CJK and Arabic ones, on the web too
- Test: Telugu text has glyphs in every family and its conjuncts are shaped
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M14: Telugu interface language
- Complete te catalog: 1,984 entries plus whole-phrase Undo/Redo; te and te_IN locales use it
- Undo and Redo labels use a catalog phrase when the language has one (Telugu puts the verb last)
- Tests, docs, attribution, parity and ROADMAP
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Add my contributor name (chiranjeevi-max)
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M14: catalog contexts for labels with two meanings
- tl_ctx! gives the signature pad's Type, the Action Wizard's Start and the comment menu's Edit their own context; Telugu translates them, other languages keep their current translation
- Preferences' All tools goes through the catalog in every language
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M14: Telugu for the look-alike web address warnings
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M14: Telugu for the image signature notices
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Telugu for Open Recent (#323)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A tab named in Arabic showed replacement boxes: no interface font had the
glyphs, and egui joins the letters of a right-to-left word but places the
runs in logical order.
- Fonts: the interface takes `Arab` faces from the craft-fonts build input
(also on wasm), and on desktop adds one font already installed on the
machine as the last fallback of every family. It is read at runtime, never
embedded; PDFCRAFT_SYSTEM_FONTS=0 turns it off and `cargo xtask screenshots`
sets that. AGENTS.md §1.4 records the rule.
- Order: bidi::visual puts single-line text in display order with a
left-to-right base, used for the painted tab label and hover text, the
recent-files name and path, and the Properties File and Location rows.
Accessibility names keep the logical text.
misc.rtl-ui stays planned: multi-line text, text input and a mirrored layout
are not done.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Czech interface language
Adds Czech (Čeština, code `cs`) next to English and Japanese:
Preferences ▸ Interface language, `ui.set language cs`, `ui.state`
and persisted preferences all accept it.
- i18n: `Language::Cs`; `tr()` picks the language's table with a match
(English has none), panic-free. The CZECH table (83 entries) covers
every label passed to `.tr("…")` (main menu, View zoom/theme/side-panel
entries, Preferences dialog), every registered File/Edit/Pages/View/Help
command shown by `registry_menu`, and every key of the Japanese table.
Written clean-room from the English meaning, not from any Acrobat
localisation.
- Tests: Czech covers every registered menu title and label, every
`.tr("…")` literal found by scanning the crate's sources, and the
Japanese keys; nothing is left identical except an explicit KEEP_AS_IS
list ("OK"); ellipses match the source; keys are unique per language;
`cs` round-trips through set_option, ui.set/ui.state and persistence.
- theme: the font setup is split into `font_definitions()`, and a test
checks with skrifa (dev-dependency, already in the tree via egui) that
Inter Regular/Medium/SemiBold and JetBrains Mono draw all Czech letters
and every Czech translation, so nothing falls back to Shippori Mincho.
- The `language` error lists `cs`; docs/localization.md, the parity note
for misc.prefs-language and the ROADMAP log mention Czech.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: translation catalogs, same system as PhotoCraft
Move the interface translations to PhotoCraft's i18n system: one TSV
catalog per language (crates/ui-egui/src/i18n/ja.tsv), the tl! macro,
entries keyed by command id, context or plural forms, and catalog tests
(format, duplicates, placeholders, command ids).
The 49 existing Japanese strings move over unchanged, plus "Auto". The
language preference now defaults to "auto" and follows the system
language (English when it has no catalog); "en" and "ja" still persist
and unknown values are rejected.
Update docs/localization.md, ROADMAP.md and the misc.prefs-language
parity entry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: add Traditional Chinese (Taiwan) UI localization
Co-Authored-By: Codex <noreply@openai.com>
* M14: translate shortcuts and expose language preferences in Edit menu
Add command-comma and verify language switching through the actual selector.
Co-authored-by: Codex <noreply@openai.com>
* M14: expand Traditional Chinese dialogs, panels and diagnostics
Preserve opaque values in translated templates and keep application diagnostics at the UI boundary. Extend native control and template regression coverage.
Co-authored-by: Codex <noreply@openai.com>
* M14: finish Traditional Chinese signature prompts and history labels
Translate generated action labels without changing filenames or field names. Complete checkbox styles, resolution labels and native picker filters.
Co-authored-by: Codex <noreply@openai.com>
* M14: complete Japanese dialogs, panels, shortcuts and diagnostics
Expand Japanese from 49 to 1830 interface entries, expose language preferences in the Edit menu, preserve opaque template and history values, and localize known application diagnostics only at the UI boundary. Add control, persistence, template and desktop/web glyph regressions.
Co-authored-by: Codex <noreply@openai.com>
* i18n: Simplified Chinese UI language option
Adds Language::Zh alongside en/ja (--language zh, preferences switch,
persisted): full Chinese translation table covering every user-visible
label, generic Hans font fallback plumbing, and a build.rs skip-guard
plus test so Noto CJK is never embedded (AGENTS.md 1.1). Pairs with
storytold/craft-fonts#5 (ChillRoundF Regular, OFL-1.1 Adobe-free
chain), which supplies the Hans glyphs. Also fixes a startup deadlock
from translating inside lazy widget_info closures.
* i18n: add Brazilian Portuguese (pt-br) UI translation
Adds Language::PtBr (code pt-br, endonym Português (Brasil)) with serde rename so it persists as "pt-br", registers it in Language::ALL (Preferences ▸ Interface language and ui.set/ui.state) and translates the same 49 menu/command keys as the Japanese table. Extends the i18n tests (table parity, codes, persistence) and updates docs/localization.md.
* M14: strict catalog validation, one-pass fmt, translation assets
Fold in the infrastructure parts of #175:
- `fmt` fills placeholders in one pass, so inserted values (a file name
containing `{n}`) are never substituted again.
- Catalog validation rejects unknown escapes, unknown `@` contexts,
placeholder, ellipsis and plural-form mismatches and duplicates. The
bundled catalogs must have no errors (tests); at run time a bad line is
still only skipped and logged, so the rest of the catalog loads.
- `.tsv` files count as assets in `cargo xtask assets`, with a
`translation` kind that may never come from Adobe; ja.tsv gets its
ATTRIBUTION.toml entry.
- Control-channel test: switching the language keeps documents, their
dirty state and command ids.
Also make the drawing language thread-local, as in PhotoCraft, so
parallel kittest harnesses can't change each other's language.
Co-Authored-By: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Brazilian Portuguese as a TSV catalog
Move #165's Portuguese table into crates/ui-egui/src/i18n/pt-br.tsv
(same 49 strings, plus Auto) and register it in LANGUAGES with
PhotoCraft's Portuguese plural rule (0 and 1 singular). Any Portuguese
locale (pt_BR, pt_PT, pt) follows the Brazilian catalog, as in
PhotoCraft. The catalog is an attributed asset (ATTRIBUTION.toml), and
the docs and parity notes list the new code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Czech as a TSV catalog
Move #68's Czech table into crates/ui-egui/src/i18n/cs.tsv (PrintCraft
labels renamed PdfCraft, plus Auto) and register it with PhotoCraft's
Czech plural rule (one / few / other). #68's tests become catalog
tests: Czech covers every registered menu title and menu command label,
keeps ellipses, uses no three-dot ellipsis or stray whitespace, and only
leaves "OK" untranslated; the code persists and `cz` is rejected. The
source-scanning `.tr("…")` test is dropped: strings now go through tl!
and the catalog tests check consistency.
The font-coverage test moves to tests/fonts.rs and covers both Latin
catalogs (cs, pt-br) against Inter and JetBrains Mono. It keeps skrifa
as a dev-dependency (the version pdfcraft-fonts already uses): egui's
has_glyphs can't answer it, since a family with only the primary face
also uses that face as its replacement face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: route every UI string through the catalog; Japanese as ja.tsv
Port #141's wiring to the catalog API from land/106:
- `crate::i18n::tr(ui, s)` becomes `tl!(s)`, `tr_fmt`/`tr_template`
become `i18n::fmt(tl!(..), ..)`, and `Language::get`/`store` (the
egui temp-data copy of the language) are gone: the drawing language
is the per-thread `i18n::current()`, set from the preference at the
start of each frame. Worker threads (export, OCR, batch actions) take
the UI's language with them for their summaries.
- ja.rs (1,830 pairs) becomes ja.tsv, merged with the existing entries
and renamed PrintCraft -> PdfCraft. The 29 diagnostic templates of
errors.rs are dropped.
- `Language::diagnostic` and errors.rs are dropped: matching engine and
OS messages against English templates breaks silently whenever the
wording changes. Error details are shown as they are, inside the
translated frame ("{label} failed: {e}").
- History labels (`command_label`/`action_label`) move into i18n and keep
captured file and field names; registry menus prefer `@id` entries.
- Preferences: Auto plus every registered language, relabelled at once.
- Tests: Japanese covers every command and All tools entry; Japanese
labels have glyphs with the desktop faces and with the web subset
(with craft-fonts); history labels; #141's control tests, with the
error-detail expectation updated.
Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Preferences in the Edit menu, Cmd/Ctrl-comma
Split out of #141: register app.preferences in the Edit menu with the
platform's usual shortcut (Command-comma on macOS, Ctrl-comma
elsewhere), so the interface language can be found and changed with no
document open. Shortcut keys may now be punctuation (egui's key name
","). #141's control test drives the menu item, the language selector
and the shortcut in Japanese and English.
Co-Authored-By: sakamoto824 <sakamoto824@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Traditional Chinese as zh-hant.tsv
Reduce #140 to its translations: zh_tw.rs becomes
crates/ui-egui/src/i18n/zh-hant.tsv (PrintCraft renamed PdfCraft; the
29 errors.rs diagnostic templates dropped, as for Japanese) and one
registry row, code `zh-hant` (Taiwan vocabulary). Locales zh_TW, zh_HK,
zh_MO and zh-Hant-* resolve to it. #140's own copy of the UI wiring and
its errors.rs diagnostics are not taken: land/141 carries the wiring.
Tests: registration, locale mapping, every command label translated.
The catalog is an attributed asset; docs, ROADMAP and parity list it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Simplified Chinese as zh-hans.tsv
Reduce #112's translations to crates/ui-egui/src/i18n/zh-hans.tsv
(PrintCraft renamed PdfCraft) and one registry row, code `zh-hans`
(#112 used `zh`; `zh`, `zh_CN`, `zh_SG` and `zh-Hans` locales still
resolve to it through the system-language match). #112's own copy of
the UI wiring is superseded by land/141. Tests: registration, locale
mapping, menu coverage. The catalog is an attributed asset; docs,
ROADMAP and parity list it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: CJK font order follows the UI language
From #112: `ui_cjk_fonts(prefer_hans)` orders the craft-fonts CJK faces
for the interface, and `theme::font_definitions_for` installs them. In
Simplified Chinese the `Hans` faces come first; otherwise the Japanese
ones (the previous behaviour). egui draws each character with the first
face that has it, so with Japanese first a Simplified-only character
(欢) would land in another face and sit on a different baseline. The
fonts are reinstalled when the language crosses that line (same family
names, so named fonts stay valid while the new set loads).
Changes from #112: within each group BIZ UDPGothic still leads, so a
serif listed first in the manifest (Shippori Mincho) never becomes the
Chinese-mode UI fallback; the unused `document_chinese_font` is left
out; and the crates/fonts/build.rs special case that skipped
"Noto Sans CJK SC" (and its test assertion and README line) is not
taken. Without CRAFT_FONTS_DIR, or without a `Hans` face, the order
logic yields only what exists: Chinese characters then show replacement
boxes and nothing panics (tests/fonts.rs, the fonts crate test, and the
control test switching ja -> zh-hans -> en).
Also document `--language` on the app's command line.
Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Petr Leo Compel <petrleocompel@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Simon Leblanc <contact@leblanc-simon.eu>
Co-authored-by: sakamoto824 <sakamoto824@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-authored-by: RicSchonfelder <ricschonfelder@icloud.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
* #163: allow replacing saved signatures and render long names fully
Add saved signature and initials previews with change/remove/add controls in
Fill & Sign. Preserve placed annotations and saved values on cancellation.
Use bounded line outlines for long names and fit typed placements to 150 pt.
Co-authored-by: Codex <noreply@openai.com>
* cargo fmt after the rename
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Shippori Mincho (and its OFL file) leaves assets/fonts/: the Japanese fonts now
live in storytold/craft-fonts. printcraft-fonts gets craft-fonts' build.rs recipe:
with CRAFT_FONTS_DIR=<checkout> it embeds the manifest's fonts as CRAFT_FONTS
(wasm32: only BIZ UDPGothic Regular); without it CRAFT_FONTS is empty and the
app builds, tests and runs as before, minus the Japanese face.
- SHIPPORI_MINCHO is now Option<&[u8]> from CRAFT_FONTS; japanese_glyph
(was shippori_glyph) uses document_japanese_font() (Shippori Mincho, then
BIZ UDMincho, then any Jpan face) and returns GlyphError::NoFont without one.
- Editing PDF text into Japanese without craft-fonts is a clear EditError that
leaves the page untouched.
- UI: every Jpan craft font (BIZ UDPGothic first) is the last fallback of
every egui family, sharing the same static bytes.
- Tests: Japanese glyph/edit tests skip without craft-fonts; new UI tests for
no-tofu Japanese with craft-fonts and working fonts without it.
- CI: the Linux test job checks out craft-fonts at a pinned SHA and runs the
tests with CRAFT_FONTS_DIR. Releases build every platform with it
(CRAFT_FONTS_REQUIRED=1) and ship each font's OFL as OFL-<family>.txt.
- ATTRIBUTION.toml gains [[build_input]] (checked by xtask assets); NOTICE,
AGENTS.md, CLAUDE.md, README and docs say fonts live in craft-fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Comments: a text box's rectangle follows its edited text
Editing a text box (or typewriter or callout) rewrote /Contents but
kept the old /Rect: the appearance clipped the new text to the old box
and silently dropped lines that no longer fit, while shorter text left
an oversized box. set_contents now re-measures the text with the same
Helvetica widths as at creation and re-fits the box: the wrap width and
top edge stay, the height fits the wrapped lines. A callout's leader
line keeps its place and re-attaches to the moved box (that geometry
moved from set_rect into a shared apply_text_box). A locked box keeps
its text editable, so it re-fits too.
Covered by annot tests (grow and shrink with nothing clipped, callout
leader re-attach, locked box) and a comment_edit round trip through the
automation tools; parity entries cite them.
Co-Authored-By: GLM <noreply@z.ai>
* Polish edit text and images tool
* feat(edit-text): select source font style
* fix(edit-text): preserve detected PDF font styles
* feat(edit-text): support Japanese replacements
* fix(forms): escape non-ASCII format script strings
* Forms: unwrap_or instead of map_or with identity (clippy map_or_identity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- js: `host()` returns a script error instead of panicking when the host state is missing.
- optimize, a11y: the graphics-state stack is read with `last`/`last_mut` instead of `expect`.
- fonts: the standard-14 ASCII map is built from `u8`, which can't fail.
- filters: zlib encoding into a `Vec` keeps the standard's narrow, documented `expect` exception.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>