Browse Source

Add documentation.

Kestrel 10 months ago
parent
commit
d402cf7677
5 changed files with 172 additions and 33 deletions
  1. 6 0
      Cargo.toml
  2. 15 0
      LICENSE
  3. 35 0
      README.md
  4. 115 32
      src/lib.rs
  5. 1 1
      src/test.rs

+ 6 - 0
Cargo.toml

@@ -2,6 +2,12 @@
 name = "schrift-rs"
 version = "0.1.0"
 edition = "2021"
+authors = ["Kestrel <kestrel@flying-kestrel.ca>"]
+repository = "https://git.flying-kestrel.ca/kestrel/schrift-rs"
+description = "Rust wrapper around the libschrift TrueType rendering library."
+license = "ISC"
+keywords = ["truetype", "font", "font-rendering", "text"]
+categories = ["api-bindings", "rendering"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 

+ 15 - 0
LICENSE

@@ -0,0 +1,15 @@
+ISC License
+
+© 2024 Kestrel Yarrow and contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+## schrift-rs
+
+[libschrift](https://github.com/tomolt/libschrift) is a lightweight TrueType
+rendering library implemented in C, and this crate wraps the libschrift library
+into safe Rust.
+
+The following example code renders a single glyph into a PGM file:
+```rust
+use schrift_rs::*;
+let font = Font::load_from_file(
+    concat!(
+        env!("CARGO_MANIFEST_DIR"),
+        "/libschrift/resources/FiraGO-Regular.ttf")).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).expect("couldn't render lowercase 'a'");
+
+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();
+```

+ 115 - 32
src/lib.rs

@@ -1,3 +1,6 @@
+#![doc = include_str!("../README.md")]
+#![warn(missing_docs)]
+
 use std::{collections::HashMap, ffi::c_void, rc::Rc};
 
 #[allow(unused)]
@@ -9,6 +12,7 @@ mod bindings {
     include!(concat!(env!("OUT_DIR"), "/schrift_bindings.rs"));
 }
 
+/// Retrieve the libschrift version information string.
 pub fn version_info() -> &'static str {
     unsafe {
         std::ffi::CStr::from_ptr(bindings::sft_version())
@@ -17,26 +21,40 @@ pub fn version_info() -> &'static str {
     }
 }
 
+/// Error type for schrift-rs operations.
 #[derive(Debug)]
 pub enum Error {
-    FormatError,
+    /// Logic error in invocation of schrift-rs.
+    LogicError(&'static str),
+    /// Internal libschrift error from a malformed font file.
+    FormatError(&'static str),
+    /// I/O error while loading font.
     IOError(std::io::Error),
 }
 
-/// Font information, from a single TTF file
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Debug::fmt(self, f)
+    }
+}
+
+impl std::error::Error for Error { }
+
+/// Size-independent font information, loaded from a single TTF file.
 pub struct Font {
     _data: Box<[u8]>,
     sft_font: *mut bindings::SFT_Font,
 }
 
 impl Font {
+    /// Load font information from a filesystem path.
     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);
+            return Err(Error::FormatError("could not load font"));
         }
 
         Ok(Self {
@@ -55,22 +73,35 @@ impl Drop for Font {
     }
 }
 
+/// Line metrics for a [`Schrift`] instance.
 #[derive(Debug)]
 pub struct LineMetrics {
+    /// Line ascender (height above baseline)
     pub ascender: f64,
+    /// Line descender, as a negative value (height below baseline)
     pub descender: f64,
+    /// Linegap (recommended spacing between lines). Use (ascender + descender + line_gap) for
+    /// baseline-to-baseline distance.
     pub line_gap: f64,
 }
 
+/// Glyph metrics from a [`Schrift`] instance.
 #[derive(Debug)]
 pub struct GlyphMetrics {
+    /// Horizontal advancing distance for this glyph.
     pub advance: f64,
+    /// Left-bearing for this glyph, or distance between origin and min-x of the glyph's bounding
+    /// box.
     pub left_bearing: f64,
+    /// Pixel rendering y-offset from baseline.
     pub y_offset: i32,
+    /// Minimum width for a pixel rendering of this glyph.
     pub min_width: i32,
+    /// Minimum height for a pixel rendering of this glyph.
     pub min_height: i32,
 }
 
+/// TrueType glyph index type.
 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
 pub struct Glyph(bindings::SFT_Glyph);
 
@@ -80,13 +111,16 @@ impl std::fmt::Debug for Glyph {
     }
 }
 
