rustybuzz 0.17.0

A complete harfbuzz shaping algorithm port to Rust.
Documentation
use std::path::PathBuf;
use std::str::FromStr;

const HELP: &str = "\
USAGE:
    shape [OPTIONS] <FONT-FILE> [TEXT]

OPTIONS:
    -h, --help                          Show help options
        --version                       Show version number
        --font-file PATH                Set font file-name
        --face-index INDEX              Set face index [default: 0]
        --font-ptem NUMBER              Set font point-size
        --variations LIST               Set comma-separated list of font variations
        --text TEXT                     Set input text
        --text-file PATH                Set input text file
    -u, --unicodes LIST                 Set comma-separated list of input Unicode codepoints
                                        Examples: 'U+0056,U+0057'
        --direction DIRECTION           Set text direction
                                        [possible values: ltr, rtl, ttb, btt]
        --language LANG                 Set text language [default: LC_CTYPE]
        --script TAG                    Set text script as ISO-15924 tag
        --utf8-clusters                 Use UTF-8 byte indices, not char indices
        --cluster-level N               Cluster merging level [default: 0]
                                        [possible values: 0, 1, 2]
        --features LIST                 Set comma-separated list of font features
        --no-glyph-names                Output glyph indices instead of names
        --no-positions                  Do not output glyph positions
        --no-advances                   Do not output glyph advances
        --no-clusters                   Do not output cluster indices
        --show-extents                  Output glyph extents
        --show-flags                    Output glyph flags
        --single-par                    Treat the input string as a single paragraph
        --ned                           No Extra Data; Do not output clusters or advances

ARGS:
    <FONT-FILE>                         A font file
    [TEXT]                              An optional text
";

struct Args {
    help: bool,
    version: bool,
    font_file: Option<PathBuf>,
    face_index: u32,
    font_ptem: Option<f32>,
    variations: Vec<rustybuzz::Variation>,
    text: Option<String>,
    text_file: Option<PathBuf>,
    unicodes: Option<String>,
    direction: Option<rustybuzz::Direction>,
    language: rustybuzz::Language,
    script: Option<rustybuzz::Script>,
    utf8_clusters: bool,
    cluster_level: rustybuzz::BufferClusterLevel,
    features: Vec<rustybuzz::Feature>,
    no_glyph_names: bool,
    no_positions: bool,
    no_advances: bool,
    no_clusters: bool,
    show_extents: bool,
    show_flags: bool,
    single_par: bool,
    ned: bool,
    free: Vec<String>,
}

fn parse_args() -> Result<Args, pico_args::Error> {
    let mut args = pico_args::Arguments::from_env();
    let args = Args {
        help: args.contains(["-h", "--help"]),
        version: args.contains("--version"),
        font_file: args.opt_value_from_str("--font-file")?,
        face_index: args.opt_value_from_str("--face-index")?.unwrap_or(0),
        font_ptem: args.opt_value_from_str("--font-ptem")?,
        variations: args
            .opt_value_from_fn("--variations", parse_variations)?
            .unwrap_or_default(),
        text: args.opt_value_from_str("--text")?,
        text_file: args.opt_value_from_str("--text-file")?,
        unicodes: args.opt_value_from_fn(["-u", "--unicodes"], parse_unicodes)?,
        direction: args.opt_value_from_str("--direction")?,
        language: args
            .opt_value_from_str("--language")?
            .unwrap_or(system_language()),
        script: args.opt_value_from_str("--script")?,
        utf8_clusters: args.contains("--utf8-clusters"),
        cluster_level: args
            .opt_value_from_fn("--cluster-level", parse_cluster)?
            .unwrap_or_default(),
        features: args
            .opt_value_from_fn("--features", parse_features)?
            .unwrap_or_default(),
        no_glyph_names: args.contains("--no-glyph-names"),
        no_positions: args.contains("--no-positions"),
        no_advances: args.contains("--no-advances"),
        no_clusters: args.contains("--no-clusters"),
        show_extents: args.contains("--show-extents"),
        show_flags: args.contains("--show-flags"),
        single_par: args.contains("--single-par"),
        ned: args.contains("--ned"),
        free: args
            .finish()
            .iter()
            .map(|s| s.to_string_lossy().to_string())
            .collect(),
    };

    Ok(args)
}

