conversion between UTF-8 and CP-437
This commit is contained in:
		
							parent
							
								
									c7764c49e1
								
							
						
					
					
						commit
						3d47b41106
					
				
					 7 changed files with 154 additions and 15 deletions
				
			
		
							
								
								
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -617,6 +617,7 @@ dependencies = [ | |||
|  "clap", | ||||
|  "flate2", | ||||
|  "log", | ||||
|  "once_cell", | ||||
|  "rand", | ||||
|  "rust-lzma", | ||||
|  "tungstenite", | ||||
|  |  | |||
|  | @ -21,9 +21,10 @@ zstd = { version = "0.13", optional = true } | |||
| rust-lzma = { version = "0.6.0", optional = true } | ||||
| rand = { version = "0.8", optional = true } | ||||
| tungstenite = { version = "0.24.0", optional = true } | ||||
| once_cell = { version = "1.20.2", optional = true } | ||||
| 
 | ||||
| [features] | ||||
| default = ["compression_lzma", "protocol_udp"] | ||||
| default = ["compression_lzma", "protocol_udp", "cp437"] | ||||
| compression_zlib = ["dep:flate2"] | ||||
| compression_bzip2 = ["dep:bzip2"] | ||||
| compression_lzma = ["dep:rust-lzma"] | ||||
|  | @ -32,15 +33,19 @@ all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", | |||
| rand = ["dep:rand"] | ||||
| protocol_udp = [] | ||||
| protocol_websocket = ["dep:tungstenite"] | ||||
| cp437 = ["dep:once_cell"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "random_brightness" | ||||
| required-features = ["rand"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "game_of_life" | ||||
| required-features = ["rand"] | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| # for examples | ||||
| clap = { version = "4.5", features = ["derive"] } | ||||
| rand = "0.8" | ||||
| 
 | ||||
| [lints] | ||||
| workspace = true | ||||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| use clap::Parser; | ||||
| 
 | ||||
| use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin}; | ||||
| use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; | ||||
| 
 | ||||
| #[derive(Parser, Debug)] | ||||
| struct Cli { | ||||
|  | @ -31,19 +31,15 @@ fn main() { | |||
|             .expect("sending clear failed"); | ||||
|     } | ||||
| 
 | ||||
|     let max_width = cli.text.iter().map(|t| t.len()).max().unwrap(); | ||||
|     let text = cli.text.iter().fold(String::new(), move |str, line| { | ||||
|         let is_first = str.is_empty(); | ||||
|         str + if is_first { "" } else { "\n" } + line | ||||
|     }); | ||||
| 
 | ||||
|     let mut chars = Cp437Grid::new(max_width, cli.text.len()); | ||||
|     for y in 0..cli.text.len() { | ||||
|         let row = &cli.text[y]; | ||||
| 
 | ||||
|         for (x, char) in row.chars().enumerate() { | ||||
|             let char = char.try_into().expect("invalid input char"); | ||||
|             chars.set(x, y, char); | ||||
|         } | ||||
|     } | ||||
|     let grid = CharGrid::from(&*text); | ||||
|     let cp437_grid = Cp437Grid::from(&grid); | ||||
| 
 | ||||
|     connection | ||||
|         .send(Command::Cp437Data(Origin::new(0, 0), chars)) | ||||
|         .send(Command::Cp437Data(Origin::new(0, 0), cp437_grid)) | ||||
|         .expect("sending text failed"); | ||||
| } | ||||
|  |  | |||
|  | @ -86,6 +86,15 @@ pub enum Command { | |||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # use servicepoint::{Command, Connection, Origin};
 | ||||
|     /// # let connection = Connection::Fake;
 | ||||
|     /// use servicepoint::{CharGrid, Cp437Grid};
 | ||||
|     /// let grid = CharGrid::from(&"Hello,\nWorld!");
 | ||||
|     /// let grid = Cp437Grid::from(grid);
 | ||||
|     /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed");
 | ||||
|     /// ```
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # use servicepoint::{Command, Connection, Cp437Grid, Origin};
 | ||||
|     /// # let connection = Connection::Fake;
 | ||||
|     /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
 | ||||
|  |  | |||
|  | @ -1,11 +1,15 @@ | |||
| use crate::cp437::Cp437LoadError::InvalidChar; | ||||
| use crate::{Grid, PrimitiveGrid}; | ||||
| use std::collections::HashMap; | ||||
| 
 | ||||
| /// A grid containing codepage 437 characters.
 | ||||
| ///
 | ||||
| /// The encoding is currently not enforced.
 | ||||
| pub type Cp437Grid = PrimitiveGrid<u8>; | ||||
| 
 | ||||
| /// A grid containing UTF-8 characters.
 | ||||
| pub type CharGrid = PrimitiveGrid<char>; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum Cp437LoadError { | ||||
|     InvalidChar { index: usize, char: char }, | ||||
|  | @ -72,6 +76,109 @@ impl Cp437Grid { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(unused)] // depends on features
 | ||||
| pub use feature_cp437::*; | ||||
| 
 | ||||
| #[cfg(feature = "cp437")] | ||||
| mod feature_cp437 { | ||||
|     use super::*; | ||||
| 
 | ||||
|     /// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
 | ||||
|     ///
 | ||||
|     /// Mostly follows CP437, except for:
 | ||||
|     ///  * 0x0A & 0x0D are kept for use as line endings.
 | ||||
|     ///  * 0x1A is used for SAUCE.
 | ||||
|     ///  * 0x1B is used for ANSI escape sequences.
 | ||||
|     ///
 | ||||
|     /// These exclusions should be fine since most programs can't even use them
 | ||||
|     /// without issues. And this makes rendering simpler too.
 | ||||
|     ///
 | ||||
|     /// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
 | ||||
|     ///
 | ||||
|     /// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0
 | ||||
|     #[rustfmt::skip] | ||||
|     const CP437_TO_UTF8: [char; 256] = [ | ||||
|         /* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼', | ||||
|         /* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔',  '▲', '▼', | ||||
|         /* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/', | ||||
|         /* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', | ||||
|         /* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', | ||||
|         /* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_', | ||||
|         /* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', | ||||
|         /* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂', | ||||
|         /* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å', | ||||
|         /* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', | ||||
|         /* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', | ||||
|         /* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', | ||||
|         /* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', | ||||
|         /* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', | ||||
|         /* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', | ||||
|         /* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ', | ||||
|     ]; | ||||
| 
 | ||||
|     const UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> = | ||||
|         once_cell::sync::Lazy::new(|| { | ||||
|             let pairs = CP437_TO_UTF8 | ||||
|                 .iter() | ||||
|                 .enumerate() | ||||
|                 .map(move |(index, char)| (*char, index as u8)); | ||||
|             HashMap::from_iter(pairs) | ||||
|         }); | ||||
| 
 | ||||
|     const MISSING_CHAR_CP437: u8 = 0x3F; | ||||
| 
 | ||||
|     impl From<&Cp437Grid> for CharGrid { | ||||
|         fn from(value: &Cp437Grid) -> Self { | ||||
|             let mut grid = Self::new(value.width(), value.height()); | ||||
| 
 | ||||
|             for y in 0..grid.height() { | ||||
|                 for x in 0..grid.width() { | ||||
|                     let converted = CP437_TO_UTF8[value.get(x, y) as usize]; | ||||
|                     grid.set(x, y, converted); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl From<&CharGrid> for Cp437Grid { | ||||
|         fn from(value: &CharGrid) -> Self { | ||||
|             let mut grid = Self::new(value.width(), value.height()); | ||||
| 
 | ||||
|             for y in 0..grid.height() { | ||||
|                 for x in 0..grid.width() { | ||||
|                     let char = value.get(x, y); | ||||
|                     let converted = *UTF8_TO_CP437 | ||||
|                         .get(&char) | ||||
|                         .unwrap_or(&MISSING_CHAR_CP437); | ||||
|                     grid.set(x, y, converted); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl From<&str> for CharGrid { | ||||
|         fn from(value: &str) -> Self { | ||||
|             let value = value.replace("\r\n", "\n"); | ||||
|             let lines = value.split('\n').collect::<Vec<_>>(); | ||||
|             let width = | ||||
|                 lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len())); | ||||
| 
 | ||||
|             let mut grid = Self::new(width, lines.len()); | ||||
|             for (y, line) in lines.iter().enumerate() { | ||||
|                 for (x, char) in line.chars().enumerate() { | ||||
|                     grid.set(x, y, char); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | @ -98,3 +205,17 @@ mod tests { | |||
|         assert_eq!(actual, expected); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| #[cfg(feature = "cp437")] | ||||
| mod tests_feature_cp437 { | ||||
|     use crate::{CharGrid, Cp437Grid}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn round_trip_cp437() { | ||||
|         let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); | ||||
|         let cp437 = Cp437Grid::from(&utf8); | ||||
|         let actual = CharGrid::from(&cp437); | ||||
|         assert_eq!(actual, utf8); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ pub use crate::brightness::{Brightness, BrightnessGrid}; | |||
| pub use crate::command::{Command, Offset}; | ||||
| pub use crate::compression_code::CompressionCode; | ||||
| pub use crate::connection::Connection; | ||||
| pub use crate::cp437::Cp437Grid; | ||||
| pub use crate::cp437::{CharGrid, Cp437Grid}; | ||||
| pub use crate::data_ref::DataRef; | ||||
| pub use crate::grid::Grid; | ||||
| pub use crate::origin::{Origin, Pixels, Tiles}; | ||||
|  |  | |||
|  | @ -12,6 +12,13 @@ pub struct Origin<Unit: DisplayUnit> { | |||
| } | ||||
| 
 | ||||
| impl<Unit: DisplayUnit> Origin<Unit> { | ||||
|     /// Top-left. Equivalent to `Origin::new(0, 0)`.
 | ||||
|     pub const ZERO: Self = Self { | ||||
|         x: 0, | ||||
|         y: 0, | ||||
|         phantom_data: PhantomData, | ||||
|     }; | ||||
| 
 | ||||
|     /// Create a new [Origin] instance for the provided position.
 | ||||
|     pub fn new(x: usize, y: usize) -> Self { | ||||
|         Self { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter