diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 9dc8fe0b..b777e26b 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -27,6 +27,12 @@ jobs: timeout-minutes: 300 steps: - uses: actions/checkout@v4 + # Optional font build input, so the Japanese font tests run (they skip without it). + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + 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 +50,6 @@ jobs: # The checkout is synced in under another owner; let tests that call git read it. git config --global --add safe.directory "*" rustc --version + 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 97cf75d5..cae4a17d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,12 @@ jobs: LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 + # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin, x86_64-apple-darwin @@ -87,6 +93,8 @@ jobs: run: packaging/macos/import-cert.sh - name: Build, sign, notarize env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} @@ -120,6 +128,12 @@ jobs: LIGHTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 + # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} @@ -136,6 +150,8 @@ jobs: - name: Build, sign, package shell: pwsh env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + CRAFT_FONTS_REQUIRED: "1" WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -170,6 +186,12 @@ jobs: NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@v4 + # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately + path: craft-fonts - name: Install build and packaging tools run: | sudo apt-get update @@ -187,6 +209,9 @@ jobs: if: inputs.version != '' run: cargo xtask version set "$LIGHTCRAFT_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: | @@ -215,6 +240,12 @@ jobs: WASM_BINDGEN_VERSION: 0.2.129 steps: - uses: actions/checkout@v4 + # Fonts (optional build input; required for releases): see craftrules standards/fonts.md. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: storytold/craft-fonts + ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately + path: craft-fonts - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown @@ -232,6 +263,9 @@ jobs: if: inputs.version != '' run: cargo xtask version set "$LIGHTCRAFT_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 fe958866..823f9276 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ plan/ /log .DS_Store .claude/ +/craft-fonts diff --git a/AGENTS.md b/AGENTS.md index fdda2e93..393db2b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,10 +62,21 @@ of it. Full standard: `../craftrules/standards/never-crash.md` ## Assets: icons, images, fonts (ABSOLUTE RULE — never violate) - **Never use any iconography, image, artwork, font, sound or other asset from Adobe products** (no Lightroom/Creative Cloud icons, no screenshots, no presets/profiles/LUTs, no UI bitmaps — not even as a temporary placeholder or "reference copy"). Observing Adobe's UI to imitate *layout and behaviour* is allowed; copying or tracing its assets is not. - **This includes Adobe's open-licensed assets**: no Source Sans/Serif/Code or Source Han fonts, no Adobe Fonts, no - Adobe-published icon sets, sample photos, colour profiles or LUTs — even when OFL/MIT. The UI font is Inter (OFL). + Adobe-published icon sets, sample photos, colour profiles or LUTs — even when OFL/MIT. The UI font is Inter (OFL); + Japanese fonts come from craft-fonts (below). - Every asset in the repository must be one of: **our own original work** (e.g. icons drawn in code as vectors, procedurally generated demo photos), **public domain / CC0**, **Creative Commons** (CC-BY / CC-BY-SA with attribution honoured), **OFL** (fonts), or **permissive open-source** (MIT/Apache-2.0/BSD/ISC) — or contributed by a person who created the asset and licenses it openly. - **Exception: `docs/brand/`.** The ArtCraft name, wordmark and logos there are ArtCraft Team trademarks, not open source and not covered by LightCraft's MIT OR Apache-2.0 licence (`LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE`); their terms are in `docs/brand/LICENSE-brand.txt`. Use them only unmodified and never redraw, recolour or derive from them. - **Every asset must have an entry in `assets/ATTRIBUTION.md`** (path, title, author/creator, source URL or "original work", licence, date added, modifications) and its licence text when required (e.g. `assets/fonts/OFL-*.txt`). Add the entry in the same commit as the asset. Assets without an attribution entry must not be committed. +- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo.** Don't commit + font files here (Inter, already in `assets/fonts/`, is the one exception); add new fonts to craft-fonts. LightCraft + uses it as the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts` + then `CRAFT_FONTS_DIR=../craft-fonts cargo run -p lightcraft` (or any cargo/xtask command). `crates/engine/build.rs` + embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); the UI + (`theme::font_definitions`) and the export watermark renderer use its Japanese faces as fallbacks after Inter. Unset, + `CRAFT_FONTS` is empty: everything builds, tests and runs, but Japanese text has no glyphs. Releases always build + with it (`release.yml`, `CRAFT_FONTS_REQUIRED=1`) and ship the fonts' OFL licences. Tests that need these fonts skip + without it; the FreeBSD CI job runs them with it. Rules: craftrules + [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). - Icons drawn in code (e.g. `crates/ui-egui/src/icons.rs`) are original work and are recorded in `assets/ATTRIBUTION.md` as such; do not trace them from Adobe icons. - Demo/test images: generated procedurally by `lightcraft-scenes`, or CC0 downloads kept in the gitignored `corpus/` with their source recorded. Screenshots of Adobe apps live only in the gitignored `plan/` and are never committed or published. - **Enforced:** `cargo xtask assets` (in `ci`) fails when an image/icon/font/sound/video/raw/ICC/XMP file is not matched diff --git a/CLAUDE.md b/CLAUDE.md index fdda2e93..393db2b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,10 +62,21 @@ of it. Full standard: `../craftrules/standards/never-crash.md` ## Assets: icons, images, fonts (ABSOLUTE RULE — never violate) - **Never use any iconography, image, artwork, font, sound or other asset from Adobe products** (no Lightroom/Creative Cloud icons, no screenshots, no presets/profiles/LUTs, no UI bitmaps — not even as a temporary placeholder or "reference copy"). Observing Adobe's UI to imitate *layout and behaviour* is allowed; copying or tracing its assets is not. - **This includes Adobe's open-licensed assets**: no Source Sans/Serif/Code or Source Han fonts, no Adobe Fonts, no - Adobe-published icon sets, sample photos, colour profiles or LUTs — even when OFL/MIT. The UI font is Inter (OFL). + Adobe-published icon sets, sample photos, colour profiles or LUTs — even when OFL/MIT. The UI font is Inter (OFL); + Japanese fonts come from craft-fonts (below). - Every asset in the repository must be one of: **our own original work** (e.g. icons drawn in code as vectors, procedurally generated demo photos), **public domain / CC0**, **Creative Commons** (CC-BY / CC-BY-SA with attribution honoured), **OFL** (fonts), or **permissive open-source** (MIT/Apache-2.0/BSD/ISC) — or contributed by a person who created the asset and licenses it openly. - **Exception: `docs/brand/`.** The ArtCraft name, wordmark and logos there are ArtCraft Team trademarks, not open source and not covered by LightCraft's MIT OR Apache-2.0 licence (`LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE`); their terms are in `docs/brand/LICENSE-brand.txt`. Use them only unmodified and never redraw, recolour or derive from them. - **Every asset must have an entry in `assets/ATTRIBUTION.md`** (path, title, author/creator, source URL or "original work", licence, date added, modifications) and its licence text when required (e.g. `assets/fonts/OFL-*.txt`). Add the entry in the same commit as the asset. Assets without an attribution entry must not be committed. +- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo.** Don't commit + font files here (Inter, already in `assets/fonts/`, is the one exception); add new fonts to craft-fonts. LightCraft + uses it as the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts` + then `CRAFT_FONTS_DIR=../craft-fonts cargo run -p lightcraft` (or any cargo/xtask command). `crates/engine/build.rs` + embeds the manifest's fonts as `lightcraft_engine::CRAFT_FONTS` (wasm32: BIZ UDPGothic Regular only); the UI + (`theme::font_definitions`) and the export watermark renderer use its Japanese faces as fallbacks after Inter. Unset, + `CRAFT_FONTS` is empty: everything builds, tests and runs, but Japanese text has no glyphs. Releases always build + with it (`release.yml`, `CRAFT_FONTS_REQUIRED=1`) and ship the fonts' OFL licences. Tests that need these fonts skip + without it; the FreeBSD CI job runs them with it. Rules: craftrules + [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). - Icons drawn in code (e.g. `crates/ui-egui/src/icons.rs`) are original work and are recorded in `assets/ATTRIBUTION.md` as such; do not trace them from Adobe icons. - Demo/test images: generated procedurally by `lightcraft-scenes`, or CC0 downloads kept in the gitignored `corpus/` with their source recorded. Screenshots of Adobe apps live only in the gitignored `plan/` and are never committed or published. - **Enforced:** `cargo xtask assets` (in `ci`) fails when an image/icon/font/sound/video/raw/ICC/XMP file is not matched diff --git a/NOTICE b/NOTICE index a95eb76a..e6f95a8a 100644 --- a/NOTICE +++ b/NOTICE @@ -15,6 +15,8 @@ list (author, source, license) is in assets/ATTRIBUTION.md. Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. Showcase photographs by Ansel Adams, Dorothea Lange and NASA (docs/images/) — public domain. See assets/ATTRIBUTION.md. -BIZ UDMincho: Copyright 2022 The BIZ UDMincho Project Authors -(https://github.com/googlefonts/morisawa-biz-ud-mincho). SIL Open Font License 1.1; -see assets/fonts/OFL-BIZUDMincho.txt. +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-.txt. diff --git a/README.md b/README.md index 28fc1ac0..e7454f24 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,16 @@ cargo run --release -p lightcraft-cli -- render photo.jpg -o out.jpg --set light cargo xtask ci # fmt, clippy, tests, layering, wasm checks ``` +**Japanese text** needs the shared font repo, an optional build input (official releases always include it): + +```sh +git clone https://github.com/storytold/craft-fonts ../craft-fonts +CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p lightcraft +``` + +Without it LightCraft builds and runs the same, but Japanese text has no glyphs. Fonts are never committed to this +repo; see [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md). + The web build needs the `wasm32-unknown-unknown` target and the matching `wasm-bindgen` CLI (`cargo xtask web` prints the exact install command); see [docs/web.md](docs/web.md). @@ -356,7 +366,8 @@ Humans and agents follow the same rules, so read [AGENTS.md](AGENTS.md) first. T work from public specs and black-box observation. - **No Adobe assets, ever:** no icons, screenshots, presets, profiles, LUTs or fonts from Adobe products. Every image, icon and font in the repo is original, public domain, Creative Commons, OFL or permissively licensed, and has - an entry in [assets/ATTRIBUTION.md](assets/ATTRIBUTION.md) added in the same commit. + an entry in [assets/ATTRIBUTION.md](assets/ATTRIBUTION.md) added in the same commit. New fonts go to + [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not here. - **Pure Rust**, enforced crate layering, everything is a command, and `cargo xtask ci` green before every commit (one task id per commit). - **Never crash.** Non-test code returns errors instead of panicking: no `unwrap()`, `expect()`, `panic!` or @@ -420,7 +431,9 @@ with its author, source and license in [assets/ATTRIBUTION.md](assets/ATTRIBUTIO Showcase photographs are public-domain works, used via Wikimedia Commons: Ansel Adams, *The Tetons and the Snake River* (1942, U.S. National Archives); Dorothea Lange, *Migrant Mother* (1936, Library of Congress); Bill Anders / NASA, *Earthrise* (1968); NASA, *The Blue Marble* (1972). The demo library is procedurally generated by LightCraft. UI font: -Inter (SIL OFL 1.1). All icons are original. +Inter (SIL OFL 1.1). Builds made with [craft-fonts](https://github.com/storytold/craft-fonts) (all official releases) +also embed its Japanese fonts (BIZ UDPGothic, BIZ UDMincho, Shippori Mincho; SIL OFL 1.1), listed in its +[ATTRIBUTION.md](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md). All icons are original. The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as diff --git a/ROADMAP.md b/ROADMAP.md index a0836d77..693f06b0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -194,8 +194,9 @@ own chart-based calibration (M11.4). Adobe matrices are never used. ## Japanese interface and text watermarks English/Japanese interface language is persisted in UI state. Core menus have Japanese -translations; untranslated panels and dialogs retain English. BIZ UDMincho is bundled -under OFL for Japanese glyph coverage without system fonts. Text watermarks now accept +translations; untranslated panels and dialogs retain English. Japanese glyphs (UI: BIZ UDPGothic; +watermarks: BIZ UDMincho) come from storytold/craft-fonts, embedded by builds made with the +optional `CRAFT_FONTS_DIR` input (all releases), so no system fonts are needed. Text watermarks now accept `vertical: true` in export JSON/presets and expose an orientation selector. Japanese characters stay upright in top-to-bottom columns, with newlines starting columns to the left. This is basic lettering, without tate-chu-yoko, ruby, kinsoku, or general vertical diff --git a/assets/ATTRIBUTION.md b/assets/ATTRIBUTION.md index 3d2133c5..654f9002 100644 --- a/assets/ATTRIBUTION.md +++ b/assets/ATTRIBUTION.md @@ -9,8 +9,6 @@ covers a whole directory), or if a licence file referenced in the Licence column | Path | Asset | Creator | Source | Licence | Added | Modifications | |---|---|---|---|---|---|---| | `assets/fonts/Inter-*.ttf` (Regular, Medium, SemiBold) | Inter | Rasmus Andersson | https://github.com/rsms/inter | SIL Open Font License 1.1 (`assets/fonts/OFL-Inter.txt`) | 2026-09-30 | none | -| `assets/fonts/BIZUDMincho-Regular.ttf` | BIZ UDMincho Regular | The BIZ UDMincho Project Authors (Morisawa Inc., design TypeBank Co., Ltd.) | https://github.com/google/fonts/tree/63833b7d10bb1f86a8f0b91cba2d3ae1f68d1aa3/ofl/bizudmincho | OFL-1.1 (`assets/fonts/OFL-BIZUDMincho.txt`) | 2026-10-05 | none | -| `assets/fonts/OFL-BIZUDMincho.txt` | BIZ UDMincho licence | The BIZ UDMincho Project Authors | https://github.com/google/fonts/blob/63833b7d10bb1f86a8f0b91cba2d3ae1f68d1aa3/ofl/bizudmincho/OFL.txt | OFL-1.1 | 2026-10-05 | none | | `assets/app-icon/` (`lightcraft.svg` and the PNG/ICNS/ICO/SVG files derived from it) | LightCraft app icon (lynx) | LightCraft project owner (drawn in ArtCraft) | original work (vectorised from the owner's drawing; see `assets/app-icon/README.md`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | 2026-10-02 | vectorised to SVG; rendered to PNG/ICNS/ICO by `packaging/icons.sh` | | `crates/scenes/` (generated images) | Procedural demo photographs | LightCraft contributors | original work (generated at runtime, no source imagery) | MIT OR Apache-2.0 | 2026-09-30 | n/a | | `crates/ui-egui/src/icons.rs` | UI icons drawn as vector paths in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-09-30 | n/a | @@ -20,9 +18,12 @@ 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 | - -| `assets/fonts/BIZUDPGothic-*.ttf` | BIZ UDPGothic (Regular, Bold) | Morisawa / BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic/tree/18934af56b9c003ca58c54bffbf226848cb11032 | SIL Open Font License 1.1 (`assets/fonts/OFL-BIZUDGothic.txt`) | 2026-10-05 | none | +| craft-fonts: optional build input, not files in this repository | BIZ UDPGothic (Regular, Bold), BIZ UDMincho Regular, Shippori Mincho Regular | Morisawa Inc. / The BIZ UDGothic and BIZ UDMincho Project Authors; The Shippori Mincho Project Authors (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-.txt) | 2026-10-06 | none; embedded only in builds made with `CRAFT_FONTS_DIR` (all official releases); the web (wasm32) build embeds BIZ UDPGothic Regular only | Notes - Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are OFL-licensed: the asset rule excludes anything from Adobe. Source Sans 3 was removed on 2026-09-30 and replaced by Inter. +- Japanese fonts (BIZ UDPGothic, BIZ UDMincho, Shippori Mincho) are not in this repository. They live in + [storytold/craft-fonts](https://github.com/storytold/craft-fonts), with their licences and attribution in its + `ATTRIBUTION.md`, and are embedded only in builds made with the optional `CRAFT_FONTS_DIR` build input (all official + releases). Never commit font files here; add new fonts to craft-fonts (craftrules `standards/fonts.md`). diff --git a/assets/fonts/BIZUDMincho-Regular.ttf b/assets/fonts/BIZUDMincho-Regular.ttf deleted file mode 100644 index b5a3f4cc..00000000 Binary files a/assets/fonts/BIZUDMincho-Regular.ttf and /dev/null differ diff --git a/assets/fonts/BIZUDPGothic-Bold.ttf b/assets/fonts/BIZUDPGothic-Bold.ttf deleted file mode 100644 index 8cfe3455..00000000 Binary files a/assets/fonts/BIZUDPGothic-Bold.ttf and /dev/null differ diff --git a/assets/fonts/BIZUDPGothic-Regular.ttf b/assets/fonts/BIZUDPGothic-Regular.ttf deleted file mode 100644 index 0753da09..00000000 Binary files a/assets/fonts/BIZUDPGothic-Regular.ttf and /dev/null differ diff --git a/assets/fonts/OFL-BIZUDGothic.txt b/assets/fonts/OFL-BIZUDGothic.txt deleted file mode 100644 index 4df8604d..00000000 --- a/assets/fonts/OFL-BIZUDGothic.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho) - -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: -https://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/OFL-BIZUDMincho.txt b/assets/fonts/OFL-BIZUDMincho.txt deleted file mode 100644 index 1664f9f6..00000000 --- a/assets/fonts/OFL-BIZUDMincho.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2022 The BIZ UDMincho Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho) - -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: -https://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/crates/engine/build.rs b/crates/engine/build.rs new file mode 100644 index 00000000..6665cf44 --- /dev/null +++ b/crates/engine/build.rs @@ -0,0 +1,59 @@ +//! Optional build input: with `CRAFT_FONTS_DIR=`, embed every font +//! in its `fonts/manifest.txt` as `CRAFT_FONTS` (see src/fonts.rs). Unset, `CRAFT_FONTS` is empty. +//! 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 +//! `.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 _; +use std::path::PathBuf; + +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + 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) { + 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()); + } +} + +/// One `CraftFont { .. }` initialiser per manifest line. +fn craft_fonts(dir: &std::path::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, *style) != ("BIZ UDPGothic", "Regular") { + 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/engine/src/export.rs b/crates/engine/src/export.rs index 6c30b1a0..860c4d7c 100644 --- a/crates/engine/src/export.rs +++ b/crates/engine/src/export.rs @@ -244,14 +244,23 @@ impl Default for Watermark { /// Inter SemiBold (OFL, see assets/ATTRIBUTION.md). static WATERMARK_FONT: &[u8] = include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"); -/// BIZ UDMincho (OFL), Japanese fallback on both native and web. -/// Bundled OFL Japanese font bytes, shared by watermarks and UI glyph fallback. -pub static WATERMARK_JAPANESE_FONT: &[u8] = include_bytes!("../../../assets/fonts/BIZUDMincho-Regular.ttf"); + +/// The watermark faces: Inter first, then the craft-fonts Japanese faces (Mincho first, the +/// watermark's serif look; then any other Japanese face). Without craft-fonts that is Inter alone, +/// and Japanese characters draw as Inter's missing-glyph box. +fn watermark_fonts(craft: &'static [crate::fonts::CraftFont]) -> Vec> { + let mut japanese: Vec<_> = crate::fonts::japanese(craft).collect(); + japanese.sort_by_key(|f| !f.is_mincho()); + std::iter::once(WATERMARK_FONT) + .chain(japanese.into_iter().map(|f| f.bytes)) + .filter_map(|bytes| ab_glyph::FontRef::try_from_slice(bytes).ok()) + .collect() +} /// Draw `wm` onto `img` (straight alpha blending of the encoded values). pub fn draw_watermark(img: &mut Rgba8, wm: &Watermark) { let width = img.width; - watermark_coverage(img.width, img.height, wm, |x, y, k, col| { + watermark_coverage(img.width, img.height, wm, crate::fonts::CRAFT_FONTS, |x, y, k, col| { let p = &mut img.data[y * width + x]; for c in 0..3 { p[c] = (p[c] as f32 + (col[c] as f32 - p[c] as f32) * k).round() as u8; @@ -264,14 +273,14 @@ pub fn draw_watermark(img: &mut Rgba8, wm: &Watermark) { pub fn draw_watermark_deep(img: &mut DeepImage, wm: &Watermark) { let (width, trc) = (img.width, img.space.trc()); match &mut img.samples { - DeepSamples::U16(v) => watermark_coverage(img.width, img.height, wm, |x, y, k, col| { + DeepSamples::U16(v) => watermark_coverage(img.width, img.height, wm, crate::fonts::CRAFT_FONTS, |x, y, k, col| { let i = (y * width + x) * 3; for c in 0..3 { let p = v[i + c] as f32; v[i + c] = (p + (col[c] as f32 * 257.0 - p) * k).round().clamp(0.0, 65535.0) as u16; } }), - DeepSamples::F32(v) => watermark_coverage(img.width, img.height, wm, |x, y, k, col| { + DeepSamples::F32(v) => watermark_coverage(img.width, img.height, wm, crate::fonts::CRAFT_FONTS, |x, y, k, col| { let i = (y * width + x) * 3; for c in 0..3 { let target = trc.decode(col[c] as f32 / 255.0); @@ -282,9 +291,16 @@ pub fn draw_watermark_deep(img: &mut DeepImage, wm: &Watermark) { } /// Lay out `wm` on a `width × height` image and call `blend(x, y, coverage × opacity, colour)` for -/// every covered pixel (shadow pass first). -fn watermark_coverage(width: usize, height: usize, wm: &Watermark, mut blend_px: impl FnMut(usize, usize, f32, [u8; 3])) { - use ab_glyph::{Font, FontRef, PxScale, ScaleFont, point}; +/// every covered pixel (shadow pass first). `craft` is [`crate::fonts::CRAFT_FONTS`] (a parameter +/// so tests can render without it). +fn watermark_coverage( + width: usize, + height: usize, + wm: &Watermark, + craft: &'static [crate::fonts::CraftFont], + mut blend_px: impl FnMut(usize, usize, f32, [u8; 3]), +) { + use ab_glyph::{Font, PxScale, ScaleFont, point}; if !wm.image.trim().is_empty() { logo_coverage(width, height, wm, blend_px); return; @@ -293,9 +309,8 @@ fn watermark_coverage(width: usize, height: usize, wm: &Watermark, mut blend_px: if text.is_empty() || width == 0 || height == 0 { return; } - let Ok(font) = FontRef::try_from_slice(WATERMARK_FONT) else { return }; - let Ok(japanese) = FontRef::try_from_slice(WATERMARK_JAPANESE_FONT) else { return }; - let fonts = [font, japanese]; + let fonts = watermark_fonts(craft); + let Some(latin) = fonts.first() else { return }; let short = width.min(height) as f32; let px = (wm.size.clamp(0.005, 0.5) * short).max(6.0); let mut glyphs = Vec::new(); @@ -333,9 +348,11 @@ fn watermark_coverage(width: usize, height: usize, wm: &Watermark, mut blend_px: '…' => '︙', other => other, }; - let ch = if wm.vertical && fonts[1].glyph_id(vertical_form).0 != 0 { vertical_form } else { original }; - let face = usize::from(fonts[0].glyph_id(ch).0 == 0); - let sf = fonts[face].as_scaled(PxScale::from(px)); + let has = |font: &ab_glyph::FontRef<'_>, c: char| font.glyph_id(c).0 != 0; + let ch = if wm.vertical && fonts.iter().skip(1).any(|f| has(f, vertical_form)) { vertical_form } else { original }; + // The first face with the glyph; Inter (its missing-glyph box) when none has it. + let face = fonts.iter().position(|f| has(f, ch)).unwrap_or(0); + let sf = fonts.get(face).unwrap_or(latin).as_scaled(PxScale::from(px)); let id = sf.glyph_id(ch); if wm.vertical { let left = columns.saturating_sub(column.saturating_add(1)) as f32 * px; @@ -1334,21 +1351,46 @@ mod tests { assert!(ExportOptions::from_json(&serde_json::json!({"watermark": ""})).watermark.is_none()); } - #[test] - fn japanese_watermarks_support_vertical_columns_and_legacy_defaults() { - use ab_glyph::Font; - let font = ab_glyph::FontRef::try_from_slice(WATERMARK_JAPANESE_FONT).unwrap(); - for c in "日本語".chars() { - assert_ne!(font.glyph_id(c).0, 0); - } - let old = ExportOptions::from_json(&json!({"watermark": {"text": "日本語"}})); - assert!(!old.watermark.unwrap().vertical); + fn vertical_japanese() -> Watermark { let options = ExportOptions::from_json(&json!({"watermark": {"text": "日本語", "vertical": true, "size": 0.1, "shadow": false, "opacity": 1.0}})); - let wm = options.watermark.unwrap(); + options.watermark.unwrap() + } + + #[test] + fn japanese_watermark_options_and_legacy_defaults() { + let old = ExportOptions::from_json(&json!({"watermark": {"text": "日本語"}})); + assert!(!old.watermark.unwrap().vertical); + let wm = vertical_japanese(); assert!(wm.vertical); let round: Watermark = serde_json::from_value(serde_json::to_value(&wm).unwrap()).unwrap(); assert!(round.vertical); + } + + /// Built without craft-fonts, a Japanese watermark still lays out and draws (Inter's + /// missing-glyph boxes), and Latin text is unaffected. + #[test] + fn watermarks_work_without_craft_fonts() { + assert_eq!(watermark_fonts(&[]).len(), 1, "Inter only"); + for wm in [vertical_japanese(), Watermark { text: "LightCraft 日本語".into(), ..Watermark::default() }] { + let mut covered = 0usize; + watermark_coverage(400, 300, &wm, &[], |_, _, k, _| covered += usize::from(k > 0.0)); + assert!(covered > 0, "{:?} draws something", wm.text); + } + } + + #[test] + fn japanese_watermarks_support_vertical_columns() { + use ab_glyph::Font; + let fonts = watermark_fonts(crate::fonts::CRAFT_FONTS); + if fonts.len() < 2 { + eprintln!("skipped: built without CRAFT_FONTS_DIR, so there is no Japanese watermark face"); + return; + } + for c in "日本語の文字".chars() { + assert!(fonts[1..].iter().any(|f| f.glyph_id(c).0 != 0), "a craft-fonts face has {c}"); + } + let wm = vertical_japanese(); let mut img = Rgba8::new(400, 300); img.data.fill([0, 0, 0, 255]); draw_watermark(&mut img, &wm); diff --git a/crates/engine/src/fonts.rs b/crates/engine/src/fonts.rs new file mode 100644 index 00000000..954d9263 --- /dev/null +++ b/crates/engine/src/fonts.rs @@ -0,0 +1,52 @@ +//! Fonts from the optional craft-fonts build input (storytold/craft-fonts). +//! +//! Built with `CRAFT_FONTS_DIR=`, [`CRAFT_FONTS`] holds every font in its +//! manifest (today: the Japanese UI and document fonts); otherwise it is empty and LightCraft uses +//! only its own bundled fonts (Inter). Both the egui UI and the export watermark renderer read it +//! from here. See 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 { + /// Whether the font is meant for `script` (ISO 15924, e.g. `"Jpan"`). + pub fn covers(&self, script: &str) -> bool { + self.scripts.contains(&script) + } + + /// Mincho (serif) faces, the preferred Japanese faces for document-like text. + pub fn is_mincho(&self) -> bool { + self.family.contains("Mincho") + } +} + +/// The craft-fonts faces for Japanese, in `fonts`' order (empty without craft-fonts). +pub fn japanese(fonts: &'static [CraftFont]) -> impl Iterator { + fonts.iter().filter(|f| f.covers("Jpan")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn craft_fonts_are_well_formed_or_absent() { + if CRAFT_FONTS.is_empty() { + eprintln!("built without CRAFT_FONTS_DIR: no craft-fonts to check"); + return; + } + for f in CRAFT_FONTS { + assert!(!f.family.is_empty() && !f.style.is_empty() && !f.scripts.is_empty(), "{}", f.family); + assert!(ab_glyph::FontRef::try_from_slice(f.bytes).is_ok(), "{} {} parses", f.family, f.style); + } + assert!(japanese(CRAFT_FONTS).next().is_some(), "craft-fonts carries Japanese faces"); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b76d7a15..68200a35 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -19,6 +19,7 @@ pub mod demo; pub mod devices; pub mod export; pub mod files; +pub mod fonts; pub mod guard; pub mod import; mod import_move; @@ -38,6 +39,7 @@ mod view; use std::sync::Arc; pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; +pub use fonts::{CRAFT_FONTS, CraftFont}; use lightcraft_catalog::{Catalog, Filter, Op, PhotoId, Sort}; use lightcraft_develop::DevelopSettings; pub use media::{RenderJob, SourceLevel}; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 92355c30..0e2389a4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -143,20 +143,82 @@ mod tests { } assert!(text.contains("マイフォト"), "{text}"); assert!(text.contains("すべての写真"), "{text}"); - ctx.fonts_mut(|fonts| { - for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] { - let font = egui::FontId::new(13.0, family); - for message in japanese().values() { - for ch in message.chars().filter(|ch| !ch.is_whitespace()) { - assert!(fonts.has_glyph(&font, ch), "Missing glyph {ch} in {message}"); + if lightcraft_engine::fonts::japanese(lightcraft_engine::CRAFT_FONTS).next().is_none() { + eprintln!("skipped glyph coverage: built without CRAFT_FONTS_DIR, so there is no Japanese UI font"); + } else { + ctx.fonts_mut(|fonts| { + for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] { + let font = egui::FontId::new(13.0, family); + for message in japanese().values() { + for ch in message.chars().filter(|ch| !ch.is_whitespace()) { + assert!(fonts.has_glyph(&font, ch), "Missing glyph {ch} in {message}"); + } } } - } - }); + }); + } // Locale affects presentation only: command ids remain the same. let ids = |app: &crate::LightcraftApp| crate::menus::menu_entries(app).into_iter().map(|entry| entry.id).collect::>(); let japanese_ids = ids(&app); set_language(Language::En); assert_eq!(japanese_ids, ids(&app)); } + /// The UI families, after one frame so the font definitions are loaded. + fn fonts_ctx(craft: &'static [lightcraft_engine::CraftFont]) -> egui::Context { + let ctx = egui::Context::default(); + ctx.set_fonts(crate::theme::font_definitions(craft)); + let mut out = ctx.run_ui(egui::RawInput::default(), |_| {}); + out.textures_delta.clear(); + ctx + } + + fn ui_families() -> [egui::FontFamily; 3] { + [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into()), egui::FontFamily::Monospace] + } + + /// Built with craft-fonts, Japanese renders with real glyphs (no tofu) in every UI family. + #[test] + fn craft_fonts_render_japanese_in_the_ui() { + if lightcraft_engine::fonts::japanese(lightcraft_engine::CRAFT_FONTS).next().is_none() { + eprintln!("skipped: built without CRAFT_FONTS_DIR, so there is no Japanese UI font"); + return; + } + let ctx = fonts_ctx(lightcraft_engine::CRAFT_FONTS); + ctx.fonts_mut(|fonts| { + for family in ui_families() { + let font = egui::FontId::new(13.0, family); + for ch in "日本語の文字".chars() { + assert!(fonts.has_glyph(&font, ch), "{ch} in {font:?}"); + } + let galley = fonts.layout_no_wrap("日本語の文字".into(), font.clone(), egui::Color32::WHITE); + assert!(galley.size().x > 13.0 * 5.0, "{font:?}: six full-width glyphs, {:?}", galley.size()); + } + }); + } + + /// Built without craft-fonts, the UI (in Japanese, too) still installs its fonts and runs; + /// Latin text keeps Inter. + #[test] + fn the_ui_works_without_craft_fonts() { + let ctx = fonts_ctx(&[]); + ctx.fonts_mut(|fonts| { + // (Not Monospace: egui's `has_glyph` reports false for glyphs of the family's + // replacement-glyph face, which there is Hack, the face that draws Latin.) + for family in [egui::FontFamily::Proportional, egui::FontFamily::Name(crate::theme::FONT_SEMIBOLD.into())] { + let font = egui::FontId::new(13.0, family); + assert!("LightCraft".chars().all(|ch| fonts.has_glyph(&font, ch)), "{font:?}"); + } + }); + let mut app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), Default::default()); + app.ui.language = Language::Ja; + for frame in 0..3 { + let input = crate::headless::HeadlessView::raw_input(egui::vec2(1200.0, 800.0), 1.0, frame as f64 / 60.0, vec![]); + let mut out = ctx.run_ui(input, |ui| { + app.logic(ui.ctx()); + app.ui(ui); + }); + out.textures_delta.clear(); + } + set_language(Language::En); + } } diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 6f94330e..fe8b0478 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -708,7 +708,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { Dialog::About => { ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text)); ui.label(crate::i18n::tr_format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION"))); - ui.label("MIT OR Apache-2.0. Fonts: Inter / BIZ UDPGothic (OFL). Icons: original."); + ui.label(format!("MIT OR Apache-2.0. Fonts: {} (OFL). Icons: original.", crate::theme::font_credits())); ui.add_space(10.0); let discord = egui::Button::new(egui::RichText::new("Join the ArtCraft Discord").font(t.semibold(15.0)).color(egui::Color32::WHITE)) .fill(t.accent) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index a62e8313..b5db692d 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -107,20 +107,53 @@ impl Tokens { } pub fn install_fonts(ctx: &egui::Context) { + ctx.set_fonts(font_definitions(lightcraft_engine::CRAFT_FONTS)); +} + +/// Inter (bundled) for Latin text, egui's default fonts, then the craft-fonts Japanese faces as +/// the last fallback of every family (BIZ UDPGothic first; Bold first for the semibold family). +/// Without craft-fonts (`craft` empty) Japanese text has no glyphs and shows as boxes. +pub fn font_definitions(craft: &'static [lightcraft_engine::CraftFont]) -> FontDefinitions { let mut fonts = FontDefinitions::default(); fonts.font_data.insert("Inter".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-Regular.ttf")))); fonts.font_data.insert("Inter-SemiBold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf")))); - fonts.font_data.insert("Japanese".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/BIZUDPGothic-Regular.ttf")))); - fonts.font_data.insert("Japanese-Bold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/BIZUDPGothic-Bold.ttf")))); - let fallback: Vec = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default(); - let mut prop = vec!["Inter".to_string(), "Japanese".to_string()]; - prop.extend(fallback.clone()); + let japanese: Vec<_> = lightcraft_engine::fonts::japanese(craft).collect(); + for f in &japanese { + fonts.font_data.insert(craft_font_name(f), Arc::new(FontData::from_static(f.bytes))); + } + // Craft-fonts faces in preference order for a family drawn in `style`. + let fallback = |style: &str| { + let mut faces = japanese.clone(); + faces.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != style, f.family.contains("Mincho"))); + faces.into_iter().map(craft_font_name).collect::>() + }; + let defaults: Vec = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default(); + let mut prop = vec!["Inter".to_string()]; + prop.extend(defaults.iter().cloned()); + prop.extend(fallback("Regular")); fonts.families.insert(FontFamily::Proportional, prop); - let mut semi = vec!["Inter-SemiBold".to_string(), "Japanese-Bold".to_string()]; - semi.extend(fallback); + let mut semi = vec!["Inter-SemiBold".to_string()]; + semi.extend(defaults); + semi.extend(fallback("Bold")); fonts.families.insert(FontFamily::Name(FONT_SEMIBOLD.into()), semi); - fonts.families.entry(FontFamily::Monospace).or_default().push("Japanese".into()); - ctx.set_fonts(fonts); + fonts.families.entry(FontFamily::Monospace).or_default().extend(fallback("Regular")); + fonts +} + +/// The embedded font families for the About box: Inter, plus the craft-fonts families when built +/// with them. +pub fn font_credits() -> String { + let mut families = vec!["Inter"]; + for f in lightcraft_engine::CRAFT_FONTS { + if !families.contains(&f.family) { + families.push(f.family); + } + } + families.join(" / ") +} + +fn craft_font_name(f: &lightcraft_engine::CraftFont) -> String { + format!("craft-fonts {} {}", f.family, f.style) } pub fn apply(ctx: &egui::Context) { diff --git a/docs/localization-ja.md b/docs/localization-ja.md index 1cd41b46..4d1aa627 100644 --- a/docs/localization-ja.md +++ b/docs/localization-ja.md @@ -5,7 +5,10 @@ LightCraft は日本語と英語(English)の表示に対応しています - 「編集 → 言語」または「設定 → 一般 → 言語」で日本語とEnglishを切り替えます。選択は次回起動にも引き継ぎます。 - メニュー、写真編集、マスク、切り抜き、設定、読み込み、書き出し、主な処理状況を翻訳しています。 -- 通常表示と太字の双方に、SIL OFLのBIZ UDPGothicを組み込みました。 +- 通常表示と太字の双方に、SIL OFLのBIZ UDPGothicを使います。日本語フォントはこのリポジトリではなく + [storytold/craft-fonts](https://github.com/storytold/craft-fonts) にあり、任意のビルド入力 + `CRAFT_FONTS_DIR=../craft-fonts` を指定したビルド(公式リリースはすべて)に組み込まれます。 + 指定しない場合も動作しますが、日本語の文字は表示されません。 - 翻訳は表示層で行います。操作コマンドのID、写真のファイル名、入力したメタデータを変更しません。 - 翻訳されていない技術的なエラー、追加情報、リリースノートは英語で表示します。 @@ -18,4 +21,4 @@ LightCraft は日本語と英語(English)の表示に対応しています `LIGHTCRAFT_LANGUAGE=ja lightcraft-cli snapshot ...` で日本語の画面を描画できます。 表示・フォント・言語の切り替え・設定の保存・コマンドIDの保持は -`cargo test -p lightcraft-ui-egui i18n::tests` で検証します。 +`cargo test -p lightcraft-ui-egui i18n::tests` で検証します(グリフの検査は `CRAFT_FONTS_DIR` 指定時のみ実行)。 diff --git a/docs/parity.md b/docs/parity.md index cfc61c96..aaffc4f6 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -797,7 +797,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- ### Japanese text watermarks `app.export` watermark objects accept `vertical` (boolean, defaults to false). The export -dialog offers Horizontal/Vertical and multiline text. Japanese glyphs fall back to bundled -BIZ UDMincho. Vertical lettering uses upright em cells and right-to-left newline columns; +dialog offers Horizontal/Vertical and multiline text. Japanese glyphs fall back to the craft-fonts +Mincho faces (BIZ UDMincho; on the web build, BIZ UDPGothic) when built with `CRAFT_FONTS_DIR`. Vertical lettering uses upright em cells and right-to-left newline columns; Latin stays upright. Advanced Japanese composition remains open. Regression coverage: -`export::tests::japanese_watermarks_support_vertical_columns_and_legacy_defaults`. +`export::tests::japanese_watermarks_support_vertical_columns`, `japanese_watermark_options_and_legacy_defaults`, `watermarks_work_without_craft_fonts`. diff --git a/docs/web.md b/docs/web.md index ed3efcfe..6d3283c8 100644 --- a/docs/web.md +++ b/docs/web.md @@ -52,6 +52,14 @@ codecs) at opt-level 3 and the rest (egui, eframe, serde, glue) at "s". What goe is the brotli column: 3.1 MB, 11 % less than the old build's brotli size and 41 % less than its 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 Japanese 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 the watermark fallback), so +the module stays well under Cloudflare's 25 MiB per-file limit: measured 2026-10-06, 17.1 MB +without craft-fonts and 21.8 MB with it (brotli 3.8 MB / 6.3 MB). Without it the web build +works, but Japanese text has no glyphs. + ## Deploying: headers `cargo xtask web --serve` sends these on every response. A production server may send them too; diff --git a/packaging/env.sh b/packaging/env.sh index 297f41a7..11221a5e 100644 --- a/packaging/env.sh +++ b/packaging/env.sh @@ -44,12 +44,26 @@ warn() { if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi } -# Copy licence and readme files that exist into a package directory. +# Copy licence and readme files that exist into a package directory (plus the craft-fonts licences +# when building with CRAFT_FONTS_DIR). copy_docs() { local dest="$1" f - for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do + for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT NOTICE; do if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi done + copy_font_licences "$dest" +} + +# Builds made with CRAFT_FONTS_DIR (storytold/craft-fonts, an optional build input) embed its fonts; +# their OFL licences go with the package as OFL-.txt. Nothing to do without it. +copy_font_licences() { + local dest="$1" lic family + [ -n "${CRAFT_FONTS_DIR:-}" ] || return 0 + for lic in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do + [ -f "$lic" ] || continue + family="$(basename "$(dirname "$lic")")" + cp "$lic" "$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 b46187d7..c6070055 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -108,6 +108,9 @@ 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" +# Licences (and the craft-fonts font licences when built with CRAFT_FONTS_DIR) inside the bundle. +mkdir -p "$APP/Contents/Resources/Licenses" +copy_docs "$APP/Contents/Resources/Licenses" # 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 45ced0e5..dae31757 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -106,10 +106,16 @@ $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 -foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') { +foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE') { $p = Join-Path $Root $f if (Test-Path $p) { Copy-Item $p $Portable } } +# Builds made with CRAFT_FONTS_DIR (storytold/craft-fonts) embed its fonts: ship their OFL licences. +if ($env:CRAFT_FONTS_DIR) { + Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue | ForEach-Object { + Copy-Item $_.FullName (Join-Path $Portable "OFL-$($_.Directory.Name).txt") + } +} $Zip = Join-Path $Dist "lightcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip