Fonts: use craft-fonts as an optional build input; move the Japanese fonts out

Shippori Mincho (8.7 MB) and its licence leave assets/fonts. Japanese fonts now
come from storytold/craft-fonts when built with CRAFT_FONTS_DIR (never a
Cargo.toml dependency): crates/fonts/build.rs embeds its manifest as
designcraft_fonts::CRAFT_FONTS. Native builds embed all four fonts; wasm32
embeds BIZ UDPGothic Regular only (web size budget).

- UI (egui): the Jpan faces at the end of every family, BIZ UDPGothic first
  (Bold first in the semibold family).
- Documents: FontDb loads them after the bundled fonts, Mincho first, so they
  are the Japanese fallback ahead of system fonts; SVG text gets them too.
- Vertical alternates test runs on every craft-fonts face and skips when
  CRAFT_FONTS is empty; new UI no-tofu test and without-craft-fonts tests.
- FreeBSD CI tests with craft-fonts; release.yml builds every platform with
  CRAFT_FONTS_DIR + CRAFT_FONTS_REQUIRED=1 at a pinned commit, packages carry
  OFL-<family>.txt; NOTICE, ASSETS.md, AGENTS.md/CLAUDE.md, README, ROADMAP.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-06 11:01:35 -04:00
1 parent 4855259801
commit a6519d8a55
19 files changed
+297 -132

No files matched your search

