main.rs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. use std::fs::File;
  2. use clap::Parser;
  3. use serde::Deserialize;
  4. mod image;
  5. use crate::image::{Image, Pixel};
  6. #[derive(Deserialize, Debug)]
  7. struct ColorEntry {
  8. color: Pixel,
  9. symbol: String,
  10. }
  11. #[derive(Deserialize, Debug)]
  12. struct Mapping(Vec<ColorEntry>);
  13. impl Mapping {
  14. fn load(path: impl AsRef<std::path::Path>) -> Result<Mapping, Box<dyn std::error::Error>> {
  15. Ok(serde_yaml::from_reader(File::open(path)?)?)
  16. }
  17. fn lookup(&self, color: Pixel) -> &str {
  18. let found = self.0.iter().find(|entry| entry.color == color);
  19. match found {
  20. Some(entry) => &entry.symbol,
  21. None => panic!("Unable to find entry for {:?}", color),
  22. }
  23. }
  24. }
  25. #[derive(Deserialize)]
  26. struct Config {
  27. grid_every: u32,
  28. line_weight: f64,
  29. major_line_weight: f64,
  30. grid_size: f64,
  31. font: String,
  32. }
  33. impl Config {
  34. fn scale(&self, n: u32) -> f64 {
  35. n as f64 * self.grid_size
  36. }
  37. }
  38. impl Default for Config {
  39. fn default() -> Config {
  40. Config {
  41. grid_every: 10,
  42. line_weight: 1.0,
  43. major_line_weight: 3.0,
  44. grid_size: 24.0,
  45. font: "Fira Sans 12".to_string(),
  46. }
  47. }
  48. }
  49. #[derive(Parser, Debug)]
  50. #[clap(author, version, about, long_about = None)]
  51. struct Options {
  52. image: String,
  53. mapping: String,
  54. output: Option<String>,
  55. #[clap(long, default_value_t = 10)]
  56. grid: u32,
  57. #[clap(long, default_value_t = 1.0)]
  58. line_weight: f64,
  59. #[clap(long, default_value_t = 3.0)]
  60. major_line_weight: f64,
  61. #[clap(long, default_value_t = 24.0)]
  62. size: f64,
  63. #[clap(long, default_value_t = String::from("Fira Sans 12"))]
  64. font: String,
  65. }
  66. fn main() -> Result<(), Box<dyn std::error::Error>> {
  67. let opts = Options::parse();
  68. let img = Image::load(opts.image)?;
  69. let mapping = Mapping::load(opts.mapping)?;
  70. let config = Config {
  71. grid_every: opts.grid,
  72. line_weight: opts.line_weight,
  73. major_line_weight: opts.major_line_weight,
  74. grid_size: opts.size,
  75. font: opts.font.clone(),
  76. };
  77. let surf = cairo::SvgSurface::new(
  78. config.scale(img.width),
  79. config.scale(img.height),
  80. Some(opts.output.unwrap_or_else(|| "output.svg".to_string())),
  81. )?;
  82. let ctx = cairo::Context::new(&surf)?;
  83. ctx.set_source_rgb(1.0, 1.0, 1.0);
  84. ctx.paint()?;
  85. let layout = pangocairo::functions::create_layout(&ctx).unwrap();
  86. let font = pango::FontDescription::from_string(&config.font);
  87. layout.set_width(2880);
  88. layout.set_font_description(Some(&font));
  89. ctx.set_source_rgb(0.0, 0.0, 0.0);
  90. for i in 0..=img.width {
  91. ctx.move_to(config.scale(i), 0.0);
  92. ctx.line_to(config.scale(i), config.scale(img.height));
  93. ctx.set_line_width(if i % config.grid_every == 0 {
  94. config.major_line_weight
  95. } else {
  96. config.line_weight
  97. });
  98. ctx.stroke()?;
  99. }
  100. for j in 0..=img.height {
  101. ctx.move_to(0.0, config.scale(j));
  102. ctx.line_to(config.scale(img.width), config.scale(j));
  103. ctx.set_line_width(if j % config.grid_every == 0 {
  104. config.major_line_weight
  105. } else {
  106. config.line_weight
  107. });
  108. ctx.stroke()?;
  109. }
  110. ctx.set_source_rgb(0.0, 0.0, 0.0);
  111. for ((x, y), pixel) in img.iter() {
  112. let l = mapping.lookup(pixel);
  113. if !l.is_empty() {
  114. ctx.move_to(config.scale(x) + 5.0, config.scale(y) + 3.0);
  115. layout.set_text(l);
  116. pangocairo::functions::show_layout(&ctx, &layout);
  117. }
  118. }
  119. surf.finish();
  120. Ok(())
  121. }