Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active September 5, 2023 00:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stecman/1a512b275bf45ff7e739b0f65470bb1b to your computer and use it in GitHub Desktop.
Save stecman/1a512b275bf45ff7e739b0f65470bb1b to your computer and use it in GitHub Desktop.
Render glyphs to image with Rust

Bake glyphs from fonts to image files (Rust)

This is an appendix item for Unicode Input Terminal.

Find all of the Unicode codepoints a font can represent and renders them to invidiaul image files. Note the output directory is hard-coded to /tmp/rendered.

cargo run <font-file, ...>

This was part of the early work on my Unicode Binary Input Terminal project. The code has been removed from the master branch, but it can still be accessed at this commit:

stecman/unicode-input-panel@6e75f8ad6faacb83f8b661fd8d8278a463f2b1e4

[package]
name = "rust-bake-codepoints"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fontdue = "0.7.2"
glob = "0.3.0"
png = "0.17.5"
resvg = "0.23.0"
ttf-parser = "0.15.2"
usvg = "0.23.0"
use std::{env, fs::File, collections::HashMap};
use std::io::prelude::*;
struct FontFace {
buf: Vec<u8>,
renderer: fontdue::Font,
name: String,
}
impl FontFace {
pub fn load_path(path: &String) -> Result<FontFace, std::io::Error> {
let mut file = File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
// The fontdue API consumes/moves the passed data even though it doesn't hold onto it
// We have to copy it here to be able to continue accessing it with ttf_parser later...
let font = fontdue::Font::from_bytes(buf.to_vec(), fontdue::FontSettings::default()).unwrap();
// Use filename as the name for debugging
let name = std::path::Path::new(&path).file_name().unwrap();
Ok(FontFace {
buf,
renderer: font,
name: name.to_str().unwrap().into(),
})
}
pub fn get_face(&self) -> ttf_parser::Face {
let face = ttf_parser::Face::from_slice(&self.buf, 0).unwrap();
return face;
}
}
fn make_output_path(codepoint: u32) -> (String, String) {
let mut out_path = "/tmp/rendered".to_owned();
let mut hex_str = std::format!("{codepoint:04X}");
// Ensure we have an even length string for codepoints > 2 bytes
if (hex_str.len() % 2) != 0 {
hex_str = std::format!("0{hex_str}");
}
// Build folder structure to match codepoint bytes
for (i, c) in hex_str.char_indices() {
if (i % 2) == 0 {
out_path.push_str("/");
}
out_path.push(c);
}
out_path.push_str(".png");
let (dir,file) = out_path.split_at(out_path.len() - 6);
return (dir.to_owned(), file.to_owned());
}
fn main() {
let paths: Vec<String> = env::args().skip(1).collect();
if paths.len() == 0 {
panic!("No fonts were passed");
}
// Load all font-faces that were passed
let fonts: Vec<FontFace> = paths.iter().map(|path| {
//println!("Loading font: {}", path);
let font = match FontFace::load_path(path) {
Ok(f) => f,
Err(error) => panic!("Failed to open font file: {:?}", error),
};
return font;
}).collect();
// Index available codepoints, pointing to the first font that contains it
let mut charmap = HashMap::new();
for (index, font) in fonts.iter().enumerate() {
println!("Indexing font {}: {}", index, font.name);
let face = font.get_face();
if let Some(subtable) = face.tables().cmap {
for subtable in subtable.subtables {
subtable.codepoints(|codepoint| {
if !charmap.contains_key(&codepoint) {
charmap.insert(codepoint, font);
}
});
}
}
}
println!("Collected {} codepoints from {} fonts", charmap.len(), paths.len());
let mut keys: Vec<&u32> = charmap.keys().collect();
keys.sort();
for &codepoint in keys.iter() {
// Create destination path
let (out_dir, filename) = make_output_path(*codepoint);
let out_path = std::format!("{out_dir}{filename}");
std::fs::create_dir_all(out_dir).unwrap();
const MAX_WIDTH: usize = 240;
const MAX_HEIGHT: usize = 200;
let mut px_size = 150;
// Try to render to bitmap
{
let fontface = charmap.get(codepoint).unwrap();
let face = fontface.get_face();
// Look for image representations if this is not a control character
// This allows colour emoji to be drawn, which glyph drawing doesn't handle.
if let Ok(chr) = (*codepoint).try_into() {
if let Some(glyph_id) = face.glyph_index(chr) {
// Check if this needs to be rendered from an SVG
if let Some(svgdata) = face.glyph_svg_image(glyph_id) {
let options = usvg::Options::default();
if let Ok(_tree) = usvg::Tree::from_data(svgdata, &options.to_ref()) {
// TODO: Actually render SVG glyphs
println!("Warning: {codepoint} has an SVG representation, but we can't render this yet");
}
}
// Check if this glyph is a stored image
if let Some(img) = face.glyph_raster_image(glyph_id, px_size) {
if img.format == ttf_parser::RasterImageFormat::PNG {
let file = std::fs::File::create(&out_path).unwrap();
let ref mut writer = std::io::BufWriter::new(file);
if let Err(e) = writer.write_all(img.data) {
panic!("Writing embedded PNG data failed: {}", e);
}
println!("{codepoint} (png) -> {out_path}");
continue;
}
}
}
// Brute-force adjust the font size to fit if needed
// Some characters end up much larger than the canvas otherwise (eg U+012486)
loop {
let metrics = fontface.renderer.metrics(chr, px_size.into());
if metrics.width > MAX_WIDTH || metrics.height > MAX_HEIGHT {
px_size -= 1;
continue;
}
break;
}
}
// Draw glyph to a bitmap
let (metrics, bitmap) = fontface.renderer.rasterize(char::from_u32(*codepoint).unwrap(), px_size.into());
if metrics.width == 0 || metrics.height == 0 {
println!("Warning: {codepoint} glyph drawing was empty");
continue;
}
// Write bitmap as a PNG
let file = std::fs::File::create(&out_path).unwrap();
let ref mut writer = std::io::BufWriter::new(file);
let mut encoder = png::Encoder::new(
writer,
u32::try_from(metrics.width).unwrap(),
u32::try_from(metrics.height).unwrap()
);
encoder.set_color(png::ColorType::Grayscale);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&bitmap).unwrap();
println!("{codepoint} (draw) -> {out_path}");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment