Fonts: finish Thai fallback delivery for desktop and web releases

Retain the complete reviewed change, including prior integration resolutions,
on current upstream.

#736: deliver Thai UI fonts in desktop and web releases

Use the merged craft-fonts input containing Noto Sans Thai across release,
FreeBSD and Nix builds. Retain the small Thai face in the wasm subset and
reject required release inputs that lack it. Exercise the reported Thai
folder name through all UI families and include original font licence texts
in Windows installers as well as portable packages.

Refs #736; completes delivery of the fallback added in #750.

Validation: UI tests, UI-only strict Clippy, 28 wasm checks, HEIF checks,
assets, layers, parity and formatting pass. All 21 unrelated workspace
failures reproduce with upstream Rust sources and the old font input;
existing Rust 1.95 metadata/engine Clippy findings remain unchanged.
This commit is contained in:
LabDennis
2026-10-14 18:52:15 +02:00
parent 3b4279fc24
commit 1f4aaaec47
13 changed files with 77 additions and 26 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
path: craft-fonts
- name: Build and test in a FreeBSD VM
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
+6 -6
View File
@@ -78,7 +78,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
@@ -136,7 +136,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
@@ -194,7 +194,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- name: Install build and packaging tools
run: |
@@ -291,7 +291,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
@@ -362,7 +362,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- name: Build and package in a FreeBSD VM (tar.gz)
# vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build).
@@ -411,7 +411,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0 # bump deliberately
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 # bump deliberately
path: craft-fonts
- uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
+4 -3
View File
@@ -51,9 +51,10 @@ no darktable, RawSpeed, LibRaw or other GPL decoder/runtime code is included.
Builds made with craft-fonts (the optional CRAFT_FONTS_DIR build input; all official releases)
also embed the fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md
(the Japanese fonts BIZ UDPGothic, BIZ UDMincho and Shippori Mincho; the web build embeds only BIZ
UDPGothic Regular), under the SIL Open Font License 1.1. Release packages include each font's
licence as OFL-<family>.txt.
(BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic and Noto Sans
Thai), under the SIL Open Font License 1.1. The web build embeds BIZ UDPGothic Regular and Noto
Sans Thai Regular, and loads Noto Sans CJK SC separately. Release packages include each font's
licence as OFL-<family>.txt; Windows installers include their texts in FONT-LICENCES.txt.
Place names for reverse geocoding (Photo > Fill Location from GPS) are not part of LightCraft and
are never distributed with it. On the user's request (Photo > Download Place Names) the app
+1 -1
View File
@@ -24,7 +24,7 @@ covers a whole directory), or if a licence file referenced in the Licence column
| `docs/images/*-blue-marble.jpg`, `docs/images/grid-pd.jpg` | "The Blue Marble" (AS17-148-22727) | NASA, Apollo 17 crew (1972) | https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots |
| `docs/images/*.jpg` (UI) | LightCraft application screenshots | LightCraft contributors | original work (captured with `docs/showcase/`) | MIT OR Apache-2.0 | 2026-09-30 | n/a |
| `docs/brand/` (artcraft-logo and artcraft-mark, SVG and PNG) | ArtCraft wordmark and mark | ArtCraft Team | original work (https://getartcraft.com/) | ArtCraft trademark, see `docs/brand/LICENSE-brand.txt` (not open source) | 2026-10-01 | none |
| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular, Noto Sans CJK SC Regular | Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors; Google and Adobe (Noto Sans CJK, maintainer-approved exception; see AGENTS.md) (per craft-fonts' attribution) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | SIL Open Font License 1.1 (each font's OFL.txt in craft-fonts; release packages ship it as OFL-<family>.txt) | 2026-10-06 | none; native builds made with `CRAFT_FONTS_DIR` embed the manifest fonts (all official releases); the web build embeds BIZ UDPGothic Regular and ships Noto Sans CJK SC Regular as a separate font file |
| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular, Noto Sans CJK SC Regular, Noto Sans Arabic Regular, Noto Sans Thai Regular | The Noto Project Authors (Arabic and Thai); Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors; Google and Adobe (Noto Sans CJK, maintainer-approved exception; see AGENTS.md) (per craft-fonts' attribution) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | SIL Open Font License 1.1 (each font's OFL.txt in craft-fonts; release packages ship it as OFL-<family>.txt) | 2026-10-06 | none; native builds made with `CRAFT_FONTS_DIR` embed the manifest fonts (all official releases); the web build embeds BIZ UDPGothic Regular and Noto Sans Thai Regular and ships Noto Sans CJK SC Regular as a separate font file |
| `crates/ui-egui/locales/uk.json`, `crates/ui-egui/locales/uk-formats.json` | Ukrainian interface catalogs | @dmatviichuk with Codex; PhotoCraft, FilmCraft and VectorCraft translation contributors for reused labels | original translations from English source; 337 initial English-key matches reviewed from [PhotoCraft 69c41f3](https://github.com/storytold/photocraft/blob/69c41f3523e147eb215da8c7b4c29b18b35d1c63/crates/ui-egui/src/i18n/uk.tsv), [FilmCraft 4854c27](https://github.com/storytold/filmcraft/blob/4854c277536fe8118257362304b6a7d82d8e704b/crates/ui-egui/src/i18n/uk.tsv), [VectorCraft 4b44265](https://github.com/storytold/vectorcraft/blob/4b44265965fdd6ffedfd6182a892c1a6c360d344/crates/ui-egui/src/i18n/uk.tsv) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, also the source repositories' licences) | 2026-10-09 | adapted to LightCraft photo-development context, completed catalogs, reviewed spelling and placeholders |
Notes
+7 -2
View File
@@ -3,7 +3,7 @@
//! Recipe from craft-fonts' docs/integration.md; craft-fonts is never a Cargo dependency, and this
//! script only reads a local directory (no network).
//!
//! wasm32 (the web build) embeds only the UI font, BIZ UDPGothic Regular (~4.5 MB), to keep the
//! wasm32 embeds BIZ UDPGothic Regular (~4.5 MB) and Noto Sans Thai (~214 KiB), to keep the
//! `.wasm` within the web size budget (Cloudflare serves at most 25 MiB per file); the export
//! watermark falls back to it there. Native builds embed every font.
use std::fmt::Write as _;
@@ -69,6 +69,7 @@ fn craft_fonts(dir: &Path, required: bool) -> Result<String, String> {
let mut out = String::new();
let mut has_japanese_ui = false;
let mut has_chinese_ui = false;
let mut has_thai_ui = false;
for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) {
let f: Vec<&str> = line.split(" | ").map(str::trim).collect();
let [family, style, file, scripts, ..] = f.as_slice() else {
@@ -76,7 +77,8 @@ fn craft_fonts(dir: &Path, required: bool) -> Result<String, String> {
};
has_japanese_ui |= (*family, *style) == ("BIZ UDPGothic", "Regular");
has_chinese_ui |= (*family, *style) == ("Noto Sans CJK SC", "Regular") && scripts.split(',').any(|script| script.trim() == "Hans");
if wasm && (*family, *style) != ("BIZ UDPGothic", "Regular") {
has_thai_ui |= (*family, *style) == ("Noto Sans Thai", "Regular") && scripts.split(',').any(|script| script.trim() == "Thai");
if wasm && !matches!((*family, *style), ("BIZ UDPGothic" | "Noto Sans Thai", "Regular")) {
continue;
}
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
@@ -89,6 +91,9 @@ fn craft_fonts(dir: &Path, required: bool) -> Result<String, String> {
path.display().to_string(),
);
}
if required && !has_thai_ui {
return Err("required Thai UI face Noto Sans Thai Regular (Thai) is missing from fonts/manifest.txt".into());
}
if required && !has_japanese_ui {
return Err("required Japanese UI face BIZ UDPGothic Regular is missing from fonts/manifest.txt".into());
}
+1 -1
View File
@@ -780,7 +780,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
// the render failed while a stand-in is up: what is on screen is all there will be (also while
// a library change retries it)
let failed = (!rendering
&& !(show_before && !split)
&& (!show_before || split)
&& !matches!(shown, "render" | "none" | "hover")
&& (!app.renderer.is_pending(Slot::Main) || app.renderer.retrying(Slot::Main)))
.then(|| app.renderer.failure_of(Slot::Main, id))
+21 -6
View File
@@ -462,14 +462,15 @@ mod tests {
}
}
/// #736: a Thai folder name drew boxes because only CJK craft-fonts faces were fallbacks. With a
/// craft-fonts build that carries a Thai face, an English UI offers it in every family.
/// #736: the release input must carry Thai, not silently skip the check with an older pin.
/// Exercise the Thai characters of the reported folder name through every egui family.
#[test]
fn thai_face_is_a_fallback_in_every_ui_family_when_craft_fonts_carry_one() {
let Some(thai) = lightcraft_engine::CRAFT_FONTS.iter().find(|f| f.covers("Thai")) else {
eprintln!("no Thai face in craft-fonts: nothing to check");
fn thai_folder_name_has_glyphs_in_every_ui_family() {
if lightcraft_engine::CRAFT_FONTS.is_empty() {
eprintln!("built without CRAFT_FONTS_DIR: no craft-fonts to check");
return;
};
}
let thai = lightcraft_engine::CRAFT_FONTS.iter().find(|f| f.covers("Thai")).expect("font input includes Thai");
set_language(Locale::En);
let definitions = font_definitions(lightcraft_engine::CRAFT_FONTS);
let name = craft_font_name(thai);
@@ -480,6 +481,20 @@ mod tests {
let names = definitions.families.get(&family).expect("UI font family");
assert!(names.contains(&name) || (family == semibold_family() && names.contains(&strong)), "{family:?} falls back on {name}");
}
let ctx = egui::Context::default();
ctx.set_fonts(definitions);
let mut frame = ctx.run_ui(egui::RawInput::default(), |_| {});
frame.textures_delta.clear();
ctx.fonts_mut(|fonts| {
for family in [FontFamily::Proportional, FontFamily::Name(FONT_SEMIBOLD.into()), FontFamily::Monospace] {
let id = FontId::new(13.0, family);
// Check the Thai portion; egui 0.36's has_glyph reports false for characters
// in the replacement glyph's own face (including normal monospace Latin).
for ch in "Studio34 ถ่ายภาพงานแต่งงาน".chars().filter(|ch| !ch.is_ascii()) {
assert!(fonts.has_glyph(&id, ch), "{id:?} has {ch}");
}
}
});
}
#[test]
+2 -1
View File
@@ -55,7 +55,8 @@ gzip size (5.25 MB). `wasm-opt -Oz`, when installed, shrinks it further.
**Fonts.** The browser has no system fonts to fall back on, so CJK text comes from
[craft-fonts](https://github.com/storytold/craft-fonts), the optional `CRAFT_FONTS_DIR` build
input (`CRAFT_FONTS_DIR=../craft-fonts cargo xtask web`; release builds always set it). On wasm32
`crates/engine/build.rs` embeds only BIZ UDPGothic Regular (UI and watermark fallback). The web
`crates/engine/build.rs` embeds BIZ UDPGothic Regular (UI and watermark fallback) and Noto Sans
Thai Regular (~214 KiB, for Thai file and folder names in every UI language). The web
bundle also ships Noto Sans CJK SC Regular as `lightcraft_zh_hans.otf` and loads it before the UI
starts. Keeping the 16 MB Chinese face separate leaves the module under Cloudflare's 25 MiB
per-file limit (the older Japanese-only module measured 21.8 MB). Without `CRAFT_FONTS_DIR`, the
Generated
+4 -4
View File
@@ -3,17 +3,17 @@
"craft-fonts": {
"flake": false,
"locked": {
"lastModified": 1791311399,
"narHash": "sha256-4E9OrthE5z39xFXpvpwn4Fn6RH3wX2sF56rZZejZT+I=",
"lastModified": 1791722036,
"narHash": "sha256-mNnDzBcOp1X10dzmiW0iBAkb74QjuzMO4slWudxXCTE=",
"owner": "storytold",
"repo": "craft-fonts",
"rev": "8dcdacd5153e64560d109541a47d806f26f048c0",
"rev": "3d9ab3a132ed0eff457e424226a435a901da30d3",
"type": "github"
},
"original": {
"owner": "storytold",
"repo": "craft-fonts",
"rev": "8dcdacd5153e64560d109541a47d806f26f048c0",
"rev": "3d9ab3a132ed0eff457e424226a435a901da30d3",
"type": "github"
}
},
+1 -1
View File
@@ -12,7 +12,7 @@
# glyphs. Pinned to the revision the release workflow uses — bump deliberately
# (`nix flake update craft-fonts`); craftrules standards/fonts.md.
craft-fonts = {
url = "github:storytold/craft-fonts/8dcdacd5153e64560d109541a47d806f26f048c0";
url = "github:storytold/craft-fonts/3d9ab3a132ed0eff457e424226a435a901da30d3";
flake = false;
};
};
+22
View File
@@ -0,0 +1,22 @@
# Keep the upstream licence texts intact, including the copyright notices above the OFL.
function Write-FontLicences([string] $Destination, [string] $InterLicence, [string] $CraftFontsDir) {
if ($env:CRAFT_FONTS_REQUIRED -and -not $CraftFontsDir) { throw 'CRAFT_FONTS_REQUIRED is set but CRAFT_FONTS_DIR is missing' }
$parts = [Collections.Generic.List[string]]::new()
$parts.Add("LightCraft font licences`n")
$parts.Add([IO.File]::ReadAllText($InterLicence))
if ($CraftFontsDir) {
$manifest = Join-Path $CraftFontsDir 'fonts\manifest.txt'
$licences = Get-Content -LiteralPath $manifest | ForEach-Object {
if ($_.Trim() -and -not $_.Trim().StartsWith('#')) {
$fields = $_ -split '\|'
if ($fields.Count -lt 6) { throw "Malformed font manifest line: $_" }
$fields[5].Trim()
}
} | Sort-Object -Unique
foreach ($licence in $licences) {
$parts.Add("`n--- $licence ---`n")
$parts.Add([IO.File]::ReadAllText((Join-Path $CraftFontsDir $licence)))
}
}
[IO.File]::WriteAllText($Destination, ($parts -join "`n"), [Text.UTF8Encoding]::new($false))
}
+3
View File
@@ -206,6 +206,9 @@
<Component Id="LightcraftCli">
<File Id="LightcraftCliExe" Source="$(var.BinDir)\lightcraft-cli.exe" KeyPath="yes" />
</Component>
<Component Id="LightcraftFontLicences">
<File Id="LightcraftFontLicencesFile" Source="$(var.BinDir)\FONT-LICENCES.txt" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
+4
View File
@@ -112,6 +112,9 @@ $Stage = Join-Path $TargetDir "windows-package\$Arch"
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
Copy-Item (Join-Path $Bin 'lightcraft.exe'), (Join-Path $Bin 'lightcraft-cli.exe') $Stage
# Include the embedded fonts' unmodified OFL texts in both installer and portable releases.
. (Join-Path $PSScriptRoot 'font-licences.ps1')
Write-FontLicences (Join-Path $Stage 'FONT-LICENCES.txt') (Join-Path $Root 'assets\fonts\OFL-Inter.txt') $env:CRAFT_FONTS_DIR
& (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'lightcraft.exe') (Join-Path $Stage 'lightcraft-cli.exe')
@@ -147,6 +150,7 @@ $Portable = Join-Path $TargetDir "windows-package\lightcraft-$Version-windows-$A
Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Portable | Out-Null
Copy-Item (Join-Path $Stage '*.exe') $Portable
Copy-Item (Join-Path $Stage 'FONT-LICENCES.txt') $Portable
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE') {
$p = Join-Path $Root $f
if (Test-Path $p) { Copy-Item $p $Portable }