conversion between UTF-8 and CP-437

This commit is contained in:
Vinzenz Schroeter 2024-10-12 18:23:36 +02:00
parent c7764c49e1
commit 3d47b41106
7 changed files with 154 additions and 15 deletions

1
Cargo.lock generated
View file

@ -617,6 +617,7 @@ dependencies = [
"clap",
"flate2",
"log",
"once_cell",
"rand",
"rust-lzma",
"tungstenite",

View file

@ -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

View file

@ -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");
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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};

View file

@ -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 {