Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .agents/skills/build-cadmus-native/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ it when documentation sources change.

## One-time setup

### Download runtime assets (optional)
### Runtime assets

**Required for Kobo builds** — run both before `cargo xtask build-kobo`:

```bash
cargo xtask download-assets
cargo xtask download-fonts
```

Pulls static assets (fonts, icons, etc.) from the latest GitHub release. Not
strictly required for compilation, but the emulator and some tests expect them.
`download-assets` pulls Plato runtime directories (`bin/`, `resources/`,
`hyphenation-patterns/`). `download-fonts` assembles the `fonts/`.

Native dependencies (MuPDF, libwebp, and the C wrapper) are now built
automatically by `build.rs` when you run any Cargo command that compiles
Expand Down
5 changes: 5 additions & 0 deletions .github/actions/build-kobo/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ runs:
- name: Cache Plato static assets
uses: ./.github/actions/cache-assets

- name: Cache bundled fonts
uses: ./.github/actions/cache-fonts
with:
gh-token: ${{ inputs.gh-token }}

- name: Install system packages
uses: ./.github/actions/apt

Expand Down
56 changes: 56 additions & 0 deletions .github/actions/cache-fonts/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Cache bundled fonts
description: >
Restore or save the assembled fonts/ directory. The cache key incorporates
pinned release versions from download_fonts.rs and the gitlink SHAs of the
Noto and Google font submodules so the key changes when any font source
moves. On cache miss, downloads fonts via cargo xtask download-fonts.

Defaults to restore-only. Pass `mode: save` in the cache-warmup job to
write a fresh cache.

inputs:
gh-token:
description: "GitHub token for downloading release assets"
required: true
mode:
description: "`restore` (default) or `save` (read-write)"
required: false
default: restore

outputs:
cache-hit:
description: "Whether a matching cache entry was found"
value: ${{ steps.save.outputs.cache-hit || steps.restore.outputs.cache-hit }}

runs:
using: composite
steps:
- name: Compute font submodule hash
id: font-submodules
shell: bash
run: |
sha=$(git ls-tree HEAD thirdparty/noto-fonts thirdparty/google-fonts | sha256sum | cut -c1-12)
echo "hash=$sha" >> "$GITHUB_OUTPUT"

- name: Restore bundled fonts cache
if: inputs.mode == 'restore'
id: restore
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: fonts/
key: fonts-${{ hashFiles('xtask/src/tasks/download_fonts/**') }}-${{ steps.font-submodules.outputs.hash }}

- name: Save bundled fonts cache
if: inputs.mode == 'save'
id: save
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: fonts/
key: fonts-${{ hashFiles('xtask/src/tasks/download_fonts/**') }}-${{ steps.font-submodules.outputs.hash }}

- name: Download bundled fonts
if: steps.restore.outputs.cache-hit != 'true' && steps.save.outputs.cache-hit != 'true'
shell: bash
env:
GH_TOKEN: ${{ inputs.gh-token }}
run: cargo xtask download-fonts
11 changes: 11 additions & 0 deletions .github/workflows/cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- '.github/actions/build-kobo/**'
- '.github/actions/build-docs-epub/**'
- '.github/actions/cargo-cache/**'
- '.github/actions/cache-fonts/**'
- '.github/actions/install-doc-tools/**'
- 'build-scripts/**'
- '.gitmodules'
Expand Down Expand Up @@ -186,6 +187,11 @@ jobs:
if: steps.cargo-llvm-cov-cache.outputs.cache-hit != 'true'
uses: taiki-e/install-action@9458fed45835344deb5e69e06e831be047c96abf # cargo-llvm-cov

- name: Cache bundled fonts
uses: ./.github/actions/cache-fonts
with:
mode: save
gh-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download documentation EPUB
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
Expand Down Expand Up @@ -361,6 +367,11 @@ jobs:
if: runner.os == 'macOS'
run: brew install sdl2 freetype harfbuzz openjpeg jpeg-turbo libpng jbig2dec gumbo-parser djvulibre pkg-config

