aoer_plotty_rs/context/
typography.rs

1use font_kit::font::Font;
2use font_kit::hinting::HintingOptions;
3use geo::bounding_rect::BoundingRect;
4use geo::map_coords::MapCoords;
5use geo::translate::Translate;
6use geo_types::{coord, Geometry, GeometryCollection, Rect};
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::sync::Arc;
10
11use crate::context::glyph_proxy::GlyphProxy;
12use crate::geo_types::ToGTGeometry;
13use pathfinder_geometry::rect::RectF;
14use pathfinder_geometry::vector::Vector2F;
15
16type TypographicBounds = RectF;
17
18#[derive(Debug)]
19pub enum TypographyError {
20    FontError,
21    NoFontSet,
22    GlyphNotFound(u32),
23}
24
25impl Display for TypographyError {
26    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{:?}", self)
28    }
29}
30
31impl Error for TypographyError {}
32
33#[derive(Clone, Debug)]
34pub enum TextAlignment {
35    Left,
36    Center,
37    Right,
38}
39
40#[derive(Debug)]
41pub struct RenderedGlyph {
42    geo: Geometry<f64>,
43    pub bounds: TypographicBounds,
44    pub advance: Vector2F,
45}
46
47#[derive(Clone, Debug)]
48pub struct Typography {
49    font: Option<Font>,
50    hinting: HintingOptions,
51    em: f64,
52    close: bool,
53    align: TextAlignment, // TODO: Add text-align, other stuff.
54}
55
56impl Typography {
57    /// Default font
58    pub fn default_font() -> Font {
59        let font_data =
60            include_bytes!("../../resources/fonts/ReliefSingleLineOutline-Regular.otf").to_vec();
61        Font::from_bytes(Arc::new(font_data), 0).unwrap() // We know this font is OK
62    }
63
64    /// Generate a blank Typography instance
65    pub fn new() -> Self {
66        Typography {
67            font: Some(Self::default_font()),
68            hinting: HintingOptions::None,
69            em: 1.0,
70            close: false,
71            align: TextAlignment::Left,
72        }
73    }
74
75    pub fn mm_per_em() -> f64 {
76        8.0f64 * 0.3527777778f64
77    }
78
79    pub fn align(&mut self, align: TextAlignment) -> &mut Self {
80        self.align = align;
81        self
82    }
83
84    pub fn close(&mut self, close: bool) -> &mut Self {
85        self.close = close;
86        self
87    }
88
89    pub fn hint(&mut self, hint: HintingOptions) -> &mut Self {
90        self.hinting = hint;
91        self
92    }
93
94    pub fn font(&mut self, font: &Font) -> &mut Self {
95        self.font = Some(font.clone());
96        self
97    }
98
99    pub fn size(&mut self, em: f64) -> &mut Self {
100        self.em = em;
101        self
102    }
103
104    pub fn render(&self, text: &String, accuracy: f64) -> Result<Geometry<f64>, Box<dyn Error>> {
105        let font = match &self.font {
106            None => return Err(Box::new(TypographyError::NoFontSet)),
107            Some(font) => font.clone(),
108        };
109        let metrics = font.metrics();
110        let mut glyphs: Vec<RenderedGlyph> = vec![];
111        let mut advance = Vector2F::new(0.0, 0.0);
112        for char in text.chars() {
113            let mut gp = GlyphProxy::new(self.close);
114            let glyph = font.glyph_for_char(char).or(Some(32)).unwrap();
115            font.outline(glyph, self.hinting, &mut gp)?;
116            let thisadvance = font.advance(glyph)?;
117            let gtgeo = gp.path().to_gt_geometry(accuracy)?;
118            // let gbounds = font.typographic_bounds(glyph)?;
119            // println!("Advancing: {:?} for {:?} which has bounds: {:?} and self-advance of {:?}", advance.x(), char, gbounds.0.x(), thisadvance.x());
120            let rglyph = RenderedGlyph {
121                geo: gtgeo.translate(advance.x().into(), advance.y().into()),
122                bounds: font.typographic_bounds(glyph)?,
123                advance: advance.clone(),
124            };
125            advance = advance + thisadvance;
126            //println!("GLYPH PUSHED: {:?}", &rglyph);
127            glyphs.push(rglyph);
128        }
129        // Ok, now we have a collection of glyphs, we have to scale and transform (center, etc)
130        // println!("OK, now we do a metrics calculation for scaling: {:?}", &metrics.units_per_em);
131        let units_per_em: f64 = &self.em / f64::from(metrics.units_per_em);
132        let output_geometries: Vec<Geometry<f64>> = glyphs
133            .iter()
134            .map(|g| {
135                g.geo.map_coords(|xy| {
136                    coord!(
137                        x: units_per_em * xy.x * Self::mm_per_em(),
138                        y: units_per_em * xy.y * Self::mm_per_em(),
139                    )
140                }) //.clone()
141            })
142            .collect();
143        let output_geo_collection = GeometryCollection::new_from(output_geometries);
144        let bounds = output_geo_collection
145            .bounding_rect()
146            .unwrap_or(Rect::new(coord! {x: 0.0, y:0.0}, coord! {x:0.0, y:0.0}));
147        // println!("After scasling: {:?}", output_geo_collection);
148        // println!("Scaled bounds: {:?}", bounds);
149        let output_geo_collection = match &self.align {
150            TextAlignment::Left => output_geo_collection,
151            TextAlignment::Right => {
152                output_geo_collection.translate(-(bounds.max().x - bounds.min().x), 0.0)
153            }
154            TextAlignment::Center => {
155                output_geo_collection.translate(-(bounds.max().x - bounds.min().x) / 2.0, 0.0)
156            }
157        };
158        Ok(Geometry::GeometryCollection(output_geo_collection))
159    }
160}
161
162#[cfg(test)]
163pub mod tests {
164    use crate::context::typography::Typography;
165    use font_kit::font::Font;
166    use std::sync::Arc;
167
168    #[test]
169    fn test_simple() {
170        let fdata = include_bytes!("../../resources/fonts/ReliefSingleLine-Regular.ttf").to_vec();
171        let f = Font::from_bytes(Arc::new(fdata), 0).unwrap(); // We know this font is OK
172        let mut t = Typography::new();
173        let _geo = t
174            .size(2.0)
175            .font(&f)
176            .render(&"YES: This is some text XXX".to_string(), 0.1);
177    }
178}