mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-14 19:26:59 -04:00
Fonts: use craft-fonts as an optional build input
crates/text/build.rs embeds the fonts of a storytold/craft-fonts checkout named by CRAFT_FONTS_DIR as filmcraft_text::fonts::CRAFT_FONTS (native: all of them; wasm32: only BIZ UDPGothic Regular). Unset, CRAFT_FONTS is empty and nothing changes. A bad CRAFT_FONTS_DIR is a warning, or an error with CRAFT_FONTS_REQUIRED=1. Never a Cargo dependency, no network. - UI: the Japanese faces are the last fallback of every egui font family (BIZ UDPGothic first, Bold first in the semibold/medium families). - Text engine: registered after the bundled faces; fallback_for picks them after the bundled faces and before system faces (Mincho for serif text, Gothic otherwise, nearest weight). - Tests: Japanese glyphs (no tofu) in the UI and title paths, skipped without craft-fonts; the theme works with CRAFT_FONTS empty. - Release: every build job checks out craft-fonts at a pinned commit and builds with CRAFT_FONTS_DIR + CRAFT_FONTS_REQUIRED; packages carry OFL-<family>.txt. NOTICE, ATTRIBUTION.md (optional build input), AGENTS.md, CLAUDE.md, README and docs/contributing.md describe it. FilmCraft main bundles no Japanese font, so no font files move. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a2f6ba8b44
commit
9953befe28
15 files changed
+356
-10
No files matched your search
@@ -70,6 +70,12 @@ jobs:
|
|||||||
FILMCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
FILMCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# Fonts (optional build input, required for releases): docs/contributing.md#building-with-craft-fonts
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
repository: storytold/craft-fonts
|
||||||
|
ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately
|
||||||
|
path: craft-fonts
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||||
@@ -90,6 +96,8 @@ jobs:
|
|||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
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
|
run: packaging/macos/package.sh --arch universal
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -120,6 +128,12 @@ jobs:
|
|||||||
FILMCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
FILMCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# Fonts (optional build input, required for releases): docs/contributing.md#building-with-craft-fonts
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
repository: storytold/craft-fonts
|
||||||
|
ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately
|
||||||
|
path: craft-fonts
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
@@ -144,6 +158,8 @@ jobs:
|
|||||||
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
|
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
|
||||||
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
||||||
AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }}
|
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 }}
|
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -170,6 +186,12 @@ jobs:
|
|||||||
NFPM_VERSION: 2.47.0
|
NFPM_VERSION: 2.47.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# Fonts (optional build input, required for releases): docs/contributing.md#building-with-craft-fonts
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
repository: storytold/craft-fonts
|
||||||
|
ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately
|
||||||
|
path: craft-fonts
|
||||||
- name: Install build and packaging tools
|
- name: Install build and packaging tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -187,6 +209,9 @@ jobs:
|
|||||||
if: inputs.version != ''
|
if: inputs.version != ''
|
||||||
run: cargo xtask version set "$FILMCRAFT_VERSION"
|
run: cargo xtask version set "$FILMCRAFT_VERSION"
|
||||||
- name: Build and package (AppImage, deb, rpm, tar.gz)
|
- 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
|
run: packaging/linux/package.sh
|
||||||
- name: Check the packages
|
- name: Check the packages
|
||||||
run: |
|
run: |
|
||||||
@@ -215,6 +240,12 @@ jobs:
|
|||||||
WASM_BINDGEN_VERSION: 0.2.129
|
WASM_BINDGEN_VERSION: 0.2.129
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
# Fonts (optional build input, required for releases): docs/contributing.md#building-with-craft-fonts
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||||
|
with:
|
||||||
|
repository: storytold/craft-fonts
|
||||||
|
ref: abb83316d96aa59c1cf64784289e378fe9fa5695 # bump deliberately
|
||||||
|
path: craft-fonts
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
@@ -232,6 +263,9 @@ jobs:
|
|||||||
if: inputs.version != ''
|
if: inputs.version != ''
|
||||||
run: cargo xtask version set "$FILMCRAFT_VERSION"
|
run: cargo xtask version set "$FILMCRAFT_VERSION"
|
||||||
- name: Build and zip
|
- name: Build and zip
|
||||||
|
env:
|
||||||
|
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
|
||||||
|
CRAFT_FONTS_REQUIRED: "1"
|
||||||
run: packaging/web/package.sh
|
run: packaging/web/package.sh
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ reproduce an image (for example, point lists traced from someone else's icon).
|
|||||||
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
|
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
|
||||||
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
|
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
|
||||||
and other logos stay out; draw a generic icon instead).
|
and other logos stay out; draw a generic icon instead).
|
||||||
|
9. **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||||
|
repo.** Don't commit new font files (the small Latin UI fonts already in `assets/fonts/` stay). A
|
||||||
|
font FilmCraft needs is added to craft-fonts, which the app reads through the optional build input
|
||||||
|
`CRAFT_FONTS_DIR=<craft-fonts checkout>` (`crates/text/build.rs` embeds the fonts in its
|
||||||
|
`fonts/manifest.txt` as `filmcraft_text::fonts::CRAFT_FONTS`; unset, it is empty and the app uses its
|
||||||
|
bundled and system fonts). Never add craft-fonts to a `Cargo.toml`. Code using `CRAFT_FONTS` must work
|
||||||
|
when it is empty. Standard: [`craftrules/standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
|
||||||
|
|
||||||
## 2. Clean-room code
|
## 2. Clean-room code
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,21 @@ is traced or derived from Adobe artwork.
|
|||||||
| Lumetri Presets (Effects panel: Cinematic, Film Emulation, Monochrome, Technical) and their preview picture | `crates/render/src/lumetri_presets.rs`: Lumetri parameter values chosen by contributors, descriptive names, and a procedural preview picture (dusk sky, ridges, lake, grey ramp, colour chips) |
|
| Lumetri Presets (Effects panel: Cinematic, Film Emulation, Monochrome, Technical) and their preview picture | `crates/render/src/lumetri_presets.rs`: Lumetri parameter values chosen by contributors, descriptive names, and a procedural preview picture (dusk sky, ridges, lake, grey ramp, colour chips) |
|
||||||
| Colour palette and layout metrics | `crates/ui-egui/src/theme.rs`: colour values and sizes only; no artwork |
|
| Colour palette and layout metrics | `crates/ui-egui/src/theme.rs`: colour values and sizes only; no artwork |
|
||||||
|
|
||||||
|
## Optional build input: craft-fonts
|
||||||
|
|
||||||
|
These fonts are **not files in this repository**. Builds made with the optional build input
|
||||||
|
`CRAFT_FONTS_DIR` (all official releases; see [docs/contributing.md](docs/contributing.md#building-with-craft-fonts))
|
||||||
|
embed them from [storytold/craft-fonts](https://github.com/storytold/craft-fonts), where each file,
|
||||||
|
its author, pinned upstream source and licence text are listed in its
|
||||||
|
[ATTRIBUTION.md](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md). Release packages
|
||||||
|
carry each font's licence as `OFL-<family>.txt`.
|
||||||
|
|
||||||
|
| Font | Use in FilmCraft | Licence |
|
||||||
|
|---|---|---|
|
||||||
|
| BIZ UDPGothic Regular, Bold (Morisawa) | Japanese UI text; Japanese fallback for sans titles (the web build embeds only Regular) | OFL-1.1 |
|
||||||
|
| Shippori Mincho Regular (FONTDASU) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
|
||||||
|
| BIZ UDMincho Regular (Morisawa) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
|
||||||
|
|
||||||
## First-party brand marks
|
## First-party brand marks
|
||||||
|
|
||||||
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and
|
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ FilmCraft is a clean-room, open-source, pure-Rust non-linear video editor target
|
|||||||
## Non-negotiables
|
## Non-negotiables
|
||||||
**Read [`AGENTS.md`](AGENTS.md) first; its rules override everything here.**
|
**Read [`AGENTS.md`](AGENTS.md) first; its rules override everything here.**
|
||||||
- **Never crash** ([`AGENTS.md`](AGENTS.md) §0, standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md)). People depend on FilmCraft; a crash loses their work, so this outranks feature work. No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!` or `unsafe` outside tests: return `Result<T, E>` and propagate with `?` (`ok_or`, `let … else`, `if let`). Treat every number from media, project files, presets or CLI/MCP/control params as hostile: `get()` instead of `[i]`, checked/saturating math, no division by possibly-zero values, capped allocations, bounded recursion. `lock().unwrap_or_else(PoisonError::into_inner)`; background jobs run under `catch_unwind`. The panic hook + UI `catch_unwind` (`crates/ui-egui/src/crash.rs`) are the last-resort guard, not a licence. Every crash fix gets a regression test that panicked before. In particular (§1): never use Adobe iconography, images or any other Adobe asset. Every asset must be openly licensed (OSS / public domain / Creative Commons, or original work by a contributor) and must have a `<file>.attribution` sidecar plus an entry in `ATTRIBUTION.md`. `cargo xtask assets` enforces this.
|
- **Never crash** ([`AGENTS.md`](AGENTS.md) §0, standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md)). People depend on FilmCraft; a crash loses their work, so this outranks feature work. No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!` or `unsafe` outside tests: return `Result<T, E>` and propagate with `?` (`ok_or`, `let … else`, `if let`). Treat every number from media, project files, presets or CLI/MCP/control params as hostile: `get()` instead of `[i]`, checked/saturating math, no division by possibly-zero values, capped allocations, bounded recursion. `lock().unwrap_or_else(PoisonError::into_inner)`; background jobs run under `catch_unwind`. The panic hook + UI `catch_unwind` (`crates/ui-egui/src/crash.rs`) are the last-resort guard, not a licence. Every crash fix gets a regression test that panicked before. In particular (§1): never use Adobe iconography, images or any other Adobe asset. Every asset must be openly licensed (OSS / public domain / Creative Commons, or original work by a contributor) and must have a `<file>.attribution` sidecar plus an entry in `ATTRIBUTION.md`. `cargo xtask assets` enforces this.
|
||||||
|
- **Fonts** live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo (no new font files here; [`AGENTS.md`](AGENTS.md) §1.9, standard: [`craftrules/standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). Build with them: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR=../craft-fonts cargo run -p filmcraft` (`crates/text/build.rs` embeds them as `filmcraft_text::fonts::CRAFT_FONTS`, used for Japanese in the UI and titles). Unset, `CRAFT_FONTS` is empty and everything must still build, test and run; never add craft-fonts to a `Cargo.toml`. Tests on its glyphs skip when it is empty, so run the gates with and without `CRAFT_FONTS_DIR` when you touch fonts.
|
||||||
- **Clean-room.** Never read/disassemble anything inside Adobe app bundles (names/listings only). Never copy Adobe icons, presets, LUTs, fonts. Never copy GPL/LGPL/AGPL code (FFmpeg, x264, x265, MLT, Kdenlive, Shotcut, Olive, LAME…). ffmpeg/ffprobe run only as external test oracles / fixture generators — never linked or shipped.
|
- **Clean-room.** Never read/disassemble anything inside Adobe app bundles (names/listings only). Never copy Adobe icons, presets, LUTs, fonts. Never copy GPL/LGPL/AGPL code (FFmpeg, x264, x265, MLT, Kdenlive, Shotcut, Olive, LAME…). ffmpeg/ffprobe run only as external test oracles / fixture generators — never linked or shipped.
|
||||||
- **Pure Rust** in the product. OS media APIs only via Rust bindings in `crates/platform` (hardware decoding; the one crate allowed `unsafe`, see `AGENTS.md` §0.3 and `docs/adr/0001-platform-ffi.md`), optional, behind traits, with our decoders as the fallback.
|
- **Pure Rust** in the product. OS media APIs only via Rust bindings in `crates/platform` (hardware decoding; the one crate allowed `unsafe`, see `AGENTS.md` §0.3 and `docs/adr/0001-platform-ffi.md`), optional, behind traits, with our decoders as the fallback.
|
||||||
- **Layering** ([`docs/architecture.md`](docs/architecture.md) §1, enforced by `cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal. L0 codec/container crates depend only on `filmcraft-bitstream`.
|
- **Layering** ([`docs/architecture.md`](docs/architecture.md) §1, enforced by `cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal. L0 codec/container crates depend only on `filmcraft-bitstream`.
|
||||||
|
|||||||
@@ -15,4 +15,7 @@ list (author, source, license) is in ATTRIBUTION.md.
|
|||||||
Inter font (assets/fonts/Inter-*.ttf) — OFL-1.1. See assets/fonts/OFL-Inter.txt.
|
Inter font (assets/fonts/Inter-*.ttf) — OFL-1.1. See assets/fonts/OFL-Inter.txt.
|
||||||
Noto Serif font (assets/fonts/NotoSerif-Regular.ttf) — OFL-1.1. See assets/fonts/OFL-NotoSerif.txt.
|
Noto Serif font (assets/fonts/NotoSerif-Regular.ttf) — OFL-1.1. See assets/fonts/OFL-NotoSerif.txt.
|
||||||
JetBrains Mono font (assets/fonts/JetBrainsMono-Regular.ttf) — OFL-1.1. See assets/fonts/OFL-JetBrainsMono.txt.
|
JetBrains Mono font (assets/fonts/JetBrainsMono-Regular.ttf) — OFL-1.1. See assets/fonts/OFL-JetBrainsMono.txt.
|
||||||
|
Builds made with craft-fonts (CRAFT_FONTS_DIR; all official releases) also embed the fonts listed in
|
||||||
|
https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, Shippori Mincho,
|
||||||
|
BIZ UDMincho) — OFL-1.1. Their licence texts ship with the packages as OFL-<family>.txt.
|
||||||
Public-domain film and NASA frames and CC0 music shown in README screenshots (docs/images/) — public domain / CC0-1.0. See ATTRIBUTION.md.
|
Public-domain film and NASA frames and CC0 music shown in README screenshots (docs/images/) — public domain / CC0-1.0. See ATTRIBUTION.md.
|
||||||
@@ -252,6 +252,13 @@ cargo run --release -p filmcraft-cli -- commands # list every engine c
|
|||||||
cargo run --release -p filmcraft-cli -- mcp # MCP server (headless)
|
cargo run --release -p filmcraft-cli -- mcp # MCP server (headless)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Japanese text in the interface and in titles comes from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (release builds always include it; without it FilmCraft uses its own and the system's fonts):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/storytold/craft-fonts ../craft-fonts
|
||||||
|
CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p filmcraft
|
||||||
|
```
|
||||||
|
|
||||||
The control protocol is documented in [docs/control-protocol.md](docs/control-protocol.md). Stuck, or want to show what you made? Ask in [Discord](https://discord.gg/artcraft).
|
The control protocol is documented in [docs/control-protocol.md](docs/control-protocol.md). Stuck, or want to show what you made? Ask in [Discord](https://discord.gg/artcraft).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -351,7 +358,9 @@ FilmCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APAC
|
|||||||
Copyright (c) 2026 ArtCraft Team and the FilmCraft contributors. Required notices are in [NOTICE](NOTICE).
|
Copyright (c) 2026 ArtCraft Team and the FilmCraft contributors. Required notices are in [NOTICE](NOTICE).
|
||||||
|
|
||||||
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
|
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
|
||||||
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
|
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). Builds made with
|
||||||
|
[craft-fonts](https://github.com/storytold/craft-fonts) (all official releases) also embed its fonts
|
||||||
|
(OFL-1.1), listed in its [ATTRIBUTION.md](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md).
|
||||||
|
|
||||||
**Footage and music in the screenshots:** NASA's Apollo 11 film and television footage from [images.nasa.gov](https://images.nasa.gov) (launch, Launch Control Center, lunar surface and recovery) and the Apollo 11 air-to-ground voice transcript, all US Government works in the public domain (NASA does not endorse this project); *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA; Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
|
**Footage and music in the screenshots:** NASA's Apollo 11 film and television footage from [images.nasa.gov](https://images.nasa.gov) (launch, Launch Control Center, lunar surface and recovery) and the Apollo 11 air-to-ground voice transcript, all US Government works in the public domain (NASA does not endorse this project); *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA; Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ and the Timecode / Clip Name effects. Pure Rust; it builds for `wasm32-unknown-u
|
|||||||
|
|
||||||
| Module | What |
|
| Module | What |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `fonts` | Font database. Bundled faces (always present, also on the web): Inter Regular / Medium / SemiBold / Bold / Italic, JetBrains Mono Regular, Noto Serif Regular — all SIL OFL 1.1, unmodified, in `assets/fonts/` with `.attribution` sidecars. More faces come from `FontSource`s: `DirectorySource::system()` scans the platform font folders on first need (only the `name` and `OS/2` tables are read, via `sfnt`); on wasm the scan finds nothing and hosts can call `add_font_data`. `resolve(family, style)` picks a face, synthesises bold/italic when the family lacks them, and falls back to Inter (flagged `missing`). `fallback_for` finds a face for characters the chosen face lacks. |
|
| `fonts` | Font database. Bundled faces (always present, also on the web): Inter Regular / Medium / SemiBold / Bold / Italic, JetBrains Mono Regular, Noto Serif Regular — all SIL OFL 1.1, unmodified, in `assets/fonts/` with `.attribution` sidecars. More faces come from `FontSource`s: `DirectorySource::system()` scans the platform font folders on first need (only the `name` and `OS/2` tables are read, via `sfnt`); on wasm the scan finds nothing and hosts can call `add_font_data`. `resolve(family, style)` picks a face, synthesises bold/italic when the family lacks them, and falls back to Inter (flagged `missing`). `fallback_for` finds a face for characters the chosen face lacks: bundled faces first, then the optional craft-fonts faces (`CRAFT_FONTS`, embedded by `build.rs` when built with `CRAFT_FONTS_DIR`; Mincho for serif text, Gothic otherwise, nearest weight), then loaded system faces. |
|
||||||
| `layout` | Paragraph layout: per-character font fallback → bidi levels (UAX #9) → items (runs of one face, direction and size) shaped with harfrust (OpenType kerning, ligatures, marks, complex scripts) → tracking → line breaking at UAX #14 opportunities (area text) → visual reordering per line → alignment (left / centre / right / justify), leading (added to the natural line height), baseline shift, all caps / synthesised small caps (78 %), underline. Every character boundary gets a caret stop (ligatures are split evenly), so editors can place carets, hit-test and draw selections. Layouts are cached by text + style (512 entries, LRU). |
|
| `layout` | Paragraph layout: per-character font fallback → bidi levels (UAX #9) → items (runs of one face, direction and size) shaped with harfrust (OpenType kerning, ligatures, marks, complex scripts) → tracking → line breaking at UAX #14 opportunities (area text) → visual reordering per line → alignment (left / centre / right / justify), leading (added to the natural line height), baseline shift, all caps / synthesised small caps (78 %), underline. Every character boundary gets a caret stop (ligatures are split evenly), so editors can place carets, hit-test and draw selections. Layouts are cached by text + style (512 entries, LRU). |
|
||||||
| `layout_rich` | Per-character style runs (`StyleRun`: a byte range with its own `TextStyle`): runs split shaping items (font, size, tracking, caps, faux styles), baseline shift and underline apply per run, a line holding a larger run is pushed down from the line above (baseline distance = max(base line height, previous descent + this line's ascent + leading)), and every glyph / underline carries its style index so renderers can colour runs (`render::draw_run`). |
|
| `layout_rich` | Per-character style runs (`StyleRun`: a byte range with its own `TextStyle`): runs split shaping items (font, size, tracking, caps, faux styles), baseline shift and underline apply per run, a line holding a larger run is pushed down from the line above (baseline distance = max(base line height, previous descent + this line's ascent + leading)), and every glyph / underline carries its style index so renderers can colour runs (`render::draw_run`). |
|
||||||
| `raster` | Our anti-aliased rasteriser (non-zero winding, 5 sub-scanlines, exact horizontal coverage, active-edge list), contour paths with transforms, rounded rectangles, ellipses, polygons and outline emboldening. Coverage is linear, so compositing in linear light is correct. |
|
| `raster` | Our anti-aliased rasteriser (non-zero winding, 5 sub-scanlines, exact horizontal coverage, active-edge list), contour paths with transforms, rounded rectangles, ellipses, polygons and outline emboldening. Coverage is linear, so compositing in linear light is correct. |
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//! Optional craft-fonts build input (see storytold/craft-fonts `docs/integration.md` and
|
||||||
|
//! craftrules `standards/fonts.md`).
|
||||||
|
//!
|
||||||
|
//! With `CRAFT_FONTS_DIR=<craft-fonts checkout>` this embeds every font listed in its
|
||||||
|
//! `fonts/manifest.txt` as [`CRAFT_FONTS`](crate::fonts::CRAFT_FONTS); unset, `CRAFT_FONTS` is
|
||||||
|
//! empty and nothing else changes. A `CRAFT_FONTS_DIR` that is not a checkout is a warning, or an
|
||||||
|
//! error with `CRAFT_FONTS_REQUIRED=1` (release builds).
|
||||||
|
//!
|
||||||
|
//! Native builds embed every font. The web (wasm32) build embeds only the UI font (BIZ UDPGothic
|
||||||
|
//! Regular, ~4.5 MB) to stay under the web size budget (Cloudflare serves files up to 25 MiB).
|
||||||
|
//! Only the local directory is read: no network.
|
||||||
|
|
||||||
|
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<String, String> {
|
||||||
|
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").as_deref() == Ok("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 == "BIZ UDPGothic" && *style == "Regular") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
|
||||||
|
println!("cargo::rerun-if-changed={}", path.display());
|
||||||
|
let scripts: Vec<String> = 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)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! The font database: bundled fonts (always available, also on the web) plus fonts discovered by
|
//! The font database: bundled fonts (always available, also on the web), the optional
|
||||||
//! [`FontSource`]s (system font folders on native platforms).
|
//! [`CRAFT_FONTS`] (present when built with `CRAFT_FONTS_DIR`, also on the web) plus fonts
|
||||||
|
//! discovered by [`FontSource`]s (system font folders on native platforms).
|
||||||
//!
|
//!
|
||||||
//! Faces are addressed by [`FaceId`] (an index that never changes for the life of the process).
|
//! Faces are addressed by [`FaceId`] (an index that never changes for the life of the process).
|
||||||
//! [`resolve`] maps a family + style name (as stored in projects) to a face and the synthetic
|
//! [`resolve`] maps a family + style name (as stored in projects) to a face and the synthetic
|
||||||
@@ -22,6 +23,27 @@ pub static INTER_ITALIC: &[u8] = include_bytes!("../../../assets/fonts/Inter-Ita
|
|||||||
pub static JETBRAINS_MONO_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
pub static JETBRAINS_MONO_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
||||||
pub static NOTO_SERIF_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/NotoSerif-Regular.ttf");
|
pub static NOTO_SERIF_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/NotoSerif-Regular.ttf");
|
||||||
|
|
||||||
|
/// A font from the optional craft-fonts build input (empty unless built with `CRAFT_FONTS_DIR`; see
|
||||||
|
/// `build.rs` and storytold/craft-fonts `docs/integration.md`).
|
||||||
|
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"));
|
||||||
|
|
||||||
|
/// The craft-fonts entries for Japanese (`Jpan`), in [`CRAFT_FONTS`] order. Empty when the app was
|
||||||
|
/// built without craft-fonts.
|
||||||
|
pub fn craft_japanese() -> impl Iterator<Item = &'static CraftFont> {
|
||||||
|
CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Jpan"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Origin of faces registered from [`CRAFT_FONTS`].
|
||||||
|
pub const CRAFT_ORIGIN: &str = "craft-fonts";
|
||||||
|
|
||||||
/// The default family for new text.
|
/// The default family for new text.
|
||||||
pub const DEFAULT_FAMILY: &str = "Inter";
|
pub const DEFAULT_FAMILY: &str = "Inter";
|
||||||
|
|
||||||
@@ -240,6 +262,13 @@ fn db() -> &'static RwLock<Db> {
|
|||||||
push(&mut db, info_from(n, FaceData::Static(b), "bundled"));
|
push(&mut db, info_from(n, FaceData::Static(b), "bundled"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// after the bundled faces, before anything a FontSource finds (a system copy of the same
|
||||||
|
// family + style is then skipped as a duplicate)
|
||||||
|
for f in CRAFT_FONTS {
|
||||||
|
for n in read_faces_bytes(f.bytes) {
|
||||||
|
push(&mut db, info_from(n, FaceData::Static(f.bytes), CRAFT_ORIGIN));
|
||||||
|
}
|
||||||
|
}
|
||||||
RwLock::new(db)
|
RwLock::new(db)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -394,7 +423,15 @@ fn resolve_in(family: &str, style: &str) -> Option<Resolved> {
|
|||||||
Some(Resolved { face: best.id, synth_bold: w >= 600 && best.info.weight <= 500, synth_italic: it && !best.info.italic, missing: false })
|
Some(Resolved { face: best.id, synth_bold: w >= 600 && best.info.weight <= 500, synth_italic: it && !best.info.italic, missing: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A face that covers `c`, preferring `prefer`; bundled and already-registered faces are searched.
|
/// Whether a family name reads as a serif face (Noto Serif, Shippori Mincho, …), to pick a Mincho
|
||||||
|
/// fallback for serif text and a Gothic one otherwise.
|
||||||
|
fn is_serif(family: &str) -> bool {
|
||||||
|
let f = family.to_ascii_lowercase();
|
||||||
|
(f.contains("serif") && !f.contains("sans")) || f.contains("mincho")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A face that covers `c`, preferring `prefer`; bundled, craft-fonts and already-registered faces
|
||||||
|
/// are searched, in that order.
|
||||||
pub fn fallback_for(c: char, prefer: FaceId) -> FaceId {
|
pub fn fallback_for(c: char, prefer: FaceId) -> FaceId {
|
||||||
let p = face(prefer);
|
let p = face(prefer);
|
||||||
if p.has_char(c) || c.is_control() {
|
if p.has_char(c) || c.is_control() {
|
||||||
@@ -407,6 +444,15 @@ pub fn fallback_for(c: char, prefer: FaceId) -> FaceId {
|
|||||||
return f.id;
|
return f.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// then the craft-fonts faces (Japanese): Mincho for serif text, Gothic otherwise, nearest weight
|
||||||
|
let serif = is_serif(&p.info.family);
|
||||||
|
let mut craft: Vec<&Arc<Face>> = faces.iter().filter(|f| f.info.origin == CRAFT_ORIGIN).collect();
|
||||||
|
craft.sort_by_key(|f| (is_serif(&f.info.family) != serif, f.info.italic != p.info.italic, f.info.weight.abs_diff(p.info.weight)));
|
||||||
|
for f in craft {
|
||||||
|
if f.has_char(c) {
|
||||||
|
return f.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
for f in faces.iter().filter(|f| f.info.origin != "bundled" && f.bytes.get().is_some()) {
|
for f in faces.iter().filter(|f| f.info.origin != "bundled" && f.bytes.get().is_some()) {
|
||||||
if f.has_char(c) {
|
if f.has_char(c) {
|
||||||
return f.id;
|
return f.id;
|
||||||
@@ -464,6 +510,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// craft-fonts faces are registered after the bundled ones and serve Japanese characters the
|
||||||
|
/// requested face lacks: Gothic for sans text, Mincho for serif text.
|
||||||
|
#[test]
|
||||||
|
fn japanese_falls_back_to_craft_fonts() {
|
||||||
|
let craft: Vec<_> = all_faces().into_iter().filter(|f| f.info.origin == CRAFT_ORIGIN).collect();
|
||||||
|
if craft_japanese().next().is_none() {
|
||||||
|
eprintln!("SKIPPED: built without craft-fonts (set CRAFT_FONTS_DIR to run)");
|
||||||
|
assert!(craft.is_empty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(craft.len() >= craft_japanese().count(), "{} faces", craft.len());
|
||||||
|
let bundled = all_faces().iter().take_while(|f| f.info.origin == "bundled").count();
|
||||||
|
assert!(craft.iter().all(|f| f.id >= bundled), "craft-fonts faces come after the bundled faces");
|
||||||
|
let sans = resolve("Inter", "Regular").face;
|
||||||
|
let serif = resolve("Noto Serif", "Regular").face;
|
||||||
|
for c in "日本語の文字".chars() {
|
||||||
|
let g = face(fallback_for(c, sans));
|
||||||
|
assert_eq!(g.info.origin, CRAFT_ORIGIN, "{c}");
|
||||||
|
assert!(g.has_char(c) && !is_serif(&g.info.family), "{c}: {}", g.info.family);
|
||||||
|
let m = face(fallback_for(c, serif));
|
||||||
|
assert!(m.has_char(c) && is_serif(&m.info.family), "{c}: {}", m.info.family);
|
||||||
|
}
|
||||||
|
// the bold UI face for bold text
|
||||||
|
let bold = face(fallback_for('日', resolve("Inter", "Bold").face));
|
||||||
|
assert!(bold.info.weight >= 600, "{} {}", bold.info.family, bold.info.style);
|
||||||
|
// and laid out with real glyphs (no tofu)
|
||||||
|
let l = crate::layout("日本語の文字", &crate::TextStyle { size: 40.0, ..Default::default() }, &Default::default());
|
||||||
|
assert_eq!(l.glyphs.len(), 6);
|
||||||
|
assert!(l.glyphs.iter().all(|g| g.id != 0), "{:?}", l.glyphs);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn metrics_and_coverage() {
|
fn metrics_and_coverage() {
|
||||||
let f = face(resolve("Inter", "Regular").face);
|
let f = face(resolve("Inter", "Regular").face);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! FilmCraft text engine (layer L1).
|
//! FilmCraft text engine (layer L1).
|
||||||
//!
|
//!
|
||||||
//! - [`fonts`]: font database — bundled OFL fonts (Inter, JetBrains Mono, Noto Serif) plus faces
|
//! - [`fonts`]: font database — bundled OFL fonts (Inter, JetBrains Mono, Noto Serif), the optional
|
||||||
|
//! craft-fonts ([`fonts::CRAFT_FONTS`], Japanese; empty unless built with `CRAFT_FONTS_DIR`) plus faces
|
||||||
//! discovered by [`fonts::FontSource`]s (system font folders on native platforms; nothing on the
|
//! discovered by [`fonts::FontSource`]s (system font folders on native platforms; nothing on the
|
||||||
//! web unless the host registers font data), family/style resolution with synthetic bold/italic
|
//! web unless the host registers font data), family/style resolution with synthetic bold/italic
|
||||||
//! and per-character fallback.
|
//! and per-character fallback.
|
||||||
|
|||||||
@@ -230,13 +230,14 @@ impl Tokens {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install fonts (Inter, Inter SemiBold, JetBrains Mono) and egui visuals.
|
/// Every font family the theme defines (fallback fonts such as the Japanese fonts are added to each
|
||||||
/// Every font family the theme defines (fallback fonts such as the system Japanese font are added to
|
/// of them).
|
||||||
/// each of them).
|
|
||||||
pub fn font_families() -> Vec<FontFamily> {
|
pub fn font_families() -> Vec<FontFamily> {
|
||||||
vec![FontFamily::Proportional, FontFamily::Monospace, FontFamily::Name("semibold".into()), FontFamily::Name("medium".into())]
|
vec![FontFamily::Proportional, FontFamily::Monospace, FontFamily::Name("semibold".into()), FontFamily::Name("medium".into())]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install fonts (Inter, Inter SemiBold, JetBrains Mono, then the Japanese craft-fonts when built with
|
||||||
|
/// `CRAFT_FONTS_DIR`) and egui visuals.
|
||||||
pub fn install(ctx: &egui::Context, t: &Tokens) {
|
pub fn install(ctx: &egui::Context, t: &Tokens) {
|
||||||
let mut fonts = FontDefinitions::default();
|
let mut fonts = FontDefinitions::default();
|
||||||
fonts.font_data.insert("inter".into(), Arc::new(FontData::from_static(filmcraft_text::fonts::INTER_REGULAR)));
|
fonts.font_data.insert("inter".into(), Arc::new(FontData::from_static(filmcraft_text::fonts::INTER_REGULAR)));
|
||||||
@@ -247,10 +248,32 @@ pub fn install(ctx: &egui::Context, t: &Tokens) {
|
|||||||
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "jbmono".into());
|
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "jbmono".into());
|
||||||
fonts.families.insert(FontFamily::Name("semibold".into()), vec!["inter-semibold".into(), "inter".into()]);
|
fonts.families.insert(FontFamily::Name("semibold".into()), vec!["inter-semibold".into(), "inter".into()]);
|
||||||
fonts.families.insert(FontFamily::Name("medium".into()), vec!["inter-medium".into(), "inter".into()]);
|
fonts.families.insert(FontFamily::Name("medium".into()), vec!["inter-medium".into(), "inter".into()]);
|
||||||
|
add_craft_fonts(&mut fonts);
|
||||||
ctx.set_fonts(fonts);
|
ctx.set_fonts(fonts);
|
||||||
apply_visuals(ctx, t);
|
apply_visuals(ctx, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Name of the egui font for a craft-fonts entry.
|
||||||
|
fn craft_font_name(f: &filmcraft_text::fonts::CraftFont) -> String {
|
||||||
|
format!("craft:{} {}", f.family, f.style)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append the Japanese craft-fonts (empty unless built with `CRAFT_FONTS_DIR`) as the last fallbacks
|
||||||
|
/// of every font family, after the app's own fonts: BIZ UDPGothic first (bold before regular in
|
||||||
|
/// the "semibold" and "medium" families), then the other Japanese faces.
|
||||||
|
fn add_craft_fonts(fonts: &mut FontDefinitions) {
|
||||||
|
let jpan: Vec<_> = filmcraft_text::fonts::craft_japanese().collect();
|
||||||
|
for f in &jpan {
|
||||||
|
fonts.font_data.insert(craft_font_name(f), Arc::new(FontData::from_static(f.bytes)));
|
||||||
|
}
|
||||||
|
for (family, stack) in fonts.families.iter_mut() {
|
||||||
|
let heavy = matches!(family, FontFamily::Name(n) if matches!(n.as_ref(), "semibold" | "medium"));
|
||||||
|
let mut order = jpan.clone();
|
||||||
|
order.sort_by_key(|f| (!f.family.contains("Gothic"), (f.style == "Bold") != heavy));
|
||||||
|
stack.extend(order.iter().map(|f| craft_font_name(f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) {
|
pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) {
|
||||||
let mut v = if t.kind == ThemeKind::Light { Visuals::light() } else { Visuals::dark() };
|
let mut v = if t.kind == ThemeKind::Light { Visuals::light() } else { Visuals::dark() };
|
||||||
v.panel_fill = t.panel_bg;
|
v.panel_fill = t.panel_bg;
|
||||||
@@ -300,3 +323,66 @@ pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) {
|
|||||||
s.animation_time = 0.12;
|
s.animation_time = 0.12;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const JAPANESE: &str = "日本語の文字";
|
||||||
|
|
||||||
|
fn installed() -> egui::Context {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
install(&ctx, &Tokens::for_kind(ThemeKind::default()));
|
||||||
|
let mut out = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||||
|
out.textures_delta.clear();
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Built with craft-fonts: every font family ends with the Japanese faces (BIZ UDPGothic
|
||||||
|
/// first), and Japanese text gets real glyphs, not the replacement box.
|
||||||
|
#[test]
|
||||||
|
fn japanese_renders_with_craft_fonts() {
|
||||||
|
if filmcraft_text::fonts::craft_japanese().next().is_none() {
|
||||||
|
eprintln!("SKIPPED: built without craft-fonts (set CRAFT_FONTS_DIR to run)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ctx = installed();
|
||||||
|
ctx.fonts_mut(|fonts| {
|
||||||
|
let defs = fonts.definitions().clone();
|
||||||
|
for (family, stack) in &defs.families {
|
||||||
|
let first = stack.iter().position(|n| n.starts_with("craft:")).unwrap_or(stack.len());
|
||||||
|
assert!(first > 0 && stack[first..].iter().all(|n| n.starts_with("craft:")), "{family:?}: {stack:?}");
|
||||||
|
assert!(stack[first].starts_with("craft:BIZ UDPGothic"), "{family:?}: {stack:?}");
|
||||||
|
}
|
||||||
|
for family in [FontFamily::Proportional, FontFamily::Monospace] {
|
||||||
|
let font = FontId::new(13.0, family);
|
||||||
|
for ch in JAPANESE.chars() {
|
||||||
|
assert!(fonts.has_glyph(&font, ch), "missing {ch} in {font:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// laid out and drawn: the replacement box (U+FFFD) would show as one glyph per character
|
||||||
|
// from the fallback face; a real face gives each character its own width
|
||||||
|
let galley = ctx.fonts_mut(|f| f.layout_no_wrap(JAPANESE.into(), FontId::new(13.0, FontFamily::Proportional), Color32::WHITE));
|
||||||
|
assert_eq!(galley.rows.iter().map(|r| r.glyphs.len()).sum::<usize>(), JAPANESE.chars().count());
|
||||||
|
assert!(galley.size().x > 13.0 * 4.0, "{:?}", galley.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Built without craft-fonts: the theme installs only the app's own fonts and Latin text
|
||||||
|
/// works as before.
|
||||||
|
#[test]
|
||||||
|
fn works_without_craft_fonts() {
|
||||||
|
let ctx = installed();
|
||||||
|
let n = filmcraft_text::fonts::craft_japanese().count();
|
||||||
|
ctx.fonts_mut(|fonts| {
|
||||||
|
let defs = fonts.definitions().clone();
|
||||||
|
assert_eq!(defs.font_data.keys().filter(|k| k.starts_with("craft:")).count(), n);
|
||||||
|
assert_eq!(defs.families[&FontFamily::Proportional].first().map(String::as_str), Some("inter"));
|
||||||
|
assert_eq!(defs.families[&FontFamily::Monospace].first().map(String::as_str), Some("jbmono"));
|
||||||
|
assert!(fonts.has_glyph(&FontId::new(13.0, FontFamily::Proportional), 'A'));
|
||||||
|
if n == 0 {
|
||||||
|
assert!(defs.families.values().flatten().all(|name| !name.starts_with("craft:")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,26 @@ code 2 instead of a window.
|
|||||||
Dev builds compile dependencies at `opt-level = 2` and workspace crates at `opt-level = 1`. For
|
Dev builds compile dependencies at `opt-level = 2` and workspace crates at `opt-level = 1`. For
|
||||||
playback and codec speed, use `--release`.
|
playback and codec speed, use `--release`.
|
||||||
|
|
||||||
|
### Building with craft-fonts
|
||||||
|
|
||||||
|
Font assets live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this
|
||||||
|
repo ([standard](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). It is an optional build input, not a Cargo
|
||||||
|
dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/storytold/craft-fonts ../craft-fonts
|
||||||
|
CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p filmcraft
|
||||||
|
```
|
||||||
|
|
||||||
|
`crates/text/build.rs` reads its `fonts/manifest.txt` and embeds the fonts as
|
||||||
|
`filmcraft_text::fonts::CRAFT_FONTS` (native: all of them; the web build: only BIZ UDPGothic
|
||||||
|
Regular, to keep the wasm small). The Japanese ones become the last fallback of every UI font family
|
||||||
|
(`crates/ui-egui/src/theme.rs`, BIZ UDPGothic first) and fallback faces in the text engine (Gothic
|
||||||
|
for sans text, Mincho for serif text). Without `CRAFT_FONTS_DIR`, `CRAFT_FONTS` is empty and
|
||||||
|
FilmCraft uses its bundled and system fonts. A `CRAFT_FONTS_DIR` that is not a checkout is a build
|
||||||
|
warning, or an error with `CRAFT_FONTS_REQUIRED=1` (release builds set both). Tests on these fonts'
|
||||||
|
glyphs skip when it is empty; when you touch fonts, run the gates both with and without it.
|
||||||
|
|
||||||
## 3. Quality gates
|
## 3. Quality gates
|
||||||
|
|
||||||
Every commit must pass all of these:
|
Every commit must pass all of these:
|
||||||
@@ -59,6 +79,8 @@ Every commit must pass all of these:
|
|||||||
| wasm | `cargo xtask wasm` (checks every L0–L4 crate, `filmcraft-ui-egui` and `filmcraft-web` for `wasm32-unknown-unknown`) |
|
| wasm | `cargo xtask wasm` (checks every L0–L4 crate, `filmcraft-ui-egui` and `filmcraft-web` for `wasm32-unknown-unknown`) |
|
||||||
| All of the above | `cargo xtask ci` (runs clippy and tests with `--release`) |
|
| All of the above | `cargo xtask ci` (runs clippy and tests with `--release`) |
|
||||||
|
|
||||||
|
Run them with `CRAFT_FONTS_DIR` set too when you change font code ([craft-fonts](#building-with-craft-fonts)).
|
||||||
|
|
||||||
`cargo xtask` is an alias in `.cargo/config.toml` for `cargo run -p xtask --`.
|
`cargo xtask` is an alias in `.cargo/config.toml` for `cargo run -p xtask --`.
|
||||||
|
|
||||||
### Never crash
|
### Never crash
|
||||||
@@ -209,6 +231,12 @@ In short:
|
|||||||
- Screenshots show only FilmCraft (or other open projects) with media we generated or that is
|
- Screenshots show only FilmCraft (or other open projects) with media we generated or that is
|
||||||
openly licensed.
|
openly licensed.
|
||||||
|
|
||||||
|
- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not here.**
|
||||||
|
Don't commit new font files to this repo (the Latin UI fonts already in `assets/fonts/` stay): add
|
||||||
|
the font to craft-fonts (file, licence, manifest line, `ATTRIBUTION.md` row) and use it through
|
||||||
|
`filmcraft_text::fonts::CRAFT_FONTS`. See [Building with craft-fonts](#building-with-craft-fonts)
|
||||||
|
and [`craftrules/standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
|
||||||
|
|
||||||
For each asset file `X`, commit a sidecar `X.attribution`. This is the real one for
|
For each asset file `X`, commit a sidecar `X.attribution`. This is the real one for
|
||||||
`assets/fonts/Inter-Regular.ttf`:
|
`assets/fonts/Inter-Regular.ttf`:
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -44,12 +44,26 @@ warn() {
|
|||||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
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 licences of the
|
||||||
|
# craft-fonts fonts the build embedded (only when built with CRAFT_FONTS_DIR).
|
||||||
copy_docs() {
|
copy_docs() {
|
||||||
local dest="$1" f
|
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; do
|
||||||
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
|
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
|
||||||
done
|
done
|
||||||
|
copy_font_licences "$dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy each craft-fonts licence ($CRAFT_FONTS_DIR/fonts/<family>/OFL.txt) to <dest>/OFL-<family>.txt.
|
||||||
|
# Does nothing without CRAFT_FONTS_DIR.
|
||||||
|
copy_font_licences() {
|
||||||
|
local dest="$1" lic family
|
||||||
|
if [ -z "${CRAFT_FONTS_DIR:-}" ]; then return 0; fi
|
||||||
|
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).
|
# Portable SHA-256 of a file (prints just the hash).
|
||||||
|
|||||||
@@ -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"
|
"$HERE/Info.plist.in" >"$APP/Contents/Info.plist"
|
||||||
plutil -lint "$APP/Contents/Info.plist"
|
plutil -lint "$APP/Contents/Info.plist"
|
||||||
printf 'APPL????' >"$APP/Contents/PkgInfo"
|
printf 'APPL????' >"$APP/Contents/PkgInfo"
|
||||||
|
# licences of the embedded craft-fonts fonts (builds with CRAFT_FONTS_DIR)
|
||||||
|
copy_font_licences "$APP/Contents/Resources"
|
||||||
|
|
||||||
# Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature).
|
# 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.
|
# Today the only nested code is the main executable; frameworks/helpers would be signed here too.
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
|
|||||||
$p = Join-Path $Root $f
|
$p = Join-Path $Root $f
|
||||||
if (Test-Path $p) { Copy-Item $p $Portable }
|
if (Test-Path $p) { Copy-Item $p $Portable }
|
||||||
}
|
}
|
||||||
|
# Licences of the embedded craft-fonts fonts (builds with CRAFT_FONTS_DIR) as OFL-<family>.txt.
|
||||||
|
if ($env:CRAFT_FONTS_DIR) {
|
||||||
|
foreach ($lic in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue) {
|
||||||
|
Copy-Item $lic.FullName (Join-Path $Portable "OFL-$($lic.Directory.Name).txt")
|
||||||
|
}
|
||||||
|
}
|
||||||
$Zip = Join-Path $Dist "filmcraft-$Version-windows-$Arch-portable.zip"
|
$Zip = Join-Path $Dist "filmcraft-$Version-windows-$Arch-portable.zip"
|
||||||
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
|
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
|
||||||
Compress-Archive -Path $Portable -DestinationPath $Zip
|
Compress-Archive -Path $Portable -DestinationPath $Zip
|
||||||
|
|||||||
Reference in new issue
Block a user