- name: Restore bundled fonts
uses: ./.github/actions/cache-fonts
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}

- name: Restore shared cargo cache
uses: ./.github/actions/cargo-cache
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/css/*-user.css
/hyphenation-patterns
/dictionaries
/fonts
/dist
/bundle
docs/book/
Expand Down
10 changes: 10 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,13 @@
path = thirdparty/mdbook-i18n-helpers
url = https://github.com/OGKevin/mdbook-i18n-helpers.git
branch = ogkevin/push/qsonrqyvotux
[submodule "thirdparty/noto-fonts"]
path = thirdparty/noto-fonts
url = https://github.com/notofonts/noto-fonts
branch = v20201206-phase3
shallow = true
[submodule "thirdparty/google-fonts"]
path = thirdparty/google-fonts
url = https://github.com/google/fonts
branch = main
Comment thread
coderabbitai[bot] marked this conversation as resolved.
shallow = true
2 changes: 1 addition & 1 deletion contrib/Settings-sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ corner-width = 0.4
# Note: this does not exist by default.
font-path = "/mnt/onboard/fonts"
# The default serif font.
font-family = "Libertinus Serif"
font-family = "Libron"
# The default, minimum and maximum font sizes, in points.
font-size = 11.0
min-font-size = 5.5
Expand Down
24 changes: 21 additions & 3 deletions crates/build-deps/src/markers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,27 @@ pub fn is_built(root: &Path, dir: &Path, submodule_path: &str) -> bool {
pub fn mark_built(root: &Path, dir: &Path, name: &str, submodule_path: &str) -> Result<()> {
let hash = submodule_commit(root, submodule_path)
.with_context(|| format!("failed to resolve submodule commit for {submodule_path}"))?;
let marker_path = dir.join(BUILT_MARKER);
std::fs::write(&marker_path, hash.as_bytes())
.with_context(|| format!("failed to write build marker for {name}"))?;
mark_version(dir, name, &hash)
}

/// Returns `true` when `dir` has a `.built` marker whose content matches
/// `expected`.
///
/// Use this for pinned release tags or other version strings when there is no
/// submodule gitlink to compare against.
pub fn is_version_current(dir: &Path, expected: &str) -> bool {
std::fs::read_to_string(built_marker_path(dir)).is_ok_and(|stored| stored.trim() == expected)
}

/// Write a `.built` marker in `dir` with `version`, recording that `name`
/// is up to date at that revision.
///
/// # Errors
///
/// Returns an error if the marker file cannot be written.
pub fn mark_version(dir: &Path, name: &str, version: &str) -> Result<()> {
std::fs::write(built_marker_path(dir), version.as_bytes())
.with_context(|| format!("failed to write version marker for {name}"))?;
Ok(())
}

Expand Down
39 changes: 26 additions & 13 deletions crates/core/src/document/html/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use paragraph_breaker::{standard_fit, total_fit};
use percent_encoding::percent_decode_str;
use septem::Roman;
use std::convert::TryFrom;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use xi_unicode::LineBreakIterator;

Expand Down Expand Up @@ -102,16 +102,18 @@ impl Engine {
self.hyphenation_patterns.clone().unwrap()
}

/// Lazily loads [`build_default_reader_fonts`] from the install `fonts/` directory.
#[inline]
pub fn load_fonts(&mut self) {
if self.fonts.is_none() {
self.fonts = build_fonts(&self.install_dir).ok();
self.fonts = build_default_reader_fonts(&self.install_dir).ok();
}
}

/// Lazily loads [`build_default_reader_fonts`] from `root_dir/fonts/`.
pub fn load_fonts_from(&mut self, root_dir: std::path::PathBuf) {
if self.fonts.is_none() {
self.fonts = build_fonts(&root_dir).ok();
self.fonts = build_default_reader_fonts(&root_dir).ok();
}
}

Expand Down Expand Up @@ -142,8 +144,14 @@ impl Engine {
self.text_align = text_align;
}

/// Sets the reading font from `family_name`, searching bundled install fonts
/// before `search_path`.
pub fn set_font_family(&mut self, family_name: &str, search_path: &str) {
if let Ok(serif_family) = FontFamily::from_name(family_name, search_path) {
let install_fonts = self.install_dir.join("fonts");
if let Ok(serif_family) = FontFamily::from_name_in_paths(
family_name,
&[install_fonts.as_path(), Path::new(search_path)],
) {
self.load_fonts();
if let Some(fonts) = self.fonts.as_mut() {
fonts.serif = serif_family;
Expand Down Expand Up @@ -2461,21 +2469,26 @@ fn format_list_prefix(kind: ListStyleType, index: usize) -> Option<String> {
}
}

fn build_fonts(install_dir: &std::path::Path) -> Result<Fonts, Error> {
/// Loads bundled default stacks for CSS generic font families (`serif`,
/// `sans-serif`, `monospace`, `cursive`, `fantasy`) used by the HTML reader.
///
/// User-selected reading fonts replace only the `serif` stack via
/// [`Engine::set_font_family`].
fn build_default_reader_fonts(install_dir: &std::path::Path) -> Result<Fonts, Error> {
let opener = FontOpener::new()?;
let font_path = |name: &str| -> std::path::PathBuf { install_dir.join("fonts").join(name) };
let mut fonts = Fonts {
serif: FontFamily {
regular: opener.open(font_path("LibertinusSerif-Regular.otf").as_path())?,
italic: opener.open(font_path("LibertinusSerif-Italic.otf").as_path())?,
bold: opener.open(font_path("LibertinusSerif-Bold.otf").as_path())?,
bold_italic: opener.open(font_path("LibertinusSerif-BoldItalic.otf").as_path())?,
regular: opener.open(font_path("Libron-Regular.ttf").as_path())?,
italic: opener.open(font_path("Libron-Italic.ttf").as_path())?,
bold: opener.open(font_path("Libron-Bold.ttf").as_path())?,
bold_italic: opener.open(font_path("Libron-BoldItalic.ttf").as_path())?,
},
sans_serif: FontFamily {
regular: opener.open(font_path("NotoSans-Regular.ttf").as_path())?,
italic: opener.open(font_path("NotoSans-Italic.ttf").as_path())?,
bold: opener.open(font_path("NotoSans-Bold.ttf").as_path())?,
bold_italic: opener.open(font_path("NotoSans-BoldItalic.ttf").as_path())?,
regular: opener.open(font_path("NV_Jost-Regular.ttf").as_path())?,
italic: opener.open(font_path("NV_Jost-Italic.ttf").as_path())?,
bold: opener.open(font_path("NV_Jost-Bold.ttf").as_path())?,
bold_italic: opener.open(font_path("NV_Jost-BoldItalic.ttf").as_path())?,
},
monospace: FontFamily {
regular: opener.open(font_path("SourceCodeVariable-Roman.otf").as_path())?,
Expand Down
61 changes: 60 additions & 1 deletion crates/core/src/font/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,15 +596,39 @@ pub fn family_names<P: AsRef<Path>>(search_path: P) -> Result<BTreeSet<String>,
}

impl FontFamily {
/// Resolves `family_name` by searching `search_paths` in order.
///
/// # Errors
///
/// Returns an error when `search_paths` is empty or the family cannot be
/// found in any path.
pub fn from_name_in_paths(
family_name: &str,
search_paths: &[impl AsRef<Path>],
) -> Result<FontFamily, Error> {
let mut last_err = None;
for path in search_paths {
match Self::from_name_in_directory(family_name, path.as_ref()) {
Ok(family) => return Ok(family),
Err(err) => last_err = Some(err),
}
}
Err(last_err.unwrap_or_else(|| format_err!("no font search paths provided")))
}

pub fn from_name<P: AsRef<Path>>(
family_name: &str,
search_path: P,
) -> Result<FontFamily, Error> {
Self::from_name_in_paths(family_name, &[search_path.as_ref()])
}

fn from_name_in_directory(family_name: &str, search_path: &Path) -> Result<FontFamily, Error> {
let opener = FontOpener::new()?;
let glob = Glob::new("**/*.[ot]tf")?.compile_matcher();
let mut styles = FxHashMap::default();

