aboutsummaryrefslogtreecommitdiff
path: root/src/style/font
diff options
context:
space:
mode:
Diffstat (limited to 'src/style/font')
-rw-r--r--src/style/font/font_desc.rs296
-rw-r--r--src/style/font/mod.rs42
-rw-r--r--src/style/font/naive.rs40
-rw-r--r--src/style/font/ttf.rs209
-rw-r--r--src/style/font/web.rs46
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)))
+ }
+}