//! `quasi-type` — cut a house face from a pinned base. use std::path::{Path, PathBuf}; use std::process::ExitCode; use quasi_type::base::{self, BaseParams}; use quasi_type::compose::{self, Identity}; use quasi_type::manifest::{Manifest, format_codepoint}; use quasi_type::pins::Pins; use quasi_type::proof; use quasi_type::{Error, HOUSE_SET, PINS}; const USAGE: &str = "\ quasi-type — a pinned base plus the house glyph set in, a Quasi face out quasi-type build cut every face of a slot, verify, write to out/ quasi-type verify cut without writing; assert coverage only quasi-type params print what each base face measures quasi-type list print the house glyph set and the pinned slots quasi-type proof rasterise the cut face to out/, so it can be seen Options --out where faces are written (default: out/) --offline fail rather than fetch a base that is not cached --base measure a pinned base directly, for a base no slot names yet --px proof sheet type size in pixels (default: 24) "; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); match run(&args) { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("quasi-type: {e}"); ExitCode::FAILURE } } } fn run(args: &[String]) -> Result<(), Error> { let mut positional: Vec<&str> = Vec::new(); let mut out = PathBuf::from("out"); let mut offline = false; let mut base_id: Option<&str> = None; let mut px = 24.0_f32; let mut rest = args.iter(); while let Some(arg) = rest.next() { match arg.as_str() { "--offline" => offline = true, "--px" => { px = rest .next() .and_then(|n| n.parse().ok()) .ok_or_else(|| Error::Pins("--px needs a size in pixels".into()))?; } "--base" => { base_id = Some( rest.next() .map(String::as_str) .ok_or_else(|| Error::Pins("--base needs a base id".into()))?, ); } "--out" => { out = rest .next() .map(PathBuf::from) .ok_or_else(|| Error::Pins("--out needs a directory".into()))?; } "-h" | "--help" => { print!("{USAGE}"); return Ok(()); } other => positional.push(other), } } let root = repo_root(); let pins = Pins::parse(PINS)?; let manifest = Manifest::parse(HOUSE_SET)?; match positional.split_first() { None => { print!("{USAGE}"); Ok(()) } Some((&"list", _)) => { println!("{} v{}", manifest.set.name, manifest.set.version); for glyph in &manifest.glyphs { println!( " {} {:<10} {:<13} {}", format_codepoint(glyph.codepoint), glyph.name, glyph.shape.kind(), glyph.role ); } println!("\nslots"); for slot in &pins.slots { let base = pins.base(&slot.base)?; println!( " {:<12} {:<12} from {} {}", slot.id, slot.family, base.family, base.version ); } Ok(()) } Some((&"params", tail)) => { // A base no slot names yet is still measurable, which is how a base // gets read before anything is cut from it. let base_pin = match base_id { Some(id) => pins.base(id)?, None => pins.base( &pins .slot(tail.first().copied().unwrap_or("quasi-mono"))? .base, )?, }; let faces = base::load(base_pin, &base::cache_dir(&root), offline)?; for face in &faces { let name = format!("{} {}", base_pin.family, face.style); match base::variation(&face.bytes)? { None => print_params(&name, &base::measure(&face.bytes)?), Some(axis) => { println!( "{name} variable: {} {}-{}, default {} ({})", axis.tag, axis.min, axis.max, axis.default, axis.default_style ); let mut at = vec![axis.default_master()]; at.extend(axis.delta_masters()); for master in at { let params = base::measure_at(&face.bytes, &axis, master)?; print_params(&format!(" {} {}", axis.tag, master.user), ¶ms); } } } } Ok(()) } Some((&"build", tail)) => { let slot_id = tail.first().copied().unwrap_or("quasi-mono"); cut(&root, &out, slot_id, offline, true) } Some((&"verify", tail)) => { let slot_id = tail.first().copied().unwrap_or("quasi-mono"); cut(&root, &out, slot_id, offline, false) } Some((&"proof", tail)) => { let slot_id = tail.first().copied().unwrap_or("quasi-mono"); proof(&root, &out, &pins, &manifest, slot_id, offline, px) } Some((other, _)) => { print!("{USAGE}"); Err(Error::Pins(format!("no command `{other}`"))) } } } fn cut(root: &Path, out: &Path, slot_id: &str, offline: bool, write: bool) -> Result<(), Error> { let cut = quasi_type::cut(slot_id, &base::cache_dir(root), offline)?; println!( "{} from {} {} ({} marks)", cut.family, cut.base_family, cut.base_version, cut.selected ); if write { std::fs::create_dir_all(out).map_err(|e| Error::Io(out.to_path_buf(), e))?; } for face in &cut.faces { println!( " {:<22} ttf {:>7} woff2 {:>7} +{} marks, {}", format!("{} {}", cut.family, face.style), human(face.ttf.len()), human(face.woff2.len()), face.added.len(), face.verdict, ); if let Some(axis) = &face.variation { println!( " {:<22} {} {}-{}, and the marks vary with it. At rest this face is {} {} \ ({}), so a consumer names the weight it wants: `font-weight: {} {}`.", "", axis.tag, axis.min, axis.max, axis.tag, axis.default, axis.default_style, axis.min, axis.max, ); } if write { write_file(&out.join(format!("{}.ttf", face.stem)), &face.ttf)?; write_file(&out.join(format!("{}.woff2", face.stem)), &face.woff2)?; } } if write { // OFL 1.1 requires the licence to travel with a modified build, and the // gap this closes is live: MNW serves three families with no licence // beside them today. // Named for the family rather than `OFL.txt`, because two slots write // into the same directory and their bases are two different licence // files. goingson already does it this way for Reglo. let license = format!("OFL-{}.txt", cut.family.replace(' ', "")); write_file(&out.join(&license), &cut.license)?; println!(" {license:<22} the base's licence, as OFL requires"); println!("\nwritten to {}", out.display()); } Ok(()) } /// Cut the slot and rasterise it, one sheet per end of the axis. /// /// A variable cut gets a sheet at each master rather than at its default /// alone, because the defects the axis introduces — a mark whose stroke closes /// its own counter at the heavy end — are invisible at rest, which is exactly /// where every other check reads it. fn proof( root: &Path, out: &Path, pins: &Pins, manifest: &Manifest, slot_id: &str, offline: bool, px: f32, ) -> Result<(), Error> { let slot = pins.slot(slot_id)?; let base_pin = pins.base(&slot.base)?; let selected: Vec<&quasi_type::manifest::GlyphSpec> = manifest .glyphs .iter() .filter(|g| slot.glyphs.includes(g)) .collect(); let faces = base::load(base_pin, &base::cache_dir(root), offline)?; let version = format!("{}.{}", manifest.set.version, base_pin.version); std::fs::create_dir_all(out).map_err(|e| Error::Io(out.to_path_buf(), e))?; println!("{} at {}px", slot.family, px); for face in &faces { let id = Identity { family: &slot.family, style: &face.style, version: &version, set_version: manifest.set.version, base: base_pin, }; let built = compose::build(&face.bytes, &selected, &id)?; let coverage = compose::coverage(&built.bytes)?; let rows = proof::house_rows(&coverage); // The axis default is one of its ends often enough to be worth saying // once: Atkinson Mono's is its minimum, so a sheet per master is two // sheets rather than three. let mut locations: Vec> = match &built.variation { Some(axis) => vec![Some(axis.min), Some(axis.default), Some(axis.max)], None => vec![None], }; locations.dedup(); for wght in locations { let page = proof::Page { rows: rows.clone(), px, wght, }; let sheet = proof::render(&built.bytes, &page)?; let name = match wght { Some(w) => format!("proof-{slot_id}-wght{w:.0}.png"), None => format!("proof-{slot_id}-{}.png", face.style.replace(' ', "")), }; let path = out.join(&name); write_file(&path, &proof::png(&sheet))?; println!(" {name:<28} {} x {}", sheet.width, sheet.height); } } println!("\nwritten to {}", out.display()); Ok(()) } fn print_params(name: &str, p: &BaseParams) { println!("{name}"); println!(" upem {}", p.upem); println!(" advance {}", p.advance); println!(" cap height {}", p.cap_height); println!(" x height {}", p.x_height); println!(" stem (|) {}", p.stem); println!(" stroke (-) {}", p.stroke); println!( " band (+) x[{}, {}] y[{}, {}]", p.band_x0, p.band_x1, p.band_y0, p.band_y1 ); println!( " cell {} x {} (asc {}, desc {}, rule at {})", p.advance, p.cell_height(), p.ascent, p.descent, p.cell_center_y() ); } fn write_file(path: &Path, bytes: &[u8]) -> Result<(), Error> { std::fs::write(path, bytes).map_err(|e| Error::Io(path.to_path_buf(), e)) } fn human(bytes: usize) -> String { if bytes >= 1024 { format!("{:.1}K", bytes as f64 / 1024.0) } else { format!("{bytes}B") } } /// The checkout, so `quasi-type` works from any working directory. fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) }