#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>
pdfcraft-fonts
Layer L2. Font helpers for the appearance streams PdfCraft generates (comments, form fields).
Today it holds what generated appearances need without a font program:
helvetica_width: the exact standard-14 advance widths, viaStd14::Helvetica. It was an estimate by character class until the generated tables landed; those tables are Adobe-authored non-visual technical data, the itemAGENTS.md§1.1 permits, carried instd14_metrics.rswithadobe_data = trueinATTRIBUTION.toml. No font program or typeface from any vendor is bundled — widths and encodings only. Reach forStd14directly for any other face.wrap: greedy line breaking with that measure (paragraphs on newlines, long words split).win_ansi: Unicode → WinAnsiEncoding bytes (?for characters it can't represent), andliteralto write bytes as a PDF literal string. Nothing writes that?silently. Callers checkfirst_undrawable(orUnicodeCMap::first_unencodable, for a character beyond U+FFFF in UCS-2), then either refuse withundrawable_message(one wording that names the character and what can be written instead) where the?would be permanent page content, or, for a form value or a comment's text, which is stored as typed, draw the?and mark the annotation withset_undrawn_mark(UNDRAWN_MARK, read back byundrawn_mark) so the edit is reported and flattening refuses.script_missingnames what the typed-signature script font and its Inter fallback both lack.
It also holds the fonts of the optional craft-fonts
build input. build.rs embeds them as CRAFT_FONTS when the build sets
CRAFT_FONTS_DIR=<checkout> (and fails if it can't while CRAFT_FONTS_REQUIRED=1, as releases
set). Without that variable CRAFT_FONTS is empty and everything below copes:
ui_japanese_fonts: theJpanfaces for the interface, BIZ UDPGothic first.ui_chinese_fonts: theHansfaces for the interface, in manifest order (without one, Chinese characters the Japanese faces lack show the replacement glyph).ui_cjk_fonts(prefer_hans): both in fallback order for the UI language (Chinese group first in Chinese mode, so one line never mixes faces with different vertical metrics).ui_arabic_fonts: theArabfaces for Arabic-script interface text (file names, document titles), in manifest order.ui_telugu_fonts: theTelufaces for Telugu interface text (the Telugu catalog, file names, document titles), in manifest order.ui_hebrew_fonts/document_hebrew_font(bold): theHebrfaces (Noto Sans Hebrew) for Hebrew interface text, and the face whose outlines become the Type 3 fallback for Hebrew text written by Edit text (Bold when asked for and present, else Regular).Nonewithout a craft-fonts input that has one; the editor then reports a clear error.shape_hebrew/hebrew_has: the RegularHebrface shaped with harfrust for Hebrew Add Text content, one cluster per letter with its points and cantillation marks (positioned by the face), drawn by the same Type 3 writer as Arabic and Thai.GlyphError::NoFontwithout the face.document_japanese_font/japanese_glyph: the face (Shippori Mincho, then BIZ UDMincho) whose outlines become the Type 3 fallback font for Japanese text written into PDFs. Without it,japanese_glyphreturnsGlyphError::NoFontand the editor reports a clear error.document_japanese_font_for_style/japanese_glyph_from: select and draw a real Japanese fallback face for edited text. Sans sources prefer BIZ UDPGothic Regular/Bold; serif sources keep Mincho. Type 3 paths and advances both come from that selected face. Missing weights fall back to a real Regular face, never synthetic bold; no italic face is supplied. The defaultdocument_japanese_font/japanese_glyphAPIs still use regular document Mincho.document_arabic_font/shape_arabic/arabic_glyph: the firstArabface, shaped with harfrust (joining forms, ligatures, mark positions) into clusters for Arabic text written into PDFs; the editor draws each cluster as one Type 3 glyph with a ToUnicode entry and per-line ActualText for logical copy/search order. Without the face they returnGlyphError::NoFontand the editor reports a clear error.document_japanese_fonts_for_style: everyJpanface in that order of preference, so the editor can use the first one that has all of the replacement's glyphs (the faces differ in coverage, e.g. of Cyrillic).ui_thai_fonts/document_thai_font/shape_thai: theThaifaces for the interface and shaped Thai Add Text content. Harfrust positions stacked vowel/tone marks and decomposes Sara Am; cluster ToUnicode entries preserve the original text. The shared Type 3 writer embeds outlines, so reopening does not depend on installed fonts.SHIPPORI_MINCHO: Shippori Mincho's bytes, orNone.
Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect
FontDescriptor. pdf::Metrics::base_font uses its FontName when the Type 3 dictionary has
no BaseFont, so text inspection and edit-tool results identify the fallback after save/reopen.
The descriptor does not change glyph programs or replace the Type 3 FontBBox metrics when
Ascent/Descent are omitted (PDF Reference 1.7, tables 5.9 and 5.19). The small web input has
only Gothic Regular, so it cannot preserve bold. No new font assets or changes to that input are
needed.
wasm32 builds embed only BIZ UDPGothic Regular and any Arab, Telu, Hebr or Thai face, to keep the web build small: the web build
currently has no Chinese face, so Chinese there still shows the replacement glyph.
Font files are never
committed here (AGENTS.md §1.4; team members: craftrules standards/fonts.md, internal).
The full font subsystem (parsing, shaping, subsetting and embedding) arrives with M2.2/M7.
Document font inputs use embedding: bounded static TrueType/OpenType-CFF validation,
exact program preservation, Unicode/CID/GID mapping and full-face subset completion.
shaping uses the same program with harfrust and UAX #9; source UI fallback fonts are
never exported. See existing-text editing.