+/// Glyph-pair kerning information.
 #[derive(Debug)]
 pub struct Kerning {
+    /// Additional x-shift on top of usual glyph spacing information.
     pub x_shift: f64,
+    /// Additional y-shift on top of usual glyph spacing information.
     pub y_shift: f64,
 }
 
-/// Context with both a Font and size information.
+/// Size-dependent font functionality.
 pub struct Schrift {
     // keep a reference to Font to keep the sft_font pointer in sft valid
     _font: Rc<Font>,
@@ -94,11 +128,13 @@ pub struct Schrift {
 }
 
 impl Schrift {
+    /// Instantiate a [`SchriftBuilder`] with a given font.
     pub fn build(font: Rc<Font>) -> SchriftBuilder {
         SchriftBuilder::new(font)
     }
 
-    pub fn line_metrics(&self) -> Option<LineMetrics> {
+    /// Compute the line metrics for this font context.
+    pub fn line_metrics(&self) -> Result<LineMetrics, Error> {
         unsafe {
             let mut lm = bindings::SFT_LMetrics {
                 ascender: 0.,
@@ -106,37 +142,38 @@ impl Schrift {
                 lineGap: 0.,
             };
             if bindings::sft_lmetrics(&self.sft as *const _, &mut lm as *mut _) != 0 {
-                None
+                Err(Error::FormatError("line metrics table"))
             } else {
-                LineMetrics {
+                Ok(LineMetrics {
                     ascender: lm.ascender,
                     descender: lm.descender,
                     line_gap: lm.lineGap,
-                }
-                .into()
+                })
             }
         }
     }
 
-    pub fn glyph_lookup(&self, c: char) -> Option<Glyph> {
+    /// Perform unicode-to-glyph lookup.
+    pub fn glyph_lookup(&self, c: char) -> Result<Glyph, Error> {
         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
+                Err(Error::FormatError("glyph cmap"))
             } else {
-                Some(Glyph(g))
+                Ok(Glyph(g))
             }
         }
     }
 
-    pub fn glyph_metrics(&self, glyph: Glyph) -> Option<GlyphMetrics> {
+    /// Look up glyph metrics for a given glyph index.
+    pub fn glyph_metrics(&self, glyph: Glyph) -> Result<GlyphMetrics, Error> {
         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
+                Err(Error::FormatError("glyph mtx"))
             } else {
                 let gmet = gmet.assume_init();
-                Some(GlyphMetrics {
+                Ok(GlyphMetrics {
                     advance: gmet.advanceWidth,
                     left_bearing: gmet.leftSideBearing,
                     y_offset: gmet.yOffset,
@@ -147,28 +184,37 @@ impl Schrift {
         }
     }
 
-    pub fn kerning(&self, left: Glyph, right: Glyph) -> Kerning {
+    /// Look up kerning information for a given pair of glyphs.
+    pub fn kerning(&self, left: Glyph, right: Glyph) -> Result<Kerning, Error> {
         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.,
-                }
+                Err(Error::FormatError("kerning table"))
             } else {
                 let kern = kern.assume_init();
-                Kerning {
+                Ok(Kerning {
                     x_shift: kern.xShift,
                     y_shift: kern.yShift,
-                }
+                })
             }
         }
     }
 
-    pub fn render(&self, glyph: Glyph, alpha_map: &mut [u8], width: usize, height: usize) {
+    /// Render a glyph into an alpha-valued bitmap.
+    ///
+    /// Note that 0=no colour, 255=full colour. Subpixel hinting is out of scope for libschrift.
+    pub fn render(
+        &self,
+        glyph: Glyph,
+        alpha_map: &mut [u8],
+        width: usize,
+        height: usize,
+    ) -> Result<(), Error> {
         if alpha_map.len() < width * height {
-            return;
+            return Err(Error::LogicError(
+                "alpha_map not large enough for given width and height",
+            ));
         }
 
         unsafe {
@@ -177,11 +223,16 @@ impl Schrift {
                 width: width as i32,
                 height: height as i32,
             };
-            bindings::sft_render(&self.sft as *const _, glyph.0, img);
+            if bindings::sft_render(&self.sft as *const _, glyph.0, img) != 0 {
+                Err(Error::FormatError("rendering failed"))
+            } else {
+                Ok(())
+            }
         }
     }
 }
 
+/// Builder pattern for a [`Schrift`] instance.
 pub struct SchriftBuilder {
     font: Rc<Font>,
     x_scale: Option<f64>,
@@ -203,21 +254,26 @@ impl SchriftBuilder {
         }
     }
 
+    /// Set both x- and y- scaling information.
     pub fn with_scale(mut self, scale: f64) -> Self {
         self.x_scale = Some(scale);
         self.y_scale = Some(scale);
         self
     }
+    /// Set render offset information.
     pub fn with_offset(mut self, x: f64, y: f64) -> Self {
         self.x_offset = Some(x);
         self.y_offset = Some(y);
         self
     }
+    /// Make the resulting coordinate space consider +y to be the bottom of the glyph, instead of
+    /// the top.
     pub fn flag_y_downwards(mut self) -> Self {
         self.downward_y = Some(true);
         self
     }
 
+    /// Construct a [`Schrift`] instance.
     pub fn build(self) -> Schrift {
         let scales = match (self.x_scale, self.y_scale) {
             (Some(xs), Some(ys)) => (xs, ys),
@@ -244,53 +300,80 @@ impl SchriftBuilder {
     }
 }
 
-/// Caching wrapper around [`Schrift`]
+/// Caching wrapper around [`Schrift`] that treats format errors as panics.
 pub struct CachedSchrift {
     schrift: Schrift,
     lmetrics: LineMetrics,
+    glyph_cache: HashMap<char, Glyph>,
     image_cache: HashMap<Glyph, (Box<[u8]>, u32, u32)>,
     metric_cache: HashMap<Glyph, GlyphMetrics>,
     kern_cache: HashMap<(Glyph, Glyph), Kerning>,
 }
 
 impl CachedSchrift {
+    /// Wrap a [`Schrift`] instance with this caching layer.
     pub fn new(schrift: Schrift) -> Self {
         Self {
-            lmetrics: schrift.line_metrics().unwrap(),
+            lmetrics: schrift.line_metrics().expect("couldn't get line metrics"),
             schrift,
+            glyph_cache: Default::default(),
             image_cache: Default::default(),
             metric_cache: Default::default(),
             kern_cache: Default::default(),
         }
     }
 
+    /// Get line metrics.
     pub fn line_metrics(&self) -> &LineMetrics {
         &self.lmetrics
     }
 
+    /// Perform glyph lookup.
+    pub fn glyph_lookup(&mut self, ch: char) -> Glyph {
+        *self.glyph_cache.entry(ch).or_insert_with(|| {
+            self.schrift
+                .glyph_lookup(ch)
+                .expect("couldn't lookup glyph")
+        })
+    }
+
+    /// Get glyph metrics.
     pub fn glyph_metrics(&mut self, glyph: Glyph) -> &GlyphMetrics {
-        self.metric_cache
-            .entry(glyph)
-            .or_insert_with(|| self.schrift.glyph_metrics(glyph).unwrap())
+        self.metric_cache.entry(glyph).or_insert_with(|| {
+            self.schrift
+                .glyph_metrics(glyph)
+                .expect("couldn't lookup glyph metrics")
+        })
     }
 
+    /// Get a rendered alphamap image for a given glyph.
     pub fn glyph_image(&mut self, glyph: Glyph) -> &(Box<[u8]>, u32, u32) {
+        // this is written as an explicit contains_key()/get().unwrap() so that the long-lived
+        // borrow happens inside the if block.
         if self.image_cache.contains_key(&glyph) {
+            // this unwrap is always OK
             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.schrift
+            .render(glyph, &mut data, w, h)
+            .expect("couldn't render glyph");
 
         self.image_cache
             .entry(glyph)
             .or_insert((data.into_boxed_slice(), w as u32, h as u32))
     }
 
+    /// Get the kerning information for a given pair of glyphs.
     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))
+        self.kern_cache.entry((left, right)).or_insert_with(|| {
+            self.schrift
+                .kerning(left, right)
+                .expect("couldn't get kerning")
+        })
     }
 }
 

+ 1 - 1
src/test.rs

@@ -50,7 +50,7 @@ fn render_test() {
 
     let mut pxdata = vec![0u8; 256];
 
-    sch.render(aglyph, pxdata.as_mut_slice(), 16, 16);
+    sch.render(aglyph, pxdata.as_mut_slice(), 16, 16).expect("couldn't render lowercase 'a'");
 
     let mut pgm = String::new();
     pgm.push_str("P2\n16 16\n255\n");