use std::path::PathBuf; use std::str::FromStr; const HELP: &str = "\ USAGE: shape [OPTIONS] [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: A font file [TEXT] An optional text "; struct Args { help: bool, version: bool, font_file: Option, face_index: u32, font_ptem: Option, variations: Vec, text: Option, text_file: Option, unicodes: Option, direction: Option, language: rustybuzz::Language, script: Option, utf8_clusters: bool, cluster_level: rustybuzz::BufferClusterLevel, features: Vec, 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, } fn parse_args() -> Result { 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 { 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, 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, 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 { 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() } }