use std::fs::File; use clap::Parser; use serde::Deserialize; mod image; use crate::image::{Image, Pixel}; #[derive(Deserialize, Debug)] struct ColorEntry { color: Pixel, symbol: String, } #[derive(Deserialize, Debug)] struct Mapping(Vec); impl Mapping { fn load(path: impl AsRef) -> Result> { Ok(serde_yaml::from_reader(File::open(path)?)?) } fn lookup(&self, color: Pixel) -> &str { let found = self.0.iter().find(|entry| entry.color == color); match found { Some(entry) => &entry.symbol, None => panic!("Unable to find entry for {:?}", color), } } } #[derive(Deserialize)] struct Config { grid_every: u32, line_weight: f64, major_line_weight: f64, grid_size: f64, font: String, } impl Config { fn scale(&self, n: u32) -> f64 { n as f64 * self.grid_size } } impl Default for Config { fn default() -> Config { Config { grid_every: 10, line_weight: 1.0, major_line_weight: 3.0, grid_size: 24.0, font: "Fira Sans 12".to_string(), } } } #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Options { image: String, mapping: String, output: Option, #[clap(long, default_value_t = 10)] grid: u32, #[clap(long, default_value_t = 1.0)] line_weight: f64, #[clap(long, default_value_t = 3.0)] major_line_weight: f64, #[clap(long, default_value_t = 24.0)] size: f64, #[clap(long, default_value_t = String::from("Fira Sans 12"))] font: String, } fn main() -> Result<(), Box> { let opts = Options::parse(); let img = Image::load(opts.image)?; let mapping = Mapping::load(opts.mapping)?; let config = Config { grid_every: opts.grid, line_weight: opts.line_weight, major_line_weight: opts.major_line_weight, grid_size: opts.size, font: opts.font.clone(), }; let surf = cairo::SvgSurface::new( config.scale(img.width), config.scale(img.height), Some(opts.output.unwrap_or_else(|| "output.svg".to_string())), )?; let ctx = cairo::Context::new(&surf)?; ctx.set_source_rgb(1.0, 1.0, 1.0); ctx.paint()?; let layout = pangocairo::functions::create_layout(&ctx).unwrap(); let font = pango::FontDescription::from_string(&config.font); layout.set_width(2880); layout.set_font_description(Some(&font)); ctx.set_source_rgb(0.0, 0.0, 0.0); for i in 0..=img.width { ctx.move_to(config.scale(i), 0.0); ctx.line_to(config.scale(i), config.scale(img.height)); ctx.set_line_width(if i % config.grid_every == 0 { config.major_line_weight } else { config.line_weight }); ctx.stroke()?; } for j in 0..=img.height { ctx.move_to(0.0, config.scale(j)); ctx.line_to(config.scale(img.width), config.scale(j)); ctx.set_line_width(if j % config.grid_every == 0 { config.major_line_weight } else { config.line_weight }); ctx.stroke()?; } ctx.set_source_rgb(0.0, 0.0, 0.0); for ((x, y), pixel) in img.iter() { let l = mapping.lookup(pixel); if !l.is_empty() { ctx.move_to(config.scale(x) + 5.0, config.scale(y) + 3.0); layout.set_text(l); pangocairo::functions::show_layout(&ctx, &layout); } } surf.finish(); Ok(()) }