klyff_msdf 0.1.3

MSDF generation library with optional GPU acceleration.
Documentation
use crate::{
    Aabb, OutlineProvider, ReadGlyphOutlineError,
    math_segment::Segment,
    msdf::{compute_msdf, compute_mtsdf},
    polygon_clipping::{IntersectRemoveState, remove_intersecting_shapes},
    segment::SegmentCollector,
    segment_soa::SegmentSoa,
};

/// MSDF/MTSDF generator.
///
/// This stores all allocated collections necessary for glyph generation. It can be used to reads glyphs
/// and produce a [`GlyphOutline`], from which you can generate distance fields.
#[derive(Default)]
pub struct MsdfGenerator {
    pub(crate) cached_segment_vec: Vec<Segment>,
    pub(crate) cached_soa: SegmentSoa,
    cached_color: Vec<u8>,
    intersect_remove_state: IntersectRemoveState,
    segment_deviation_threshold: f32,
}

impl MsdfGenerator {
    /// Create a new instance.
    pub fn new() -> Self {
        Self {
            cached_segment_vec: vec![],
            cached_soa: SegmentSoa::new(),
            cached_color: vec![],
            intersect_remove_state: Default::default(),
            segment_deviation_threshold: 0.05,
        }
    }

    /// Sets the maximum deviation of bezier curve, unit is em.
    ///
    /// # Panics
    /// Panics if provided value is negative or zero.
    ///
    /// # Remarks
    /// The deviation is the distance from bezier control points to the line connecting its start
    /// and end points. Bezier curve with smaller deviation is flatter.
    ///
    /// .            p1 (control point)
    /// deviation -> |
    /// .            |
    /// p0 ----------x--------- p2
    ///
    /// Bezier curve is recursively split until each segment is flat enough, i.e its deviation is
    /// smaller than the value provided in this method.
    pub fn set_segment_deviation_threshold(&mut self, em_ratio: f32) {
        assert!(em_ratio > 0.);
        self.segment_deviation_threshold = em_ratio;
    }

    /// Reads outline data of a single glyph.
    ///
    /// # Example
    /// ```rust
    /// # #[cfg(feature = "skrifa")]
    /// # {
    /// use klyff_msdf::{MsdfGenerator, AtlasRegionSize};
    /// use klyff_msdf::SkrifaOutlineProvider;
    /// use klyff_msdf::skrifa::{FontRef, GlyphId};
    ///
    /// fn gen_msdf(font: FontRef<'_>, glyph_id: GlyphId){
    ///     let mut generator = MsdfGenerator::new();
    ///     let outline = match generator.read_glyph(&SkrifaOutlineProvider::new(font, glyph_id)) {
    ///         Ok(outline) => outline,
    ///         Err(error) => todo!("Handle error case"),
    ///     };
    ///
    ///     let size: AtlasRegionSize = todo!("Allocate a region in your texture atlas");
    ///     let bytes = outline.generate_mtsdf(size);
    ///     todo!("Write the bytes to your texture atlas");
    /// }
    /// # }
    /// ```
    ///
    /// # Errors
    /// See [`ReadGlyphOutlineError`]. Notably, this returns an error if a glyph is empty, or if a
    /// glyph can't be rendered with MSDF.
    ///
    /// # Returns
    /// An instance of [`GlyphOutline`], which contains sizing information of a glyph and other
    /// data to compute the final distance field.
    /// Note that to reuse allocations, the instance of [`GlyphOutline`] contains a reference
    /// to this instance, so you must drop it first before using this instance to read another glyph.
    pub fn read_glyph<'a, Provider: OutlineProvider>(
        &'a mut self,
        provider: &Provider,
    ) -> Result<GlyphOutline<'a>, ReadGlyphOutlineError<Provider::ReadFontError>> {
        profiling::scope!("MsdfGenerator::read_glyph");
        self.cached_segment_vec.clear();

        let units_per_em = provider.length_per_em();

        let mut collector = SegmentCollector::new(
            &mut self.cached_segment_vec,
            units_per_em * self.segment_deviation_threshold,
        );
        let bound = provider.collect_outline(&mut collector)?;
        if self.cached_segment_vec.is_empty() {
            return Err(ReadGlyphOutlineError::EmptyGlyph);
        }

        let closest_non_intesecting_dist = remove_intersecting_shapes(
            &mut self.cached_segment_vec,
            &mut self.intersect_remove_state,
        );

        self.cached_soa.populate(&self.cached_segment_vec);

        let bound_size = bound.max - bound.min;
        Ok(GlyphOutline {
            generator: self,
            width: bound_size.x,
            height: bound_size.y,
            units_per_em,
            closest_non_intesecting_dist,
            bound,
        })
    }
}