for entry in WalkDir::new(search_path.as_ref())
for entry in WalkDir::new(search_path)
.min_depth(1)
.into_iter()
.filter_entry(|e| !e.is_hidden())
Expand Down Expand Up @@ -2777,3 +2801,38 @@ impl From<FtError> for FreetypeError {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

fn workspace_fonts_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fonts")
}

#[test]
fn from_name_in_paths_finds_family_in_later_path() {
let fonts = workspace_fonts_dir();
if !fonts.join("Libron-Regular.ttf").exists() {
return;
}
let empty = tempfile::tempdir().unwrap();
FontFamily::from_name_in_paths("Libron", &[empty.path(), fonts.as_path()]).unwrap();
}

#[test]
fn from_name_in_paths_errors_when_paths_empty() {
let result = FontFamily::from_name_in_paths("Libron", &[] as &[&Path]);
assert!(result.is_err());
}

#[test]
fn from_name_delegates_to_from_name_in_paths() {
let fonts = workspace_fonts_dir();
if !fonts.join("Libron-Regular.ttf").exists() {
return;
}
FontFamily::from_name("Libron", &fonts).unwrap();
}
}
4 changes: 2 additions & 2 deletions crates/core/src/ota/cleanup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ mod tests {
std::fs::create_dir_all(install_dir.join("icons")).unwrap();
std::fs::create_dir_all(install_dir.join("libs")).unwrap();

std::fs::write(install_dir.join("fonts/NotoSans-Regular.ttf"), b"owned").unwrap();
std::fs::write(install_dir.join("fonts/Libron-Regular.ttf"), b"owned").unwrap();
std::fs::write(install_dir.join("fonts/custom.ttf"), b"user").unwrap();
std::fs::write(install_dir.join("icons/home.svg"), b"owned").unwrap();
std::fs::write(install_dir.join("libs/libfoo.so.1"), b"owned").unwrap();
std::fs::write(install_dir.join("Settings.toml"), b"user").unwrap();

clean_bundled_files(&install_dir).unwrap();

assert!(!install_dir.join("fonts/NotoSans-Regular.ttf").exists());
assert!(!install_dir.join("fonts/Libron-Regular.ttf").exists());
assert!(install_dir.join("fonts/custom.ttf").exists());
assert!(!install_dir.join("icons/home.svg").exists());
assert!(!install_dir.join("libs/libfoo.so.1").exists());
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub const DEFAULT_MARGIN_WIDTH: i32 = 8;
// Default line height in ems.
pub const DEFAULT_LINE_HEIGHT: f32 = 1.2;
// Default font family name.
pub const DEFAULT_FONT_FAMILY: &str = "Libertinus Serif";
pub const DEFAULT_FONT_FAMILY: &str = "Libron";
// Default text alignment.
pub const DEFAULT_TEXT_ALIGN: TextAlign = TextAlign::Left;
pub const HYPHEN_PENALTY: i32 = 50;
Expand Down
11 changes: 4 additions & 7 deletions crates/core/src/view/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2708,6 +2708,9 @@ impl Reader {
let mut families = family_names(&context.settings.reader.font_path)
.map_err(|e| error!("Can't get family names: {:#}.", e))
.unwrap_or_default();
if let Ok(install_families) = family_names(context.device.install_path("fonts")) {
families.extend(install_families);
}
let current_family = self
.info
.reader
Expand Down Expand Up @@ -3364,13 +3367,7 @@ impl Reader {

{
let mut doc = self.doc.lock().unwrap();
let font_path = if font_family == DEFAULT_FONT_FAMILY {
"fonts"
} else {
&context.settings.reader.font_path
};

doc.set_font_family(font_family, font_path);
doc.set_font_family(font_family, &context.settings.reader.font_path);

if self.synthetic {
let current_page = self.current_page.min(doc.pages_count() - 1);
Expand Down
Loading
Loading