Browse Source

Add caching wrapper.

Kestrel 10 months ago
parent
commit
7d4bfae907
4 changed files with 367 additions and 9 deletions
  1. 1 0
      Cargo.toml
  2. 9 0
      build.rs
  3. 293 9
      src/lib.rs
  4. 64 0
      src/test.rs

+ 1 - 0
Cargo.toml

@@ -7,3 +7,4 @@ edition = "2021"
 
 [build-dependencies]
 cc = "1.0"
+bindgen = "0.69"

+ 9 - 0
build.rs

@@ -1,4 +1,13 @@
 fn main() {
+    let bindings = bindgen::Builder::default()
+        .header("libschrift/schrift.h")
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
+        .generate()
+        .expect("couldn't generate bindings");
+    let path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(path.join("schrift_bindings.rs"))
+        .expect("couldn't write bindings to file");
     cc::Build::new()
         .flag("-std=c99")
         .flag("-pedantic")

+ 293 - 9
src/lib.rs

@@ -1,14 +1,298 @@
-pub fn add(left: usize, right: usize) -> usize {
-    left + right
+use std::{collections::HashMap, ffi::c_void, rc::Rc};
+
+#[allow(unused)]
+#[allow(non_snake_case)]
+#[allow(non_camel_case_types)]
+#[allow(non_upper_case_globals)]
+#[allow(clippy::upper_case_acronyms)]
+mod bindings {
+    include!(concat!(env!("OUT_DIR"), "/schrift_bindings.rs"));
 }
 
-#[cfg(test)]
-mod tests {
-    use super::*;
+pub fn version_info() -> &'static str {
+    unsafe {
+        std::ffi::CStr::from_ptr(bindings::sft_version())
+            .to_str()
+            .unwrap()
+    }
+}
+
+#[derive(Debug)]
+pub enum Error {
+    FormatError,
+    IOError(std::io::Error),
+}
+
+/// Font information, from a single TTF file
+pub struct Font {
+    _data: Box<[u8]>,
+    sft_font: *mut bindings::SFT_Font,
+}
+
+impl Font {
+    pub fn load_from_file(path: impl AsRef<std::path::Path>) -> Result<Rc<Self>, Error> {
+        let font_data = std::fs::read(path.as_ref()).map_err(Error::IOError)?;
+        let data = font_data.into_boxed_slice();
+
+        let sft_font = unsafe { bindings::sft_loadmem(data.as_ptr() as *const c_void, data.len()) };
+        if sft_font.is_null() {
+            return Err(Error::FormatError);
+        }
+
+        Ok(Self {
+            _data: data,
+            sft_font,
+        }
+        .into())
+    }
+}
+
+impl Drop for Font {
+    fn drop(&mut self) {
+        unsafe {
+            bindings::sft_freefont(self.sft_font);
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct LineMetrics {
+    pub ascender: f64,
+    pub descender: f64,
+    pub line_gap: f64,
+}
+
+#[derive(Debug)]
+pub struct GlyphMetrics {
+    pub advance: f64,
+    pub left_bearing: f64,
+    pub y_offset: i32,
+    pub min_width: i32,
+    pub min_height: i32,
+}
 
-    #[test]
-    fn it_works() {
-        let result = add(2, 2);
-        assert_eq!(result, 4);
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Glyph(bindings::SFT_Glyph);
+
+impl std::fmt::Debug for Glyph {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple("Glyph").field(&self.0).finish()
     }
 }
+
+#[derive(Debug)]
+pub struct Kerning {
+    pub x_shift: f64,
+    pub y_shift: f64,
+}
+
+/// Context with both a Font and size information.
+pub struct Schrift {
+    // keep a reference to Font to keep the sft_font pointer in sft valid
+    _font: Rc<Font>,
+    sft: bindings::SFT,
+}
+
+impl Schrift {
+    pub fn build(font: Rc<Font>) -> SchriftBuilder {
+        SchriftBuilder::new(font)
+    }
+
+    pub fn line_metrics(&self) -> Option<LineMetrics> {
+        unsafe {
+            let mut lm = bindings::SFT_LMetrics {
+                ascender: 0.,
+                descender: 0.,
+                lineGap: 0.,
+            };
+            if bindings::sft_lmetrics(&self.sft as *const _, &mut lm as *mut _) != 0 {
+                None
+            } else {
+                LineMetrics {
+                    ascender: lm.ascender,
+                    descender: lm.descender,
+                    line_gap: lm.lineGap,
+                }
+                .into()
+            }
+        }
+    }
+
+    pub fn glyph_lookup(&self, c: char) -> Option<Glyph> {
+        unsafe {
+            let mut g = bindings::SFT_Glyph::default();
+            if bindings::sft_lookup(&self.sft as *const _, c as u32, &mut g as *mut _) != 0 {
+                None
+            } else {
+                Some(Glyph(g))
+            }
+        }
+    }
+
+    pub fn glyph_metrics(&self, glyph: Glyph) -> Option<GlyphMetrics> {
+        unsafe {
+            let mut gmet = std::mem::MaybeUninit::<bindings::SFT_GMetrics>::uninit();
+            if bindings::sft_gmetrics(&self.sft as *const _, glyph.0, gmet.as_mut_ptr()) != 0 {
+                None
+            } else {
+                let gmet = gmet.assume_init();
+                Some(GlyphMetrics {
+                    advance: gmet.advanceWidth,
+                    left_bearing: gmet.leftSideBearing,
+                    y_offset: gmet.yOffset,
+                    min_width: gmet.minWidth,
+                    min_height: gmet.minHeight,
+                })
+            }
+        }
+    }
+
+    pub fn kerning(&self, left: Glyph, right: Glyph) -> Kerning {
+        unsafe {
+            let mut kern = std::mem::MaybeUninit::<bindings::SFT_Kerning>::uninit();
+            if bindings::sft_kerning(&self.sft as *const _, left.0, right.0, kern.as_mut_ptr()) != 0
+            {
+                Kerning {
+                    x_shift: 0.,
+                    y_shift: 0.,
+                }
+            } else {
+                let kern = kern.assume_init();
+                Kerning {
+                    x_shift: kern.xShift,
+                    y_shift: kern.yShift,
+                }
+            }
+        }
+    }
+
+    pub fn render(&self, glyph: Glyph, alpha_map: &mut [u8], width: usize, height: usize) {
+        if alpha_map.len() < width * height {
+            return;
+        }
+
+        unsafe {
+            let img = bindings::SFT_Image {
+                pixels: alpha_map.as_mut_ptr() as *mut c_void,
+                width: width as i32,
+                height: height as i32,
+            };
+            bindings::sft_render(&self.sft as *const _, glyph.0, img);
+        }
+    }
+}
+
+pub struct SchriftBuilder {
+    font: Rc<Font>,
+    x_scale: Option<f64>,
+    y_scale: Option<f64>,
+    x_offset: Option<f64>,
+    y_offset: Option<f64>,
+    downward_y: Option<bool>,
+}
+
+impl SchriftBuilder {
+    fn new(font: Rc<Font>) -> Self {
+        Self {
+            font,
+            x_scale: None,
+            y_scale: None,
+            x_offset: None,
+            y_offset: None,
+            downward_y: None,
+        }
+    }
+
+    pub fn with_scale(mut self, scale: f64) -> Self {
+        self.x_scale = Some(scale);
+        self.y_scale = Some(scale);
+        self
+    }
+    pub fn with_offset(mut self, x: f64, y: f64) -> Self {
+        self.x_offset = Some(x);
+        self.y_offset = Some(y);
+        self
+    }
+    pub fn flag_y_downwards(mut self) -> Self {
+        self.downward_y = Some(true);
+        self
+    }
+
+    pub fn build(self) -> Schrift {
+        let scales = match (self.x_scale, self.y_scale) {
+            (Some(xs), Some(ys)) => (xs, ys),
+            (Some(xs), None) => (xs, xs),
+            (None, Some(ys)) => (ys, ys),
+            (None, None) => (12.0, 12.0),
+        };
+
+        Schrift {
+            sft: bindings::SFT {
+                font: self.font.sft_font,
+                xScale: scales.0,
+                yScale: scales.1,
+                xOffset: self.x_offset.unwrap_or(0.0),
+                yOffset: self.y_offset.unwrap_or(0.0),
+                flags: if self.downward_y.unwrap_or(false) {
+                    bindings::SFT_DOWNWARD_Y as i32
+                } else {
+                    0
+                },
+            },
+            _font: self.font,
+        }
+    }
+}
+
+/// Caching wrapper around [`Schrift`]
+pub struct CachedSchrift {
+    schrift: Schrift,
+    lmetrics: LineMetrics,
+    image_cache: HashMap<Glyph, (Box<[u8]>, u32, u32)>,
+    metric_cache: HashMap<Glyph, GlyphMetrics>,
+    kern_cache: HashMap<(Glyph, Glyph), Kerning>,
+}
+
+impl CachedSchrift {
+    pub fn new(schrift: Schrift) -> Self {
+        Self {
+            lmetrics: schrift.line_metrics().unwrap(),
+            schrift,
+            image_cache: Default::default(),
+            metric_cache: Default::default(),
+            kern_cache: Default::default(),
+        }
+    }
+
+    pub fn line_metrics(&self) -> &LineMetrics {
+        &self.lmetrics
+    }
+
+    pub fn glyph_metrics(&mut self, glyph: Glyph) -> &GlyphMetrics {
+        self.metric_cache
+            .entry(glyph)
+            .or_insert_with(|| self.schrift.glyph_metrics(glyph).unwrap())
+    }
+
+    pub fn glyph_image(&mut self, glyph: Glyph) -> &(Box<[u8]>, u32, u32) {
+        if self.image_cache.contains_key(&glyph) {
+            return self.image_cache.get(&glyph).unwrap();
+        }
+        let metrics = self.glyph_metrics(glyph);
+        let w = metrics.min_width as usize;
+        let h = metrics.min_height as usize;
+        let mut data = vec![0u8; (metrics.min_width * metrics.min_height) as usize];
+        self.schrift.render(glyph, &mut data, w, h);
+
+        self.image_cache
+            .entry(glyph)
+            .or_insert((data.into_boxed_slice(), w as u32, h as u32))
+    }
+
+    pub fn glyph_kerning(&mut self, left: Glyph, right: Glyph) -> &Kerning {
+        self.kern_cache.entry((left, right)).or_insert_with(|| self.schrift.kerning(left, right))
+    }
+}
+
+#[cfg(test)]
+mod test;

+ 64 - 0
src/test.rs

@@ -0,0 +1,64 @@
+use super::*;
+
+const FONT_PATH: &'static str = "libschrift/resources/FiraGO-Regular.ttf";
+
+#[test]
+fn version_test() {
+    assert_eq!(version_info(), "0.10.2");
+}
+
+#[test]
+fn load_test() {
+    let _font = Font::load_from_file(FONT_PATH).unwrap();
+}
+
+#[test]
+fn lookup_test() {
+    let font = Font::load_from_file(FONT_PATH).unwrap();
+    let sch = Schrift::build(font).build();
+    sch.glyph_lookup('a')
+        .expect("couldn't look up lowercase 'a'");
+    sch.glyph_lookup('\u{13080}')
+        .expect("couldn't look up hieroglyph");
+}
+
+#[test]
+fn metrics_test() {
+    let font = Font::load_from_file(FONT_PATH).unwrap();
+    let sch = Schrift::build(font).with_scale(10.0).build();
+    let aglyph = sch
+        .glyph_lookup('a')
+        .expect("couldn't look up lowercase 'a'");
+    let agm = sch
+        .glyph_metrics(aglyph)
+        .expect("couldn't get glyph metrics");
+    // check the extracted advance value
+    println!("{agm:?}");
+    assert!((agm.advance - 5.44).abs() < 1e-8);
+}
+
+#[test]
+fn render_test() {
+    let font = Font::load_from_file(FONT_PATH).unwrap();
+    let sch = Schrift::build(font)
+        .with_scale(10.0)
+        .flag_y_downwards()
+        .build();
+    let aglyph = sch
+        .glyph_lookup('a')
+        .expect("couldn't look up lowercase 'a'");
+
+    let mut pxdata = vec![0u8; 256];
+
+    sch.render(aglyph, pxdata.as_mut_slice(), 16, 16);
+
+    let mut pgm = String::new();
+    pgm.push_str("P2\n16 16\n255\n");
+    for y in 0..16 {
+        for x in 0..16 {
+            pgm.push_str(format!(" {}", pxdata[x + (y * 16)]).as_str());
+        }
+        pgm.push_str("\n");
+    }
+    std::fs::write(concat!(env!("OUT_DIR"), "render_test.pgm"), pgm).unwrap();
+}