diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c1bed..7d2ea20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,14 @@ jobs: with: package: sdf_glyph_renderer feature-group: default-features - features: freetype + features: font - name: Check semver (PBF Font Tools) uses: obi1kenobi/cargo-semver-checks-action@v2 with: package: pbf_font_tools feature-group: default-features - features: freetype + features: generate test: name: Run CI @@ -56,13 +56,18 @@ jobs: run: cargo fmt --all -- --check - name: Test Debug - run: cargo test --workspace --features freetype --locked + run: cargo test --workspace --locked - name: Test Release - run: cargo test --workspace --release --features freetype --locked + run: cargo test --workspace --release --locked + + - name: Test without optional font generation + run: | + cargo test -p sdf_glyph_renderer --no-default-features --locked + cargo test -p pbf_font_tools --no-default-features --locked - name: Lint - run: cargo clippy --workspace --all-targets --features freetype -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings - name: Test docs run: | @@ -96,14 +101,8 @@ jobs: - name: Report Rust version run: rustc --version && cargo --version - # freetype-sys 0.23.0 bundled build is broken on Windows (links system zlib) - name: Run tests - run: cargo test --workspace --features freetype --locked - if: ${{ !startsWith(matrix.os, 'windows') }} - - - name: Run tests (no freetype) - run: cargo test --workspace --exclude build_pbf_glyphs --locked - if: ${{ startsWith(matrix.os, 'windows') }} + run: cargo test --workspace --locked # Make sure the vendored protoc-generated file is up to date by only checking it on one platform # We remove the file first to make sure it gets properly regenerated. @@ -125,11 +124,11 @@ jobs: uses: vmactions/freebsd-vm@v1 with: prepare: | - pkg install -y rust freetype2 + pkg install -y rust run: | - # Run tests with freetype - cargo test --workspace --features freetype --locked + # Run tests + cargo test --workspace --locked # Run tests in release mode - cargo test --workspace --release --features freetype --locked + cargo test --workspace --release --locked diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ca0f821..83ad4ca 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ on: release: - types: [published] + types: [ published ] name: Cargo Publish @@ -22,11 +22,11 @@ jobs: case "$PREFIX" in "sdf") CRATE_NAME="sdf_glyph_renderer" - FLAGS="--features freetype" + FLAGS="--features font" ;; "tools") CRATE_NAME="pbf_font_tools" - FLAGS="--features freetype" + FLAGS="--features generate" ;; "cli") CRATE_NAME="build_pbf_glyphs" diff --git a/Cargo.lock b/Cargo.lock index fdd683c..b4adbeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,26 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bytes" version = "1.11.1" @@ -335,25 +355,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "freetype-rs" -version = "0.38.0" +name = "font-types" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d228d6de56c90dd7585341f341849441b3490180c62d27133e525eb726809b4" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ - "bitflags", - "freetype-sys", - "libc", -] - -[[package]] -name = "freetype-sys" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab537ce43cab850c64b4cdc390ce7e4f47f877485ddc323208e268280c308ae" -dependencies = [ - "cc", - "libz-sys", - "pkg-config", + "bytemuck", ] [[package]] @@ -559,18 +566,6 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" -[[package]] -name = "libz-sys" -version = "1.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -634,7 +629,7 @@ dependencies = [ [[package]] name = "pbf_font_tools" -version = "3.1.1" +version = "4.0.0" dependencies = [ "futures", "prost", @@ -663,12 +658,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "plotters" version = "0.3.7" @@ -875,6 +864,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "regex" version = "1.12.3" @@ -934,10 +933,10 @@ dependencies = [ [[package]] name = "sdf_glyph_renderer" -version = "1.1.0" +version = "2.0.0" dependencies = [ "criterion", - "freetype-rs", + "swash", "thiserror", ] @@ -996,6 +995,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" @@ -1014,6 +1023,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "swash" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "2.0.117" @@ -1108,12 +1128,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "walkdir" version = "2.5.0" @@ -1365,6 +1379,18 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.8.42" diff --git a/Cargo.toml b/Cargo.toml index 8b68e66..74df2b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ rust-version = "1.87.0" [workspace.dependencies] clap = { version = "4.5.60", features = ["derive"] } -freetype-rs = "0.38.0" futures = "0.3.32" -pbf_font_tools = { version = "3", path = "pbf_font_tools", default-features = false } +pbf_font_tools = { version = "4", path = "pbf_font_tools", default-features = false } prost = "0.14.3" prost-build = "0.14.3" protobuf-src = "2.1.1" protoc-bin-vendored = "3.2.0" -sdf_glyph_renderer = { version = "1", path = "sdf_glyph_renderer" } +sdf_glyph_renderer = { version = "2", path = "sdf_glyph_renderer" } serde_json = "1.0.149" spmc = "0.3.0" +swash = "0.2.7" thiserror = "2.0.18" tokio = { version = "1.50.0", features = ["rt", "fs", "io-util"] } diff --git a/build_pbf_glyphs/Cargo.toml b/build_pbf_glyphs/Cargo.toml index ebafaaf..4ba3ae5 100644 --- a/build_pbf_glyphs/Cargo.toml +++ b/build_pbf_glyphs/Cargo.toml @@ -18,7 +18,7 @@ protoc-vendored = ["pbf_font_tools/protoc-vendored"] [dependencies] clap.workspace = true -pbf_font_tools = { workspace = true, features = ["freetype"] } +pbf_font_tools = { workspace = true, features = ["generate"] } prost.workspace = true serde_json.workspace = true spmc.workspace = true diff --git a/build_pbf_glyphs/README.md b/build_pbf_glyphs/README.md index 8d785f3..a179d62 100644 --- a/build_pbf_glyphs/README.md +++ b/build_pbf_glyphs/README.md @@ -8,7 +8,7 @@ isn't really anything novel; it's just a frontend to [pbf_font_tools](../pbf_font_tools) that behaves similar to [node-fontnik](https://github.com/mapbox/node-fontnik), but is faster and (in our opinion) a bit easier to use since it doesn't depend on node and all its headaches, or C++ libraries -that need to be built from scratch. FreeType is bundled and compiled from source by default. +that need to be built from scratch. Check out [sdf_glyph_renderer](https://github.com/stadiamaps/sdf_font_tools/tree/main/sdf_glyph_renderer) for more technical diff --git a/build_pbf_glyphs/src/main.rs b/build_pbf_glyphs/src/main.rs index aa1b50c..9c08689 100644 --- a/build_pbf_glyphs/src/main.rs +++ b/build_pbf_glyphs/src/main.rs @@ -4,7 +4,7 @@ //! [pbf_font_tools](https://github.com/stadiamaps/pbf_font_tools) that behaves similar to //! [node-fontnik](https://github.com/mapbox/node-fontnik), but is faster and (in our opinion) //! a bit easier to use since it doesn't depend on node and all its headaches, or C++ libraries -//! that need to be built from scratch. FreeType is bundled and compiled from source by default. +//! that need to be built from scratch. //! //! Check out //! [sdf_glyph_renderer](https://github.com/stadiamaps/sdf_glyph_renderer) for more technical @@ -24,12 +24,12 @@ use std::collections::HashMap; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::thread; use std::time::Instant; use clap::Parser; -use pbf_font_tools::freetype::{Face, Library}; -use pbf_font_tools::{get_named_font_stack, glyph_range_for_face, Glyphs}; +use pbf_font_tools::{get_named_font_stack, glyph_range_for_face, FontFace, Glyphs}; use prost::Message; use spmc::{channel, Receiver}; use tokio::fs::{create_dir_all, File}; @@ -107,8 +107,6 @@ fn render_worker( cutoff: f64, rx: Receiver>, ) { - let lib = Library::init().expect("Unable to initialize FreeType"); - while let Ok(Some((path, stem))) = rx.recv() { let out_dir = base_out_dir.join(stem.to_str().expect("Unable to extract file stem")); std::fs::create_dir_all(&out_dir).expect("Unable to create output directory"); @@ -116,15 +114,11 @@ fn render_worker( println!("Processing {}", path.display()); // Load the font once to save useless I/O - // FIXME: lib.new_face is called twice for face_index=0 - // instead, call it once, create a pre-allocated vector of faces for num_faces count - // add the already open 0th, and add all remaining ones to it - let face = lib.new_face(&path, 0).expect("Unable to load font"); - let num_faces = face.num_faces() as usize; - let faces: Vec = (0..num_faces) + let data: Arc<[u8]> = std::fs::read(&path).expect("Unable to read font").into(); + let num_faces = FontFace::count(data.as_ref()).expect("Unable to count font faces"); + let faces: Vec = (0..num_faces) .map(|face_index| { - lib.new_face(&path, face_index as isize) - .expect("Unable to load face") + FontFace::from_bytes(Arc::clone(&data), face_index).expect("Unable to load face") }) .collect(); diff --git a/pbf_font_tools/Cargo.toml b/pbf_font_tools/Cargo.toml index 5181f2f..d0e4c85 100644 --- a/pbf_font_tools/Cargo.toml +++ b/pbf_font_tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pbf_font_tools" -version = "3.1.1" +version = "4.0.0" description = "Tools for working with SDF font glyphs encoded in protobuf format." readme = "README.md" keywords = ["sdf", "protobuf", "fonts"] @@ -13,8 +13,7 @@ license.workspace = true [features] default = [] -freetype = ["dep:sdf_glyph_renderer", "sdf_glyph_renderer?/freetype"] -freetype-system = ["dep:sdf_glyph_renderer", "sdf_glyph_renderer?/freetype-system"] +generate = ["dep:sdf_glyph_renderer", "sdf_glyph_renderer?/font"] # Regenerate the protobuf source file using protoc, and write it back to the source tree. # Combine with protoc-vendored or protoc-from-src to control which protoc binary is used. # Without either of those features, protoc must be available in $PATH. diff --git a/pbf_font_tools/README.md b/pbf_font_tools/README.md index 5bd8946..60a99dc 100644 --- a/pbf_font_tools/README.md +++ b/pbf_font_tools/README.md @@ -14,8 +14,8 @@ If you're looking for a CLI tool to generate PBF ranges en masse like [node-fontnik](https://github.com/mapbox/node-fontnik)), but faster, check out [build_pbf_glyphs](https://github.com/stadiamaps/sdf_font_tools/tree/main/build_pbf_glyphs). -The `freetype` feature (which bundles FreeType from source) is used for glyph generation. -Use the `freetype-system` feature instead to link against a system-installed FreeType. +The optional `generate` feature enables glyph generation from source font files. +Without it, this crate can still load and combine existing PBF glyph ranges. ## References diff --git a/pbf_font_tools/src/error.rs b/pbf_font_tools/src/error.rs index 421d0d1..fe568d8 100644 --- a/pbf_font_tools/src/error.rs +++ b/pbf_font_tools/src/error.rs @@ -4,14 +4,11 @@ pub enum PbfFontError { JoinError(#[from] tokio::task::JoinError), #[error("Protobuf decoding error: {0}")] ProtobufError(#[from] prost::DecodeError), - #[cfg(feature = "freetype")] + #[cfg(feature = "generate")] #[error("SDF glyph error: {0}")] SdfGlyphError(#[from] sdf_glyph_renderer::SdfGlyphError), #[error("Font family name is not set")] MissingFontFamilyName, - #[cfg(feature = "freetype")] - #[error("Freetype error: {0}")] - FreetypeError(#[from] crate::freetype::Error), #[error(transparent)] IoError(#[from] std::io::Error), } diff --git a/pbf_font_tools/src/ft_generate.rs b/pbf_font_tools/src/font_generate.rs similarity index 65% rename from pbf_font_tools/src/ft_generate.rs rename to pbf_font_tools/src/font_generate.rs index ce0cb0f..2384d01 100644 --- a/pbf_font_tools/src/ft_generate.rs +++ b/pbf_font_tools/src/font_generate.rs @@ -1,19 +1,22 @@ use std::path::Path; +use std::sync::Arc; -use sdf_glyph_renderer::{clamp_to_u8, render_sdf_from_face}; +use sdf_glyph_renderer::{clamp_to_u8, FontFace, FontRenderer}; use crate::error::PbfFontError; -use crate::{freetype, Fontstack, Glyph, Glyphs}; +use crate::{Fontstack, Glyph, Glyphs}; /// Renders a single glyph for the given font face into a Glyph message. pub fn render_sdf_glyph( - face: &freetype::Face, + renderer: &mut FontRenderer, + face: &FontFace, char_code: u32, + size: usize, buffer: usize, radius: usize, cutoff: f64, ) -> Result { - let glyph = render_sdf_from_face(face, char_code, buffer, radius)?; + let glyph = renderer.render_sdf_from_face(face, char_code, size as f32, buffer, radius)?; Ok(Glyph { id: char_code, @@ -38,20 +41,16 @@ pub fn render_sdf_glyph( /// encoded as a vector of bytes, which have no sign). The value selected must be /// between 0 and 1. pub fn glyph_range_for_face( - face: &freetype::Face, + face: &FontFace, start: u32, end: u32, size: usize, radius: usize, cutoff: f64, ) -> Result { - let Some(mut family_name) = face.family_name() else { + let Some(family_name) = face.name() else { return Err(PbfFontError::MissingFontFamilyName); }; - if let Some(style_name) = face.style_name() { - family_name.push(' '); - family_name.push_str(&style_name); - } let mut stack = Fontstack { name: family_name, @@ -59,21 +58,14 @@ pub fn glyph_range_for_face( glyphs: Vec::with_capacity((end - start) as usize), }; - // FreeType conventions: char width or height of zero means "use the same value" - // and setting both resolution values to zero results in the default value - // of 72 dpi. - // - // See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#ft_set_char_size - // and https://www.freetype.org/freetype2/docs/tutorial/step1.html for details. - face.set_char_size(0, (size << 6) as isize, 0, 0)?; - + let mut renderer = FontRenderer::new(); for char_code in start..=end { - match render_sdf_glyph(face, char_code, 3, radius, cutoff) { + match render_sdf_glyph(&mut renderer, face, char_code, size, 3, radius, cutoff) { Ok(glyph) => { stack.glyphs.push(glyph); } - Err(PbfFontError::SdfGlyphError(sdf_glyph_renderer::SdfGlyphError::FreeTypeError( - freetype::Error::InvalidGlyphIndex, + Err(PbfFontError::SdfGlyphError(sdf_glyph_renderer::SdfGlyphError::MissingGlyph( + _, ))) => { // Do nothing; not all glyphs will be present in a font. } @@ -94,18 +86,14 @@ pub fn glyph_range_for_font>( radius: usize, cutoff: f64, ) -> Result { - let lib = freetype::Library::init()?; - let mut face = lib.new_face(font_path.as_ref(), 0)?; - let num_faces = face.num_faces(); + let data: Arc<[u8]> = std::fs::read(font_path)?.into(); + let num_faces = FontFace::count(data.as_ref())?; let mut result = Glyphs::default(); - result.stacks.reserve(num_faces as usize); + result.stacks.reserve(num_faces); for face_index in 0..num_faces { - if face_index > 0 { - face = lib.new_face(font_path.as_ref(), face_index as isize)?; - } - + let face = FontFace::from_bytes(Arc::clone(&data), face_index)?; let stack = glyph_range_for_face(&face, start, end, size, radius, cutoff)?; result.stacks.push(stack); } diff --git a/pbf_font_tools/src/lib.rs b/pbf_font_tools/src/lib.rs index 4d44efb..8bf4487 100644 --- a/pbf_font_tools/src/lib.rs +++ b/pbf_font_tools/src/lib.rs @@ -3,8 +3,8 @@ //! Tools for working with SDF font glyphs in PBF format. //! //! This crate lets you combine multiple glyphs from multiple fonts into a single stack. -//! Generating glyphs from a TrueType/OpenType font (a la [node-fontnik](https://github.com/mapbox/node-fontnik)) -//! is planned for a future release. +//! The optional `generate` feature enables glyph generation from TrueType/OpenType fonts +//! (a la [node-fontnik](https://github.com/mapbox/node-fontnik)). //! //! ## References //! * [glyph-pbf-composite](https://github.com/mapbox/glyph-pbf-composite) @@ -14,17 +14,16 @@ mod error; mod proto; mod tools; -#[cfg(feature = "freetype")] -mod ft_generate; +#[cfg(feature = "generate")] +mod font_generate; // Re-export protobuf lib pub use prost; pub use proto::{Fontstack, Glyph, Glyphs}; -// Re-export freetype lib -#[cfg(feature = "freetype")] -pub use sdf_glyph_renderer::freetype; +#[cfg(feature = "generate")] +pub use sdf_glyph_renderer::FontFace; pub use crate::error::PbfFontError; -#[cfg(feature = "freetype")] -pub use crate::ft_generate::*; +#[cfg(feature = "generate")] +pub use crate::font_generate::*; pub use crate::tools::*; diff --git a/pbf_font_tools/tests/integration_tests.rs b/pbf_font_tools/tests/integration_tests.rs index 2ce1855..3eda593 100644 --- a/pbf_font_tools/tests/integration_tests.rs +++ b/pbf_font_tools/tests/integration_tests.rs @@ -109,7 +109,7 @@ async fn test_get_font_stack() { } } -#[cfg(feature = "freetype")] +#[cfg(feature = "generate")] #[tokio::test] async fn test_glyph_generation() { let font_path = Path::new("tests").join("glyphs"); @@ -117,20 +117,22 @@ async fn test_glyph_generation() { let otf_path = font_path.join(font_name).join(format!("{font_name}.ttf")); let rendered_glyphs = pbf_font_tools::glyph_range_for_font(&otf_path, 0, 255, 24, 8, 0.25) .expect("Unable to render glyphs"); - let fixture_glyphs = pbf_font_tools::load_glyphs(font_path.as_path(), font_name, 0, 255) - .await - .expect("Unable to load fixtures"); let rendered_stack = &rendered_glyphs.stacks[0]; - let fixture_stack = &fixture_glyphs.stacks[0]; + assert_eq!(rendered_stack.name, font_name); + assert_eq!(rendered_stack.range, "0-255"); + assert!(!rendered_stack.glyphs.is_empty()); - let rendered_glyph_count = rendered_stack.glyphs.len(); - let fixture_glyph_count = fixture_stack.glyphs.len(); - assert_eq!(rendered_glyph_count, fixture_glyph_count); - - rendered_stack + let glyph = rendered_stack .glyphs .iter() - .zip(fixture_stack.glyphs.iter()) - .for_each(|(glyph, fixture)| assert_eq!(glyph, fixture)); + .find(|glyph| glyph.id == u32::from('A')) + .expect("Expected to render an A glyph"); + assert!(glyph.width > 0); + assert!(glyph.height > 0); + assert!(glyph.advance > 0); + assert_eq!( + glyph.bitmap.as_ref().expect("No bitmap").len(), + (glyph.width as usize + 6) * (glyph.height as usize + 6) + ); } diff --git a/sdf_glyph_renderer/Cargo.toml b/sdf_glyph_renderer/Cargo.toml index 22db900..f9b081d 100644 --- a/sdf_glyph_renderer/Cargo.toml +++ b/sdf_glyph_renderer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sdf_glyph_renderer" -version = "1.1.0" +version = "2.0.0" description = "Tools for generating SDF font glyphs from bitmap images." readme = "README.md" keywords = ["sdf", "fonts", "glyphs"] @@ -12,11 +12,11 @@ repository.workspace = true license.workspace = true [features] -freetype = ["dep:freetype-rs", "freetype-rs?/bundled"] -freetype-system = ["dep:freetype-rs"] +default = [] +font = ["dep:swash"] [dependencies] -freetype-rs = { workspace = true, optional = true } +swash = { workspace = true, optional = true } thiserror.workspace = true [dev-dependencies] @@ -25,4 +25,3 @@ criterion = "0.8.2" [[bench]] name = "benchmark_sdf" harness = false - diff --git a/sdf_glyph_renderer/README.md b/sdf_glyph_renderer/README.md index 483e5c4..0ebadb7 100644 --- a/sdf_glyph_renderer/README.md +++ b/sdf_glyph_renderer/README.md @@ -6,8 +6,7 @@ This crate is a Rust implementation of the signed distance field generation tech demonstrated by [Valve](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf) and [Mapbox](https://blog.mapbox.com/drawing-text-with-signed-distance-fields-in-mapbox-gl-b0933af6f817). The generic interface works with any bitmap, and a high level interface enables easy operation -with FreeType faces when the optional `freetype` feature is enabled (bundles FreeType from source). -Use the `freetype-system` feature instead to link against a system-installed FreeType. +with font files when the optional `font` feature is enabled. The approach taken by this crate is similar to [TinySDF](https://github.com/mapbox/tiny-sdf); it works from a raster bitmap rather than directly from vector outlines. This keeps the @@ -15,13 +14,13 @@ crate simple and allows it to be used generically with any bitmap. The SDF is ca using the same algorithm described in [this paper](http://cs.brown.edu/people/pfelzens/papers/dt-final.pdf) by Felzenszwalb & Huttenlocher. -Rather than re-invent the rasterisation process for fonts, this crate relies on FreeType to -generate the bitmap. This is quite fast (we're talking µs/glyph), and the results are +Rather than re-invent the rasterisation process for fonts, this crate relies on a Rust font +renderer to generate the bitmap. This is quite fast (we're talking µs/glyph), and the results are almost always indistinguishable from the more sophisticated vector-based approach of [sdf-glyph-foundry](https://github.com/mapbox/sdf-glyph-foundry). This crate is used by [pbf_font_tools](https://github.com/stadiamaps/sdf_font_tools/tree/main/pbf_font_tools) to generate -SDF glyphs from any FreeType-readable font. If you're looking for a batch generation tool, +SDF glyphs from source fonts. If you're looking for a batch generation tool, check out [build_pbf_glyphs](https://github.com/stadiamaps/sdf_font_tools/tree/main/build_pbf_glyphs). # Example Usage diff --git a/sdf_glyph_renderer/src/error.rs b/sdf_glyph_renderer/src/error.rs index 915dc9f..4631189 100644 --- a/sdf_glyph_renderer/src/error.rs +++ b/sdf_glyph_renderer/src/error.rs @@ -5,13 +5,17 @@ pub enum SdfGlyphError { #[error("Missing size metrics")] MissingSizeMetrics, + #[cfg(feature = "font")] + #[error("Invalid font data")] + InvalidFontData, + + #[cfg(feature = "font")] + #[error("Missing glyph for code point {0}")] + MissingGlyph(u32), + #[error("Invalid bitmap dimensions: The data length must be equal to {0} = {1}, but is equal to {2}.")] InvalidDataDimensions(&'static str, usize, usize), #[error("Cutoff values must be between 0 and 1 (both non-inclusive), but {0} was provided.")] InvalidCutoff(f64), - - #[cfg(feature = "freetype")] - #[error("FreeType error: {0}")] - FreeTypeError(#[from] freetype::Error), } diff --git a/sdf_glyph_renderer/src/ft.rs b/sdf_glyph_renderer/src/ft.rs index 56e5402..2fc13dc 100644 --- a/sdf_glyph_renderer/src/ft.rs +++ b/sdf_glyph_renderer/src/ft.rs @@ -1,16 +1,101 @@ -use freetype::face::LoadFlag; -use freetype::Face; +use std::sync::Arc; + +use swash::scale::{Render, ScaleContext, Source}; +use swash::{CacheKey, FontDataRef, FontRef, StringId}; use crate::{BitmapGlyph, SdfGlyphError}; +pub struct FontFace { + data: Arc<[u8]>, + offset: u32, + key: CacheKey, +} + +impl FontFace { + pub fn from_bytes(data: Arc<[u8]>, index: usize) -> Result { + let font = + FontRef::from_index(data.as_ref(), index).ok_or(SdfGlyphError::InvalidFontData)?; + let offset = font.offset; + let key = font.key; + Ok(Self { data, offset, key }) + } + + pub fn count(data: &[u8]) -> Result { + FontDataRef::new(data) + .map(|font_data| font_data.len()) + .ok_or(SdfGlyphError::InvalidFontData) + } + + pub fn name(&self) -> Option { + let font = self.as_ref(); + let strings = font.localized_strings(); + strings + .find_by_id(StringId::Full, None) + .map(|name| name.to_string()) + .filter(|name| !name.is_empty()) + .or_else(|| { + let family = strings.find_by_id(StringId::Family, None)?.to_string(); + if family.is_empty() { + return None; + } + + let subfamily = strings + .find_by_id(StringId::SubFamily, None) + .map(|name| name.to_string()) + .filter(|name| !name.is_empty()); + + Some(match subfamily { + Some(subfamily) => format!("{family} {subfamily}"), + None => family, + }) + }) + } + + fn as_ref(&self) -> FontRef<'_> { + FontRef { + data: self.data.as_ref(), + offset: self.offset, + key: self.key, + } + } +} + +pub struct FontRenderer { + context: ScaleContext, +} + +impl FontRenderer { + #[must_use] + pub fn new() -> Self { + Self { + context: ScaleContext::new(), + } + } + + pub fn render_sdf_from_face( + &mut self, + face: &FontFace, + char_code: u32, + size: f32, + buffer: usize, + radius: usize, + ) -> Result { + render_sdf_from_face_with_context(&mut self.context, face, char_code, size, buffer, radius) + } +} + +impl Default for FontRenderer { + fn default() -> Self { + Self::new() + } +} + pub struct SdfGlyph { pub sdf: Vec, pub metrics: GlyphMetrics, } -/// For an explanation of the technical terms used when describing the glyph metrics, -/// the [FreeType tutorial](https://www.freetype.org/freetype2/docs/tutorial/step2.html) is a -/// fantastic reference. +/// Metrics used to position the rendered glyph bitmap relative to the baseline. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GlyphMetrics { /// The unbuffered width of the glyph in px. @@ -36,43 +121,63 @@ pub struct GlyphMetrics { pub ascender: i32, } -/// This is a convenient frontend to [`render_sdf`](BitmapGlyph::render_sdf) that accepts a FreeType -/// face as input and generates bitmaps automatically using the font's embedded metrics. +/// Renders a glyph from a font face and generates an SDF using the font's embedded metrics. pub fn render_sdf_from_face( - face: &Face, + face: &FontFace, char_code: u32, + size: f32, buffer: usize, radius: usize, ) -> Result { - let ascender = (face - .size_metrics() - .ok_or(SdfGlyphError::MissingSizeMetrics)? - .ascender - >> 6) as i32; - - let Some(glyph_index) = face.get_char_index(char_code as usize) else { - return Err(SdfGlyphError::FreeTypeError( - freetype::Error::InvalidGlyphIndex, - )); + let mut renderer = FontRenderer::new(); + renderer.render_sdf_from_face(face, char_code, size, buffer, radius) +} + +fn render_sdf_from_face_with_context( + context: &mut ScaleContext, + face: &FontFace, + char_code: u32, + size: f32, + buffer: usize, + radius: usize, +) -> Result { + let Some(_) = char::from_u32(char_code) else { + return Err(SdfGlyphError::MissingGlyph(char_code)); }; - face.load_glyph(glyph_index, LoadFlag::NO_HINTING | LoadFlag::RENDER)?; + let font = face.as_ref(); + let glyph_id = font.charmap().map(char_code); + if glyph_id == 0 { + return Err(SdfGlyphError::MissingGlyph(char_code)); + } + + let font_metrics = font.metrics(&[]).scale(size); + let glyph_metrics = font.glyph_metrics(&[]).scale(size); + let advance_width = glyph_metrics.advance_width(glyph_id).round().max(0.0) as u32; + + let mut scaler = context.builder(font).size(size).hint(false).build(); + let image = Render::new(&[Source::Outline]).render(&mut scaler, glyph_id); + + let (alpha, width, height, left_bearing, top_bearing) = if let Some(image) = image { + ( + image.data, + image.placement.width as usize, + image.placement.height as usize, + image.placement.left, + image.placement.top, + ) + } else { + (Vec::new(), 0, 0, 0, 0) + }; - let glyph = face.glyph(); - let glyph_bitmap = glyph.bitmap(); - let bitmap = BitmapGlyph::from_unbuffered( - glyph_bitmap.buffer(), - glyph_bitmap.width() as usize, - glyph_bitmap.rows() as usize, - buffer, - )?; + let bitmap = BitmapGlyph::from_unbuffered(&alpha, width, height, buffer)?; let metrics = GlyphMetrics { width: bitmap.width, height: bitmap.height, - left_bearing: glyph.bitmap_left(), - top_bearing: glyph.bitmap_top(), - h_advance: (glyph.metrics().horiAdvance >> 6) as u32, - ascender, + left_bearing, + top_bearing, + h_advance: advance_width, + ascender: font_metrics.ascent.round() as i32, }; Ok(SdfGlyph { diff --git a/sdf_glyph_renderer/src/lib.rs b/sdf_glyph_renderer/src/lib.rs index 39430ca..8b57362 100644 --- a/sdf_glyph_renderer/src/lib.rs +++ b/sdf_glyph_renderer/src/lib.rs @@ -2,8 +2,7 @@ //! demonstrated by [Valve](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf) //! and [Mapbox](https://blog.mapbox.com/drawing-text-with-signed-distance-fields-in-mapbox-gl-b0933af6f817). //! The generic interface works with any bitmap, and a high level interface enables easy operation -//! with FreeType faces when the optional `freetype` feature is enabled (bundles FreeType from source). -//! Use the `freetype-system` feature instead to link against a system-installed FreeType. +//! with font files when the optional `font` feature is enabled. //! //! The approach taken by this crate is similar to [TinySDF](https://github.com/mapbox/tiny-sdf); //! it works from a raster bitmap rather than directly from vector outlines. This keeps the @@ -11,13 +10,13 @@ //! using the same algorithm described in [this paper](http://cs.brown.edu/people/pfelzens/papers/dt-final.pdf) //! by Felzenszwalb & Huttenlocher. //! -//! Rather than re-invent the rasterisation process for fonts, this crate relies on FreeType to -//! generate the bitmap. This is quite fast (we're talking µs/glyph), and the results are +//! Rather than re-invent the rasterisation process for fonts, this crate relies on a Rust font +//! renderer to generate the bitmap. This is quite fast (we're talking µs/glyph), and the results are //! almost always indistinguishable from the more sophisticated vector-based approach of //! [sdf-glyph-foundry](https://github.com/mapbox/sdf-glyph-foundry). //! //! This crate is used by [pbf_font_tools](https://github.com/stadiamaps/pbf_font_tools) to generate -//! SDF glyphs from any FreeType-readable font. If you're looking for a batch generation tool, +//! SDF glyphs from source fonts. If you're looking for a batch generation tool, //! check out [build_pbf_glyphs](https://github.com/stadiamaps/build_pbf_glyphs). mod core; @@ -26,12 +25,8 @@ pub use crate::core::*; mod error; pub use crate::error::SdfGlyphError; -#[cfg(feature = "freetype")] +#[cfg(feature = "font")] mod ft; -// Re-export freetype crate if the feature is enabled -#[cfg(feature = "freetype")] -pub use freetype; - -#[cfg(feature = "freetype")] +#[cfg(feature = "font")] pub use crate::ft::*;