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

Shippori Mincho (and its OFL file) leaves assets/fonts/: the Japanese fonts now
live in storytold/craft-fonts. printcraft-fonts gets craft-fonts' build.rs recipe:
with CRAFT_FONTS_DIR=<checkout> it embeds the manifest's fonts as CRAFT_FONTS
(wasm32: only BIZ UDPGothic Regular); without it CRAFT_FONTS is empty and the
app builds, tests and runs as before, minus the Japanese face.

- SHIPPORI_MINCHO is now Option<&[u8]> from CRAFT_FONTS; japanese_glyph
  (was shippori_glyph) uses document_japanese_font() (Shippori Mincho, then
  BIZ UDMincho, then any Jpan face) and returns GlyphError::NoFont without one.
- Editing PDF text into Japanese without craft-fonts is a clear EditError that
  leaves the page untouched.
- UI: every Jpan craft font (BIZ UDPGothic first) is the last fallback of
  every egui family, sharing the same static bytes.
- Tests: Japanese glyph/edit tests skip without craft-fonts; new UI tests for
  no-tofu Japanese with craft-fonts and working fonts without it.
- CI: the Linux test job checks out craft-fonts at a pinned SHA and runs the
  tests with CRAFT_FONTS_DIR. Releases build every platform with it
  (CRAFT_FONTS_REQUIRED=1) and ship each font's OFL as OFL-<family>.txt.
- ATTRIBUTION.toml gains [[build_input]] (checked by xtask assets); NOTICE,
  AGENTS.md, CLAUDE.md, README and docs say fonts live in craft-fonts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-06 11:36:23 -04:00
1 parent 2e49a84ace
commit 1d67833599
25 files changed
+530 -144

No files matched your search

