diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 9dc8fe0b..9fcb2fb2 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -27,6 +27,13 @@ jobs: timeout-minutes: 300 steps: - uses: actions/checkout@v4 + # The optional craft-fonts build input (Japanese fonts), so the tests that need it run here + # rather than skip. Pinned; bump deliberately (craftrules standards/fonts.md). + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 + path: craft-fonts - name: Build and test in a FreeBSD VM uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 with: @@ -44,5 +51,7 @@ jobs: # The checkout is synced in under another owner; let tests that call git read it. git config --global --add safe.directory "*" rustc --version + # The checkout above is synced into the VM with the workspace. + export CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70ebb9c6..1812e260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,13 @@ jobs: VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 + # Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build + # step). Bump the commit deliberately; see craftrules standards/fonts.md. + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin @@ -90,6 +97,8 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" run: packaging/macos/package.sh --arch universal - uses: actions/upload-artifact@v4 with: @@ -117,6 +126,13 @@ jobs: VECTORCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 + # Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build + # step). Bump the commit deliberately; see craftrules standards/fonts.md. + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} @@ -141,6 +157,8 @@ jobs: AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} - uses: actions/upload-artifact@v4 with: @@ -167,6 +185,13 @@ jobs: NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@v4 + # Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build + # step). Bump the commit deliberately; see craftrules standards/fonts.md. + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 + path: craft-fonts - name: Install build and packaging tools run: | sudo apt-get update @@ -184,6 +209,9 @@ jobs: if: inputs.version != '' run: cargo xtask version set "$VECTORCRAFT_VERSION" - name: Build and package (AppImage, deb, rpm, tar.gz) + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" run: packaging/linux/package.sh - name: Check the packages run: | @@ -212,6 +240,13 @@ jobs: TRUNK_VERSION: 0.21.14 steps: - uses: actions/checkout@v4 + # Japanese UI/document fonts, embedded by crates/text/build.rs (CRAFT_FONTS_DIR on the build + # step). Bump the commit deliberately; see craftrules standards/fonts.md. + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown @@ -227,6 +262,9 @@ jobs: if: inputs.version != '' run: cargo xtask version set "$VECTORCRAFT_VERSION" - name: Build and zip + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" run: packaging/web/package.sh - uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 0d78b09d..8c493ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ plan/ /corpus /log .DS_Store +# A craft-fonts checkout for CRAFT_FONTS_DIR (CI and release workflows check it out here) +/craft-fonts diff --git a/AGENTS.md b/AGENTS.md index 5ed3a901..b87ead67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format - Creative Commons with redistribution allowed. - **Every asset file must have a row in [`ASSETS.md`](ASSETS.md)** (path, author, source URL, licence, notes). Put licence texts next to the assets (e.g. `assets/fonts/OFL-*.txt`) and summarize them in `NOTICE`. - `cargo xtask assets` (run by `cargo xtask ci`) fails on any unattributed asset. + - **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo** (rules: craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). Never commit a font file here; add new fonts to craft-fonts. The app uses it only through the optional `CRAFT_FONTS_DIR` build option (`CRAFT_FONTS_DIR=../craft-fonts cargo xtask ci`, path relative to the workspace root), read by `crates/text/build.rs` into `vectorcraft_text::CRAFT_FONTS`; never add it to a `Cargo.toml`. Everything must build, test and run with `CRAFT_FONTS` empty, and tests of its glyphs skip when it is. Details: `docs/development.md` › Fonts. - Code and original assets are MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE`). The ArtCraft logos in `docs/brand/` are trademarks, not open source, under `docs/brand/LICENSE-brand.txt`: never modify them or use them outside VectorCraft. - Prefer art generated in code for defaults (swatches, brushes, symbols, patterns, cursors). If the provenance of an asset is unclear, don't add it. - Reference screenshots of Illustrator stay local under the gitignored `plan/` and are never committed, published or used as assets. diff --git a/ASSETS.md b/ASSETS.md index 6f5aa75f..77bd061a 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -6,6 +6,10 @@ Every non-code asset in this repository (icons, images, fonts, example art, colo Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets and the vector tool cursors (`crates/ui-egui/src/cursors.rs`). +**Font files are never committed here.** Fonts shared by the Crafting Apps live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts) (rules: craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). The small Latin UI/document fonts below predate that rule and stay. + +**Optional build input: craft-fonts.** Builds made with `CRAFT_FONTS_DIR=` (all official releases) embed the Japanese fonts listed in its [`ATTRIBUTION.md`](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md): BIZ UDPGothic Regular and Bold, Shippori Mincho Regular and BIZ UDMincho Regular (web builds: BIZ UDPGothic Regular only), all under the SIL Open Font License 1.1. They are not files in this repository; release packages carry each one's `OFL-.txt`. Builds without `CRAFT_FONTS_DIR` embed none of them. + | Asset | Author | Source | Licence | Notes | |---|---|---|---|---| | `docs/brand/artcraft-logo-white.png` | ArtCraft team (project owner) | https://getartcraft.com/ | ArtCraft trademark, see docs/brand/LICENSE-brand.txt (not open source) | Used in README only; not shipped in the app | @@ -46,8 +50,6 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/fonts/OFL-JetBrainsMono.txt` | (licence text) | upstream project | — | | | `assets/fonts/OFL-SourceSans3.txt` | (licence text) | upstream project | — | | | `assets/fonts/OFL-SourceSerif4.txt` | (licence text) | upstream project | — | | -| `assets/fonts/ShipporiMincho-Regular.ttf` | The Shippori Mincho Project Authors (FONTDASU) | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho | OFL-1.1 (`assets/fonts/OFL-ShipporiMincho.txt`) | Japanese UI/document fallback; SHA-256 `769b5269f0f9bc6534b352c0e6bd856a566e03ff788f107191c2d835863570b2` | -| `assets/fonts/OFL-ShipporiMincho.txt` | The Shippori Mincho Project Authors (FONTDASU) | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/OFL.txt | OFL-1.1 | Licence for Shippori Mincho | | `assets/icons/LICENSE-lucide.txt` | (licence text) | upstream project | — | | | `assets/icons/align-center-horizontal.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/align-center-vertical.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | diff --git a/CLAUDE.md b/CLAUDE.md index 5ed3a901..b87ead67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format - Creative Commons with redistribution allowed. - **Every asset file must have a row in [`ASSETS.md`](ASSETS.md)** (path, author, source URL, licence, notes). Put licence texts next to the assets (e.g. `assets/fonts/OFL-*.txt`) and summarize them in `NOTICE`. - `cargo xtask assets` (run by `cargo xtask ci`) fails on any unattributed asset. + - **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo** (rules: craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). Never commit a font file here; add new fonts to craft-fonts. The app uses it only through the optional `CRAFT_FONTS_DIR` build option (`CRAFT_FONTS_DIR=../craft-fonts cargo xtask ci`, path relative to the workspace root), read by `crates/text/build.rs` into `vectorcraft_text::CRAFT_FONTS`; never add it to a `Cargo.toml`. Everything must build, test and run with `CRAFT_FONTS` empty, and tests of its glyphs skip when it is. Details: `docs/development.md` › Fonts. - Code and original assets are MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE`). The ArtCraft logos in `docs/brand/` are trademarks, not open source, under `docs/brand/LICENSE-brand.txt`: never modify them or use them outside VectorCraft. - Prefer art generated in code for defaults (swatches, brushes, symbols, patterns, cursors). If the provenance of an asset is unclear, don't add it. - Reference screenshots of Illustrator stay local under the gitignored `plan/` and are never committed, published or used as assets. diff --git a/NOTICE b/NOTICE index 089ff379..ba97f852 100644 --- a/NOTICE +++ b/NOTICE @@ -30,6 +30,7 @@ based in part on the work of the Independent JPEG Group. WebAssembly plug-ins run in the wasmi interpreter (MIT OR Apache-2.0); its wasmparser dependency is under the Apache License 2.0 with the LLVM exception. -Shippori Mincho: Copyright 2021 The Shippori Mincho Project Authors -(https://github.com/fontdasu/ShipporiMincho). SIL Open Font License 1.1; -see assets/fonts/OFL-ShipporiMincho.txt. +Builds made with craft-fonts (the CRAFT_FONTS_DIR build option; all official releases) embed the +fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (today BIZ +UDPGothic, Shippori Mincho and BIZ UDMincho; web builds BIZ UDPGothic Regular only) under the SIL +Open Font License 1.1. Their license texts ship with the release packages as OFL-.txt. diff --git a/README.md b/README.md index ab1ca583..388f8b95 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,10 @@ cd apps/vectorcraft-web && trunk build --release # web build → dist cargo xtask ci # fmt, clippy, tests, layering, wasm, vendor names ``` +Japanese fonts come from [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional +build input (releases always use it): `CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p vectorcraft`. +Without it, Japanese text uses the installed system fonts. See [`docs/development.md`](docs/development.md#fonts-craft-fonts-optional-build-input). + ### Use it from Claude Code and other agents Register the MCP server with Claude Code: @@ -241,7 +245,9 @@ VectorCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-AP Copyright (c) 2026 ArtCraft Team and the VectorCraft contributors. Required notices are in [NOTICE](NOTICE). Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed -with its author, source and license in [ASSETS.md](ASSETS.md). +with its author, source and license in [ASSETS.md](ASSETS.md). Release builds also embed the +Japanese fonts of [craft-fonts](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md) +(SIL Open Font License 1.1). The app icon (an engraved dragon on VectorCraft red, `#e8573f`) is the owner's original artwork; its palette and files are in [`assets/app-icon/`](assets/app-icon/README.md). diff --git a/assets/fonts/OFL-ShipporiMincho.txt b/assets/fonts/OFL-ShipporiMincho.txt deleted file mode 100644 index a3f24cb5..00000000 --- a/assets/fonts/OFL-ShipporiMincho.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2021 The Shippori Mincho Project Authors (https://github.com/fontdasu/ShipporiMincho) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/ShipporiMincho-Regular.ttf b/assets/fonts/ShipporiMincho-Regular.ttf deleted file mode 100644 index cfc25ceb..00000000 Binary files a/assets/fonts/ShipporiMincho-Regular.ttf and /dev/null differ diff --git a/crates/text/build.rs b/crates/text/build.rs new file mode 100644 index 00000000..1bdbbd31 --- /dev/null +++ b/crates/text/build.rs @@ -0,0 +1,74 @@ +//! The optional craft-fonts build input (https://github.com/storytold/craft-fonts, +//! `docs/integration.md`): with `CRAFT_FONTS_DIR=` set, every font in its +//! `fonts/manifest.txt` is embedded as `CRAFT_FONTS`; unset, `CRAFT_FONTS` is empty and the app +//! uses its own and the installed fonts. `CRAFT_FONTS_REQUIRED=1` turns a bad checkout into a build +//! error (release builds). Nothing is fetched: the build reads only the local checkout. +//! +//! Web (wasm32) builds embed only the UI font, BIZ UDPGothic Regular (~4.7 MB): the browser has no +//! system fonts, and all four fonts (~24 MB) would push the `.wasm` past static hosts' per-file +//! limits (Cloudflare Pages: 25 MiB). + +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; + +fn main() { + println!("cargo::rerun-if-env-changed=CRAFT_FONTS_DIR"); + println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED"); + let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n"); + if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from) { + let dir = from_workspace(dir); + match craft_fonts(&dir) { + Ok(entries) => src.push_str(&entries), + Err(e) if std::env::var_os("CRAFT_FONTS_REQUIRED").is_some() => { + println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()); + } + Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {e}", dir.display()), + } + } + src.push_str("];\n"); + let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("craft_fonts.rs"); + if let Err(e) = std::fs::write(&out, src) { + println!("cargo::error=writing {}: {e}", out.display()); + } +} + +/// A relative `CRAFT_FONTS_DIR` is taken from the workspace root (where `cargo` is usually run), +/// not from this crate's directory, which is where build scripts run. +fn from_workspace(dir: PathBuf) -> PathBuf { + if dir.is_absolute() { + return dir; + } + let crate_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default()); + crate_dir.join("../..").join(dir) +} + +/// The one craft-fonts face web builds embed (family, style). +const WEB_FONT: (&str, &str) = ("BIZ UDPGothic", "Regular"); + +/// One `CraftFont { .. }` initialiser per manifest line. +fn craft_fonts(dir: &Path) -> Result { + let manifest = dir.join("fonts/manifest.txt"); + println!("cargo::rerun-if-changed={}", manifest.display()); + let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?; + let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); + let mut out = String::new(); + 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 { + return Err(format!("malformed manifest line: {line}")); + }; + if wasm && !(*family == WEB_FONT.0 && *style == WEB_FONT.1) { + continue; + } + let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?; + println!("cargo::rerun-if-changed={}", path.display()); + let scripts: Vec = scripts.split(',').map(|s| format!("{:?}", s.trim())).collect(); + let _ = writeln!( + out, + " CraftFont {{ family: {family:?}, style: {style:?}, scripts: &[{}], bytes: include_bytes!({:?}) }},", + scripts.join(", "), + path.display().to_string(), + ); + } + Ok(out) +} diff --git a/crates/text/src/craft_fonts.rs b/crates/text/src/craft_fonts.rs new file mode 100644 index 00000000..00b94270 --- /dev/null +++ b/crates/text/src/craft_fonts.rs @@ -0,0 +1,52 @@ +//! Fonts from the optional craft-fonts build input (). +//! +//! Built with `CRAFT_FONTS_DIR=`, [`CRAFT_FONTS`] holds every font in its +//! manifest (today the Japanese UI and document fonts); built without it, it is empty and Japanese +//! text falls back to the installed system fonts. Release builds always set it. See `build.rs` +//! and craftrules `standards/fonts.md`. + +/// A font from the optional craft-fonts build input (empty unless built with `CRAFT_FONTS_DIR`). +pub struct CraftFont { + pub family: &'static str, + pub style: &'static str, + /// ISO 15924 scripts the font is for, e.g. `"Jpan"`. + pub scripts: &'static [&'static str], + pub bytes: &'static [u8], +} + +include!(concat!(env!("OUT_DIR"), "/craft_fonts.rs")); + +impl CraftFont { + /// Is the font for Japanese text? + pub fn is_japanese(&self) -> bool { + self.scripts.contains(&"Jpan") + } + /// A serif (Mincho) family, for document text. + pub fn is_mincho(&self) -> bool { + self.family.contains("Mincho") + } + /// A sans (Gothic) family, for UI text. + pub fn is_gothic(&self) -> bool { + self.family.contains("Gothic") + } + /// A bold face. + pub fn is_bold(&self) -> bool { + crate::style_weight(self.style) >= 600.0 + } +} + +/// The craft-fonts faces for Japanese text, best for document text first: Mincho (serif) families, +/// then the others, regular weights before bold. Empty when built without craft-fonts. +pub fn japanese_document_fonts() -> Vec<&'static CraftFont> { + let mut v: Vec<&'static CraftFont> = CRAFT_FONTS.iter().filter(|f| f.is_japanese()).collect(); + v.sort_by_key(|f| (!f.is_mincho(), f.is_bold())); + v +} + +/// The craft-fonts faces for Japanese text, best for UI text first: Gothic (sans) families, then +/// the others; `bold` puts bold faces before regular ones. Empty when built without craft-fonts. +pub fn japanese_ui_fonts(bold: bool) -> Vec<&'static CraftFont> { + let mut v: Vec<&'static CraftFont> = CRAFT_FONTS.iter().filter(|f| f.is_japanese()).collect(); + v.sort_by_key(|f| (!f.is_gothic(), f.is_bold() != bold)); + v +} diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 08765f4a..ff5fa7a1 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -16,9 +16,6 @@ use skrifa::{GlyphId, MetadataProvider}; /// The family used when a requested family is unknown (Illustrator's Myriad Pro analogue). pub const FALLBACK_FAMILY: &str = "Source Sans 3"; -/// Bundled Japanese font bytes, shared with UI glyph fallback. -pub static SHIPPORI_MINCHO_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/ShipporiMincho-Regular.ttf"); - static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"), include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf"), @@ -29,7 +26,6 @@ static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/Inter-Medium.ttf"), include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"), include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), - SHIPPORI_MINCHO_REGULAR, ]; enum FontBytes { @@ -387,7 +383,10 @@ impl FontDb { /// A database holding the bundled fonts, whose system font scan reads `font_dirs`. pub fn with_font_dirs(font_dirs: Vec) -> Self { let mut faces = Vec::new(); - for data in BUNDLED { + // The bundled fonts, then the Japanese craft-fonts faces (when built with them), Mincho + // first: fallbacks for Japanese text after the requested and bundled fonts. + let craft = crate::craft_fonts::japanese_document_fonts().into_iter().map(|f| f.bytes); + for data in BUNDLED.iter().copied().chain(craft) { for (i, family, style) in enumerate_faces(data) { if let Some(f) = make_face(FontBytes::Static(data), i, family, style, None) { faces.push(Arc::new(f)); diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index cdcdfa28..bf251e69 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -10,6 +10,7 @@ #![forbid(unsafe_code)] mod composer; +pub mod craft_fonts; pub mod edit; pub mod embed; mod features; @@ -19,8 +20,9 @@ mod layout; mod shape; pub mod thread; +pub use craft_fonts::{CRAFT_FONTS, CraftFont}; pub use features::OtFeatures; -pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace, SHIPPORI_MINCHO_REGULAR, style_weight, system_font_dirs}; +pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace, style_weight, system_font_dirs}; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; pub use layout::{layout, layout_with}; pub use vectorcraft_doc::TextObject; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 28dfd379..dde4c2a9 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -422,8 +422,47 @@ fn tab_stops_position_text() { assert!((x_of(&p, 4) - 72.0).abs() < 1e-6); } +/// Are the Japanese craft-fonts faces built in? Tests of Japanese glyphs skip (and say so) when +/// they aren't: the bundled fonts have none, and [`db`] reads no system fonts. +fn japanese_fonts() -> bool { + let built_in = CRAFT_FONTS.iter().any(|f| f.is_japanese()); + if !built_in { + eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)"); + } + built_in +} + +#[test] +fn japanese_text_uses_the_craft_fonts_mincho_after_the_bundled_fonts() { + if !japanese_fonts() { + return; + } + let l = layout(db(), &point("日本語の文字", style(20.0))); + assert_eq!(l.glyphs.len(), 6); + assert!(l.glyphs.iter().all(|g| g.gid != 0 && !g.outline.elements().is_empty()), "real glyphs, no tofu"); + let face = db().face_covering('日').unwrap(); + assert!(face.family.contains("Mincho"), "document text falls back to a Mincho face: {}", face.family); + assert!(CRAFT_FONTS.iter().any(|f| f.family == face.family)); + // Latin keeps the bundled fallback family. + assert_eq!(db().face_covering('a').unwrap().family, FALLBACK_FAMILY); +} + +#[test] +fn japanese_text_lays_out_without_the_craft_fonts() { + // Built either way, Japanese text lays out (as missing glyphs when no font has them). + let l = layout(db(), &point("日本語の文字 abc", style(20.0))); + assert_eq!(l.lines.len(), 1); + assert!(l.glyphs.iter().all(|g| g.advance.is_finite())); + if CRAFT_FONTS.is_empty() { + assert!(db().families().iter().all(|f| !f.contains("Mincho") && !f.contains("Gothic")), "no Japanese font is bundled"); + } +} + #[test] fn vertical_japanese_columns_have_upright_ink_and_edit_geometry() { + if !japanese_fonts() { + return; + } let mut t = point("日本語\n縦書き", style(30.0)); t.vertical = true; let l = layout(db(), &t); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index b0f6240b..67a6f84b 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -254,13 +254,28 @@ pub fn install_fonts(ctx: &egui::Context) { let mut mono = vec!["JetBrainsMono".to_string()]; mono.extend(fallback); fonts.families.insert(FontFamily::Name(FONT_MONO.into()), mono); - add(&mut fonts, "japanese", vectorcraft_text::SHIPPORI_MINCHO_REGULAR); - for stack in fonts.families.values_mut() { - stack.push("japanese".into()); - } + add_craft_fonts(&mut fonts); ctx.set_fonts(fonts); } +/// The egui name of a craft-fonts face. +pub(crate) fn craft_font_name(f: &vectorcraft_text::CraftFont) -> String { + format!("craft-fonts {} {}", f.family, f.style) +} + +/// The Japanese craft-fonts faces (when built with `CRAFT_FONTS_DIR`) as fallbacks at the end of +/// every family, after the app's own fonts and before the installed fonts `ui_fonts` adds: BIZ +/// UDPGothic first (bold first in the semibold family), then the Mincho faces. +fn add_craft_fonts(fonts: &mut FontDefinitions) { + for f in vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.is_japanese()) { + fonts.font_data.insert(craft_font_name(f), Arc::new(FontData::from_static(f.bytes))); + } + for (family, stack) in fonts.families.iter_mut() { + let bold = *family == FontFamily::Name(FONT_UI_SEMIBOLD.into()); + stack.extend(vectorcraft_text::craft_fonts::japanese_ui_fonts(bold).into_iter().map(craft_font_name)); + } +} + /// Apply tokens to egui's global style. pub fn apply(ctx: &egui::Context, b: Brightness) { let t = Tokens::for_brightness(b); diff --git a/crates/ui-egui/src/ui_fonts.rs b/crates/ui-egui/src/ui_fonts.rs index a8f44dfb..c971a8c6 100644 --- a/crates/ui-egui/src/ui_fonts.rs +++ b/crates/ui-egui/src/ui_fonts.rs @@ -1,10 +1,12 @@ //! Fallback fonts for the app's own text: layer and file names (and anything else the UI shows) in //! Chinese, Japanese, Korean or other scripts the UI fonts lack are drawn in an installed font that -//! covers them. Nothing is bundled for them: the first time a frame paints a character the UI fonts +//! covers them. Builds made with craft-fonts (`CRAFT_FONTS_DIR`, all releases) carry Japanese fonts, +//! which `theme::install_fonts` adds after the UI fonts, so Japanese never gets here; nothing else +//! is bundled for them: the first time a frame paints a character the UI fonts //! don't have, an installed font covering it is looked for (in the background on native) and added //! to egui's families as their last fallback, from the next frame on. Text the UI fonts cover costs //! no font loading or memory. On the web, which has no system fonts, such characters stay -//! missing-glyph boxes. +//! missing-glyph boxes (Japanese too, unless built with craft-fonts). use std::collections::HashSet; use std::sync::Arc; @@ -171,6 +173,50 @@ mod tests { assert!(fonts.pending.is_none() && fonts.added.is_empty()); } + #[test] + fn japanese_renders_with_the_craft_fonts_without_installed_fonts() { + if !vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_japanese()) { + eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)"); + return; + } + let text = "日本語の文字"; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut fonts = UiFonts::default(); + frame(&ctx, &mut fonts, text); + // Real glyphs (no tofu) in every family from the first frame, with no installed font looked for. + assert!(fonts.pending.is_none() && fonts.added.is_empty(), "no system-font search"); + let families: Vec = ctx.fonts(|f| f.definitions().families.keys().cloned().collect()); + for family in families { + let stack = ctx.fonts(|f| f.definitions().families.get(&family).cloned().unwrap_or_default()); + assert!(stack.last().is_some_and(|n| n.starts_with("craft-fonts ")), "{family:?}: craft-fonts last: {stack:?}"); + for c in text.chars() { + assert!(ctx.fonts_mut(|f| f.has_glyph(&FontId::new(13.0, family.clone()), c)), "{family:?} draws {c}"); + } + } + // The UI prefers BIZ UDPGothic. + let stack = ctx.fonts(|f| f.definitions().families.get(&FontFamily::Proportional).cloned().unwrap_or_default()); + let first = stack.iter().find(|n| n.starts_with("craft-fonts ")).cloned().unwrap_or_default(); + assert_eq!(first, "craft-fonts BIZ UDPGothic Regular", "{stack:?}"); + } + + #[test] + fn the_ui_works_without_the_craft_fonts() { + // Built either way, the fonts install and Japanese text lays out (as installed-font + // fallbacks or missing-glyph boxes when no font has it). + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut fonts = UiFonts::default(); + frame(&ctx, &mut fonts, "日本語の文字 Layer 1"); + fonts.finish(&ctx); + frame(&ctx, &mut fonts, "日本語の文字 Layer 1"); + if vectorcraft_text::CRAFT_FONTS.is_empty() { + let names: Vec = ctx.fonts(|f| f.definitions().font_data.keys().cloned().collect()); + assert!(names.iter().all(|n| !n.starts_with("craft-fonts")), "{names:?}"); + } + assert!(has_glyph(&ctx, 'L')); + } + #[test] fn cjk_names_get_an_installed_fallback_font() { let text = "Logo 标志 ロゴ 로고"; diff --git a/docs/development.md b/docs/development.md index 718271cd..d03d4173 100644 --- a/docs/development.md +++ b/docs/development.md @@ -24,6 +24,37 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop: - **No control server:** browsers can't listen on TCP. To automate the web build, drive headless Chrome with `--remote-debugging-port`. - Quick smoke test: `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --enable-unsafe-webgpu --screenshot=web.png --window-size=1440,900 --virtual-time-budget=15000 http://127.0.0.1:8766/` (headless Chrome on macOS gets a real WebGPU adapter). +## Fonts: craft-fonts (optional build input) + +Font files are never committed to this repository. Fonts shared by the Crafting Apps live in +[storytold/craft-fonts](https://github.com/storytold/craft-fonts); the rules are in craftrules +[`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). To add +a font, add it there. (The Latin fonts in `assets/fonts/` predate the rule and stay.) + +VectorCraft builds, tests and runs without craft-fonts. To embed its Japanese fonts (BIZ UDPGothic +for UI text, Shippori Mincho and BIZ UDMincho for document text), point the `CRAFT_FONTS_DIR` build +option at a checkout: + +```sh +git clone https://github.com/storytold/craft-fonts ../craft-fonts +CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p vectorcraft # relative to the workspace root +CRAFT_FONTS_DIR=../craft-fonts cargo xtask ci # also runs the Japanese-glyph tests +``` + +- `crates/text/build.rs` reads the checkout's `fonts/manifest.txt` and embeds every font it lists as + `vectorcraft_text::CRAFT_FONTS` (empty without `CRAFT_FONTS_DIR`). Nothing is downloaded, and + craft-fonts is never a `Cargo.toml` dependency. A bad path is a build warning, or an error with + `CRAFT_FONTS_REQUIRED=1` (release builds). +- Web (wasm32) builds embed only BIZ UDPGothic Regular, to keep the `.wasm` small enough for static + hosts. +- Document text: the Japanese faces join the font database after the bundled fonts (Mincho first), + so they are the fallback for Japanese after the requested font; installed system fonts come after. +- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first), after + the UI fonts and before the installed fonts `ui_fonts.rs` discovers for other scripts. +- Tests that need Japanese glyphs skip with a message when built without craft-fonts. The FreeBSD + CI job and every release job (`release.yml`) check craft-fonts out at a pinned commit; release + packages carry each embedded font's `OFL-.txt`. + ## Vendor names gate `cargo xtask brands` (part of `cargo xtask ci`) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), `Cargo.toml` descriptions and packaging files. Comments and test code are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, carries a `brand-ok` comment. diff --git a/packaging/env.sh b/packaging/env.sh index c70004ff..8b5391fc 100644 --- a/packaging/env.sh +++ b/packaging/env.sh @@ -50,6 +50,19 @@ copy_docs() { for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi done + copy_font_licences "$dest" +} + +# Built with craft-fonts (CRAFT_FONTS_DIR, set by release.yml): the embedded fonts' licences go with +# the package, as OFL-.txt. Nothing to do without it. +copy_font_licences() { + local dest="$1" ofl family + [ -n "${CRAFT_FONTS_DIR:-}" ] || return 0 + for ofl in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do + [ -f "$ofl" ] || continue + family="$(basename "$(dirname "$ofl")")" + cp "$ofl" "$dest/OFL-$family.txt" + done } # Portable SHA-256 of a file (prints just the hash). diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index 6c07e5c9..65356490 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -108,6 +108,8 @@ sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" plutil -lint "$APP/Contents/Info.plist" printf 'APPL????' >"$APP/Contents/PkgInfo" +# The licences of the craft-fonts fonts embedded in the binary (release builds). +copy_font_licences "$APP/Contents/Resources" # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). # Today the only nested code is the main executable; frameworks/helpers would be signed here too. diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index 65d8ace9..7efd5e26 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -92,6 +92,12 @@ foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') { $p = Join-Path $Root $f if (Test-Path $p) { Copy-Item $p $Portable } } +# Built with craft-fonts (CRAFT_FONTS_DIR, release builds): the embedded fonts' licences. +if ($env:CRAFT_FONTS_DIR) { + foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue) { + Copy-Item $ofl.FullName (Join-Path $Portable "OFL-$($ofl.Directory.Name).txt") + } +} $Zip = Join-Path $Dist "vectorcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip