# Multilingual rendering fonts

These are official Noto static Regular (weight 400) fonts, downloaded 2026-09-20. Full source URLs, byte counts, SHA-256 and family names are in `metadata.json`; retain both `OFL-*.txt` license files with deployed assets. The Latin, Arabic and Thai sources are the official archived static Noto repository. CJK sources are the official current Noto CJK repository.

Use these six files for deployment:

| Locale/script | Font file | SVG font family |
|---|---|---|
| English, Spanish, French, German, Italian, Portuguese, Russian | NotoSans-Regular.ttf | Noto Sans |
| Arabic | NotoSansArabic-Regular.ttf | Noto Sans Arabic |
| Thai | NotoSansThai-Regular.ttf | Noto Sans Thai |
| Simplified Chinese | NotoSansCJKsc-Greetings.otf | Noto Sans CJK SC |
| Japanese | NotoSansCJKjp-Greetings.otf | Noto Sans CJK JP |
| Korean | NotoSansCJKkr-Greetings.otf | Noto Sans CJK KR |

Keep the original `NotoSansCJK*-Regular.otf` files outside the deployed public assets. The `Greetings` CJK files are unhinted derivatives built with FontTools 4.61.0. Final derivatives preserve **all encoded characters of the originals**, all script layout features and font names; they avoid dropping supplementary-plane Japanese name forms such as 𠮷 (U+20BB7). Removing hinting reduces file size without changing the Unicode repertoire or advance widths. Both original and derivative have 44,810 mapped Unicode code points, including all 11,172 modern Hangul syllables, standard hiragana/katakana and common/extension CJK ideographs. Source coverage itself lacks U+9FF0–U+9FFF, so no complete-Unicode claim is made.

The exact build command, run once for each `sc`, `jp`, `kr`, is:

```sh
PYTHONPATH=/private/tmp/greetings-fonttools python3 -m fontTools.subset NotoSansCJKjp-Regular.otf --output-file=NotoSansCJKjp-Greetings.otf --unicodes='*' --layout-features='*' --name-IDs='*' --name-languages='*' --glyph-names --notdef-glyph --notdef-outline --no-hinting
```

## Metrics and validation

The matching `metrics/<font-stem>.json` files avoid loading opentype.js into the Worker. Each has:

```ts
{
  fontFile: string,
  family: string,
  unitsPerEm: number,
  ascender: number,
  descender: number,
  lineGap: number,
  weight: 400,
  advanceRanges: [firstCodePoint: number, lastCodePoint: number, advance: number][]
}
```

Ranges are sorted and endpoints inclusive. Binary-search the Unicode code point (`codePointAt(0)`, not UTF-16 code units). `advance / unitsPerEm` is width in em. An absent range means the font lacks that character. Zero advance is valid for combining marks. These are unshaped advance metrics: they intentionally contain no kerning, ligature, bidi or Arabic/Thai shaping, so use a fit margin and let resvg shape the actual whole text. Do not truncate or wrap inside a grapheme cluster. Kerning-aware measurements can be narrower; joining can substantially change Arabic widths.

`build-metrics.py` uses FontTools to decompile all tables with checksum validation and extract widths. `validate.mjs` parses every font with opentype.js and actually renders sample PNGs through the installed resvg-wasm 2.6.2. The originals and final derivatives must both pass. `validation.json` records test strings and missing-glyph results. Sample images live in `samples/`. The Arabic sample is `صباح الخير يا ليلى سنة سعيدة ٢٠٢٦`; visual inspection showed connected letters with the expected right-to-left phrase. The Thai sample also rendered readable marks. This is a small visual smoke test, not exhaustive linguistic QA.

## Fallback and memory policy

- Load Noto Sans as the small Latin fallback plus the required script face. Arabic and Thai fonts do not replace Latin fallback coverage.
- For CJK, use the locale-specific SC, JP or KR face. Each one includes the other basic CJK scripts too, so mixed Japanese/Korean names do not inherently require loading all three faces.
- Bound font caching by **total bytes**, not only font count. Keep at most one CJK font resident under the proposed 20MB cache budget. Account for transient original ArrayBuffers, WASM copies, decoded images and PNG surfaces separately.
- Do not cache parsed opentype objects in production: validation observed tens of MiB per CJK parse. opentype.js can be removed from runtime dependencies or kept only as a development validation dependency.
- Check the selected font and fallback metrics before export. If a name has a character missing from every permitted face, return a controlled validation result rather than a silent tofu box or dropping the character. Emoji require explicit raster assets; these families are not color-emoji fonts.
- Exact dictionary coverage can be verified once the translation files exist; preserving the entire original encoded repertoire avoids making text-specific subsets that break later names/translations.

## WebP finding

**resvg-wasm 2.6.2 does not support embedded WebP.** Its official tag pins the `3495d870` resvg fork. That source's parser accepts JPEG/PNG/GIF/SVG/raw data, and its raster decoder has no WebP path. Use JPEG versions for photographic backgrounds and PNG for transparent stickers. UI WebP variants can remain separate.

`check-image-formats.mjs` also verifies this against the installed WASM binary. The WebP test renders zero nontransparent pixels; JPEG and PNG render visible pixels. Results are recorded in `image-format-validation.json`.

Primary sources: [resvg-js 2.6.2 manifest](https://raw.githubusercontent.com/thx/resvg-js/v2.6.2/Cargo.toml), [exact pinned image parser](https://raw.githubusercontent.com/zimond/resvg/3495d870/crates/usvg-parser/src/image.rs), [exact pinned raster decoder](https://raw.githubusercontent.com/zimond/resvg/3495d870/crates/resvg/src/image.rs).
