icu_lib 0.11.0

Image Converter Ultra Library
Documentation
use crate::endecoder::{EnDecoder, ImageInfo};
use crate::midata::{FontData, FreeTypeFontData, FreeTypeGlyph, MiData};
use image::RgbaImage;
use mirx::{scene::PathCmd, types::Point};
use serde_json::json;

fn decompress_woff(data: &[u8]) -> Option<Vec<u8>> {
    if data.len() < 4 {
        return None;
    }
    match &data[..4] {
        b"wOF2" | b"wOFF" => None,
        _ => None,
    }
}
use ttf_parser::{name, Face, OutlineBuilder};

pub struct FreeType;

struct OutlineCollector {
    cmds: Vec<PathCmd>,
}

impl OutlineCollector {
    fn new() -> Self {
        Self { cmds: Vec::new() }
    }

    fn map(&self, x: f32, y: f32) -> Point {
        Point::new(
            mirui::types::Fixed::from_f32(x).into(),
            mirui::types::Fixed::from_f32(y).into(),
        )
    }
}

impl OutlineBuilder for OutlineCollector {
    fn move_to(&mut self, x: f32, y: f32) {
        self.cmds.push(PathCmd::MoveTo(self.map(x, y)));
    }
    fn line_to(&mut self, x: f32, y: f32) {
        self.cmds.push(PathCmd::LineTo(self.map(x, y)));
    }
    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
        self.cmds.push(PathCmd::QuadTo {
            ctrl: self.map(x1, y1),
            end: self.map(x, y),
        });
    }
    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
        self.cmds.push(PathCmd::CubicTo {
            ctrl1: self.map(x1, y1),
            ctrl2: self.map(x2, y2),
            end: self.map(x, y),
        });
    }
    fn close(&mut self) {
        self.cmds.push(PathCmd::Close);
    }
}

fn decode_name_bytes(n: &ttf_parser::name::Name) -> Option<String> {
    if n.is_unicode() {
        let mut name: Vec<u16> = Vec::new();
        for c in ttf_parser::LazyArray16::<u16>::new(n.name) {
            name.push(c);
        }
        String::from_utf16(&name).ok()
    } else if n.platform_id == ttf_parser::name::PlatformId::Macintosh && n.encoding_id == 0 {
        let mut s = String::with_capacity(n.name.len());
        for &b in n.name {
            s.push(b as char);
        }
        Some(s)
    } else {
        None
    }
}

fn extract_name(face: &Face, name_id: u16) -> String {
    let mut best: Option<String> = None;
    for n in face.names().into_iter() {
        if n.name_id != name_id {
            continue;
        }
        if let Some(s) = decode_name_bytes(&n) {
            if best.is_none() || n.is_unicode() {
                best = Some(s);
                if n.is_unicode() {
                    break;
                }
            }
        }
    }
    best.unwrap_or_default()
}

fn parse_face(face: &Face) -> FreeTypeFontData {
    let family = extract_name(face, name::name_id::FAMILY);
    let style = extract_name(face, name::name_id::SUBFAMILY);
    let units_per_em = face.units_per_em();
    let ascender = face.ascender();
    let descender = face.descender();
    let line_height = face.height();
    let glyph_count = face.number_of_glyphs() as u32;

    let mut glyphs = Vec::new();
    let mut seen = std::collections::HashSet::new();
    let mut codepoints: Vec<u32> = Vec::new();
    if let Some(cmap) = face.tables().cmap {
        for subtable in cmap.subtables {
            subtable.codepoints(|cp| {
                if seen.insert(cp) {
                    codepoints.push(cp);
                }
            });
        }
    }
    for codepoint in codepoints {
        let ch = match char::from_u32(codepoint) {
            Some(c) => c,
            None => continue,
        };
        let gid = match face.glyph_index(ch) {
            Some(g) => g,
            None => continue,
        };
        let mut collector = OutlineCollector::new();
        let bbox = match face.outline_glyph(gid, &mut collector) {
            Some(b) => b,
            None => continue,
        };
        let advance = face.glyph_hor_advance(gid).unwrap_or(0);
        let side_bearing = face.glyph_hor_side_bearing(gid).unwrap_or(0);
        glyphs.push(FreeTypeGlyph {
            codepoint,
            advance,
            bearing_x: side_bearing,
            bearing_y: 0,
            bbox: (bbox.x_min, bbox.y_min, bbox.x_max, bbox.y_max),
            outline: collector.cmds,
        });
    }

    FreeTypeFontData {
        family,
        style,
        units_per_em,
        ascender,
        descender,
        line_height,
        glyph_count,
        glyphs,
    }
}