/// Parsed outline of a glyph.
///
/// The lifetime is bound to the [`MsdfGenerator`] that produced this instance: you can not use the
/// generator again until this instance is dropped.
pub struct GlyphOutline<'a> {
    pub(crate) generator: &'a mut MsdfGenerator,
    pub(crate) units_per_em: f32,
    pub(crate) height: f32,
    pub(crate) width: f32,
    pub(crate) closest_non_intesecting_dist: f32,
    pub(crate) bound: Aabb,
}

/// Region of a texture atlas.
///
/// The total region uploaded to the atlas is `inner_width + padding_x * 2`
/// wide and `inner_height + padding_y * 2` tall. The inner region holds the
/// glyph; the padding around it carries extra signed-distance information.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AtlasRegionSize {
    /// Width of the inner (glyph) region, in pixels.
    pub inner_width: usize,
    /// Height of the inner (glyph) region, in pixels.
    pub inner_height: usize,
    /// Padding around the inner region on the x axis, in pixels.
    pub padding_x: usize,
    /// Padding around the inner region on the y axis, in pixels.
    pub padding_y: usize,
}

impl AtlasRegionSize {
    /// Total width of the region (inner + padding on both sides).
    pub fn total_width(&self) -> usize {
        self.inner_width + self.padding_x * 2
    }

    /// Total height of the region (inner + padding on both sides).
    pub fn total_height(&self) -> usize {
        self.inner_height + self.padding_y * 2
    }
}

impl<'a> GlyphOutline<'a> {
    /// Width of the glyph bounding box, in em.
    pub fn width_em(&self) -> f32 {
        self.width / self.units_per_em
    }

    /// Height of the glyph bounding box, in em.
    pub fn height_em(&self) -> f32 {
        self.height / self.units_per_em
    }

    /// X coordinate of the left edge of the glyph bounding box, in em, relative to the pen origin.
    pub fn min_x_em(&self) -> f32 {
        self.bound.min.x / self.units_per_em
    }

    /// Y coordinate of the bottom edge of the glyph bounding box, in em, relative to the baseline.
    /// Positive is above baseline, negative is below.
    pub fn min_y_em(&self) -> f32 {
        self.bound.min.y / self.units_per_em
    }

    /// Length in font unit corresponding to 1em.
    pub fn font_unit_length_per_em(&self) -> f32 {
        self.units_per_em
    }

    /// Closest distance between two segments of the glyph that don't intersect, in em.
    ///
    /// You may decide to allocate a larger region to a glyph if this distance is small,
    /// since a smaller distance indicates finer details.
    pub fn closest_non_intesecting_dist_em(&self) -> f32 {
        self.closest_non_intesecting_dist / self.units_per_em
    }