+18
View File
@@ -27,6 +27,16 @@ jobs:
steps:
- uses: actions/checkout@v4
# The optional craft-fonts build input (Japanese fonts), so the Japanese font tests run on
# Linux; macOS and Windows test the build without it. See craftrules standards/fonts.md.
- name: Check out craft-fonts (Linux)
if: runner.os == 'Linux'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- name: Install Linux system dependencies (egui/winit/rfd)
if: runner.os == 'Linux'
run: |
@@ -49,6 +59,14 @@ jobs:
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
if: runner.os != 'Linux'
run: cargo test --workspace
- name: Test (with craft-fonts)
if: runner.os == 'Linux'
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: cargo test --workspace
- name: Layering
+46
View File
@@ -70,6 +70,13 @@ jobs:
PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@v4
# The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately.
- name: Check out craft-fonts
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
@@ -90,6 +97,8 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: packaging/macos/package.sh --arch universal
- uses: actions/upload-artifact@v4
with:
@@ -120,6 +129,13 @@ jobs:
PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@v4
# The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately.
- name: Check out craft-fonts
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
@@ -144,6 +160,8 @@ jobs:
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }}
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
- uses: actions/upload-artifact@v4
with:
@@ -170,6 +188,13 @@ jobs:
NFPM_VERSION: 2.47.0
steps:
- uses: actions/checkout@v4
# The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately.
- name: Check out craft-fonts
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- name: Install build and packaging tools
run: |
sudo apt-get update
@@ -187,6 +212,9 @@ jobs:
if: inputs.version != ''
run: cargo xtask version set "$PRINTCRAFT_VERSION"
- name: Build and package (AppImage, deb, rpm, tar.gz)
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: packaging/linux/package.sh
- name: Check the packages
run: |
@@ -217,6 +245,13 @@ jobs:
VERSION_OVERRIDE: ${{ inputs.version }}
steps:
- uses: actions/checkout@v4
# The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately.
- name: Check out craft-fonts
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- name: Build and package in a FreeBSD VM (tar.gz)
# vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build).
uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc
@@ -234,6 +269,7 @@ jobs:
set -e
. "$HOME/.cargo/env"
git config --global --add safe.directory "$PWD"
export CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1
if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$PRINTCRAFT_VERSION"; fi
packaging/freebsd/package.sh
# Only dist/ comes back out of the VM: copying the target dir back fails on large builds.
@@ -255,6 +291,13 @@ jobs:
TRUNK_VERSION: 0.21.14
steps:
- uses: actions/checkout@v4
# The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately.
- name: Check out craft-fonts
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
repository: storytold/craft-fonts
ref: abb83316d96aa59c1cf64784289e378fe9fa5695
path: craft-fonts
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
@@ -270,6 +313,9 @@ jobs:
if: inputs.version != ''
run: cargo xtask version set "$PRINTCRAFT_VERSION"
- name: Build and zip
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
CRAFT_FONTS_REQUIRED: "1"
run: packaging/web/package.sh
- uses: actions/upload-artifact@v4
with:
+2
View File
@@ -7,3 +7,5 @@ plan/
.DS_Store
/fuzz-out
/assets/models
# A craft-fonts checkout used as the optional CRAFT_FONTS_DIR build input (CI and releases put it here).
/craft-fonts
+8 -1
View File
@@ -64,7 +64,14 @@ Anything else is forbidden, including assets whose licence you cannot prove from
- an image, icon or font names Adobe as author or source.
- **`cargo xtask demo-pdf`** downloads its fonts only from `ATTRIBUTION.toml` entries, pinned by SHA-256. Afterwards it **fails if the PDF embeds any font not in its allowlist**, which catches the browser silently falling back to a system font.
### 1.4 Procedure for adding or changing an asset
### 1.4 Fonts live in craft-fonts
- Font assets shared by the Crafting Apps, including every Japanese font, live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), not here. **Never commit a font file to this repository**; adding a font means adding it to craft-fonts. (The Latin UI fonts already in `assets/fonts/` — Inter, JetBrains Mono, Dancing Script — stay.) The rules are in [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
- craft-fonts is an **optional build input**, never a Cargo dependency: `CRAFT_FONTS_DIR=<checkout> cargo build` makes `crates/fonts/build.rs` embed its fonts as `printcraft_fonts::CRAFT_FONTS`. Without it PrintCraft builds, tests and runs, but has no Japanese face (Japanese UI text shows replacement boxes, and writing Japanese into a PDF returns a clear error). Release builds always set it (`.github/workflows/release.yml`), and `ATTRIBUTION.toml` lists it as a `[[build_input]]`.
### 1.5 Procedure for adding or changing an asset
Fonts go to craft-fonts instead (§1.4).
1. Find an asset under an allowed licence from its **original source**: the author's repository or a release, not a random mirror. Record the exact version or commit.
2. Confirm the authorship. It must not be Adobe (§1.1). Keep the licence text.
+9 -3
View File
@@ -4,7 +4,7 @@
Every asset PrintCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
## In this repository (244)
## In this repository (242)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
@@ -186,8 +186,6 @@ Every asset PrintCraft includes, bundles or uses to build its published material
| `assets/fonts/Inter-Medium.ttf` | Inter Medium | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/Inter-SemiBold.ttf` | Inter SemiBold | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono Regular | The JetBrains Mono Project Authors | OFL-1.1 | https://github.com/JetBrains/JetBrainsMono | UI monospace font; demo PDF code |
| `assets/fonts/ShipporiMincho-Regular.ttf` | Shippori Mincho Regular | The Shippori Mincho Project Authors (FONTDASU) | OFL-1.1 | https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/ShipporiMincho-Regular.ttf | Japanese fallback glyphs in edited PDF text and the edit-text UI |
| `assets/fonts/OFL-ShipporiMincho.txt` | SIL Open Font License 1.1 for Shippori Mincho | The Shippori Mincho Project Authors (FONTDASU) | OFL-1.1 | https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/OFL.txt | License for Shippori Mincho fallback font |
| `vendor/hayro-interpret/assets/FoxitDingbats.pfb` | FoxitDingbats (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
| `vendor/hayro-interpret/assets/FoxitFixed.pfb` | FoxitFixed (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
| `vendor/hayro-interpret/assets/FoxitFixedBold.pfb` | FoxitFixedBold (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
@@ -292,3 +290,11 @@ Fonts are fetched by `cargo xtask demo-pdf` into `target/demo-fonts/`, OCR model
| `Inter-Italic[opsz,wght].ttf` | Inter Italic (variable) | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/inter | Showcase PDF (cargo xtask demo-pdf) |
| `text-detection.rten` | Ocrs text detection model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0) | Scan & OCR: finds words on page images (cargo xtask models) |
| `text-recognition.rten` | Ocrs text recognition model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0) | Scan & OCR: reads the words it finds (cargo xtask models) |
## Optional build inputs (1)
Not in this repository and never downloaded by it: compiled in only when the build sets the option (official releases do). Each input attributes its own files.
| Input | Option | Title | Author | Licence | Source | Attribution | Used for |
|---|---|---|---|---|---|---|---|
| `craft-fonts` | `CRAFT_FONTS_DIR` | Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho | The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU) | OFL-1.1 | https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml) | https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); the web build embeds only BIZ UDPGothic Regular |
+13 -22
View File
@@ -4,6 +4,8 @@
# [[asset]] a file in this repository (committed or vendored); `sha256` must match the file.
# [[bundled]] a file compiled into PrintCraft by a Cargo dependency (its crate version must match Cargo.lock).
# [[fetched]] downloaded at build time by xtask (never committed), pinned by `sha256`.
# [[build_input]] an optional build input outside this repository (e.g. craft-fonts via CRAFT_FONTS_DIR);
# never committed or downloaded here, its own repository attributes each file.
#
# licence: SPDX expression from the allowlist in AGENTS.md §1.2. adobe_data = true marks the closed list of
# Adobe-authored non-visual technical data allowed by AGENTS.md §1.1.
@@ -1966,28 +1968,6 @@ kind = "font"
usage = "UI monospace font; demo PDF code"
sha256 = "e6fd0d7e91550b3ed2b735d4312474362c4716edc4fc0577a0f61ed782d5aed1"
[[asset]]
path = "assets/fonts/ShipporiMincho-Regular.ttf"
title = "Shippori Mincho Regular"
author = "The Shippori Mincho Project Authors (FONTDASU)"
source = "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/ShipporiMincho-Regular.ttf"
licence = "OFL-1.1"
licence_file = "assets/fonts/OFL-ShipporiMincho.txt"
kind = "font"
usage = "Japanese fallback glyphs in edited PDF text and the edit-text UI"
sha256 = "769b5269f0f9bc6534b352c0e6bd856a566e03ff788f107191c2d835863570b2"
[[asset]]
path = "assets/fonts/OFL-ShipporiMincho.txt"
title = "SIL Open Font License 1.1 for Shippori Mincho"
author = "The Shippori Mincho Project Authors (FONTDASU)"
source = "https://raw.githubusercontent.com/google/fonts/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/shipporimincho/OFL.txt"
licence = "OFL-1.1"
licence_file = "assets/fonts/OFL-ShipporiMincho.txt"
kind = "licence"
usage = "License for Shippori Mincho fallback font"
sha256 = "41fba056279be5f45ff9a99e44b7b53897b42732f5806d8e666e0ab49ac6bd38"
[[asset]]
path = "vendor/hayro-interpret/assets/FoxitDingbats.pfb"
title = "FoxitDingbats (standard-14 substitute font)"
@@ -3051,3 +3031,14 @@ licence_sha256 = "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1c
kind = "model"
usage = "Scan & OCR: reads the words it finds (cargo xtask models)"
sha256 = "e484866d4cce403175bd8d00b128feb08ab42e208de30e42cd9889d8f1735a6e"
[[build_input]]
name = "craft-fonts"
option = "CRAFT_FONTS_DIR"
title = "Japanese fonts: BIZ UDPGothic (Regular, Bold), Shippori Mincho, BIZ UDMincho"
author = "The BIZ UDGothic and BIZ UDMincho Project Authors (Morisawa Inc.), The Shippori Mincho Project Authors (FONTDASU)"
source = "https://github.com/storytold/craft-fonts (pinned by commit in .github/workflows/release.yml)"
licence = "OFL-1.1"
attribution = "https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md"
kind = "font"
usage = "Japanese fallback in the interface (BIZ UDPGothic first) and Japanese glyphs in edited PDF text (Shippori Mincho); the web build embeds only BIZ UDPGothic Regular"
+1
View File
@@ -13,6 +13,7 @@ PrintCraft is a clean-room, open-source, Rust-native PDF application targeting A
## Non-negotiables
- **Assets: read `AGENTS.md` §1 before adding or showing any icon, image, font or document.** No assets from Adobe products, ever. Only openly licensed or contributor-original assets are allowed, each with an entry in `ATTRIBUTION.toml`. `cargo xtask assets` enforces this. `AGENTS.md` overrides this file.
- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts).** Never commit font files here (`AGENTS.md` §1.4, [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). It is the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR=../craft-fonts cargo test --workspace` embeds the Japanese fonts (UI fallback, Japanese text in edited PDFs) and runs their tests, which otherwise skip. Code using `printcraft_fonts::CRAFT_FONTS` must work when it is empty.
- **Clean-room.**
- Never read, disassemble or copy anything inside the Acrobat bundle (names and listings only). **Never open `Contents/Resources/JavaScripts/`.**
- Behaviour comes from public docs, specs (ISO 32000-2, the Arlington model) and black-box observation (`plan/acrobat/`).
+6
View File
@@ -16,6 +16,12 @@ list (author, source, license) is in ATTRIBUTION.md.
- Inter typeface (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
- JetBrains Mono typeface (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt.
- Dancing Script typeface (assets/fonts/DancingScript.ttf), Copyright 2016 The Dancing Script Project Authors — SIL Open Font License 1.1 (Reserved Font Name "Dancing Script"). See assets/fonts/OFL-DancingScript.txt.
- Japanese fonts from craft-fonts (https://github.com/storytold/craft-fonts), an optional build input
(CRAFT_FONTS_DIR) that is not part of this repository. Builds made with it, which includes every
official release, embed the fonts listed in
https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, Shippori Mincho,
BIZ UDMincho; the web build only BIZ UDPGothic Regular) under the SIL Open Font License 1.1. Those
packages carry each font's licence as OFL-<family>.txt.
- hayro (rendering, via crates.io) — Apache-2.0 OR MIT. Its `embed-fonts` feature bundles the Foxit
standard-14 Type 1 fonts under BSD-3-Clause ("Copyright 2014 PDFium Authors"), and hayro-cmap bundles
Adobe CMap resources under BSD-3-Clause.
+12 -1
View File
@@ -351,6 +351,15 @@ cargo xtask demo-pdf # build the showcase PDF used
cargo xtask screenshots # regenerate every screenshot in this README
```
Japanese fonts come from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build
input that every release includes. To build with them (Japanese interface text, and Japanese text in
edited PDFs):
```sh
git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p printcraft -- some.pdf
```
## What's next
PrintCraft is young and moving fast. The aim is a workbench where you can view, organize, annotate, fill, sign and edit PDFs, at parity with Acrobat Pro.
@@ -416,7 +425,9 @@ PrintCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APA
Copyright (c) 2026 ArtCraft Team and the PrintCraft contributors. Required notices are in [NOTICE](NOTICE).
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). Release builds also embed
the Japanese fonts of [craft-fonts](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md)
(SIL Open Font License 1.1).
The 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
-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.
+37
View File
@@ -279,8 +279,20 @@ fn descriptor_text_page() -> Document {
Document::open(Arc::new(out)).unwrap()
}
/// The Japanese fallback face comes from craft-fonts, an optional build input.
fn without_craft_fonts(test: &str) -> bool {
if printcraft_fonts::document_japanese_font().is_some() {
return false;
}
eprintln!("skipping {test}: built without craft-fonts (set CRAFT_FONTS_DIR to run it)");
true
}
#[test]
fn japanese_line_uses_unicode_type3_fallback() {
if without_craft_fonts("japanese_line_uses_unicode_type3_fallback") {
return;
}
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
let replacement = "25362738こんにちは、お元気ですか? 2.3444";
let result = text::replace_line(&mut doc, 0, 0, replacement).unwrap();
@@ -294,12 +306,37 @@ fn japanese_line_uses_unicode_type3_fallback() {
#[test]
fn japanese_paragraph_uses_unicode_type3_fallback() {
if without_craft_fonts("japanese_paragraph_uses_unicode_type3_fallback") {
return;
}
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
let replacement = "こんにちは、お元気ですか? 2.3444 日本語の文章";
text::replace_block(&mut doc, 0, 0, replacement).unwrap();
let reopened = reopen(&doc);
assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement);
}
/// Without craft-fonts there is no Japanese face: editing in Japanese is a clear error that
/// leaves the page untouched, never a panic; Latin edits work as before.
#[test]
fn japanese_edit_without_craft_fonts_is_a_clear_error() {
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
let before = page_content_bytes(&doc, 0);
let line = text::replace_line(&mut doc, 0, 0, "日本語の文字");
let block = text::replace_block(&mut doc, 0, 0, "日本語の文字");
if printcraft_fonts::document_japanese_font().is_some() {
eprintln!("built with craft-fonts: the Japanese edits succeed (checked by the tests above)");
assert!(line.is_ok() && block.is_ok());
return;
}
for result in [line.map(|_| ()), block.map(|_| ())] {
let Err(EditError::Invalid(msg)) = result else { panic!("expected a clear error, got {result:?}") };
assert!(msg.contains("Japanese fallback font") && msg.contains("CRAFT_FONTS_DIR"), "{msg}");
}
assert_eq!(page_content_bytes(&doc, 0), before);
let latin = text::replace_line(&mut doc, 0, 0, "Hello").unwrap();
assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, "Hello", "{latin:?}");
}
#[test]
fn font_descriptor_style_is_exposed_even_with_a_neutral_name() {
let doc = descriptor_text_page();
+18 -5
View File
@@ -16,7 +16,7 @@ use std::rc::Rc;
use printcraft_content::{Matrix, Op, parse, serialize_ops};
use printcraft_cos::{Dict, Document, Object, PdfString, Stream};
use printcraft_fonts::pdf::Metrics;
use printcraft_fonts::{GlyphError, shippori_glyph};
use printcraft_fonts::{GlyphError, japanese_glyph};
use crate::EditError;
@@ -478,6 +478,8 @@ const MAX_TYPE3_GLYPHS: usize = 240;
#[derive(Clone)]
struct Type3Fallback {
name: String,
/// The family its glyphs come from ("Shippori Mincho").
family: &'static str,
codes: Vec<(char, u8, f64)>,
}
@@ -486,7 +488,8 @@ fn pdf_num(v: f64) -> String {
}
fn type3_path(ch: char) -> Result<(Vec<u8>, f64), EditError> {
let glyph = shippori_glyph(ch).map_err(|e| match e {
let glyph = japanese_glyph(ch).map_err(|e| match e {
GlyphError::NoFont => no_japanese_font(),
GlyphError::Missing => EditError::Invalid(format!("Japanese fallback font has no glyph for U+{:04X}", ch as u32)),
GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)),
})?;
@@ -510,7 +513,17 @@ fn unicode_hex(ch: char) -> String {
encoded.iter().map(|u| format!("{u:04X}")).collect()
}
/// This build has no Japanese face to draw replacement text with.
fn no_japanese_font() -> EditError {
EditError::Invalid(
"this text needs PrintCraft's Japanese fallback font, which this build doesn't include \
(official releases do; to build it in, set CRAFT_FONTS_DIR to a craft-fonts checkout)"
.into(),
)
}
fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Type3Fallback, EditError> {
let family = printcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?.family;
let mut chars = Vec::new();
for ch in text.chars() {
if !chars.contains(&ch) {
@@ -573,7 +586,7 @@ fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Ty
name = format!("PCJp{suffix}");
}
fonts_res.set(name.as_bytes().to_vec(), Object::Dict(font));
Ok(Type3Fallback { name, codes })
Ok(Type3Fallback { name, family, codes })
}
fn type3_encode(fallback: &Type3Fallback, text: &str) -> Option<Vec<u8>> {
@@ -618,7 +631,7 @@ pub fn replace_line(doc: &mut Document, page: usize, line: usize, text: &str) ->
replacement.push(Op::new("Tf", vec![Object::name(&fallback.name), printcraft_content::num(size)]));
replacement.push(Op::new("Tj", vec![Object::String(PdfString::literal(bytes))]));
replacement.push(Op::new("Tf", vec![Object::name(&target.font), printcraft_content::num(size)]));
substituted = Some("Shippori Mincho Type3".into());
substituted = Some(format!("{} Type3", fallback.family));
} else {
let win = printcraft_fonts::win_ansi(&text);
// WinAnsi turns what it can't show into '?'; refuse rather than print the wrong thing.
@@ -895,7 +908,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
} else if let Some(fallback) = type3.clone() {
let name = fallback.name.clone();
let encoder = fallback.clone();
substituted = Some("Shippori Mincho Type3".into());
substituted = Some(format!("{} Type3", fallback.family));
(name, Box::new(move |s: &str| type3_encode(&encoder, s)))
} else {
let win = printcraft_fonts::win_ansi(&text);
+14
View File
@@ -11,4 +11,18 @@ Today it holds what generated appearances need without a font program:
- `win_ansi`: Unicode → WinAnsiEncoding bytes (`?` for characters it can't represent), and
`literal` to write bytes as a PDF literal string.
It also holds the fonts of the optional [craft-fonts](https://github.com/storytold/craft-fonts)
build input. `build.rs` embeds them as `CRAFT_FONTS` when the build sets
`CRAFT_FONTS_DIR=<checkout>` (and fails if it can't while `CRAFT_FONTS_REQUIRED=1`, as releases
set). Without that variable `CRAFT_FONTS` is empty and everything below copes:
- `ui_japanese_fonts`: the `Jpan` faces for the interface, BIZ UDPGothic first.
- `document_japanese_font` / `japanese_glyph`: the face (Shippori Mincho, then BIZ UDMincho) whose
outlines become the Type 3 fallback font for Japanese text written into PDFs. Without it,
`japanese_glyph` returns `GlyphError::NoFont` and the editor reports a clear error.
- `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`.
wasm32 builds embed only BIZ UDPGothic Regular, to keep the web build small. Font files are never
committed here: see [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md).
The full font subsystem (parsing, shaping, subsetting and embedding) arrives with M2.2/M7.
+55
View File
@@ -0,0 +1,55 @@
//! Embeds the fonts of the optional craft-fonts build input (`CRAFT_FONTS_DIR`) as `CRAFT_FONTS`.
//! The recipe is craft-fonts' `docs/integration.md`; unset, `CRAFT_FONTS` is empty. It only reads
//! the local checkout (no network). On wasm32 it embeds only the UI face (BIZ UDPGothic Regular),
//! to keep the web build within hosting limits (Cloudflare Pages: 25 MiB per file).
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)
}
+103
View File
@@ -0,0 +1,103 @@
//! Fonts from the optional craft-fonts build input (<https://github.com/storytold/craft-fonts>).
//!
//! `build.rs` embeds every font in craft-fonts' manifest when PrintCraft is built with
//! `CRAFT_FONTS_DIR=<checkout>`; otherwise [`CRAFT_FONTS`] is empty and everything here returns
//! nothing. Callers must work either way.
/// A font from the optional craft-fonts build input (empty unless built with `CRAFT_FONTS_DIR`).
pub struct CraftFont {
pub family: &'static str,
pub style: &'static str,
/// ISO 15924 scripts the font is for, e.g. `"Jpan"`.
pub scripts: &'static [&'static str],
pub bytes: &'static [u8],
}
include!(concat!(env!("OUT_DIR"), "/craft_fonts.rs"));
impl CraftFont {
/// Whether the font is meant for `script` (ISO 15924, e.g. `"Jpan"`).
pub fn covers(&self, script: &str) -> bool {
self.scripts.contains(&script)
}
/// A unique name for registering the face with a font system ("BIZ UDPGothic Bold").
pub fn name(&self) -> String {
format!("{} {}", self.family, self.style)
}
}
/// The `Jpan` craft-fonts faces in the order the interface prefers them: BIZ UDPGothic (a UI
/// face) first, then the rest in manifest order. Empty without craft-fonts.
pub fn ui_japanese_fonts() -> Vec<&'static CraftFont> {
let mut fonts: Vec<&CraftFont> = CRAFT_FONTS.iter().filter(|f| f.covers("Jpan")).collect();
// Stable: manifest order within each group.
fonts.sort_by_key(|f| f.family != "BIZ UDPGothic");
fonts
}
/// The face for Japanese text written into PDFs (serif document text): Shippori Mincho, then
/// BIZ UDMincho, then any other regular `Jpan` face. `None` without craft-fonts.
pub fn document_japanese_font() -> Option<&'static CraftFont> {
let jpan = || CRAFT_FONTS.iter().filter(|f| f.covers("Jpan"));
["Shippori Mincho", "BIZ UDMincho"]
.iter()
.find_map(|family| jpan().find(|f| f.family == *family && f.style == "Regular"))
.or_else(|| jpan().find(|f| f.style == "Regular"))
.or_else(|| jpan().next())
}
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn find(family: &str, style: &str) -> Option<&'static [u8]> {
let mut i = 0;
while i < CRAFT_FONTS.len() {
let f = &CRAFT_FONTS[i];
if str_eq(f.family, family) && str_eq(f.style, style) {
return Some(f.bytes);
}
i += 1;
}
None
}
/// Shippori Mincho Regular from craft-fonts, the preferred face for Japanese document text.
/// `None` when PrintCraft was built without craft-fonts.
pub static SHIPPORI_MINCHO: Option<&[u8]> = find("Shippori Mincho", "Regular");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn craft_fonts_are_optional_and_consistent() {
// Holds with or without CRAFT_FONTS_DIR.
assert_eq!(SHIPPORI_MINCHO.is_some(), CRAFT_FONTS.iter().any(|f| f.family == "Shippori Mincho" && f.style == "Regular"));
assert_eq!(document_japanese_font().is_some(), CRAFT_FONTS.iter().any(|f| f.covers("Jpan")));
let ui = ui_japanese_fonts();
assert_eq!(ui.len(), CRAFT_FONTS.iter().filter(|f| f.covers("Jpan")).count());
if CRAFT_FONTS.is_empty() {
eprintln!("built without craft-fonts (CRAFT_FONTS_DIR unset): no Japanese faces, as expected");
assert!(SHIPPORI_MINCHO.is_none() && ui.is_empty());
return;
}
assert!(CRAFT_FONTS.iter().all(|f| !f.bytes.is_empty()));
if CRAFT_FONTS.iter().any(|f| f.family == "BIZ UDPGothic") {
assert_eq!(ui.first().map(|f| f.family), Some("BIZ UDPGothic"));
}
assert_eq!(document_japanese_font().map(|f| f.family), Some("Shippori Mincho"));
}
}
+3 -1
View File
@@ -5,10 +5,12 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod craft;
mod encodings;
pub mod pdf;
mod script;
pub use script::{GlyphError, GlyphOutline, SHIPPORI_MINCHO, ScriptOutline, script_outline, shippori_glyph};
pub use craft::{CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, ui_japanese_fonts};
pub use script::{GlyphError, GlyphOutline, ScriptOutline, japanese_glyph, script_outline};
/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.
pub fn helvetica_width(s: &str, size: f64) -> f64 {
+21 -13
View File
@@ -1,4 +1,4 @@
//! Typed signatures and bounded glyph outlines from approved bundled fonts.
//! Typed signatures and bounded glyph outlines from approved fonts (bundled, or craft-fonts).
use skrifa::instance::{LocationRef, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
@@ -6,9 +6,6 @@ use skrifa::{FontRef, MetadataProvider};
static FONT: &[u8] = include_bytes!("../../../assets/fonts/DancingScript.ttf");
/// The approved Japanese fallback face used by the editor and generated Type 3 fonts.
pub static SHIPPORI_MINCHO: &[u8] = include_bytes!("../../../assets/fonts/ShipporiMincho-Regular.ttf");
/// Outlines of a line of existing text at a font size of 1 (em units).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScriptOutline {
@@ -28,6 +25,8 @@ pub struct GlyphOutline {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GlyphError {
/// This build has no Japanese face: it was built without craft-fonts (`CRAFT_FONTS_DIR`).
NoFont,
Missing,
TooComplex,
}
@@ -109,10 +108,12 @@ impl OutlinePen for Flatten {
}
}
/// Return one Shippori Mincho glyph, bounded so hostile replacement text cannot allocate
/// unbounded outline data.
pub fn shippori_glyph(ch: char) -> Result<GlyphOutline, GlyphError> {
let Ok(font) = FontRef::new(SHIPPORI_MINCHO) else { return Err(GlyphError::Missing) };
/// Return one glyph of the Japanese document face ([`crate::document_japanese_font`], Shippori
/// Mincho from craft-fonts), bounded so hostile replacement text cannot allocate unbounded
/// outline data. [`GlyphError::NoFont`] when the build has no Japanese face.
pub fn japanese_glyph(ch: char) -> Result<GlyphOutline, GlyphError> {
let Some(face) = crate::document_japanese_font() else { return Err(GlyphError::NoFont) };
let Ok(font) = FontRef::new(face.bytes) else { return Err(GlyphError::Missing) };
let loc = LocationRef::default();
let metrics = font.metrics(Size::unscaled(), loc);
let scale = 1.0 / metrics.units_per_em.max(1) as f64;
@@ -184,10 +185,17 @@ mod tests {
}
#[test]
fn shippori_resolves_japanese_glyphs() {
let g = super::shippori_glyph('こ').expect("bundled font has Japanese glyph");
assert!(g.width > 0.2 && g.width < 2.0);
assert!(!g.contours.is_empty());
assert_eq!(super::shippori_glyph('\u{1f4a9}'), Err(super::GlyphError::Missing));
fn japanese_glyphs_come_from_craft_fonts() {
if crate::document_japanese_font().is_none() {
eprintln!("skipping the glyph checks: built without craft-fonts (set CRAFT_FONTS_DIR)");
assert_eq!(super::japanese_glyph('こ'), Err(super::GlyphError::NoFont));
return;
}
for ch in "日本語の文字こ".chars() {
let g = super::japanese_glyph(ch).expect("craft-fonts face has the Japanese glyph");
assert!(g.width > 0.2 && g.width < 2.0);
assert!(!g.contours.is_empty());
}
assert_eq!(super::japanese_glyph('\u{1f4a9}'), Err(super::GlyphError::Missing));
}
}
+16 -5
View File
@@ -105,6 +105,13 @@ impl Tokens {
}
pub fn install_fonts(ctx: &egui::Context) {
ctx.set_fonts(font_definitions());
}
/// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then
/// the Japanese faces of the optional craft-fonts build input (BIZ UDPGothic first) as the last
/// fallback in every family. Without craft-fonts there is no Japanese face.
pub fn font_definitions() -> FontDefinitions {
let mut fonts = FontDefinitions::default();
let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
fonts.font_data.insert(name.to_owned(), Arc::new(FontData::from_static(bytes)));
@@ -113,19 +120,23 @@ pub fn install_fonts(ctx: &egui::Context) {
add(&mut fonts, "Inter-Medium", include_bytes!("../../../assets/fonts/Inter-Medium.ttf"));
add(&mut fonts, "Inter-SemiBold", include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"));
add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
// The same bytes printcraft-fonts embeds for Japanese text in PDFs: one 8.7 MB copy, not two.
add(&mut fonts, "ShipporiMincho", printcraft_fonts::SHIPPORI_MINCHO);
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "Inter".to_owned());
fonts.families.entry(FontFamily::Proportional).or_default().push("ShipporiMincho".to_owned());
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "JetBrainsMono".to_owned());
fonts.families.entry(FontFamily::Monospace).or_default().push("ShipporiMincho".to_owned());
// The same static bytes printcraft-fonts uses for Japanese text in PDFs: one copy, not two.
for face in printcraft_fonts::ui_japanese_fonts() {
let name = face.name();
add(&mut fonts, &name, face.bytes);
for family in [FontFamily::Proportional, FontFamily::Monospace] {
fonts.families.entry(family).or_default().push(name.clone());
}
}
let fallback: Vec<String> = fonts.families[&FontFamily::Proportional].clone();
for (fam, primary) in [("medium", "Inter-Medium"), ("semibold", "Inter-SemiBold")] {
let mut stack = vec![primary.to_owned()];
stack.extend(fallback.iter().cloned());
fonts.families.insert(FontFamily::Name(fam.into()), stack);
}
ctx.set_fonts(fonts);
fonts
}
pub fn regular(size: f32) -> FontId {
+58
View File
@@ -0,0 +1,58 @@
//! The interface fonts with and without the optional craft-fonts build input (`CRAFT_FONTS_DIR`).
use egui::epaint::text::{Fonts, TextOptions};
use egui::{Color32, FontFamily, FontId};
use printcraft_ui_egui::theme;
const JAPANESE: &str = "日本語の文字";
fn families() -> Vec<FontId> {
vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)]
}
/// Lays `text` out in every interface family and returns each galley's width.
fn layout_widths(fonts: &mut Fonts, text: &str) -> Vec<f32> {
let mut view = fonts.with_pixels_per_point(2.0);
families().into_iter().map(|id| view.layout_no_wrap(text.to_owned(), id, Color32::BLACK).size().x).collect()
}
/// Built with craft-fonts, Japanese text renders with real glyphs (no tofu) in every family,
/// from a craft-fonts face placed after the app's own fonts.
#[test]
fn japanese_ui_text_uses_craft_fonts() {
if printcraft_fonts::ui_japanese_fonts().is_empty() {
eprintln!("skipping japanese_ui_text_uses_craft_fonts: built without craft-fonts (set CRAFT_FONTS_DIR to run it)");
return;
}
let defs = theme::font_definitions();
for family in [FontFamily::Proportional, FontFamily::Monospace] {
let stack = &defs.families[&family];
let first_jp = stack.iter().position(|n| n.starts_with("BIZ UDPGothic")).expect("BIZ UDPGothic is a fallback");
let own = stack.iter().position(|n| n == "Inter" || n == "JetBrainsMono").expect("the app's own font");
assert!(own < first_jp, "{family:?}: {stack:?}");
assert!(stack[first_jp..].iter().all(|n| printcraft_fonts::CRAFT_FONTS.iter().any(|f| f.name() == *n)), "{stack:?}");
}
let mut fonts = Fonts::new(TextOptions::default(), defs);
for id in families() {
assert!(fonts.has_glyphs(&id, JAPANESE), "{id:?} lacks {JAPANESE}");
}
// Real glyphs are about a full em wide each; tofu boxes and missing glyphs are not.
for w in layout_widths(&mut fonts, JAPANESE) {
assert!(w > 13.0 * 0.8 * JAPANESE.chars().count() as f32, "{w}");
}
}
/// Without craft-fonts the interface fonts still install and lay out any text (Japanese falls
/// back to egui's replacement glyph) without panicking; Latin text is unaffected.
#[test]
fn ui_fonts_work_without_craft_fonts() {
let mut fonts = Fonts::new(TextOptions::default(), theme::font_definitions());
for id in families() {
assert!(fonts.has_glyphs(&id, "PrintCraft"), "{id:?}");
assert_eq!(fonts.has_glyphs(&id, JAPANESE), !printcraft_fonts::ui_japanese_fonts().is_empty(), "{id:?}");
}
assert!(layout_widths(&mut fonts, JAPANESE).iter().all(|w| w.is_finite() && *w > 0.0));
assert!(layout_widths(&mut fonts, "PrintCraft").iter().all(|w| *w > 20.0));
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
}
+2
View File
@@ -11,3 +11,5 @@ The control channel exposes the setting through `ui.set`:
`ui.state` reports `language` as `en` or `ja`. Unknown language codes return an error without changing the current setting. Old preferences default to English.
This first translation pass covers the main menu and core registered menu commands. Untranslated labels use English. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing.
Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes.
+13
View File
@@ -50,6 +50,19 @@ copy_docs() {
for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE COPYRIGHT; do
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
done
copy_font_licences "$dest"
}
# Builds made with the optional craft-fonts input (CRAFT_FONTS_DIR, set for every release) embed
# its fonts, so the package carries their licences: fonts/<family>/OFL.txt -> OFL-<family>.txt.
copy_font_licences() {
local dest="$1" f family
[ -n "${CRAFT_FONTS_DIR:-}" ] || return 0
for f in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do
[ -f "$f" ] || continue
family="$(basename "$(dirname "$f")")"
cp "$f" "$dest/OFL-$family.txt"
done
}
# Portable SHA-256 of a file (prints just the hash).
+1
View File
@@ -103,6 +103,7 @@ mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile).
cp "$WORK/bin/printcraft" "$APP/Contents/MacOS/PrintCraft"
cp "$ROOT/assets/app-icon/printcraft.icns" "$APP/Contents/Resources/PrintCraft.icns"
copy_font_licences "$APP/Contents/Resources"
sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \
-e "s/@BUILD_SHA@/${PRINTCRAFT_BUILD_SHA:-unknown}/g" \
"$HERE/Info.plist.in" >"$APP/Contents/Info.plist"
+8
View File
@@ -110,6 +110,14 @@ foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
$p = Join-Path $Root $f
if (Test-Path $p) { Copy-Item $p $Portable }
}
# Builds made with craft-fonts (CRAFT_FONTS_DIR, set for every release) embed its fonts: ship their
# licences, fonts\<family>\OFL.txt -> OFL-<family>.txt.
if ($env:CRAFT_FONTS_DIR) {
Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts') -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$ofl = Join-Path $_.FullName 'OFL.txt'
if (Test-Path $ofl) { Copy-Item $ofl (Join-Path $Portable "OFL-$($_.Name).txt") }
}
}
$Zip = Join-Path $Dist "printcraft-$Version-windows-$Arch-portable.zip"
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
Compress-Archive -Path $Portable -DestinationPath $Zip
+66
View File
@@ -58,6 +58,26 @@ pub struct Manifest {
pub bundled: Vec<Bundled>,
#[serde(default)]
pub fetched: Vec<Fetched>,
#[serde(default)]
pub build_input: Vec<BuildInput>,
}
/// Material compiled in only when the builder opts in (e.g. `CRAFT_FONTS_DIR`): not in this
/// repository, not downloaded by it. Its own repository attributes each file.
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BuildInput {
pub name: String,
/// The environment variable that turns it on.
pub option: String,
pub title: String,
pub author: String,
pub source: String,
pub licence: String,
/// The input's own per-file attribution.
pub attribution: String,
pub kind: String,
pub usage: String,
}
#[derive(Deserialize)]
@@ -267,6 +287,15 @@ pub fn check(root: &Path, m: &Manifest, repo_files: &[String], lock: &BTreeSet<(
problems.push(format!("{}: URLs must be https", f.file));
}
}
for b in &m.build_input {
licence_ok(&b.name, &b.licence, &mut problems);
if visual(&b.kind) && (mentions_adobe(&b.author) || mentions_adobe(&b.source) || mentions_adobe(&b.title)) {
problems.push(format!("{}: {} from Adobe is forbidden (AGENTS.md §1.1)", b.name, b.kind));
}
if !b.source.starts_with("https://") || !b.attribution.starts_with("https://") {
problems.push(format!("{}: URLs must be https", b.name));
}
}
problems
}
@@ -361,6 +390,24 @@ pub fn render_markdown(m: &Manifest) -> String {
for f in &m.fetched {
let _ = writeln!(s, "| `{}` | {} | {} | {} | {} | {} |", f.file, esc(&f.title), esc(&f.author), f.licence, esc(&f.source), esc(&f.usage));
}
s.push_str(&format!(
"\n## Optional build inputs ({})\n\nNot in this repository and never downloaded by it: compiled in only when the build sets the option (official releases do). Each input attributes its own files.\n\n| Input | Option | Title | Author | Licence | Source | Attribution | Used for |\n|---|---|---|---|---|---|---|---|\n",
m.build_input.len()
));
for b in &m.build_input {
let _ = writeln!(
s,
"| `{}` | `{}` | {} | {} | {} | {} | {} | {} |",
b.name,
b.option,
esc(&b.title),
esc(&b.author),
b.licence,
esc(&b.source),
esc(&b.attribution),
esc(&b.usage)
);
}
s
}
@@ -399,6 +446,25 @@ mod tests {
}
}
#[test]
fn build_inputs_need_an_open_licence_and_https_links() {
let input = |licence: &str, attribution: &str| BuildInput {
name: "craft-fonts".into(),
option: "CRAFT_FONTS_DIR".into(),
title: "t".into(),
author: "a".into(),
source: "https://github.com/storytold/craft-fonts".into(),
licence: licence.into(),
attribution: attribution.into(),
kind: "font".into(),
usage: "u".into(),
};
let check_one = |b: BuildInput| check(&root(), &Manifest { build_input: vec![b], ..Default::default() }, &[], &lock(&[]));
assert!(check_one(input("OFL-1.1", "https://example.org/ATTRIBUTION.md")).is_empty());
assert_eq!(check_one(input("LicenseRef-Proprietary", "https://example.org/A.md")).len(), 1);
assert_eq!(check_one(input("OFL-1.1", "ATTRIBUTION.md")).len(), 1);
}
fn lock(entries: &[(&str, &str)]) -> BTreeSet<(String, String)> {
entries.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect()
}