impl EnDecoder for FreeType {
    fn can_decode(&self, data: &[u8]) -> bool {
        data.len() >= 4
            && matches!(
                &data[..4],
                [0x00, 0x01, 0x00, 0x00] | b"OTTO" | b"ttcf" | b"wOF2" | b"wOFF"
            )
    }

    fn encode(&self, _data: &MiData, _params: crate::EncoderParams) -> Vec<u8> {
        Vec::new()
    }

    fn decode(&self, data: Vec<u8>) -> MiData {
        let decoded = decompress_woff(&data).unwrap_or(data);
        let face = match Face::parse(&decoded, 0) {
            Ok(f) => f,
            Err(_) => return MiData::RGBA(RgbaImage::new(0, 0)),
        };
        let font_data = parse_face(&face);
        MiData::FONT(FontData::FreeType(font_data))
    }

    fn info(&self, data: &[u8]) -> ImageInfo {
        let decoded = decompress_woff(data).unwrap_or_else(|| data.to_vec());
        let face = match Face::parse(&decoded, 0) {
            Ok(f) => f,
            Err(_) => {
                return ImageInfo {
                    width: 0,
                    height: 0,
                    data_size: data.len() as u32,
                    format: "unknown".to_string(),
                    other_info: json!({}),
                }
            }
        };
        let family = extract_name(&face, name::name_id::FAMILY);
        let style = extract_name(&face, name::name_id::SUBFAMILY);
        ImageInfo {
            width: 0,
            height: 0,
            data_size: data.len() as u32,
            format: "ttf".to_string(),
            other_info: json!({
                "layout": "freetype",
                "family": family,
                "style": style,
                "units_per_em": face.units_per_em(),
                "ascender": face.ascender(),
                "descender": face.descender(),
                "line_height": face.height(),
                "glyph_count": face.number_of_glyphs(),
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    static DEJA_FONTS_DIR: &str = "/System/Library/Fonts";

    fn load_test_ttf() -> Option<Vec<u8>> {
        for name in ["DejaVuSans.ttf", "Helvetica.ttc", "Arial.ttf"] {
            let path = format!("{}/{}", DEJA_FONTS_DIR, name);
            if let Ok(data) = std::fs::read(&path) {
                if FreeType.can_decode(&data) {
                    return Some(data);
                }
            }
        }
        let candidates = [
            "/System/Library/Fonts/Supplemental/Arial.ttf",
            "/Library/Fonts/Arial.ttf",
        ];
        for path in candidates {
            if let Ok(data) = std::fs::read(path) {
                if FreeType.can_decode(&data) {
                    return Some(data);
                }
            }
        }
        None
    }

    #[test]
    fn can_decode_ttf_magic() {
        assert!(FreeType.can_decode(&[0x00, 0x01, 0x00, 0x00, 0xff]));
        assert!(FreeType.can_decode(b"OTTOextra"));
        assert!(FreeType.can_decode(b"ttcfextra"));
        assert!(!FreeType.can_decode(b"PNG\r\n"));
        assert!(!FreeType.can_decode(b"MIRX"));
    }

    #[test]
    fn decode_ttf_returns_freetype_font() {
        let data = match load_test_ttf() {
            Some(d) => d,
            None => {
                eprintln!("skip: no test TTF found");
                return;
            }
        };
        let ed = FreeType;
        let midata = ed.decode(data.clone());
        match midata {
            MiData::FONT(FontData::FreeType(f)) => {
                assert!(!f.family.is_empty(), "family should not be empty");
                assert!(f.glyph_count > 0, "should have glyphs");
                assert!(!f.glyphs.is_empty(), "should have parsed glyphs");
                let first = &f.glyphs[0];
                assert!(!first.outline.is_empty(), "glyph should have outline");
            }
            other => panic!("expected FONT FreeType, got {}", other.variant_name()),
        }
    }

    #[test]
    fn info_reports_family_and_glyph_count() {
        let data = match load_test_ttf() {
            Some(d) => d,
            None => {
                eprintln!("skip: no test TTF found");
                return;
            }
        };
        let ed = FreeType;
        let info = ed.info(&data);
        let other = &info.other_info;
        assert_eq!(
            other.get("layout").and_then(|v| v.as_str()),
            Some("freetype")
        );
        let gc = other.get("glyph_count").and_then(|v| v.as_u64());
        assert!(gc.unwrap_or(0) > 0);
    }
}