    /// Generate MSDF for the glyph.
    ///
    /// # Parameters
    /// - `size`: Specifies size and padding of the atlas region this glyph belongs to.
    /// - `msdf_pixel_type`: Format of each pixel. For uploading to GPU, use
    ///   [`MsdfPixelFormat::Rgba8`].
    ///
    /// # Returns
    /// The raw bytes data in row-major, RGB format (indexed by `(x + y * width) * 3`).
    pub fn generate_msdf(&mut self, size: AtlasRegionSize, pixel_format: MsdfPixelFormat) -> &[u8] {
        profiling::scope!("GlyphOutline::generate_msdf");
        let total_w = size.total_width();
        let total_h = size.total_height();
        self.generator
            .cached_color
            .resize(total_w * total_h * pixel_format.bytes_per_pixel(), 0);
        self.generator.cached_color.fill(0);

        let bound = self.bound;
        let bound_size = self.bound.max - self.bound.min;
        for x in 0..total_w {
            let xt = (x as f32 + 0.5 - size.padding_x as f32) / size.inner_width as f32;
            let xbound = bound.min.x + bound_size.x * xt;
            for y in 0..total_h {
                let yt = (y as f32 + 0.5 - size.padding_y as f32) / size.inner_height as f32;
                let ybound = bound.min.y + bound_size.y * yt;
                let pos = glam::vec2(xbound, ybound);
                let dists = compute_msdf(
                    pos,
                    &self.generator.cached_soa,
                    &self.generator.cached_segment_vec,
                    self.units_per_em,
                );

                let offset = ((total_h - y - 1) * total_w + x) * pixel_format.bytes_per_pixel();
                pixel_format.write(
                    &mut self.generator.cached_color,
                    offset,
                    dists[0],
                    dists[1],
                    dists[2],
                );
            }
        }

        &self.generator.cached_color
    }

    /// Generate MTSDF for the glyph.
    ///
    /// # Parameters
    /// - `size`: Specifies size and padding of the atlas region this glyph belongs to.
    ///
    /// # Returns
    /// The raw bytes data in row-major, RGBA format (indexed by `(x + y * width) * 4`).
    pub fn generate_mtsdf(&mut self, size: AtlasRegionSize) -> &[u8] {
        profiling::scope!("GlyphOutline::generate_mtsdf");
        let total_w = size.total_width();
        let total_h = size.total_height();
        self.generator.cached_color.resize(total_w * total_h * 4, 0);
        self.generator.cached_color.fill(0);

        let bound = self.bound;
        let bound_size = self.bound.max - self.bound.min;
        for x in 0..total_w {
            let xt = (x as f32 + 0.5 - size.padding_x as f32) / size.inner_width as f32;
            let xbound = bound.min.x + bound_size.x * xt;
            for y in 0..total_h {
                let yt = (y as f32 + 0.5 - size.padding_y as f32) / size.inner_height as f32;
                let ybound = bound.min.y + bound_size.y * yt;
                let pos = glam::vec2(xbound, ybound);
                let color = compute_mtsdf(
                    pos,
                    &self.generator.cached_soa,
                    &self.generator.cached_segment_vec,
                    self.units_per_em,
                );

                let offset = ((total_h - y - 1) * total_w + x) * 4;
                self.generator.cached_color[offset] = color[0];
                self.generator.cached_color[offset + 1] = color[1];
                self.generator.cached_color[offset + 2] = color[2];
                self.generator.cached_color[offset + 3] = color[3];
            }
        }

        &self.generator.cached_color
    }
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MsdfPixelFormat {
    /// 32 bits per pixel, RGBA format, with unused alpha channel.
    /// Useful for directly uploading to GPU.
    #[default]
    Rgba8,
    /// 24 bits per pixel, 8 bits for RGB channels.
    /// Useful for outputting to images.
    PackedRgb8,
    // TODO: Consider suppporting other formats like Rgb10a2, Rg11b10 or Rgb9e5.
}

impl MsdfPixelFormat {
    pub fn bytes_per_pixel(&self) -> usize {
        match self {
            MsdfPixelFormat::Rgba8 => 4,
            MsdfPixelFormat::PackedRgb8 => 3,
        }
    }

    pub fn write(&self, bytes: &mut [u8], offset_bytes: usize, vr: f32, vg: f32, vb: f32) {
        match self {
            MsdfPixelFormat::Rgba8 => {
                bytes[offset_bytes] = (vr * 255.) as u8;
                bytes[offset_bytes + 1] = (vg * 255.) as u8;
                bytes[offset_bytes + 2] = (vb * 255.) as u8;
                bytes[offset_bytes + 3] = 0;
            }
            MsdfPixelFormat::PackedRgb8 => {
                bytes[offset_bytes] = (vr * 255.) as u8;
                bytes[offset_bytes + 1] = (vg * 255.) as u8;
                bytes[offset_bytes + 2] = (vb * 255.) as u8;
            }
        }
    }
}