fn main() {
    let args = match parse_args() {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error: {}.", e);
            std::process::exit(1);
        }
    };

    if args.version {
        println!("{}", env!("CARGO_PKG_VERSION"));
        return;
    }

    if args.help {
        print!("{}", HELP);
        return;
    }

    let mut font_set_as_free_arg = false;
    let font_path = if let Some(path) = args.font_file {
        path.clone()
    } else if !args.free.is_empty() {
        font_set_as_free_arg = true;
        PathBuf::from(&args.free[0])
    } else {
        eprintln!("Error: font is not set.");
        std::process::exit(1);
    };

    if !font_path.exists() {
        eprintln!("Error: '{}' does not exist.", font_path.display());
        std::process::exit(1);
    }

    let font_data = std::fs::read(font_path).unwrap();
    let mut face = rustybuzz::Face::from_slice(&font_data, args.face_index).unwrap();

    face.set_points_per_em(args.font_ptem);

    if !args.variations.is_empty() {
        face.set_variations(&args.variations);
    }

    let text = if let Some(path) = args.text_file {
        std::fs::read_to_string(path).unwrap()
    } else if args.free.len() == 2 && font_set_as_free_arg {
        args.free[1].clone()
    } else if args.free.len() == 1 && !font_set_as_free_arg {
        args.free[0].clone()
    } else if let Some(ref text) = args.unicodes {
        text.clone()
    } else if let Some(ref text) = args.text {
        text.clone()
    } else {
        eprintln!("Error: text is not set.");
        std::process::exit(1);
    };

    let lines = if args.single_par {
        vec![text.as_str()]
    } else {
        text.split("\n").filter(|s| !s.is_empty()).collect()
    };

    for text in lines {
        let mut buffer = rustybuzz::UnicodeBuffer::new();
        buffer.push_str(&text);

        if let Some(d) = args.direction {
            buffer.set_direction(d);
        }

        buffer.set_language(args.language.clone());

        if let Some(script) = args.script {
            buffer.set_script(script);
        }

        buffer.set_cluster_level(args.cluster_level);

        if !args.utf8_clusters {
            buffer.reset_clusters();
        }

        let glyph_buffer = rustybuzz::shape(&face, &args.features, buffer);

        let mut format_flags = rustybuzz::SerializeFlags::default();
        if args.no_glyph_names {
            format_flags |= rustybuzz::SerializeFlags::NO_GLYPH_NAMES;
        }

        if args.no_clusters || args.ned {
            format_flags |= rustybuzz::SerializeFlags::NO_CLUSTERS;
        }

        if args.no_positions {
            format_flags |= rustybuzz::SerializeFlags::NO_POSITIONS;
        }

        if args.no_advances || args.ned {
            format_flags |= rustybuzz::SerializeFlags::NO_ADVANCES;
        }

        if args.show_extents {
            format_flags |= rustybuzz::SerializeFlags::GLYPH_EXTENTS;
        }

        if args.show_flags {
            format_flags |= rustybuzz::SerializeFlags::GLYPH_FLAGS;
        }

        println!("{}", glyph_buffer.serialize(&face, format_flags));
    }
}

fn parse_unicodes(s: &str) -> Result<String, String> {
    let mut text = String::new();
    for u in s.split(',') {
        let u = u32::from_str_radix(&u[2..], 16)
            .map_err(|_| format!("'{}' is not a valid codepoint", u))?;

        let c = char::try_from(u).map_err(|_| format!("{} is not a valid codepoint", u))?;

        text.push(c);
    }

    Ok(text)
}

fn parse_features(s: &str) -> Result<Vec<rustybuzz::Feature>, String> {
    let mut features = Vec::new();
    for f in s.split(',') {
        features.push(rustybuzz::Feature::from_str(&f)?);
    }

    Ok(features)
}

fn parse_variations(s: &str) -> Result<Vec<rustybuzz::Variation>, String> {
    let mut variations = Vec::new();
    for v in s.split(',') {
        variations.push(rustybuzz::Variation::from_str(&v)?);
    }

    Ok(variations)
}

fn parse_cluster(s: &str) -> Result<rustybuzz::BufferClusterLevel, String> {
    match s {
        "0" => Ok(rustybuzz::BufferClusterLevel::MonotoneGraphemes),
        "1" => Ok(rustybuzz::BufferClusterLevel::MonotoneCharacters),
        "2" => Ok(rustybuzz::BufferClusterLevel::Characters),
        _ => Err(format!("invalid cluster level")),
    }
}

fn system_language() -> rustybuzz::Language {
    unsafe {
        libc::setlocale(libc::LC_ALL, b"\0" as *const _ as *const i8);
        let s = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
        let s = std::ffi::CStr::from_ptr(s);
        let s = s.to_str().expect("locale must be ASCII");
        rustybuzz::Language::from_str(s).unwrap()
    }
}