diff --git a/plotters/Cargo.toml b/plotters/Cargo.toml index a27a8f17..254ba980 100644 --- a/plotters/Cargo.toml +++ b/plotters/Cargo.toml @@ -34,6 +34,9 @@ ttf-parser = { version = "0.15.0", optional = true } lazy_static = { version = "1.4.0", optional = true } pathfinder_geometry = { version = "0.5.1", optional = true } font-kit = { version = "0.11.0", optional = true } +ab_glyph = { version = "0.2.12", optional = true } +once_cell = { version = "1.8.0", optional = true } + [target.'cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))'.dependencies.image] version = "0.24.3" @@ -71,7 +74,7 @@ all_series = ["area_series", "line_series", "point_series", "surface_series"] all_elements = ["errorbar", "candlestick", "boxplot", "histogram"] # Tier 1 Backends -bitmap_backend = ["plotters-bitmap", "ttf"] +bitmap_backend = ["plotters-bitmap"] bitmap_encoder = ["plotters-bitmap/image_encoder"] bitmap_gif = ["plotters-bitmap/gif_backend"] svg_backend = ["plotters-svg"] @@ -97,6 +100,8 @@ ttf = ["font-kit", "ttf-parser", "lazy_static", "pathfinder_geometry"] # Can be useful for cross compiling, especially considering fontconfig has lots of C dependencies fontconfig-dlopen = ["font-kit/source-fontconfig-dlopen"] +ab_glyph = ["dep:ab_glyph", "once_cell"] + # Misc datetime = ["chrono"] evcxr = ["svg_backend"] diff --git a/plotters/src/style/font/ab_glyph.rs b/plotters/src/style/font/ab_glyph.rs new file mode 100644 index 00000000..183e4d73 --- /dev/null +++ b/plotters/src/style/font/ab_glyph.rs @@ -0,0 +1,156 @@ +use super::{FontData, FontFamily, FontStyle, LayoutBox}; +use ab_glyph::{Font, FontRef, ScaleFont}; +use core::fmt::{self, Display}; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::error::Error; +use std::sync::RwLock; + +struct FontMap { + map: HashMap>, +} +impl FontMap { + fn new() -> Self { + Self { + map: HashMap::with_capacity(4), + } + } + fn insert(&mut self, style: FontStyle, font: FontRef<'static>) -> Option> { + self.map.insert(style.as_str().to_string(), font) + } + // fn get(&self, style: FontStyle) -> Option<&FontRef<'static>> { + // self.map.get(style.as_str()) + // } + fn get_fallback(&self, style: FontStyle) -> Option<&FontRef<'static>> { + self.map + .get(style.as_str()) + .or_else(|| self.map.get(FontStyle::Normal.as_str())) + } +} + +static FONTS: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); +pub struct InvalidFont { + _priv: (), +} + +// Note for future contributors: There is nothing fundamental about the static reference requirement here. +// It would be reasonably easy to add a function which accepts an owned buffer, +// or even a reference counted buffer, instead. +/// Register a font in the fonts table. +/// +/// The `name` parameter gives the name this font shall be referred to +/// in the other APIs, like `"sans-serif"`. +/// +/// The `bytes` parameter should be the complete contents +/// of an OpenType font file, like: +/// ```ignore +/// include_bytes!("FiraGO-Regular.otf") +/// ``` +pub fn register_font( + name: &str, + style: FontStyle, + bytes: &'static [u8], +) -> Result<(), InvalidFont> { + let font = FontRef::try_from_slice(bytes).map_err(|_| InvalidFont { _priv: () })?; + let mut lock = FONTS.write().unwrap(); + lock.entry(name.to_string()) + .or_insert_with(FontMap::new) + .insert(style, font); + Ok(()) +} + +#[derive(Clone)] +pub struct FontDataInternal { + font_ref: FontRef<'static>, +} + +#[derive(Debug, Clone)] +pub enum FontError { + /// No idea what the problem is + Unknown, + /// No font data available for the requested family and style. + FontUnavailable, +} +impl Display for FontError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Since it makes literally no difference to how we'd format + // this, just delegate to the derived Debug formatter. + write!(f, "{:?}", self) + } +} +impl Error for FontError {} + +impl FontData for FontDataInternal { + // TODO: can we rename this to `Error`? + type ErrorType = FontError; + fn new(family: FontFamily<'_>, style: FontStyle) -> Result { + Ok(Self { + font_ref: FONTS + .read() + .unwrap() + .get(family.as_str()) + .and_then(|fam| fam.get_fallback(style)) + .ok_or(FontError::FontUnavailable)? + .clone(), + }) + } + // TODO: ngl, it makes no sense that this uses the same error type as `new` + fn estimate_layout(&self, size: f64, text: &str) -> Result { + let pixel_per_em = size / 1.24; + // let units_per_em = self.font_ref.units_per_em().unwrap(); + let font = self.font_ref.as_scaled(size as f32); + + let mut x_pixels = 0f32; + + let mut prev = None; + for c in text.chars() { + let glyph_id = font.glyph_id(c); + let size = font.h_advance(glyph_id); + x_pixels += size; + if let Some(pc) = prev { + x_pixels += font.kern(pc, glyph_id); + } + prev = Some(glyph_id); + } + + Ok(((0, 0), (x_pixels as i32, pixel_per_em as i32))) + } + fn draw Result<(), E>>( + &self, + pos: (i32, i32), + size: f64, + text: &str, + mut draw: DrawFunc, + ) -> Result, Self::ErrorType> { + let font = self.font_ref.as_scaled(size as f32); + let mut draw = |x: i32, y: i32, c| { + let (base_x, base_y) = pos; + draw(base_x + x, base_y + y, c) + }; + let mut x_shift = 0f32; + let mut prev = None; + for c in text.chars() { + if let Some(pc) = prev { + x_shift += font.kern(font.glyph_id(pc), font.glyph_id(c)); + } + prev = Some(c); + let glyph = font.scaled_glyph(c); + if let Some(q) = font.outline_glyph(glyph) { + let rect = q.px_bounds(); + let y_shift = ((size as f32) / 2.0 + rect.min.y) as i32; + let x_shift = x_shift as i32; + let mut buf = vec![]; + q.draw(|x, y, c| buf.push((x, y, c))); + for (x, y, c) in buf { + draw(x as i32 + x_shift, y as i32 + y_shift, c).map_err(|_e| { + // Note: If ever `plotters` adds a tracing or logging crate, + // this would be a good place to use it. + FontError::Unknown + })?; + } + } + x_shift += font.h_advance(font.glyph_id(c)); + } + Ok(Ok(())) + } +} diff --git a/plotters/src/style/font/mod.rs b/plotters/src/style/font/mod.rs index 305978fd..5f1c04c5 100644 --- a/plotters/src/style/font/mod.rs +++ b/plotters/src/style/font/mod.rs @@ -17,14 +17,28 @@ mod ttf; ))] use ttf::FontDataInternal; +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "wasi"), + feature = "ab_glyph"))] +mod ab_glyph; +#[cfg(all( + not(target_arch = "wasm32"), not(target_os = "wasi"), + feature = "ab_glyph", not(feature = "ttf") +))] +use self::ab_glyph::FontDataInternal; +#[cfg(all( + not(target_arch = "wasm32"), not(target_os = "wasi"), + feature = "ab_glyph" +))] +pub use self::ab_glyph::register_font; + #[cfg(all( not(all(target_arch = "wasm32", not(target_os = "wasi"))), - not(feature = "ttf") + not(feature = "ttf"), not(feature = "ab_glyph") ))] mod naive; #[cfg(all( not(all(target_arch = "wasm32", not(target_os = "wasi"))), - not(feature = "ttf") + not(feature = "ttf"), not(feature = "ab_glyph") ))] use naive::FontDataInternal; diff --git a/plotters/src/style/mod.rs b/plotters/src/style/mod.rs index 7d7c9ac3..abcb18ab 100644 --- a/plotters/src/style/mod.rs +++ b/plotters/src/style/mod.rs @@ -20,6 +20,9 @@ pub use colors::full_palette; pub use font::{ FontDesc, FontError, FontFamily, FontResult, FontStyle, FontTransform, IntoFont, LayoutBox, }; +#[cfg(all(not(target_arch = "wasm32"), feature = "ab_glyph"))] +pub use font::register_font; + pub use shape::ShapeStyle; pub use size::{AsRelative, RelativeSize, SizeDesc}; pub use text::text_anchor;