diff --git a/Cargo.lock b/Cargo.lock index aa4b812..b8cc995 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,56 @@ dependencies = [ "equator", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -150,12 +200,58 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "clap" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "crc32fast" version = "1.5.0" @@ -377,6 +473,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -412,6 +514,7 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" name = "karlos-helper" version = "0.1.0" dependencies = [ + "clap", "image", "serde", "serde_json", @@ -564,6 +667,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "paste" version = "1.0.15" @@ -842,6 +951,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.104" @@ -943,6 +1058,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "v_frame" version = "0.3.9" @@ -1039,6 +1160,79 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.12" diff --git a/Cargo.toml b/Cargo.toml index 099585d..dc66f0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +clap = { version = "4.5.42", features = ["derive"] } image = "0.25.6" serde = { version="1.0.219", features=["derive"] } serde_json = "1.0.141" diff --git a/src/main.rs b/src/main.rs index db4aa1c..07eb17a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::path::Path; mod image_wrapper; +use clap::Parser; use image_wrapper::BWImage; mod sff; @@ -35,6 +36,7 @@ fn load_font_dir>(dir_with_images: P) -> HashMap { assert!(!map.contains_key(&number)); map.insert(number, img); } + assert!(!map.is_empty()); map } @@ -151,59 +153,31 @@ struct Index { splitters: Vec, } -fn main() { - std::fs::remove_dir_all("font").unwrap(); - std::fs::create_dir("font").unwrap(); - let args: Vec = std::env::args().collect(); - let index: Index = serde_json::from_str(&std::fs::read_to_string(&args[1]).unwrap()).unwrap(); - for splitter in index.splitters { - match splitter { - Splitter::Table { - path, - nrows, - ncols, - start_number, - } => { - image_split_table( - path, - "font", - start_number, - nrows, - ncols, - index.char_width, - index.char_height, - ); - } - Splitter::Individual { path, numbers } => { - image_split_individual( - path, - "font", - &numbers[..], - index.char_width, - index.char_height, - ); - } - Splitter::Single { path, number } => { - image_split_individual( - path, - "font", - &[number][..], - index.char_width, - index.char_height, - ); - } - } +fn do_inspect>(path: P) { + let read_file = std::fs::read(path).unwrap(); + let fd = sff::decode(&read_file); + for i in 32..128 { + fd.print_symbol(fd.find_codepoint(i).unwrap()); } +} - let map = load_font_dir("font"); +fn do_create, Q: AsRef>(font_dir: P, dest: Q, encoding: sff::Encoding) { + let map = load_font_dir(font_dir); + let (char_width, char_height) = { + let random_element = map.values().next().unwrap(); + ( + random_element.width as usize, + random_element.height as usize, + ) + }; let mut sorted_keys: Vec = map.keys().copied().collect(); sorted_keys.sort(); let ranges = sorted_numbers_into_ranges(&sorted_keys); let fdo = sff::FileDescOut { - char_width: 8, - char_height: 18, - encoding: sff::Encoding::BitPerPix4, + char_width, + char_height, + encoding, tables: ranges .into_iter() .map(|rng| sff::TableOut { @@ -217,7 +191,121 @@ fn main() { .collect(), }; - std::fs::write("font.sff", fdo.encode()).unwrap(); + std::fs::write(dest, fdo.encode()).unwrap(); +} + +fn do_split, Q: AsRef>(index: P, font_dir: Q) { + if std::fs::exists(font_dir.as_ref()).unwrap() { + std::fs::remove_dir_all(font_dir.as_ref()).unwrap(); + } + std::fs::create_dir(font_dir.as_ref()).unwrap(); + let index: Index = serde_json::from_str(&std::fs::read_to_string(index).unwrap()).unwrap(); + println!("{:#?}", index); + for splitter in index.splitters { + match splitter { + Splitter::Table { + path, + nrows, + ncols, + start_number, + } => { + image_split_table( + path, + font_dir.as_ref(), + start_number, + nrows, + ncols, + index.char_width, + index.char_height, + ); + } + Splitter::Individual { path, numbers } => { + image_split_individual( + path, + font_dir.as_ref(), + &numbers[..], + index.char_width, + index.char_height, + ); + } + Splitter::Single { path, number } => { + image_split_individual( + path, + font_dir.as_ref(), + &[number][..], + index.char_width, + index.char_height, + ); + } + } + } +} + +#[derive(clap::Parser, Debug)] +struct Cli { + #[command(subcommand)] + cmd: Command, +} + +#[derive(clap::Subcommand, Debug)] +enum Command { + Inspect { + path: String, + }, + Create { + #[arg(long)] + font_dir: String, + #[arg(short, long)] + encoding: String, + dest: String, + }, + Split { + #[arg(long)] + index: String, + #[arg(long)] + font_dir: String, + }, + SplitAndCreate { + #[arg(long)] + index: String, + #[arg(long)] + font_dir: String, + #[arg(short, long)] + encoding: String, + dest: String, + }, +} + +fn str_to_encoding(s: &str) -> sff::Encoding { + match s { + "BitPerPix1" => sff::Encoding::BitPerPix1, + "BitPerPix2" => sff::Encoding::BitPerPix2, + "BitPerPix4" => sff::Encoding::BitPerPix4, + "BitPerPix8" => sff::Encoding::BitPerPix8, + _ => panic!("unknown encoding"), + } +} + +fn main() { + let cli = Cli::parse(); + match cli.cmd { + Command::Inspect { path } => do_inspect(path), + Command::Create { + font_dir, + encoding, + dest, + } => do_create(font_dir, dest, str_to_encoding(&encoding)), + Command::Split { index, font_dir } => do_split(index, font_dir), + Command::SplitAndCreate { + index, + font_dir, + encoding, + dest, + } => { + do_split(index, &font_dir); + do_create(&font_dir, dest, str_to_encoding(&encoding)); + } + } } #[cfg(test)] diff --git a/src/sff.rs b/src/sff.rs index 2d04443..3f154e5 100644 --- a/src/sff.rs +++ b/src/sff.rs @@ -58,7 +58,17 @@ impl Encoding { fn compress(self, input: &[u8]) -> Vec { let mut out = Vec::new(); match self { - Encoding::BitPerPix1 => todo!(), + Encoding::BitPerPix1 => { + assert!(input.len() % 8 == 0); // TODO not strictly necessary + for c in 0..input.len() / 8 { + let mut byte = 0u8; + for i in 0..8 { + let piece = input[(c << 3) + i] >> 7; + byte |= piece << (7 - i); + } + out.push(byte); + } + } Encoding::BitPerPix2 => todo!(), Encoding::BitPerPix4 => { assert!(input.len() % 2 == 0); // TODO not strictly necessary @@ -88,7 +98,7 @@ fn get_u32(input: &[u8], byte_offset: usize) -> u32 { } #[derive(Debug)] -struct FileDesc<'a> { +pub struct FileDesc<'a> { input: &'a [u8], char_width: usize, char_height: usize, @@ -96,6 +106,9 @@ struct FileDesc<'a> { tables: Vec, } +#[derive(Debug, Copy, Clone)] +pub struct GlyphOffset(u32); + impl<'a> FileDesc<'a> { fn bytes_per_codepoint(&self) -> usize { let px_per_codepoint = self.char_width * self.char_height; @@ -107,7 +120,7 @@ impl<'a> FileDesc<'a> { } } - fn find_codepoint(&self, codepoint: u32) -> Option<&[u8]> { + pub fn find_codepoint(&self, codepoint: u32) -> Option { self.tables .iter() .find(|tab| { @@ -117,26 +130,55 @@ impl<'a> FileDesc<'a> { .map(|tab| { let off = tab.offset as usize + (codepoint - tab.first_codepoint) as usize * self.bytes_per_codepoint(); - &self.input[off..off + self.bytes_per_codepoint()] + GlyphOffset(off as u32) }) } - // input should be result of find_codepoint here - fn get_pixel_value(&self, input: &[u8], row: u8, col: u8) -> Option { - if row as usize >= self.char_height || col as usize >= self.char_width { - return None; - } - let px_index = row as usize * self.char_width + col as usize; + // returns the unscaled pixel value! + pub fn get_pixel_value_raw(&self, off: GlyphOffset, row: usize, col: usize) -> u8 { + assert!(row < self.char_height); + assert!(col < self.char_width); + let px_index = row * self.char_width + col; let Access { byte_index, shift, mask, } = self.encoding.access_from_index(px_index); - Some((input[byte_index] >> shift) & mask) + (self.input[off.0 as usize + byte_index] >> shift) & mask + } + + pub fn get_pixel_value_scaled(&self, off: GlyphOffset, row: usize, col: usize) -> u8 { + let raw = self.get_pixel_value_raw(off, row, col); + match self.encoding { + Encoding::BitPerPix1 => raw * 255, + Encoding::BitPerPix2 => (raw as usize * 255 / 3) as u8, + Encoding::BitPerPix4 => (raw as usize * 255 / 15) as u8, + Encoding::BitPerPix8 => raw, + } + } + + pub fn print_symbol(&self, off: GlyphOffset) { + println!("warning: assuming dark mode console"); + let p100 = '█'; + let p75 = '▓'; + let p50 = '▒'; + let p25 = '░'; + let p0 = ' '; + let chars = [p0, p25, p50, p75, p100]; + + for row in 0..self.char_height { + for col in 0..self.char_width { + let value = self.get_pixel_value_scaled(off, row, col); + // map 0..=255 to 0..=4 + let c = chars[value as usize * 4 / 255]; + print!("{}{}", c, c); + } + println!(); + } } } -fn decode(input: &[u8]) -> FileDesc<'_> { +pub fn decode(input: &[u8]) -> FileDesc<'_> { if input[0] != b's' || input[1] != b'f' || input[2] != b'f' || input[3] != b'\0' { panic!("invalid header magic"); }