use base64::engine::general_purpose::STANDARD; use std::io::Write; use std::path::PathBuf; use ttf_parser as ttf; use ttf_parser::colr::{ClipBox, Paint}; use ttf_parser::{RgbaColor, Transform}; const FONT_SIZE: f64 = 128.0; const COLUMNS: u32 = 100; const HELP: &str = "\ Usage: font2svg font.ttf out.svg font2svg --variations 'wght:500;wdth:200' font.ttf out.svg font2svg --colr-palette 1 colr-font.ttf out.svg "; struct Args { #[allow(dead_code)] variations: Vec, colr_palette: u16, ttf_path: PathBuf, svg_path: PathBuf, } fn main() { let args = match parse_args() { Ok(v) => v, Err(e) => { eprintln!("Error: {}.", e); print!("{}", HELP); std::process::exit(1); } }; if let Err(e) = process(args) { eprintln!("Error: {}.", e); std::process::exit(1); } } fn parse_args() -> Result> { let mut args = pico_args::Arguments::from_env(); if args.contains(["-h", "--help"]) { print!("{}", HELP); std::process::exit(0); } let variations = args.opt_value_from_fn("--variations", parse_variations)?; let colr_palette: u16 = args.opt_value_from_str("--colr-palette")?.unwrap_or(0); let free = args.finish(); if free.len() != 2 { return Err("invalid number of arguments".into()); } Ok(Args { variations: variations.unwrap_or_default(), colr_palette, ttf_path: PathBuf::from(&free[0]), svg_path: PathBuf::from(&free[1]), }) } fn parse_variations(s: &str) -> Result, &'static str> { let mut variations = Vec::new(); for part in s.split(';') { let mut iter = part.split(':'); let axis = iter.next().ok_or("failed to parse a variation")?; let axis = ttf::Tag::from_bytes_lossy(axis.as_bytes()); let value = iter.next().ok_or("failed to parse a variation")?; let value: f32 = value.parse().map_err(|_| "failed to parse a variation")?; variations.push(ttf::Variation { axis, value }); } Ok(variations) } fn process(args: Args) -> Result<(), Box> { let font_data = std::fs::read(&args.ttf_path)?; // Exclude IO operations. let now = std::time::Instant::now(); #[allow(unused_mut)] let mut face = ttf::Face::parse(&font_data, 0)?; if face.is_variable() { #[cfg(feature = "variable-fonts")] { for variation in args.variations { face.set_variation(variation.axis, variation.value) .ok_or("failed to create variation coordinates")?; } } } if face.tables().colr.is_some() { if let Some(total) = face.color_palettes() { if args.colr_palette >= total.get() { return Err(format!("only {} palettes are available", total).into()); } } } let num_glyphs = face.number_of_glyphs(); let units_per_em = face.units_per_em(); let scale = FONT_SIZE / units_per_em as f64; let cell_size = face.height() as f64 * FONT_SIZE / units_per_em as f64; let rows = (num_glyphs as f64 / COLUMNS as f64).ceil() as u32; let mut svg = xmlwriter::XmlWriter::with_capacity( num_glyphs as usize * 512, xmlwriter::Options::default(), ); svg.start_element("svg"); svg.write_attribute("xmlns", "http://www.w3.org/2000/svg"); svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); svg.write_attribute_fmt( "viewBox", format_args!( "{} {} {} {}", 0, 0, cell_size * COLUMNS as f64, cell_size * rows as f64 ), ); draw_grid(num_glyphs, cell_size, &mut svg); let mut path_buf = String::with_capacity(256); let mut row = 0; let mut column = 0; let mut gradient_index = 1; let mut clip_path_index = 1; for id in 0..num_glyphs { let gid = ttf::GlyphId(id); let x = column as f64 * cell_size; let y = row as f64 * cell_size; svg.start_element("text"); svg.write_attribute("x", &(x + 2.0)); svg.write_attribute("y", &(y + cell_size - 4.0)); svg.write_attribute("font-size", "36"); svg.write_attribute("fill", "gray"); svg.write_text_fmt(format_args!("{}", &id)); svg.end_element(); if face.is_color_glyph(gid) { color_glyph( x, y, &face, args.colr_palette, gid, cell_size, scale, &mut gradient_index, &mut clip_path_index, &mut svg, &mut path_buf, ); } else if let Some(img) = face.glyph_raster_image(gid, std::u16::MAX) { svg.start_element("image"); svg.write_attribute("x", &(x + 2.0 + img.x as f64)); svg.write_attribute("y", &(y - img.y as f64)); svg.write_attribute("width", &img.width); svg.write_attribute("height", &img.height); svg.write_attribute_raw("xlink:href", |buf| { buf.extend_from_slice(b"data:image/png;base64, "); let mut enc = base64::write::EncoderWriter::new(buf, &STANDARD); enc.write_all(img.data).unwrap(); enc.finish().unwrap(); }); svg.end_element(); } else if let Some(img) = face.glyph_svg_image(gid) { svg.start_element("image"); svg.write_attribute("x", &(x + 2.0)); svg.write_attribute("y", &(y + cell_size)); svg.write_attribute("width", &cell_size); svg.write_attribute("height", &cell_size); svg.write_attribute_raw("xlink:href", |buf| { buf.extend_from_slice(b"data:image/svg+xml;base64, "); let mut enc = base64::write::EncoderWriter::new(buf, &STANDARD); enc.write_all(img.data).unwrap(); enc.finish().unwrap(); }); svg.end_element(); } else { glyph_to_path(x, y, &face, gid, cell_size, scale, &mut svg, &mut path_buf); } column += 1; if column == COLUMNS { column = 0; row += 1; } } println!("Elapsed: {}ms", now.elapsed().as_micros() as f64 / 1000.0); std::fs::write(&args.svg_path, &svg.end_document())?; Ok(()) } fn draw_grid(n_glyphs: u16, cell_size: f64, svg: &mut xmlwriter::XmlWriter) { let columns = COLUMNS; let rows = (n_glyphs as f64 / columns as f64).ceil() as u32; let width = columns as f64 * cell_size; let height = rows as f64 * cell_size; svg.start_element("path"); svg.write_attribute("fill", "none"); svg.write_attribute("stroke", "black"); svg.write_attribute("stroke-width", "5"); let mut path = String::with_capacity(256); use std::fmt::Write; let mut x = 0.0; for _ in 0..=columns { write!(&mut path, "M {} {} L {} {} ", x, 0.0, x, height).unwrap(); x += cell_size; } let mut y = 0.0; for _ in 0..=rows { write!(&mut path, "M {} {} L {} {} ", 0.0, y, width, y).unwrap(); y += cell_size; } path.pop(); svg.write_attribute("d", &path); svg.end_element(); } struct Builder<'a>(&'a mut String); impl Builder<'_> { fn finish(&mut self) { if !self.0.is_empty() { self.0.pop(); // remove trailing space } } } impl ttf::OutlineBuilder for Builder<'_> { fn move_to(&mut self, x: f32, y: f32) { use std::fmt::Write; write!(self.0, "M {} {} ", x, y).unwrap() } fn line_to(&mut self, x: f32, y: f32) { use std::fmt::Write; write!(self.0, "L {} {} ", x, y).unwrap() } fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { use std::fmt::Write; write!(self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap() } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { use std::fmt::Write; write!(self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap() } fn close(&mut self) { self.0.push_str("Z ") } } fn glyph_to_path( x: f64, y: f64, face: &ttf::Face, glyph_id: ttf::GlyphId, cell_size: f64, scale: f64, svg: &mut xmlwriter::XmlWriter, path_buf: &mut String, ) { path_buf.clear(); let mut builder = Builder(path_buf); let bbox = match face.outline_glyph(glyph_id, &mut builder) { Some(v) => v, None => return, }; builder.finish(); let bbox_w = (bbox.x_max as f64 - bbox.x_min as f64) * scale; let dx = (cell_size - bbox_w) / 2.0; let y = y + cell_size + face.descender() as f64 * scale; let transform = format!("matrix({} 0 0 {} {} {})", scale, -scale, x + dx, y); svg.start_element("path"); svg.write_attribute("d", path_buf); svg.write_attribute("transform", &transform); svg.end_element(); { let bbox_h = (bbox.y_max as f64 - bbox.y_min as f64) * scale; let bbox_x = x + dx + bbox.x_min as f64 * scale; let bbox_y = y - bbox.y_max as f64 * scale; svg.start_element("rect"); svg.write_attribute("x", &bbox_x); svg.write_attribute("y", &bbox_y); svg.write_attribute("width", &bbox_w); svg.write_attribute("height", &bbox_h); svg.write_attribute("fill", "none"); svg.write_attribute("stroke", "green"); svg.end_element(); } } // NOTE: this is not a feature-full implementation and just a demo. struct GlyphPainter<'a> { face: &'a ttf::Face<'a>, svg: &'a mut xmlwriter::XmlWriter, path_buf: &'a mut String, gradient_index: usize, clip_path_index: usize, palette_index: u16, transform: ttf::Transform, outline_transform: ttf::Transform, transforms_stack: Vec, } impl<'a> GlyphPainter<'a> { fn write_gradient_stops(&mut self, stops: ttf::colr::GradientStopsIter) { for stop in stops { self.svg.start_element("stop"); self.svg.write_attribute("offset", &stop.stop_offset); self.svg.write_color_attribute("stop-color", stop.color); let opacity = f32::from(stop.color.alpha) / 255.0; self.svg.write_attribute("stop-opacity", &opacity); self.svg.end_element(); } } fn paint_solid(&mut self, color: ttf::RgbaColor) { self.svg.start_element("path"); self.svg.write_color_attribute("fill", color); let opacity = f32::from(color.alpha) / 255.0; self.svg.write_attribute("fill-opacity", &opacity); self.svg .write_transform_attribute("transform", self.outline_transform); self.svg.write_attribute("d", self.path_buf); self.svg.end_element(); } fn paint_linear_gradient(&mut self, gradient: ttf::colr::LinearGradient<'a>) { let gradient_id = format!("lg{}", self.gradient_index); self.gradient_index += 1; let gradient_transform = paint_transform(self.outline_transform, self.transform); // TODO: We ignore x2, y2. Have to apply them somehow. // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf // we will see the actual spreadMode. We need to account for that somehow. self.svg.start_element("linearGradient"); self.svg.write_attribute("id", &gradient_id); self.svg.write_attribute("x1", &gradient.x0); self.svg.write_attribute("y1", &gradient.y0); self.svg.write_attribute("x2", &gradient.x1); self.svg.write_attribute("y2", &gradient.y1); self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); self.svg.write_spread_method_attribute(gradient.extend); self.svg .write_transform_attribute("gradientTransform", gradient_transform); self.write_gradient_stops(gradient.stops( self.palette_index, #[cfg(feature = "variable-fonts")] self.face.variation_coordinates(), )); self.svg.end_element(); self.svg.start_element("path"); self.svg .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); self.svg .write_transform_attribute("transform", self.outline_transform); self.svg.write_attribute("d", self.path_buf); self.svg.end_element(); } fn paint_radial_gradient(&mut self, gradient: ttf::colr::RadialGradient<'a>) { let gradient_id = format!("rg{}", self.gradient_index); self.gradient_index += 1; self.svg.start_element("radialGradient"); self.svg.write_attribute("id", &gradient_id); self.svg.write_attribute("cx", &gradient.x1); self.svg.write_attribute("cy", &gradient.y1); self.svg.write_attribute("r", &gradient.r1); self.svg.write_attribute("fr", &gradient.r0); self.svg.write_attribute("fx", &gradient.x0); self.svg.write_attribute("fy", &gradient.y0); self.svg.write_attribute("gradientUnits", &"userSpaceOnUse"); self.svg.write_spread_method_attribute(gradient.extend); self.svg .write_transform_attribute("gradientTransform", self.transform); self.write_gradient_stops(gradient.stops( self.palette_index, #[cfg(feature = "variable-fonts")] self.face.variation_coordinates(), )); self.svg.end_element(); self.svg.start_element("path"); self.svg .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id)); self.svg .write_transform_attribute("transform", self.outline_transform); self.svg.write_attribute("d", self.path_buf); self.svg.end_element(); } fn paint_sweep_gradient(&mut self, _: ttf::colr::SweepGradient<'a>) { println!("Warning: sweep gradients are not supported.") } } fn paint_transform(outline_transform: Transform, transform: Transform) -> Transform { let outline_transform = tiny_skia_path::Transform::from_row( outline_transform.a, outline_transform.b, outline_transform.c, outline_transform.d, outline_transform.e, outline_transform.f, ); let gradient_transform = tiny_skia_path::Transform::from_row( transform.a, transform.b, transform.c, transform.d, transform.e, transform.f, ); let gradient_transform = outline_transform .invert() .unwrap() .pre_concat(gradient_transform); ttf_parser::Transform { a: gradient_transform.sx, b: gradient_transform.ky, c: gradient_transform.kx, d: gradient_transform.sy, e: gradient_transform.tx, f: gradient_transform.ty, } } impl GlyphPainter<'_> { fn clip_with_path(&mut self, path: &str) { let clip_id = format!("cp{}", self.clip_path_index); self.clip_path_index += 1; self.svg.start_element("clipPath"); self.svg.write_attribute("id", &clip_id); self.svg.start_element("path"); self.svg .write_transform_attribute("transform", self.outline_transform); self.svg.write_attribute("d", &path); self.svg.end_element(); self.svg.end_element(); self.svg.start_element("g"); self.svg .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id)); } } impl<'a> ttf::colr::Painter<'a> for GlyphPainter<'a> { fn outline_glyph(&mut self, glyph_id: ttf::GlyphId) { self.path_buf.clear(); let mut builder = Builder(self.path_buf); match self.face.outline_glyph(glyph_id, &mut builder) { Some(v) => v, None => return, }; builder.finish(); // We have to write outline using the current transform. self.outline_transform = self.transform; } fn push_layer(&mut self, mode: ttf::colr::CompositeMode) { self.svg.start_element("g"); use ttf::colr::CompositeMode; // TODO: Need to figure out how to represent the other blend modes // in SVG. let mode = match mode { CompositeMode::SourceOver => "normal", CompositeMode::Screen => "screen", CompositeMode::Overlay => "overlay", CompositeMode::Darken => "darken", CompositeMode::Lighten => "lighten", CompositeMode::ColorDodge => "color-dodge", CompositeMode::ColorBurn => "color-burn", CompositeMode::HardLight => "hard-light", CompositeMode::SoftLight => "soft-light", CompositeMode::Difference => "difference", CompositeMode::Exclusion => "exclusion", CompositeMode::Multiply => "multiply", CompositeMode::Hue => "hue", CompositeMode::Saturation => "saturation", CompositeMode::Color => "color", CompositeMode::Luminosity => "luminosity", _ => { println!("Warning: unsupported blend mode: {:?}", mode); "normal" } }; self.svg.write_attribute_fmt( "style", format_args!("mix-blend-mode: {}; isolation: isolate", mode), ); } fn pop_layer(&mut self) { self.svg.end_element(); // g } fn push_translate(&mut self, tx: f32, ty: f32) { self.push_transform(ttf::Transform::new(1.0, 0.0, 0.0, 1.0, tx, ty)); } fn push_scale(&mut self, sx: f32, sy: f32) { self.push_transform(ttf::Transform::new(sx, 0.0, 0.0, sy, 0.0, 0.0)); } fn push_rotate(&mut self, angle: f32) { let cc = (angle * std::f32::consts::PI).cos(); let ss = (angle * std::f32::consts::PI).sin(); self.push_transform(ttf::Transform::new(cc, ss, -ss, cc, 0.0, 0.0)); } fn push_skew(&mut self, skew_x: f32, skew_y: f32) { let x = (-skew_x * std::f32::consts::PI).tan(); let y = (skew_y * std::f32::consts::PI).tan(); self.push_transform(ttf::Transform::new(1.0, y, x, 1.0, 0.0, 0.0)); } fn push_transform(&mut self, transform: ttf::Transform) { self.transforms_stack.push(self.transform); self.transform = ttf::Transform::combine(self.transform, transform); } fn paint(&mut self, paint: Paint<'a>) { match paint { Paint::Solid(color) => self.paint_solid(color), Paint::LinearGradient(lg) => self.paint_linear_gradient(lg), Paint::RadialGradient(rg) => self.paint_radial_gradient(rg), Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg), } } fn pop_transform(&mut self) { if let Some(ts) = self.transforms_stack.pop() { self.transform = ts } } fn push_clip(&mut self) { self.clip_with_path(&self.path_buf.clone()); } fn pop_clip(&mut self) { self.svg.end_element(); } fn push_clip_box(&mut self, clipbox: ClipBox) { let x_min = clipbox.x_min; let x_max = clipbox.x_max; let y_min = clipbox.y_min; let y_max = clipbox.y_max; let clip_path = format!( "M {} {} L {} {} L {} {} L {} {} Z", x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max ); self.clip_with_path(&clip_path); } } fn color_glyph( x: f64, y: f64, face: &ttf::Face, palette_index: u16, glyph_id: ttf::GlyphId, cell_size: f64, scale: f64, gradient_index: &mut usize, clip_path_index: &mut usize, svg: &mut xmlwriter::XmlWriter, path_buf: &mut String, ) { let y = y + cell_size + face.descender() as f64 * scale; let transform = format!("matrix({} 0 0 {} {} {})", scale, -scale, x, y); svg.start_element("g"); svg.write_attribute("transform", &transform); let mut painter = GlyphPainter { face, svg, path_buf, gradient_index: *gradient_index, clip_path_index: *clip_path_index, palette_index, transform: ttf::Transform::default(), outline_transform: ttf::Transform::default(), transforms_stack: vec![ttf::Transform::default()], }; face.paint_color_glyph( glyph_id, palette_index, RgbaColor::new(0, 0, 0, 255), &mut painter, ); *gradient_index = painter.gradient_index; *clip_path_index = painter.clip_path_index; svg.end_element(); } trait XmlWriterExt { fn write_color_attribute(&mut self, name: &str, ts: ttf::RgbaColor); fn write_transform_attribute(&mut self, name: &str, ts: ttf::Transform); fn write_spread_method_attribute(&mut self, method: ttf::colr::GradientExtend); } impl XmlWriterExt for xmlwriter::XmlWriter { fn write_color_attribute(&mut self, name: &str, color: ttf::RgbaColor) { self.write_attribute_fmt( name, format_args!("rgb({}, {}, {})", color.red, color.green, color.blue), ); } fn write_transform_attribute(&mut self, name: &str, ts: ttf::Transform) { if ts.is_default() { return; } self.write_attribute_fmt( name, format_args!( "matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f ), ); } fn write_spread_method_attribute(&mut self, extend: ttf::colr::GradientExtend) { self.write_attribute( "spreadMethod", match extend { ttf::colr::GradientExtend::Pad => &"pad", ttf::colr::GradientExtend::Repeat => &"repeat", ttf::colr::GradientExtend::Reflect => &"reflect", }, ); } }