diff options
Diffstat (limited to 'src/style/font')
-rw-r--r-- | src/style/font/font_desc.rs | 296 | ||||
-rw-r--r-- | src/style/font/mod.rs | 42 | ||||
-rw-r--r-- | src/style/font/naive.rs | 40 | ||||
-rw-r--r-- | src/style/font/ttf.rs | 209 | ||||
-rw-r--r-- | src/style/font/web.rs | 46 |
5 files changed, 633 insertions, 0 deletions
diff --git a/src/style/font/font_desc.rs b/src/style/font/font_desc.rs new file mode 100644 index 0000000..7caa4e4 --- /dev/null +++ b/src/style/font/font_desc.rs @@ -0,0 +1,296 @@ +use super::{FontData, FontDataInternal}; +use crate::style::text_anchor::Pos; +use crate::style::{Color, TextStyle}; + +use std::convert::From; + +/// The error type for the font implementation +pub type FontError = <FontDataInternal as FontData>::ErrorType; + +/// The type we used to represent a result of any font operations +pub type FontResult<T> = Result<T, FontError>; + +/// Specifying text transformations +#[derive(Clone)] +pub enum FontTransform { + /// Nothing to transform + None, + /// Rotating the text 90 degree clockwise + Rotate90, + /// Rotating the text 180 degree clockwise + Rotate180, + /// Rotating the text 270 degree clockwise + Rotate270, +} + +impl FontTransform { + /// Transform the coordinate to perform the rotation + /// + /// - `x`: The x coordinate in pixels before transform + /// - `y`: The y coordinate in pixels before transform + /// - **returns**: The coordinate after transform + pub fn transform(&self, x: i32, y: i32) -> (i32, i32) { + match self { + FontTransform::None => (x, y), + FontTransform::Rotate90 => (-y, x), + FontTransform::Rotate180 => (-x, -y), + FontTransform::Rotate270 => (y, -x), + } + } +} + +/// Describes a font +#[derive(Clone)] +pub struct FontDesc<'a> { + size: f64, + family: FontFamily<'a>, + data: FontResult<FontDataInternal>, + transform: FontTransform, + style: FontStyle, +} + +/// Describes font family. +/// This can be either a specific font family name, such as "arial", +/// or a general font family class, such as "serif" and "sans-serif" +#[derive(Clone, Copy)] +pub enum FontFamily<'a> { + /// The system default serif font family + Serif, + /// The system default sans-serif font family + SansSerif, + /// The system default monospace font + Monospace, + /// A specific font family name + Name(&'a str), +} + +impl<'a> FontFamily<'a> { + /// Make a CSS compatible string for the font family name. + /// This can be used as the value of `font-family` attribute in SVG. + pub fn as_str(&self) -> &str { + match self { + FontFamily::Serif => "serif", + FontFamily::SansSerif => "sans-serif", + FontFamily::Monospace => "monospace", + FontFamily::Name(face) => face, + } + } +} + +impl<'a> From<&'a str> for FontFamily<'a> { + fn from(from: &'a str) -> FontFamily<'a> { + match from.to_lowercase().as_str() { + "serif" => FontFamily::Serif, + "sans-serif" => FontFamily::SansSerif, + "monospace" => FontFamily::Monospace, + _ => FontFamily::Name(from), + } + } +} + +/// Describes the font style. Such as Italic, Oblique, etc. +#[derive(Clone, Copy)] +pub enum FontStyle { + /// The normal style + Normal, + /// The oblique style + Oblique, + /// The italic style + Italic, + /// The bold style + Bold, +} + +impl FontStyle { + /// Convert the font style into a CSS compatible string which can be used in `font-style` attribute. + pub fn as_str(&self) -> &str { + match self { + FontStyle::Normal => "normal", + FontStyle::Italic => "italic", + FontStyle::Oblique => "oblique", + FontStyle::Bold => "bold", + } + } +} + +impl<'a> From<&'a str> for FontStyle { + fn from(from: &'a str) -> FontStyle { + match from.to_lowercase().as_str() { + "normal" => FontStyle::Normal, + "italic" => FontStyle::Italic, + "oblique" => FontStyle::Oblique, + "bold" => FontStyle::Bold, + _ => FontStyle::Normal, + } + } +} + +impl<'a> From<&'a str> for FontDesc<'a> { + fn from(from: &'a str) -> FontDesc<'a> { + FontDesc::new(from.into(), 1.0, FontStyle::Normal) + } +} + +impl<'a> From<FontFamily<'a>> for FontDesc<'a> { + fn from(family: FontFamily<'a>) -> FontDesc<'a> { + FontDesc::new(family, 1.0, FontStyle::Normal) + } +} + +impl<'a, T: Into<f64>> From<(FontFamily<'a>, T)> for FontDesc<'a> { + fn from((family, size): (FontFamily<'a>, T)) -> FontDesc<'a> { + FontDesc::new(family, size.into(), FontStyle::Normal) + } +} + +impl<'a, T: Into<f64>> From<(&'a str, T)> for FontDesc<'a> { + fn from((typeface, size): (&'a str, T)) -> FontDesc<'a> { + FontDesc::new(typeface.into(), size.into(), FontStyle::Normal) + } +} + +impl<'a, T: Into<f64>, S: Into<FontStyle>> From<(FontFamily<'a>, T, S)> for FontDesc<'a> { + fn from((family, size, style): (FontFamily<'a>, T, S)) -> FontDesc<'a> { + FontDesc::new(family, size.into(), style.into()) + } +} + +impl<'a, T: Into<f64>, S: Into<FontStyle>> From<(&'a str, T, S)> for FontDesc<'a> { + fn from((typeface, size, style): (&'a str, T, S)) -> FontDesc<'a> { + FontDesc::new(typeface.into(), size.into(), style.into()) + } +} + +/// The trait that allows some type turns into a font description +pub trait IntoFont<'a> { + /// Make the font description from the source type + fn into_font(self) -> FontDesc<'a>; +} + +impl<'a, T: Into<FontDesc<'a>>> IntoFont<'a> for T { + fn into_font(self) -> FontDesc<'a> { + self.into() + } +} + +impl<'a> FontDesc<'a> { + /// Create a new font + /// + /// - `family`: The font family name + /// - `size`: The size of the font + /// - `style`: The font variations + /// - **returns** The newly created font description + pub fn new(family: FontFamily<'a>, size: f64, style: FontStyle) -> Self { + Self { + size, + family, + data: FontDataInternal::new(family, style), + transform: FontTransform::None, + style, + } + } + + /// Create a new font desc with the same font but different size + /// + /// - `size`: The new size to set + /// - **returns** The newly created font descriptor with a new size + pub fn resize(&self, size: f64) -> FontDesc<'a> { + Self { + size, + family: self.family, + data: self.data.clone(), + transform: self.transform.clone(), + style: self.style, + } + } + + /// Set the style of the font + /// + /// - `style`: The new style + /// - **returns** The new font description with this style applied + pub fn style(&self, style: FontStyle) -> Self { + Self { + size: self.size, + family: self.family, + data: self.data.clone(), + transform: self.transform.clone(), + style, + } + } + + /// Set the font transformation + /// + /// - `trans`: The new transformation + /// - **returns** The new font description with this font transformation applied + pub fn transform(&self, trans: FontTransform) -> Self { + Self { + size: self.size, + family: self.family, + data: self.data.clone(), + transform: trans, + style: self.style, + } + } + + /// Get the font transformation description + pub fn get_transform(&self) -> FontTransform { + self.transform.clone() + } + + /// Set the color of the font and return the result text style object + pub fn color<C: Color>(&self, color: &C) -> TextStyle<'a> { + TextStyle { + font: self.clone(), + color: color.to_rgba(), + pos: Pos::default(), + } + } + + /// Get the name of the font + pub fn get_name(&self) -> &str { + self.family.as_str() + } + + /// Get the name of the style + pub fn get_style(&self) -> FontStyle { + self.style + } + + /// Get the size of font + pub fn get_size(&self) -> f64 { + self.size + } + + /// Get the size of the text if rendered in this font + /// + /// For a TTF type, zero point of the layout box is the left most baseline char of the string + /// Thus the upper bound of the box is most likely be negative + pub fn layout_box(&self, text: &str) -> FontResult<((i32, i32), (i32, i32))> { + match &self.data { + Ok(ref font) => font.estimate_layout(self.size, text), + Err(e) => Err(e.clone()), + } + } + + /// Get the size of the text if rendered in this font. + /// This is similar to `layout_box` function, but it apply the font transformation + /// and estimate the overall size of the font + pub fn box_size(&self, text: &str) -> FontResult<(u32, u32)> { + let ((min_x, min_y), (max_x, max_y)) = self.layout_box(text)?; + let (w, h) = self.get_transform().transform(max_x - min_x, max_y - min_y); + Ok((w.abs() as u32, h.abs() as u32)) + } + + /// Actually draws a font with a drawing function + pub fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>( + &self, + text: &str, + (x, y): (i32, i32), + draw: DrawFunc, + ) -> FontResult<Result<(), E>> { + match &self.data { + Ok(ref font) => font.draw((x, y), self.size, text, draw), + Err(e) => Err(e.clone()), + } + } +} diff --git a/src/style/font/mod.rs b/src/style/font/mod.rs new file mode 100644 index 0000000..99c3ca6 --- /dev/null +++ b/src/style/font/mod.rs @@ -0,0 +1,42 @@ +/// The implementation of an actual font implementation +/// +/// This exists since for the image rendering task, we want to use +/// the system font. But in wasm application, we want the browser +/// to handle all the font issue. +/// +/// Thus we need different mechanism for the font implementation + +#[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))] +mod ttf; +#[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))] +use ttf::FontDataInternal; + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))] +mod naive; +#[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))] +use naive::FontDataInternal; + +#[cfg(target_arch = "wasm32")] +mod web; +#[cfg(target_arch = "wasm32")] +use web::FontDataInternal; + +mod font_desc; +pub use font_desc::*; + +pub type LayoutBox = ((i32, i32), (i32, i32)); + +pub trait FontData: Clone { + type ErrorType: Sized + std::error::Error + Clone; + fn new(family: FontFamily, style: FontStyle) -> Result<Self, Self::ErrorType>; + fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType>; + fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>( + &self, + _pos: (i32, i32), + _size: f64, + _text: &str, + _draw: DrawFunc, + ) -> Result<Result<(), E>, Self::ErrorType> { + panic!("The font implementation is unable to draw text"); + } +} diff --git a/src/style/font/naive.rs b/src/style/font/naive.rs new file mode 100644 index 0000000..9953040 --- /dev/null +++ b/src/style/font/naive.rs @@ -0,0 +1,40 @@ +use super::{FontData, FontFamily, FontStyle, LayoutBox}; + +#[derive(Debug, Clone)] +pub struct FontError; + +impl std::fmt::Display for FontError { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(fmt, "General Error")?; + Ok(()) + } +} + +impl std::error::Error for FontError {} + +#[derive(Clone)] +pub struct FontDataInternal(String, String); + +impl FontData for FontDataInternal { + type ErrorType = FontError; + fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> { + Ok(FontDataInternal( + family.as_str().into(), + style.as_str().into(), + )) + } + + /// Note: This is only a crude estimatation, since for some backend such as SVG, we have no way to + /// know the real size of the text anyway. Thus using font-kit is an overkill and doesn't helps + /// the layout. + fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> { + let em = size / 1.24 / 1.24; + Ok(( + (0, -em.round() as i32), + ( + (em * 0.7 * text.len() as f64).round() as i32, + (em * 0.24).round() as i32, + ), + )) + } +} diff --git a/src/style/font/ttf.rs b/src/style/font/ttf.rs new file mode 100644 index 0000000..c76a2df --- /dev/null +++ b/src/style/font/ttf.rs @@ -0,0 +1,209 @@ +use std::borrow::{Borrow, Cow}; +use std::collections::HashMap; +use std::i32; +use std::io::Read; +use std::sync::{Arc, RwLock}; + +use lazy_static::lazy_static; +use rusttype::{point, Error, Font, FontCollection, Scale, SharedBytes}; + +use font_kit::family_name::FamilyName; +use font_kit::handle::Handle; +use font_kit::properties::{Properties, Style, Weight}; +use font_kit::source::SystemSource; + +use super::{FontData, FontFamily, FontStyle, LayoutBox}; + +type FontResult<T> = Result<T, FontError>; + +#[derive(Debug, Clone)] +pub enum FontError { + LockError, + NoSuchFont(String, String), + FontLoadError(Arc<Error>), +} + +impl std::fmt::Display for FontError { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + FontError::LockError => write!(fmt, "Could not lock mutex"), + FontError::NoSuchFont(family, style) => { + write!(fmt, "No such font: {} {}", family, style) + } + FontError::FontLoadError(e) => write!(fmt, "Font loading error: {}", e), + } + } +} + +impl std::error::Error for FontError {} + +lazy_static! { + static ref CACHE: RwLock<HashMap<String, FontResult<Font<'static>>>> = + RwLock::new(HashMap::new()); +} + +thread_local! { + static FONT_SOURCE: SystemSource = SystemSource::new(); +} + +#[allow(dead_code)] +/// Lazily load font data. Font type doesn't own actual data, which +/// lives in the cache. +fn load_font_data(face: FontFamily, style: FontStyle) -> FontResult<Font<'static>> { + let key = match style { + FontStyle::Normal => Cow::Borrowed(face.as_str()), + _ => Cow::Owned(format!("{}, {}", face.as_str(), style.as_str())), + }; + let cache = CACHE.read().unwrap(); + if let Some(cached) = cache.get(Borrow::<str>::borrow(&key)) { + return cached.clone(); + } + drop(cache); + + let mut properties = Properties::new(); + match style { + FontStyle::Normal => properties.style(Style::Normal), + FontStyle::Italic => properties.style(Style::Italic), + FontStyle::Oblique => properties.style(Style::Oblique), + FontStyle::Bold => properties.weight(Weight::BOLD), + }; + + let family = match face { + FontFamily::Serif => FamilyName::Serif, + FontFamily::SansSerif => FamilyName::SansSerif, + FontFamily::Monospace => FamilyName::Monospace, + FontFamily::Name(name) => FamilyName::Title(name.to_owned()), + }; + + let make_not_found_error = + || FontError::NoSuchFont(face.as_str().to_owned(), style.as_str().to_owned()); + + if let Ok(handle) = FONT_SOURCE + .with(|source| source.select_best_match(&[family, FamilyName::SansSerif], &properties)) + { + let (data, id) = match handle { + Handle::Path { + path, + font_index: idx, + } => { + let mut buf = vec![]; + std::fs::File::open(path) + .map_err(|_| make_not_found_error())? + .read_to_end(&mut buf) + .map_err(|_| make_not_found_error())?; + (buf, idx) + } + Handle::Memory { + bytes, + font_index: idx, + } => (bytes[..].to_owned(), idx), + }; + // TODO: font-kit actually have rasterizer, so consider remove dependency for rusttype as + // well + let result = FontCollection::from_bytes(Into::<SharedBytes>::into(data)) + .map_err(|err| FontError::FontLoadError(Arc::new(err)))? + .font_at(id.max(0) as usize) + .map_err(|err| FontError::FontLoadError(Arc::new(err))); + + CACHE + .write() + .map_err(|_| FontError::LockError)? + .insert(key.into_owned(), result.clone()); + + return result; + } + Err(make_not_found_error()) +} + +/// Remove all cached fonts data. +#[allow(dead_code)] +pub fn clear_font_cache() -> FontResult<()> { + let mut cache = CACHE.write().map_err(|_| FontError::LockError)?; + cache.clear(); + Ok(()) +} + +#[derive(Clone)] +pub struct FontDataInternal(Font<'static>); + +impl FontData for FontDataInternal { + type ErrorType = FontError; + + fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> { + Ok(FontDataInternal(load_font_data(family, style)?)) + } + + fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> { + let scale = Scale::uniform(size as f32); + + let (mut min_x, mut min_y) = (i32::MAX, i32::MAX); + let (mut max_x, mut max_y) = (0, 0); + + let font = &self.0; + + for g in font.layout(text, scale, point(0.0, 0.0)) { + if let Some(rect) = g.pixel_bounding_box() { + min_x = min_x.min(rect.min.x); + min_y = min_y.min(rect.min.y); + max_x = max_x.max(rect.max.x); + max_y = max_y.max(rect.max.y); + } + } + + if min_x == i32::MAX || min_y == i32::MAX { + return Ok(((0, 0), (0, 0))); + } + + Ok(((min_x, min_y), (max_x, max_y))) + } + + fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>( + &self, + (base_x, base_y): (i32, i32), + size: f64, + text: &str, + mut draw: DrawFunc, + ) -> Result<Result<(), E>, Self::ErrorType> { + let scale = Scale::uniform(size as f32); + let mut result = Ok(()); + let font = &self.0; + + for g in font.layout(text, scale, point(0.0, 0.0)) { + if let Some(rect) = g.pixel_bounding_box() { + let (x0, y0) = (rect.min.x, rect.min.y); + g.draw(|x, y, v| { + let (x, y) = (x as i32 + x0, y as i32 + y0); + result = draw(x + base_x, y + base_y, v); + }); + if result.is_err() { + break; + } + } + } + Ok(result) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_font_cache() -> FontResult<()> { + clear_font_cache()?; + + // We cannot only check the size of font cache, because + // the test case may be run in parallel. Thus the font cache + // may contains other fonts. + let _a = load_font_data(FontFamily::Serif, FontStyle::Normal)?; + assert!(CACHE.read().unwrap().contains_key("serif")); + + let _b = load_font_data(FontFamily::Serif, FontStyle::Normal)?; + assert!(CACHE.read().unwrap().contains_key("serif")); + + // TODO: Check they are the same + + return Ok(()); + } +} diff --git a/src/style/font/web.rs b/src/style/font/web.rs new file mode 100644 index 0000000..e70e7b1 --- /dev/null +++ b/src/style/font/web.rs @@ -0,0 +1,46 @@ +use super::{FontData, FontFamily, FontStyle, LayoutBox}; +use wasm_bindgen::JsCast; +use web_sys::{window, HtmlElement}; + +#[derive(Debug, Clone)] +pub enum FontError { + UnknownError, +} + +impl std::fmt::Display for FontError { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + _ => write!(fmt, "Unknown error"), + } + } +} + +impl std::error::Error for FontError {} + +#[derive(Clone)] +pub struct FontDataInternal(String, String); + +impl FontData for FontDataInternal { + type ErrorType = FontError; + fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> { + Ok(FontDataInternal( + family.as_str().into(), + style.as_str().into(), + )) + } + fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> { + let window = window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + let span = document.create_element("span").unwrap(); + span.set_text_content(Some(text)); + span.set_attribute("style", &format!("display: inline-block; font-family:{}; font-size: {}px; position: fixed; top: 100%", self.0, size)).unwrap(); + let span = span.into(); + body.append_with_node_1(&span).unwrap(); + let elem = JsCast::dyn_into::<HtmlElement>(span).unwrap(); + let height = elem.offset_height() as i32; + let width = elem.offset_width() as i32; + elem.remove(); + Ok(((0, 0), (width, height))) + } +} |