+9 -1
View File
@@ -10,6 +10,7 @@ on:
- Cargo.toml - Cargo.toml
- Cargo.lock - Cargo.lock
- "**/Cargo.toml" - "**/Cargo.toml"
- "crates/fonts/**"
- .github/workflows/freebsd.yml - .github/workflows/freebsd.yml
workflow_dispatch: workflow_dispatch:
@@ -27,6 +28,13 @@ jobs:
timeout-minutes: 300 timeout-minutes: 300
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# The Japanese font tests run against craft-fonts (an optional build input; they skip without it).
- name: Check out craft-fonts
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- name: Build and test in a FreeBSD VM - name: Build and test in a FreeBSD VM
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1
with: with:
@@ -45,4 +53,4 @@ jobs:
git config --global --add safe.directory "*" git config --global --add safe.directory "*"
rustc --version rustc --version
CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked
CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked CRAFT_FONTS_DIR="$PWD/craft-fonts" CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked
+38
View File
@@ -70,6 +70,13 @@ jobs:
DESIGNCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DESIGNCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
- name: Check out craft-fonts
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
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
@@ -87,6 +94,8 @@ jobs:
run: packaging/macos/import-cert.sh run: packaging/macos/import-cert.sh
- name: Build, sign, notarize - name: Build, sign, notarize
env: env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
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 }}
@@ -120,6 +129,13 @@ jobs:
DESIGNCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} DESIGNCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
- name: Check out craft-fonts
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
@@ -136,6 +152,8 @@ jobs:
- name: Build, sign, package - name: Build, sign, package
shell: pwsh shell: pwsh
env: env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -170,6 +188,13 @@ jobs:
NFPM_VERSION: 2.47.0 NFPM_VERSION: 2.47.0
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
- name: Check out craft-fonts
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
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 +212,9 @@ jobs:
if: inputs.version != '' if: inputs.version != ''
run: cargo xtask version set "$DESIGNCRAFT_VERSION" run: cargo xtask version set "$DESIGNCRAFT_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 +243,13 @@ jobs:
TRUNK_VERSION: 0.21.14 TRUNK_VERSION: 0.21.14
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
- name: Check out craft-fonts
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: wasm32-unknown-unknown targets: wasm32-unknown-unknown
@@ -230,6 +265,9 @@ jobs:
if: inputs.version != '' if: inputs.version != ''
run: cargo xtask version set "$DESIGNCRAFT_VERSION" run: cargo xtask version set "$DESIGNCRAFT_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:
+1
View File
@@ -7,3 +7,4 @@ plan/
.DS_Store .DS_Store
/.claude/ /.claude/
/target-*/ /target-*/
/craft-fonts/
+1
View File
@@ -21,6 +21,7 @@ People trust DesignCraft with their layouts; a crash loses their work. **This ou
## Non-negotiables ## Non-negotiables
- **Clean-room.** InDesign is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic documents, take screenshots (by window id) stored only under `plan/indesign/screenshots/` (never committed). Never read, disassemble or copy anything inside the InDesign bundle (names/listings only), never copy Adobe icons, artwork, presets or wording beyond feature names, never commit files produced by InDesign. Never copy GPL/AGPL/LGPL code (Scribus, LibreOffice, Ghostscript…). - **Clean-room.** InDesign is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic documents, take screenshots (by window id) stored only under `plan/indesign/screenshots/` (never committed). Never read, disassemble or copy anything inside the InDesign bundle (names/listings only), never copy Adobe icons, artwork, presets or wording beyond feature names, never commit files produced by InDesign. Never copy GPL/AGPL/LGPL code (Scribus, LibreOffice, Ghostscript…).
- **Assets:** no Adobe iconography or images — ever. Every asset is original / OSI / CC0 / redistributable CC (except the ArtCraft trademarks in `docs/brand/`, under `docs/brand/LICENSE-brand.txt`) and has a row in `ASSETS.md` (`cargo xtask assets` enforces it). Prefer art generated in code. - **Assets:** no Adobe iconography or images — ever. Every asset is original / OSI / CC0 / redistributable CC (except the ArtCraft trademarks in `docs/brand/`, under `docs/brand/LICENSE-brand.txt`) and has a row in `ASSETS.md` (`cargo xtask assets` enforces it). Prefer art generated in code.
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo: don't commit new font files (the Latin UI/document fonts already in `assets/fonts/` stay). It is an optional build input, never a `Cargo.toml` dependency: `CRAFT_FONTS_DIR=/abs/path/to/craft-fonts cargo build` makes `crates/fonts/build.rs` embed its fonts as `designcraft_fonts::CRAFT_FONTS` (Japanese: BIZ UDPGothic for the UI, Mincho first for document text; web builds embed BIZ UDPGothic Regular only). Unset, `CRAFT_FONTS` is empty and the app uses its bundled and system fonts; code and tests must work that way (tests on its glyphs skip). Releases build with it. Standard: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
- **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus), explained in [craftrules `standards/test-corpora.md`](https://github.com/storytold/craftrules/blob/main/standards/test-corpora.md). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`. - **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus), explained in [craftrules `standards/test-corpora.md`](https://github.com/storytold/craftrules/blob/main/standards/test-corpora.md). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them. - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them.
- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI crate is swappable. - **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI crate is swappable.
+2 -3
View File
@@ -2,7 +2,7 @@
Every non-code asset in this repository (fonts, images, example art, presets) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table. Every non-code asset in this repository (fonts, images, example art, presets) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table.
**Policy (mandatory):** DesignCraft contains **no Adobe iconography, images, artwork, presets, swatch libraries or ICC profiles**. Every asset is original work by DesignCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Adobe software are never committed. The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`. **Policy (mandatory):** DesignCraft contains **no Adobe iconography, images, artwork, presets, swatch libraries or ICC profiles**. Every asset is original work by DesignCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Adobe software are never committed. Font files are not added here: shared fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (`CRAFT_FONTS_DIR`, see AGENTS.md and craftrules [`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`.
Generated-in-code art is original and has no file to list: the UI icon set (`crates/ui-egui/src/icons.rs`), default swatches, and the sample magazine's imagery (`crates/engine/src/sample.rs`). Generated-in-code art is original and has no file to list: the UI icon set (`crates/ui-egui/src/icons.rs`), default swatches, and the sample magazine's imagery (`crates/engine/src/sample.rs`).
@@ -27,8 +27,7 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `assets/fonts/SourceSerif4-It.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface | | `assets/fonts/SourceSerif4-It.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface |
| `assets/fonts/SourceSerif4-Regular.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface | | `assets/fonts/SourceSerif4-Regular.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface |
| `assets/fonts/SourceSerif4-Semibold.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface | | `assets/fonts/SourceSerif4-Semibold.ttf` | Frank Grießhammer / Adobe (released as open source) | https://github.com/adobe-fonts/source-serif | OFL-1.1 (`assets/fonts/OFL-SourceSerif4.txt`) | Open-source typeface |
| `assets/fonts/ShipporiMincho-Regular.ttf` | The Shippori Mincho Project Authors (FONTDASU) | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho | OFL-1.1 (`assets/fonts/OFL-ShipporiMincho.txt`) | Japanese UI and document fallback; SHA-256 `769b5269f0f9bc6534b352c0e6bd856a566e03ff788f107191c2d835863570b2` | | craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | https://github.com/storytold/craft-fonts, embedded only when built with `CRAFT_FONTS_DIR` (all official releases; web builds embed BIZ UDPGothic Regular only) | OFL-1.1 (licence texts ship in packages as `OFL-<family>.txt`) | Japanese UI and document fallback |
| `assets/fonts/OFL-ShipporiMincho.txt` | The Shippori Mincho Project Authors (FONTDASU) | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/OFL.txt | OFL-1.1 | Licence for Shippori Mincho |
| `docs/images/page-2.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI | | `docs/images/page-2.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
| `docs/images/page-3.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI | | `docs/images/page-3.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
| `docs/images/page-4.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI | | `docs/images/page-4.png` | DesignCraft contributors | screenshot/render of DesignCraft itself (sample document generated in code) | MIT OR Apache-2.0 | Original; no Adobe UI |
+1
View File
@@ -21,6 +21,7 @@ People trust DesignCraft with their layouts; a crash loses their work. **This ou
## Non-negotiables ## Non-negotiables
- **Clean-room.** InDesign is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic documents, take screenshots (by window id) stored only under `plan/indesign/screenshots/` (never committed). Never read, disassemble or copy anything inside the InDesign bundle (names/listings only), never copy Adobe icons, artwork, presets or wording beyond feature names, never commit files produced by InDesign. Never copy GPL/AGPL/LGPL code (Scribus, LibreOffice, Ghostscript…). - **Clean-room.** InDesign is installed on the dev machine and may be *observed* black-box: run it, use its UI with synthetic documents, take screenshots (by window id) stored only under `plan/indesign/screenshots/` (never committed). Never read, disassemble or copy anything inside the InDesign bundle (names/listings only), never copy Adobe icons, artwork, presets or wording beyond feature names, never commit files produced by InDesign. Never copy GPL/AGPL/LGPL code (Scribus, LibreOffice, Ghostscript…).
- **Assets:** no Adobe iconography or images — ever. Every asset is original / OSI / CC0 / redistributable CC (except the ArtCraft trademarks in `docs/brand/`, under `docs/brand/LICENSE-brand.txt`) and has a row in `ASSETS.md` (`cargo xtask assets` enforces it). Prefer art generated in code. - **Assets:** no Adobe iconography or images — ever. Every asset is original / OSI / CC0 / redistributable CC (except the ArtCraft trademarks in `docs/brand/`, under `docs/brand/LICENSE-brand.txt`) and has a row in `ASSETS.md` (`cargo xtask assets` enforces it). Prefer art generated in code.
- **Fonts live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts)**, never in this repo: don't commit new font files (the Latin UI/document fonts already in `assets/fonts/` stay). It is an optional build input, never a `Cargo.toml` dependency: `CRAFT_FONTS_DIR=/abs/path/to/craft-fonts cargo build` makes `crates/fonts/build.rs` embed its fonts as `designcraft_fonts::CRAFT_FONTS` (Japanese: BIZ UDPGothic for the UI, Mincho first for document text; web builds embed BIZ UDPGothic Regular only). Unset, `CRAFT_FONTS` is empty and the app uses its bundled and system fonts; code and tests must work that way (tests on its glyphs skip). Releases build with it. Standard: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
- **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus), explained in [craftrules `standards/test-corpora.md`](https://github.com/storytold/craftrules/blob/main/standards/test-corpora.md). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`. - **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus), explained in [craftrules `standards/test-corpora.md`](https://github.com/storytold/craftrules/blob/main/standards/test-corpora.md). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them. - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them.
- **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI crate is swappable. - **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI crate is swappable.
+5 -4
View File
@@ -18,13 +18,14 @@ list (author, source, license) is in ASSETS.md.
- English hyphenation word list (assets/hyphenation/en-us.dic) — derived from the Moby Hyphenator II - English hyphenation word list (assets/hyphenation/en-us.dic) — derived from the Moby Hyphenator II
list by Grady Ward, public domain (Project Gutenberg eBook #3204). The hyphenation patterns list by Grady Ward, public domain (Project Gutenberg eBook #3204). The hyphenation patterns
(assets/hyphenation/en-us.pat) are trained from it by DesignCraft and released as CC0-1.0. (assets/hyphenation/en-us.pat) are trained from it by DesignCraft and released as CC0-1.0.
- Japanese fonts (craft-fonts): builds made with craft-fonts (CRAFT_FONTS_DIR; all official
releases) embed the fonts listed in
https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, BIZ UDMincho,
Shippori Mincho), under the SIL Open Font License 1.1. Their licence texts ship with the
packages as OFL-<family>.txt. Builds without CRAFT_FONTS_DIR do not include them.
- All UI icons are drawn in code (crates/ui-egui/src/icons.rs) and are original work, MIT OR Apache-2.0. - All UI icons are drawn in code (crates/ui-egui/src/icons.rs) and are original work, MIT OR Apache-2.0.
Rust dependencies (vello_cpu, kurbo, skrifa, harfrust, image, egui, eframe, …) are permissively Rust dependencies (vello_cpu, kurbo, skrifa, harfrust, image, egui, eframe, …) are permissively
licensed (MIT/Apache-2.0/BSD/ISC/Zlib) and fetched through crates.io. licensed (MIT/Apache-2.0/BSD/ISC/Zlib) and fetched through crates.io.
No Adobe icons, images or artwork are included. No Adobe icons, images or artwork are included.
Shippori Mincho: Copyright 2021 The Shippori Mincho Project Authors
(https://github.com/fontdasu/ShipporiMincho). SIL Open Font License 1.1;
see assets/fonts/OFL-ShipporiMincho.txt.
+15 -1
View File
@@ -105,6 +105,19 @@ cargo run --release -p designcraft-cli -- commands # list every command
cargo xtask ci # fmt, clippy, tests, assets, layering, wasm cargo xtask ci # fmt, clippy, tests, assets, layering, wasm
``` ```
Japanese text (UI and documents) uses fonts from
[storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (font
files are never committed here; see craftrules
[`standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)):
```sh
git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p designcraft # absolute path
```
Without it, Japanese falls back to the system's fonts (none on the web). Release builds always
include it.
To drive a running app, send JSON lines to `127.0.0.1:7979`. The protocol is described in To drive a running app, send JSON lines to `127.0.0.1:7979`. The protocol is described in
[`docs/control-protocol.md`](docs/control-protocol.md). [`docs/control-protocol.md`](docs/control-protocol.md).
@@ -189,7 +202,8 @@ Copyright (c) 2026 ArtCraft Team and the DesignCraft contributors. Required noti
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 [ASSETS.md](ASSETS.md). with its author, source and license in [ASSETS.md](ASSETS.md).
The bundled fonts are under the SIL Open Font License; all UI icons are drawn in code and are original. The bundled fonts, and the craft-fonts fonts embedded by release builds, are under the SIL Open
Font License; all UI icons are drawn in code and are original.
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the 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 ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
+1 -1
View File
@@ -97,7 +97,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole). - Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. Interactive PDF export (`file.exportPdf {media: true}`, File ▸ Export PDF (Interactive)) embeds each file and plays it in a Screen annotation through a rendition action (controls, loop, play on page load). No in-app playback. - Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. Interactive PDF export (`file.exportPdf {media: true}`, File ▸ Export PDF (Interactive)) embeds each file and plays it in a Screen annotation through a rendition action (controls, loop, play on page load). No in-app playback.
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left. - Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Japanese glyphs (UI and documents) come from the bundled Shippori Mincho (OFL), so no OS fonts are needed, natively or on the web. - Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Japanese glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic for the UI, Mincho for documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts.
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update. - Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result. - PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted. - EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2021 The Shippori Mincho Project Authors (https://github.com/fontdasu/ShipporiMincho)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+55
View File
@@ -0,0 +1,55 @@
//! Optional craft-fonts build input (https://github.com/storytold/craft-fonts, recipe from its
//! `docs/integration.md`). With `CRAFT_FONTS_DIR=<checkout>` the fonts in its
//! `fonts/manifest.txt` are embedded as `CRAFT_FONTS`; unset, `CRAFT_FONTS` is empty. Web
//! (wasm32) builds embed only the UI face, BIZ UDPGothic Regular, to stay within the web size
//! budget. It only reads the local checkout: no network.
use std::fmt::Write as _;
use std::path::PathBuf;
fn main() {
println!("cargo::rerun-if-env-changed=CRAFT_FONTS_DIR");
println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED");
let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n");
if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from) {
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").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 == "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)
}
+7 -8
View File
@@ -15,11 +15,8 @@ use skrifa::{GlyphId, MetadataProvider};
/// The family used when a requested family is unknown (the UI sans). /// The family used when a requested family is unknown (the UI sans).
pub const FALLBACK_FAMILY: &str = "Source Sans 3"; pub const FALLBACK_FAMILY: &str = "Source Sans 3";
/// Shippori Mincho (OFL): Japanese glyphs for documents and the UI without OS fonts. One copy, /// The bundled fonts (OFL), as font file bytes. Japanese fonts are not bundled: they come from
/// shared with the UI's egui fonts (it is ~8.7 MB). /// the optional craft-fonts build input ([`crate::japanese_document_fonts`]).
pub static JAPANESE_FALLBACK: &[u8] = include_bytes!("../../../assets/fonts/ShipporiMincho-Regular.ttf");
/// The bundled fonts (OFL), as font file bytes.
pub fn bundled() -> &'static [&'static [u8]] { pub fn bundled() -> &'static [&'static [u8]] {
BUNDLED BUNDLED
} }
@@ -38,7 +35,6 @@ static BUNDLED: &[&[u8]] = &[
include_bytes!("../../../assets/fonts/Inter-Medium.ttf"), include_bytes!("../../../assets/fonts/Inter-Medium.ttf"),
include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"), include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"),
include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"),
JAPANESE_FALLBACK,
]; ];
#[derive(Clone)] #[derive(Clone)]
@@ -502,10 +498,13 @@ fn style_italic(style: &str) -> bool {
} }
impl FontDb { impl FontDb {
/// A database holding the bundled fonts, whose system font scan reads `font_dirs`. /// A database holding the bundled fonts, then the craft-fonts Japanese faces (Mincho first,
/// so they are the fallback for Japanese text after the requested and bundled fonts; empty
/// without `CRAFT_FONTS_DIR`), whose system font scan reads `font_dirs`.
pub fn with_font_dirs(font_dirs: Vec<std::path::PathBuf>) -> Self { pub fn with_font_dirs(font_dirs: Vec<std::path::PathBuf>) -> Self {
let mut faces = Vec::new(); let mut faces = Vec::new();
for data in BUNDLED { let craft = crate::japanese_document_fonts().into_iter().map(|f| f.bytes);
for data in BUNDLED.iter().copied().chain(craft) {
for (i, family, style, coords) in enumerate_faces(data) { for (i, family, style, coords) in enumerate_faces(data) {
if let Some(f) = make_face(FontBytes::Static(data), i, family, style, coords) { if let Some(f) = make_face(FontBytes::Static(data), i, family, style, coords) {
faces.push(Arc::new(f)); faces.push(Arc::new(f));
+76 -12
View File
@@ -9,13 +9,46 @@
mod fontdb; mod fontdb;
pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace, JAPANESE_FALLBACK, base_style, bundled, system_font_dirs}; pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace, base_style, bundled, system_font_dirs};
pub use harfrust::Feature; pub use harfrust::Feature;
use harfrust::{Direction, ShapeOptions, Tag, UnicodeBuffer}; use harfrust::{Direction, ShapeOptions, Tag, UnicodeBuffer};
pub use kurbo::BezPath; pub use kurbo::BezPath;
use skrifa::MetadataProvider; use skrifa::MetadataProvider;
use skrifa::instance::Size; use skrifa::instance::Size;
/// A font from the optional craft-fonts build input (https://github.com/storytold/craft-fonts;
/// empty unless built with `CRAFT_FONTS_DIR`, see `build.rs`).
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 faces for Japanese (`"Jpan"`), in manifest order. Empty without craft-fonts.
pub fn japanese_fonts() -> impl Iterator<Item = &'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.scripts.contains(&"Jpan"))
}
/// Japanese faces in document-fallback order: Mincho (serif) families first, matching the
/// serif default text font, then the others; Regular before other styles.
pub fn japanese_document_fonts() -> Vec<&'static CraftFont> {
let mut v: Vec<_> = japanese_fonts().collect();
v.sort_by_key(|f| (!f.family.contains("Mincho"), f.style != "Regular"));
v
}
/// Japanese faces in UI order: BIZ UDPGothic first (the UI face), then the others; `bold` puts
/// bold styles before regular ones.
pub fn japanese_ui_fonts(bold: bool) -> Vec<&'static CraftFont> {
let mut v: Vec<_> = japanese_fonts().collect();
v.sort_by_key(|f| (f.family != "BIZ UDPGothic", (f.style == "Bold") != bold));
v
}
/// InDesign's default text font is a serif; ours is Source Serif 4. /// InDesign's default text font is a serif; ours is Source Serif 4.
pub const DEFAULT_FAMILY: &str = "Source Serif 4"; pub const DEFAULT_FAMILY: &str = "Source Serif 4";
@@ -249,17 +282,48 @@ mod tests {
} }
#[test] #[test]
fn japanese_fallback_is_bundled_and_has_vertical_forms() { fn craft_fonts_japanese_faces_cover_japanese_and_have_vertical_forms() {
// Inspect the bundled face directly so this test cannot pass through an OS fallback. if CRAFT_FONTS.is_empty() {
let font = skrifa::FontRef::new(JAPANESE_FALLBACK).unwrap(); eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a checkout)");
for ch in "日本語縦書き横書きルビ、。「」".chars() { return;
assert_ne!(font.charmap().map(ch).unwrap_or_default(), skrifa::GlyphId::NOTDEF); }
let fonts = japanese_document_fonts();
assert!(!fonts.is_empty(), "craft-fonts has Japanese fonts");
assert!(fonts[0].family.contains("Mincho"), "documents fall back to a Mincho first");
assert_eq!(japanese_ui_fonts(false)[0].family, "BIZ UDPGothic");
assert_eq!(japanese_ui_fonts(true)[0].style, "Bold");
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
for cf in fonts {
// Inspect the embedded face directly so this cannot pass through an OS fallback.
let font = skrifa::FontRef::new(cf.bytes).unwrap();
for ch in "日本語縦書き横書きルビ、。「」".chars() {
assert_ne!(font.charmap().map(ch).unwrap_or_default(), skrifa::GlyphId::NOTDEF, "{} lacks {ch}", cf.family);
}
let face = db.face(cf.family, cf.style);
assert_eq!(face.family, cf.family);
let horizontal = shape(&face, "「」、。", &[], |c| c);
let vertical = shape(&face, "「」、。", &[feature("vert").unwrap(), feature("vrt2").unwrap()], |c| c);
assert_eq!(horizontal.len(), vertical.len());
assert!(horizontal.iter().zip(&vertical).any(|(h, v)| h.gid != v.gid), "{} has vertical forms", cf.family);
}
// Japanese in a Latin face falls back to a craft-fonts Mincho, without system fonts.
let latin = db.face(DEFAULT_FAMILY, "Regular");
let fb = db.fallback_for('語', latin.id()).unwrap();
assert!(fb.family.contains("Mincho"), "{fb:?}");
}
#[test]
fn works_without_craft_fonts() {
// Always true without CRAFT_FONTS_DIR; with it, the bundled fonts must still all load.
let db = FontDb::with_font_dirs(Vec::new());
db.set_system_fallback(false);
assert_eq!(db.face(DEFAULT_FAMILY, "Regular").family, DEFAULT_FAMILY);
assert!(!shape(&db.face(DEFAULT_FAMILY, "Regular"), "Hello", &[], |c| c).is_empty());
if CRAFT_FONTS.is_empty() {
assert!(japanese_fonts().next().is_none() && japanese_document_fonts().is_empty());
let latin = db.face(DEFAULT_FAMILY, "Regular");
assert!(db.fallback_for('語', latin.id()).is_none(), "no bundled Japanese font");
} }
let face = FontDb::global().face("Shippori Mincho", "Regular");
assert_eq!(face.family, "Shippori Mincho");
let horizontal = shape(&face, "「」、。", &[], |c| c);
let vertical = shape(&face, "「」、。", &[feature("vert").unwrap(), feature("vrt2").unwrap()], |c| c);
assert_eq!(horizontal.len(), vertical.len());
assert!(horizontal.iter().zip(&vertical).any(|(h, v)| h.gid != v.gid));
} }
} }
+4
View File
@@ -59,6 +59,10 @@ fn svg_options() -> &'static usvg::Options<'static> {
for f in designcraft_fonts::bundled() { for f in designcraft_fonts::bundled() {
db.load_font_data(f.to_vec()); db.load_font_data(f.to_vec());
} }
// Japanese text in SVGs: the craft-fonts faces (none without CRAFT_FONTS_DIR).
for f in designcraft_fonts::japanese_document_fonts() {
db.load_font_data(f.bytes.to_vec());
}
db.set_serif_family("Source Serif 4"); db.set_serif_family("Source Serif 4");
db.set_sans_serif_family("Source Sans 3"); db.set_sans_serif_family("Source Sans 3");
db.set_monospace_family("JetBrains Mono"); db.set_monospace_family("JetBrains Mono");
+63 -9
View File
@@ -254,6 +254,13 @@ impl Tokens {
} }
pub fn install_fonts(ctx: &egui::Context) { pub fn install_fonts(ctx: &egui::Context) {
ctx.set_fonts(font_definitions(designcraft_fonts::CRAFT_FONTS));
}
/// The UI fonts: the app's own, then the craft-fonts Japanese faces from `craft` (BIZ UDPGothic
/// first; empty without `CRAFT_FONTS_DIR`) as a fallback at the end of every family. egui has
/// no system-font discovery, so without craft-fonts Japanese UI text has no glyphs.
fn font_definitions(craft: &'static [designcraft_fonts::CraftFont]) -> FontDefinitions {
let mut fonts = FontDefinitions::default(); let mut fonts = FontDefinitions::default();
let add = |fonts: &mut FontDefinitions, name: &str, data: &'static [u8]| { let add = |fonts: &mut FontDefinitions, name: &str, data: &'static [u8]| {
fonts.font_data.insert(name.into(), Arc::new(FontData::from_static(data))); fonts.font_data.insert(name.into(), Arc::new(FontData::from_static(data)));
@@ -261,14 +268,25 @@ pub fn install_fonts(ctx: &egui::Context) {
add(&mut fonts, "ui", include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf")); add(&mut fonts, "ui", include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"));
add(&mut fonts, "ui-semibold", include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf")); add(&mut fonts, "ui-semibold", include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf"));
add(&mut fonts, "mono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf")); add(&mut fonts, "mono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
add(&mut fonts, "japanese", designcraft_fonts::JAPANESE_FALLBACK);
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "ui".into()); fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "ui".into());
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "mono".into()); fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "mono".into());
fonts.families.insert(FontFamily::Name("semibold".into()), vec!["ui-semibold".into(), "ui".into()]); fonts.families.insert(FontFamily::Name("semibold".into()), vec!["ui-semibold".into(), "ui".into()]);
for stack in fonts.families.values_mut() { let mut japanese: Vec<&designcraft_fonts::CraftFont> = craft.iter().filter(|f| f.scripts.contains(&"Jpan")).collect();
stack.push("japanese".into()); // BIZ UDPGothic (the UI face) first, Regular before Bold; the semibold family prefers Bold.
japanese.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != "Regular"));
let name = |f: &designcraft_fonts::CraftFont| format!("craft-{}-{}", f.family, f.style);
for f in &japanese {
add(&mut fonts, &name(f), f.bytes);
} }
ctx.set_fonts(fonts); for (family, stack) in fonts.families.iter_mut() {
let bold = *family == FontFamily::Name("semibold".into());
let mut order = japanese.clone();
if bold {
order.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != "Bold"));
}
stack.extend(order.iter().map(|f| name(f)));
}
fonts
} }
pub fn semibold(size: f32) -> FontId { pub fn semibold(size: f32) -> FontId {
@@ -338,19 +356,55 @@ impl Tokens {
#[cfg(test)] #[cfg(test)]
mod japanese_font_tests { mod japanese_font_tests {
#[test] fn families() -> [egui::FontFamily; 3] {
fn japanese_ui_glyphs_are_available_in_every_family() { [egui::FontFamily::Proportional, egui::FontFamily::Monospace, egui::FontFamily::Name("semibold".into())]
}
fn ctx_with(craft: &'static [designcraft_fonts::CraftFont]) -> egui::Context {
let ctx = egui::Context::default(); let ctx = egui::Context::default();
super::install_fonts(&ctx); ctx.set_fonts(super::font_definitions(craft));
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {}); let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
output.textures_delta.clear(); output.textures_delta.clear();
ctx
}
#[test]
fn japanese_ui_glyphs_are_available_in_every_family() {
if designcraft_fonts::CRAFT_FONTS.is_empty() {
eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a checkout)");
return;
}
let ctx = ctx_with(designcraft_fonts::CRAFT_FONTS);
ctx.fonts_mut(|fonts| { ctx.fonts_mut(|fonts| {
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace, egui::FontFamily::Name("semibold".into())] { for family in families() {
let font = egui::FontId::new(13.0, family); let font = egui::FontId::new(13.0, family);
for ch in "日本語縦書き横書き組み方向ルビ圏点".chars() { // Real glyphs (no tofu) for every character.
for ch in "日本語の文字縦書き横書き組み方向ルビ圏点".chars() {
assert!(fonts.has_glyph(&font, ch), "missing {ch} in {font:?}"); assert!(fonts.has_glyph(&font, ch), "missing {ch} in {font:?}");
} }
} }
}); });
// The UI face is BIZ UDPGothic, ahead of the Mincho faces.
let defs = super::font_definitions(designcraft_fonts::CRAFT_FONTS);
let stack = &defs.families[&egui::FontFamily::Proportional];
assert_eq!(stack.iter().find(|n| n.starts_with("craft-")).map(String::as_str), Some("craft-BIZ UDPGothic-Regular"));
}
#[test]
fn ui_works_without_craft_fonts() {
let ctx = ctx_with(&[]);
ctx.fonts_mut(|fonts| {
// (egui's has_glyph reports false for characters of the face that also supplies the
// replacement glyph, as Source Sans Semibold does in the semibold stack.)
let body = egui::FontId::new(13.0, egui::FontFamily::Proportional);
assert!(fonts.has_glyphs(&body, "DesignCraft"));
for family in families() {
let font = egui::FontId::new(13.0, family);
let galley = fonts.layout_no_wrap("日本語 DesignCraft".into(), font, egui::Color32::WHITE);
assert!(galley.size().x > 0.0);
}
});
// And the real installer works with whatever this build has.
super::install_fonts(&egui::Context::default());
} }
} }
+11
View File
@@ -50,6 +50,17 @@ copy_docs() {
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"
}
# Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so
# the package carries their licences as OFL-<family-dir>.txt. Nothing without CRAFT_FONTS_DIR.
copy_font_licences() {
local dest="$1" f
[ -n "${CRAFT_FONTS_DIR:-}" ] || return 0
for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do
if [ -f "$f" ]; then cp "$f" "$dest/OFL-$(basename "$(dirname "$f")").txt"; fi
done
} }
# Portable SHA-256 of a file (prints just the hash). # Portable SHA-256 of a file (prints just the hash).
+2
View File
@@ -103,6 +103,8 @@ mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). # Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile).
cp "$WORK/bin/designcraft" "$APP/Contents/MacOS/DesignCraft" cp "$WORK/bin/designcraft" "$APP/Contents/MacOS/DesignCraft"
cp "$ROOT/assets/app-icon/designcraft.icns" "$APP/Contents/Resources/DesignCraft.icns" cp "$ROOT/assets/app-icon/designcraft.icns" "$APP/Contents/Resources/DesignCraft.icns"
# Licences of the embedded craft-fonts fonts (only when built with CRAFT_FONTS_DIR).
copy_font_licences "$APP/Contents/Resources"
sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \
-e "s/@BUILD_SHA@/${DESIGNCRAFT_BUILD_SHA:-unknown}/g" \ -e "s/@BUILD_SHA@/${DESIGNCRAFT_BUILD_SHA:-unknown}/g" \
"$HERE/Info.plist.in" >"$APP/Contents/Info.plist" "$HERE/Info.plist.in" >"$APP/Contents/Info.plist"
+6
View File
@@ -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 }
} }
# Builds made with craft-fonts (CRAFT_FONTS_DIR) embed its fonts: ship their licences.
if ($env:CRAFT_FONTS_DIR) {
foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue) {
Copy-Item $ofl.FullName (Join-Path $Portable "OFL-$($ofl.Directory.Name).txt")
}
}
$Zip = Join-Path $Dist "designcraft-$Version-windows-$Arch-portable.zip" $Zip = Join-Path $Dist "designcraft-$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