Initial commit

This commit is contained in:
Pascal Engélibert 2026-09-26 23:37:19 +02:00
commit 63c36ebe1c
2 changed files with 1651 additions and 0 deletions

11
skrivan-cli/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "skrivan-cli"
version = "0.1.0"
edition = "2024"
[dependencies]
argp = "0.4.1"
pixglyph = "0.6"
png = "0.18"
rustybuzz = "0.20"
skrivan = { path = "../skrivan" }

123
skrivan-cli/src/main.rs Normal file
View file

@ -0,0 +1,123 @@
use skrivan::{Atlas, Glyph};
use std::{
collections::{HashMap, HashSet},
io::Write,
};
/// Render fonts to PNG altas and parsable char-to-coordinate map.
///
/// The input text file may contain multiple times the same characters,
/// they will be rendered only once.
/// Newlines and space characters are ignored.
///
/// Skrivañ, CopyLeft 2026 tuxmain (GNU AGPL v3)
#[derive(argp::FromArgs)]
struct Cli {
/// Path to the font file (TTF, OTF, AAT)
#[argp(positional)]
font: String,
/// Path to a text file containing the characters that will be rendered
#[argp(positional)]
text: String,
/// Path to the output image file
#[argp(positional)]
image: String,
/// Path to the output atlas file
#[argp(positional)]
atlas: String,
/// Font size
#[argp(positional)]
size: f32,
/// Maximum width of the image
#[argp(option, short = 'w', default = "256")]
width: u32,
}
fn main() {
let cli: Cli = argp::parse_args_or_exit(argp::DEFAULT);
let font_data = std::fs::read(cli.font).expect("Cannot read font file");
let face = rustybuzz::ttf_parser::Face::parse(&font_data, 0).expect("Cannot parse font");
let mut charset = HashSet::new();
let mut chars = HashMap::new();
let mut buffer = rustybuzz::UnicodeBuffer::new();
let text = std::fs::read_to_string(cli.text).expect("Cannot read text file");
for (i, c) in text.char_indices() {
if c.is_whitespace() {
continue;
}
if charset.insert(c) {
buffer.add(c, i as u32);
chars.insert(i, c);
}
}
let shape = rustybuzz::shape(&rustybuzz::Face::from_face(face.clone()), &[], buffer);
let mut bitmaps = HashMap::new();
let mut max_w = 0;
let mut max_h = 0;
for c in shape.glyph_infos() {
if let Some(glyph) =
pixglyph::Glyph::load(&face, rustybuzz::ttf_parser::GlyphId(c.glyph_id as u16))
{
let bitmap = glyph.rasterize(0., 0., cli.size);
max_w = max_w.max(bitmap.width);
max_h = max_h.max(bitmap.height);
bitmaps.insert(chars[&(c.cluster as usize)], bitmap);
dbg!(chars[&(c.cluster as usize)]);
}
}
let mut atlas = Atlas {
charmap: HashMap::new(),
};
let image_width = cli.width as usize;
let mut pixels = Vec::new();
let mut x: u32 = 0;
let mut y: u32 = 0;
for (c, bitmap) in bitmaps {
if x + bitmap.width >= image_width as _ {
x = 0;
y += max_h;
}
if pixels.len() <= image_width * y as usize {
pixels.extend_from_slice(&vec![0; image_width * max_h as usize]);
}
atlas.charmap.insert(
c,
Glyph {
x,
y,
w: bitmap.width,
h: bitmap.height,
offset_x: bitmap.left,
offset_y: bitmap.top,
},
);
for by in 0..bitmap.height {
for bx in 0..bitmap.width {
pixels[((y + by) * image_width as u32 + x + bx) as usize] =
bitmap.coverage[(by * bitmap.width + bx) as usize];
}
}
x += bitmap.width;
}
let output = std::fs::File::create(cli.image).expect("Cannot open output image file");
let writer = &mut std::io::BufWriter::new(output);
let mut encoder = png::Encoder::new(
writer,
image_width as u32,
(pixels.len() / image_width) as u32,
);
encoder.set_color(png::ColorType::Grayscale);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&pixels).unwrap();
writer.finish().unwrap();
let mut output = std::fs::File::create(cli.atlas).expect("Cannot open output atlas file");
output.write_all(&atlas.serialize()).unwrap();
}