hpx-browser 2.5.17

Headless browser engine for hpx: HTML parsing, rendering, CDP, and canvas support
Documentation
use std::{
    collections::HashMap,
    sync::{Mutex, OnceLock},
};

use parley::{
    Alignment, AlignmentOptions, FontContext, LayoutContext,
    fontique::Blob,
    layout::PositionedLayoutItem,
    style::{FontFamily, StyleProperty},
};

#[derive(Debug, Clone)]
pub struct Glyph {
    pub glyph_id: u32,
    pub cluster: u32,
    pub x_advance: f32,
    pub y_advance: f32,
    pub x_offset: f32,
    pub y_offset: f32,
}

#[derive(Debug, Clone)]
pub struct ShapedRun {
    pub glyphs: Vec<Glyph>,
    pub width: f32,
    pub ascent: f32,
    pub descent: f32,
    pub bbox_left: f32,
    pub bbox_right: f32,
    pub bbox_ascent: f32,
    pub bbox_descent: f32,
}

impl ShapedRun {
    pub fn empty() -> Self {
        Self {
            glyphs: Vec::new(),
            width: 0.0,
            ascent: 0.0,
            descent: 0.0,
            bbox_left: 0.0,
            bbox_right: 0.0,
            bbox_ascent: 0.0,
            bbox_descent: 0.0,
        }
    }
}

struct ParleyState {
    font_cx: Mutex<FontContext>,
    layout_cx: Mutex<LayoutContext>,
    family_cache: Mutex<HashMap<usize, String>>,
}

static PARLEY: OnceLock<ParleyState> = OnceLock::new();

fn parley_state() -> &'static ParleyState {
    PARLEY.get_or_init(|| ParleyState {
        font_cx: Mutex::new(FontContext::new()),
        layout_cx: Mutex::new(LayoutContext::new()),
        family_cache: Mutex::new(HashMap::new()),
    })
}

fn ensure_font_registered(state: &ParleyState, face_data: &[u8]) -> String {
    let key = face_data.as_ptr() as usize;
    {
        let cache = state.family_cache.lock().unwrap();
        if let Some(name) = cache.get(&key) {
            return name.clone();
        }
    }
    let mut font_cx = state.font_cx.lock().unwrap();
    let blob = Blob::from(face_data.to_vec());
    let results = font_cx.collection.register_fonts(blob, None);
    let name = results
        .first()
        .and_then(|(fid, _)| font_cx.collection.family_name(*fid))
        .unwrap_or("sans-serif")
        .to_string();
    drop(font_cx);
    let mut cache = state.family_cache.lock().unwrap();
    cache.insert(key, name.clone());
    name
}

pub fn shape(text: &str, face_data: &[u8], _face_index: u32, size_px: f32) -> ShapedRun {
    if text.is_empty() {
        return ShapedRun::empty();
    }

    let state = parley_state();
    let family_name = ensure_font_registered(state, face_data);

    let mut font_cx = state.font_cx.lock().unwrap();
    let mut layout_cx = state.layout_cx.lock().unwrap();

    let mut builder = layout_cx.ranged_builder(&mut font_cx, text, 1.0, true);
    builder.push_default(StyleProperty::FontFamily(FontFamily::named(&family_name)));
    builder.push_default(StyleProperty::FontSize(size_px));

    let mut layout = builder.build(text);
    layout.break_all_lines(Some(f32::INFINITY));
    layout.align(Alignment::Start, AlignmentOptions::default());

    let mut glyphs = Vec::new();
    let mut cursor_x = 0.0_f32;
    let mut bbox_left = f32::INFINITY;
    let mut bbox_right = f32::NEG_INFINITY;
    let mut bbox_ascent = f32::NEG_INFINITY;
    let mut bbox_descent = f32::NEG_INFINITY;
    let mut ascent = 0.0_f32;
    let mut descent = 0.0_f32;

    for line in layout.lines() {
        let line_metrics = line.metrics();
        if line_metrics.ascent > ascent {
            ascent = line_metrics.ascent;
        }
        if line_metrics.descent > descent {
            descent = line_metrics.descent;
        }

        for item in line.items() {
            if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
                for g in glyph_run.positioned_glyphs() {
                    let x_advance = g.advance;
                    let x_offset = g.x - cursor_x;
                    let y_offset = g.y;

                    let glyph_left = cursor_x + x_offset;
                    let glyph_right = glyph_left + x_advance;
                    if glyph_left < bbox_left {
                        bbox_left = glyph_left;
                    }
                    if glyph_right > bbox_right {
                        bbox_right = glyph_right;
                    }
                    if -y_offset > bbox_ascent {
                        bbox_ascent = -y_offset;
                    }
                    if y_offset > bbox_descent {
                        bbox_descent = y_offset;
                    }

                    glyphs.push(Glyph {
                        glyph_id: g.id,
                        cluster: 0,
                        x_advance,
                        y_advance: 0.0,
                        x_offset,
                        y_offset,
                    });
                    cursor_x += x_advance;
                }
            }
        }
    }

    let width = cursor_x;

    if glyphs.is_empty() {
        bbox_left = 0.0;
        bbox_right = 0.0;
        bbox_ascent = 0.0;
        bbox_descent = 0.0;
    }

    ShapedRun {
        glyphs,
        width,
        ascent,
        descent,
        bbox_left,
        bbox_right,
        bbox_ascent,
        bbox_descent,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::canvas::text::font_database::FontDatabase;

    fn arial_face() -> (&'static [u8], u32) {
        let db = FontDatabase::get();
        let id = db.query("Arial", 400, false, "Linux").expect("Arial");
        db.face_data(id).expect("face data")
    }

    #[test]
    fn empty_text_returns_empty_run() {
        let (data, idx) = arial_face();
        let run = shape("", data, idx, 14.0);
        assert!(run.glyphs.is_empty());
        assert_eq!(run.width, 0.0);
    }

    #[test]
    fn hello_world_has_nonzero_width() {
        let (data, idx) = arial_face();
        let run = shape("Hello, World!", data, idx, 14.0);
        assert!(!run.glyphs.is_empty());
        assert!(run.width > 0.0);
    }

    #[test]
    fn size_scales_width_linearly() {
        let (data, idx) = arial_face();
        let short = shape("Hello", data, idx, 14.0);
        let tall = shape("Hello", data, idx, 28.0);
        let ratio = tall.width / short.width;
        assert!((ratio - 2.0).abs() < 0.05);
    }
}