mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-13 15:09:07 +00:00
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:
1 parent
2e49a84ace
commit
1d67833599
25 files changed
+530
-144
No files matched your search
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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/`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -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
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user