Merge pull request #13 from cccb/fonts

More CP437, PrimitiveGrid::map, renamings
This commit is contained in:
Vinzenz Schroeter 2024-10-16 20:10:57 +02:00 • committed by GitHub
commit 30d74ff07d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2396 additions and 1557 deletions

10
Cargo.lock generated
View file

@ -147,9 +147,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.29"
version = "1.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
dependencies = [
"jobserver",
"libc",
@ -610,7 +610,7 @@ dependencies = [
[[package]]
name = "servicepoint"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"bitvec",
"bzip2",
@ -626,7 +626,7 @@ dependencies = [
[[package]]
name = "servicepoint_binding_c"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"cbindgen",
"servicepoint",
@ -634,7 +634,7 @@ dependencies = [
[[package]]
name = "servicepoint_binding_cs"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"csbindgen",
"servicepoint",

View file

@ -6,7 +6,7 @@ members = [
]
[workspace.package]
version = "0.9.1"
version = "0.10.0"
[workspace.lints.rust]
missing-docs = "warn"

View file

@ -17,7 +17,7 @@ cargo add servicepoint
or
```toml
[dependencies]
servicepoint = "0.9.1"
servicepoint = "0.10.0"
```
## Examples

View file

@ -6,16 +6,26 @@ use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin};
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long, default_value = "localhost:2342")]
#[arg(
short,
long,
default_value = "localhost:2342",
help = "Address of the display"
)]
destination: String,
#[arg(short, long, num_args = 1.., value_delimiter = '\n')]
#[arg(short, long, num_args = 1.., value_delimiter = '\n',
help = "Text to send - specify multiple times for multiple lines")]
text: Vec<String>,
#[arg(short, long, default_value_t = true)]
#[arg(
short,
long,
default_value_t = true,
help = "Clear screen before sending text"
)]
clear: bool,
}
/// example: `cargo run -- --text "Hallo,
/// CCCB"`
/// example: `cargo run -- --text "Hallo" --text "CCCB"`
fn main() {
let mut cli = Cli::parse();
if cli.text.is_empty() {
@ -31,15 +41,11 @@ fn main() {
.expect("sending clear failed");
}
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 text = cli.text.join("\n");
let grid = CharGrid::from(&*text);
let cp437_grid = Cp437Grid::from(&grid);
connection
.send(Command::Cp437Data(Origin::new(0, 0), cp437_grid))
.send(Command::Cp437Data(Origin::ZERO, cp437_grid))
.expect("sending text failed");
}

View file

@ -15,23 +15,24 @@ fn main() {
let connection = Connection::open(cli.destination)
.expect("could not connect to display");
let mut pixels = PixelGrid::max_sized();
let mut pixels = Bitmap::max_sized();
pixels.fill(true);
let command = Command::BitmapLinearWin(
Origin::new(0, 0),
Origin::ZERO,
pixels,
CompressionCode::Uncompressed,
);
connection.send(command).expect("send failed");
let max_brightness = usize::from(u8::from(Brightness::MAX));
let max_brightness: u8 = Brightness::MAX.into();
let mut brightnesses = BrightnessGrid::new(TILE_WIDTH, TILE_HEIGHT);
for (index, byte) in brightnesses.data_ref_mut().iter_mut().enumerate() {
*byte = Brightness::try_from((index % max_brightness) as u8).unwrap();
let level = index as u8 % max_brightness;
*byte = Brightness::try_from(level).unwrap();
}
connection
.send(Command::CharBrightness(Origin::new(0, 0), brightnesses))
.send(Command::CharBrightness(Origin::ZERO, brightnesses))
.expect("send failed");
}

View file

@ -24,7 +24,7 @@ fn main() {
loop {
let command = Command::BitmapLinearWin(
Origin::new(0, 0),
Origin::ZERO,
field.clone(),
CompressionCode::Lzma,
);
@ -34,7 +34,7 @@ fn main() {
}
}
fn iteration(field: PixelGrid) -> PixelGrid {
fn iteration(field: Bitmap) -> Bitmap {
let mut next = field.clone();
for x in 0..field.width() {
for y in 0..field.height() {
@ -51,7 +51,7 @@ fn iteration(field: PixelGrid) -> PixelGrid {
next
}
fn count_neighbors(field: &PixelGrid, x: i32, y: i32) -> i32 {
fn count_neighbors(field: &Bitmap, x: i32, y: i32) -> i32 {
let mut count = 0;
for nx in x - 1..=x + 1 {
for ny in y - 1..=y + 1 {
@ -78,8 +78,8 @@ fn count_neighbors(field: &PixelGrid, x: i32, y: i32) -> i32 {
count
}
fn make_random_field(probability: f64) -> PixelGrid {
let mut field = PixelGrid::max_sized();
fn make_random_field(probability: f64) -> Bitmap {
let mut field = Bitmap::max_sized();
let mut rng = rand::thread_rng();
let d = distributions::Bernoulli::new(probability).unwrap();
for x in 0..field.width() {

View file

@ -16,7 +16,7 @@ fn main() {
let connection = Connection::open(Cli::parse().destination)
.expect("could not connect to display");
let mut pixels = PixelGrid::max_sized();
let mut pixels = Bitmap::max_sized();
for x_offset in 0..usize::MAX {
pixels.fill(false);
@ -25,7 +25,7 @@ fn main() {
}
let command = Command::BitmapLinearWin(
Origin::new(0, 0),
Origin::ZERO,
pixels.clone(),
CompressionCode::Lzma,
);

View file

@ -28,11 +28,11 @@ fn main() {
// put all pixels in on state
if cli.enable_all {
let mut filled_grid = PixelGrid::max_sized();
let mut filled_grid = Bitmap::max_sized();
filled_grid.fill(true);
let command = BitmapLinearWin(
Origin::new(0, 0),
Origin::ZERO,
filled_grid,
CompressionCode::Lzma,
);

View file

@ -1,7 +1,7 @@
//! Example for how to use the WebSocket connection
use servicepoint::{
Command, CompressionCode, Connection, Grid, Origin, PixelGrid,
Bitmap, Command, CompressionCode, Connection, Grid, Origin,
};
fn main() {
@ -13,7 +13,7 @@ fn main() {
// use send_mut instead of send
connection.send_mut(Command::Clear).unwrap();
let mut pixels = PixelGrid::max_sized();
let mut pixels = Bitmap::max_sized();
pixels.fill(true);
// use send_mut instead of send

View file

@ -25,7 +25,7 @@ fn main() {
let connection = Connection::open(cli.destination)
.expect("could not connect to display");
let mut enabled_pixels = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
let mut enabled_pixels = Bitmap::max_sized();
enabled_pixels.fill(true);
for x_offset in 0..PIXEL_WIDTH {

View file

@ -0,0 +1,292 @@
use bitvec::order::Msb0;
use bitvec::prelude::BitSlice;
use bitvec::slice::IterMut;
use crate::{BitVec, DataRef, Grid, SpBitVec, PIXEL_HEIGHT, PIXEL_WIDTH};
/// A grid of pixels stored in packed bytes.
#[derive(Debug, Clone, PartialEq)]
pub struct Bitmap {
width: usize,
height: usize,
bit_vec: SpBitVec,
}
impl Bitmap {
/// Creates a new [Bitmap] with the specified dimensions.
///
/// # Arguments
///
/// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction
///
/// returns: [Bitmap] initialized to all pixels off
///
/// # Panics
///
/// - when the width is not dividable by 8
pub fn new(width: usize, height: usize) -> Self {
assert_eq!(width % 8, 0);
Self {
width,
height,
bit_vec: BitVec::repeat(false, width * height),
}
}
/// Creates a new pixel grid with the size of the whole screen.
#[must_use]
pub fn max_sized() -> Self {
Self::new(PIXEL_WIDTH, PIXEL_HEIGHT)
}
/// Loads a [Bitmap] with the specified dimensions from the provided data.
///
/// # Arguments
///
/// - `width`: size in pixels in x-direction
/// - `height`: size in pixels in y-direction
///
/// returns: [Bitmap] that contains a copy of the provided data
///
/// # Panics
///
/// - when the dimensions and data size do not match exactly.
/// - when the width is not dividable by 8
#[must_use]
pub fn load(width: usize, height: usize, data: &[u8]) -> Self {
assert_eq!(width % 8, 0);
assert_eq!(data.len(), height * width / 8);
Self {
width,
height,
bit_vec: BitVec::from_slice(data),
}
}
/// Iterate over all cells in [Bitmap].
///
/// Order is equivalent to the following loop:
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let grid = Bitmap::new(8,2);
/// for y in 0..grid.height() {
/// for x in 0..grid.width() {
/// grid.get(x, y);
/// }
/// }
/// ```
pub fn iter(&self) -> impl Iterator<Item = &bool> {
self.bit_vec.iter().by_refs()
}
/// Iterate over all cells in [Bitmap] mutably.
///
/// Order is equivalent to the following loop:
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2);
/// # let value = false;
/// for y in 0..grid.height() {
/// for x in 0..grid.width() {
/// grid.set(x, y, value);
/// }
/// }
/// ```
///
/// # Example
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # let mut grid = Bitmap::new(8,2);
/// # let value = false;
/// for (index, mut pixel) in grid.iter_mut().enumerate() {
/// pixel.set(index % 2 == 0)
/// }
/// ```
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
self.bit_vec.iter_mut()
}
/// Iterate over all rows in [Bitmap] top to bottom.
pub fn iter_rows(&self) -> IterRows {
IterRows {
bitmap: self,
row: 0,
}
}
}
impl Grid<bool> for Bitmap {
/// Sets the value of the specified position in the [Bitmap].
///
/// # Arguments
///
/// - `x` and `y`: position of the cell
/// - `value`: the value to write to the cell
///
/// returns: old value of the cell
///
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
fn set(&mut self, x: usize, y: usize, value: bool) {
self.assert_in_bounds(x, y);
self.bit_vec.set(x + y * self.width, value)
}
fn get(&self, x: usize, y: usize) -> bool {
self.assert_in_bounds(x, y);
self.bit_vec[x + y * self.width]
}
/// Sets the state of all pixels in the [Bitmap].
///
/// # Arguments
///
/// - `this`: instance to write to
/// - `value`: the value to set all pixels to
fn fill(&mut self, value: bool) {
self.bit_vec.fill(value);
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
impl DataRef<u8> for Bitmap {
fn data_ref_mut(&mut self) -> &mut [u8] {
self.bit_vec.as_raw_mut_slice()
}
fn data_ref(&self) -> &[u8] {
self.bit_vec.as_raw_slice()
}
}
impl From<Bitmap> for Vec<u8> {
/// Turns a [Bitmap] into the underlying [`Vec<u8>`].
fn from(value: Bitmap) -> Self {
value.bit_vec.into()
}
}
pub struct IterRows<'t> {
bitmap: &'t Bitmap,
row: usize,
}
impl<'t> Iterator for IterRows<'t> {
type Item = &'t BitSlice<u8, Msb0>;
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.bitmap.height {
return None;
}
let start = self.row * self.bitmap.width;
let end = start + self.bitmap.width;
self.row += 1;
Some(&self.bitmap.bit_vec[start..end])
}
}
#[cfg(test)]
mod tests {
use crate::{Bitmap, DataRef, Grid};
#[test]
fn fill() {
let mut grid = Bitmap::new(8, 2);
assert_eq!(grid.data_ref(), [0x00, 0x00]);
grid.fill(true);
assert_eq!(grid.data_ref(), [0xFF, 0xFF]);
grid.fill(false);
assert_eq!(grid.data_ref(), [0x00, 0x00]);
}
#[test]
fn get_set() {
let mut grid = Bitmap::new(8, 2);
assert!(!grid.get(0, 0));
assert!(!grid.get(1, 1));
grid.set(5, 0, true);
grid.set(1, 1, true);
assert_eq!(grid.data_ref(), [0x04, 0x40]);
assert!(grid.get(5, 0));
assert!(grid.get(1, 1));
assert!(!grid.get(1, 0));
}
#[test]
fn load() {
let mut grid = Bitmap::new(8, 3);
for x in 0..grid.width {
for y in 0..grid.height {
grid.set(x, y, (x + y) % 2 == 0);
}
}
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
let data: Vec<u8> = grid.into();
let grid = Bitmap::load(8, 3, &data);
assert_eq!(grid.data_ref(), [0xAA, 0x55, 0xAA]);
}
#[test]
#[should_panic]
fn out_of_bounds_x() {
let vec = Bitmap::new(8, 2);
vec.get(8, 1);
}
#[test]
#[should_panic]
fn out_of_bounds_y() {
let mut vec = Bitmap::new(8, 2);
vec.set(1, 2, false);
}
#[test]
fn iter() {
let grid = Bitmap::new(8, 2);
assert_eq!(16, grid.iter().count())
}
#[test]
fn iter_rows() {
let grid = Bitmap::load(8, 2, &[0x04, 0x40]);
let mut iter = grid.iter_rows();
assert_eq!(iter.next().unwrap().count_ones(), 1);
assert_eq!(iter.next().unwrap().count_ones(), 1);
assert_eq!(None, iter.next());
}
#[test]
fn iter_mut() {
let mut grid = Bitmap::new(8, 2);
for (index, mut pixel) in grid.iter_mut().enumerate() {
pixel.set(index % 2 == 0);
}
assert_eq!(grid.data_ref(), [0xAA, 0xAA]);
}
#[test]
fn data_ref_mut() {
let mut grid = Bitmap::new(8, 2);
let data = grid.data_ref_mut();
data[1] = 0x0F;
assert!(grid.get(7, 1));
}
}

View file

@ -60,6 +60,17 @@ impl Brightness {
pub const MAX: Brightness = Brightness(11);
/// lowest possible brightness value, 0
pub const MIN: Brightness = Brightness(0);
/// Create a brightness value without returning an error for brightnesses above [Brightness::MAX].
///
/// returns: the specified value as a [Brightness], or [Brightness::MAX].
pub fn saturating_from(value: u8) -> Brightness {
if value > Brightness::MAX.into() {
Brightness::MAX
} else {
Brightness(value)
}
}
}
impl Default for Brightness {
@ -138,4 +149,10 @@ mod tests {
let actual = PrimitiveGrid::from(&grid);
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
}
#[test]
fn saturating_convert() {
assert_eq!(Brightness::MAX, Brightness::saturating_from(100));
assert_eq!(Brightness(5), Brightness::saturating_from(5));
}
}

View file

@ -4,7 +4,7 @@ use crate::{
command_code::CommandCode,
compression::into_decompressed,
packet::{Header, Packet},
Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin, PixelGrid,
Bitmap, Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin,
Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE,
};
@ -76,12 +76,7 @@ pub enum Command {
/// Show text on the screen.
///
/// The text is sent in the form of a 2D grid of characters.
///
/// <div class="warning">
/// The library does not currently convert between UTF-8 and CP-437.
/// Because Rust expects UTF-8 strings, it might be necessary to only send ASCII for now.
/// </div>
/// The text is sent in the form of a 2D grid of [CP-437] encoded characters.
///
/// # Examples
///
@ -100,6 +95,7 @@ pub enum Command {
/// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
/// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
/// ```
/// [CP-437]: https://en.wikipedia.org/wiki/Code_page_437
Cp437Data(Origin<Tiles>, Cp437Grid),
/// Overwrites a rectangular region of pixels.
@ -109,23 +105,23 @@ pub enum Command {
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
/// # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
/// # let connection = servicepoint::Connection::Fake;
/// #
/// let mut pixels = PixelGrid::max_sized();
/// let mut pixels = Bitmap::max_sized();
/// // draw something to the pixels here
/// # pixels.set(2, 5, true);
///
/// // create command to send pixels
/// let command = Command::BitmapLinearWin(
/// servicepoint::Origin::new(0, 0),
/// servicepoint::Origin::ZERO,
/// pixels,
/// CompressionCode::Uncompressed
/// );
///
/// connection.send(command).expect("send failed");
/// ```
BitmapLinearWin(Origin<Pixels>, PixelGrid, CompressionCode),
BitmapLinearWin(Origin<Pixels>, Bitmap, CompressionCode),
/// Set the brightness of all tiles to the same value.
///
@ -217,9 +213,8 @@ pub enum Command {
BitmapLegacy,
}
#[derive(Debug)]
/// Err values for [Command::try_from].
#[derive(PartialEq)]
#[derive(Debug, PartialEq)]
pub enum TryFromPacketError {
/// the contained command code does not correspond to a known command
InvalidCommand(u16),
@ -342,7 +337,7 @@ impl Command {
Ok(Command::BitmapLinearWin(
Origin::new(tiles_x as usize * TILE_SIZE, pixels_y as usize),
PixelGrid::load(
Bitmap::load(
tile_w as usize * TILE_SIZE,
pixel_h as usize,
&payload,
@ -376,7 +371,7 @@ impl Command {
}
}
/// Helper method for Packets into `BitMapLinear*`-Commands
/// Helper method for Packets into `BitmapLinear*`-Commands
fn packet_into_linear_bitmap(
packet: Packet,
) -> Result<(SpBitVec, CompressionCode), TryFromPacketError> {
@ -500,7 +495,8 @@ mod tests {
command_code::CommandCode,
origin::Pixels,
packet::{Header, Packet},
Brightness, Command, CompressionCode, Origin, PixelGrid, PrimitiveGrid,
Bitmap, Brightness, BrightnessGrid, Command, CompressionCode, Origin,
PrimitiveGrid,
};
fn round_trip(original: Command) {
@ -592,8 +588,8 @@ mod tests {
compression,
));
round_trip(Command::BitmapLinearWin(
Origin::new(0, 0),
PixelGrid::max_sized(),
Origin::ZERO,
Bitmap::max_sized(),
compression,
));
}
@ -718,7 +714,7 @@ mod tests {
for compression in all_compressions().to_owned() {
let p: Packet = Command::BitmapLinearWin(
Origin::new(16, 8),
PixelGrid::new(8, 8),
Bitmap::new(8, 8),
compression,
)
.into();
@ -907,4 +903,28 @@ mod tests {
Origin::new(1, 0) + Origin::new(3, 2)
);
}
#[test]
fn packet_into_char_brightness_invalid() {
let grid = BrightnessGrid::new(2, 2);
let command = Command::CharBrightness(Origin::ZERO, grid);
let mut packet: Packet = command.into();
let slot = packet.payload.get_mut(1).unwrap();
*slot = 23;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(23))
);
}
#[test]
fn packet_into_brightness_invalid() {
let mut packet: Packet = Command::Brightness(Brightness::MAX).into();
let slot = packet.payload.get_mut(0).unwrap();
*slot = 42;
assert_eq!(
Command::try_from(packet),
Err(TryFromPacketError::InvalidBrightness(42))
);
}
}

View file

@ -3,14 +3,14 @@
/// # Examples
///
/// ```rust
/// # use servicepoint::{Command, CompressionCode, Origin, PixelGrid};
/// # use servicepoint::{Command, CompressionCode, Origin, Bitmap};
/// // create command without payload compression
/// # let pixels = PixelGrid::max_sized();
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Uncompressed);
/// # let pixels = Bitmap::max_sized();
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Uncompressed);
///
/// // create command with payload compressed with lzma and appropriate header flags
/// # let pixels = PixelGrid::max_sized();
/// _ = Command::BitmapLinearWin(Origin::new(0, 0), pixels, CompressionCode::Lzma);
/// # let pixels = Bitmap::max_sized();
/// _ = Command::BitmapLinearWin(Origin::ZERO, pixels, CompressionCode::Lzma);
/// ```
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq)]

View file

@ -1,4 +1,7 @@
use crate::cp437::Cp437LoadError::InvalidChar;
//! Conversion between UTF-8 and CP-437.
//!
//! Most of the functionality is only available with feature "cp437" enabled.
use crate::{Grid, PrimitiveGrid};
use std::collections::HashMap;
@ -10,9 +13,16 @@ pub type Cp437Grid = PrimitiveGrid<u8>;
/// A grid containing UTF-8 characters.
pub type CharGrid = PrimitiveGrid<char>;
#[derive(Debug)]
/// Errors that can occur when loading CP-437.
#[derive(Debug, PartialEq)]
pub enum Cp437LoadError {
InvalidChar { index: usize, char: char },
/// Invalid character in input prevented loading
InvalidChar {
/// invalid character is at this position in input
index: usize,
/// the invalid character
char: char,
},
}
impl Cp437Grid {
@ -36,7 +46,7 @@ impl Cp437Grid {
for (index, char) in value.chars().enumerate() {
if !char.is_ascii() {
return Err(InvalidChar { index, char });
return Err(Cp437LoadError::InvalidChar { index, char });
}
let is_lf = char == '\n';
@ -85,21 +95,15 @@ mod feature_cp437 {
/// 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.
/// Mostly follows CP437, except 0x0A, which is kept for use as line ending.
///
/// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
///
/// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0
/// Mostly 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 */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔', '▲', '▼',
pub const CP437_TO_UTF8: [char; 256] = [
/* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '♪', '♫', '☼',
/* 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',
@ -116,7 +120,7 @@ mod feature_cp437 {
/* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ',
];
const UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
static UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> =
once_cell::sync::Lazy::new(|| {
let pairs = CP437_TO_UTF8
.iter()
@ -125,48 +129,34 @@ mod feature_cp437 {
HashMap::from_iter(pairs)
});
const MISSING_CHAR_CP437: u8 = 0x3F;
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
value.map(cp437_to_char)
}
}
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
value.map(char_to_cp437)
}
}
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 mut lines = value
.split('\n')
.map(move |line| line.trim_end())
.collect::<Vec<_>>();
let width =
lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len()));
while lines.last().is_some_and(move |line| line.is_empty()) {
_ = lines.pop();
}
let mut grid = Self::new(width, lines.len());
for (y, line) in lines.iter().enumerate() {
for (x, char) in line.chars().enumerate() {
@ -177,6 +167,44 @@ mod feature_cp437 {
grid
}
}
impl From<&CharGrid> for String {
fn from(value: &CharGrid) -> Self {
value
.iter_rows()
.map(move |chars| {
chars
.collect::<String>()
.replace('\0', " ")
.trim_end()
.to_string()
})
.collect::<Vec<_>>()
.join("\n")
}
}
/// Convert the provided bytes to UTF-8.
pub fn cp437_to_str(cp437: &[u8]) -> String {
cp437.iter().map(move |char| cp437_to_char(*char)).collect()
}
/// Convert a single CP-437 character to UTF-8.
pub fn cp437_to_char(cp437: u8) -> char {
CP437_TO_UTF8[cp437 as usize]
}
/// Convert the provided text to CP-437 bytes.
///
/// Characters that are not available are mapped to '?'.
pub fn str_to_cp437(utf8: &str) -> Vec<u8> {
utf8.chars().map(char_to_cp437).collect()
}
/// Convert a single UTF-8 character to CP-437.
pub fn char_to_cp437(utf8: char) -> u8 {
*UTF8_TO_CP437.get(&utf8).unwrap_or(&MISSING_CHAR_CP437)
}
}
#[cfg(test)]
@ -204,12 +232,23 @@ mod tests {
// line break will be added
assert_eq!(actual, expected);
}
#[test]
fn load_ascii_invalid() {
assert_eq!(
Err(Cp437LoadError::InvalidChar {
char: '🥶',
index: 2
}),
Cp437Grid::load_ascii("?#🥶42", 3, false)
);
}
}
#[cfg(test)]
#[cfg(feature = "cp437")]
mod tests_feature_cp437 {
use crate::{CharGrid, Cp437Grid};
use super::*;
#[test]
fn round_trip_cp437() {
@ -218,4 +257,48 @@ mod tests_feature_cp437 {
let actual = CharGrid::from(&cp437);
assert_eq!(actual, utf8);
}
#[test]
fn convert_str() {
// test text from https://int10h.org/oldschool-pc-fonts/fontlist/font?ibm_bios
let utf8 = r#"A quick brown fox jumps over the lazy dog.
0123456789 ¿?¡!`'"., <>()[]{} &@%*^#$\/
* Wieniläinen sioux'ta puhuva ökyzombie diggaa Åsan roquefort-tacoja.
* Ça me fait peur de fêter noël là, sur cette île bizarroïde où une mère et sa môme essaient de me tuer avec un gâteau à la cigüe brûlé.
* Zwölf Boxkämpfer jagten Eva quer über den Sylter Deich.
* El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.
┌─┬─┐ ╔═╦═╗ ╒═╤═╕ ╓─╥─╖
│ │ │ ║ ║ ║ │ │ │ ║ ║ ║
├─┼─┤ ╠═╬═╣ ╞═╪═╡ ╟─╫─╢
└─┴─┘ ╚═╩═╝ ╘═╧═╛ ╙─╨─╜
░░░░░ ▐▀█▀▌ .·∙•○°○•∙·.
▒▒▒▒▒ ▐ █ ▌ ☺☻ ♥♦♣♠ ♪♫☼
▓▓▓▓▓ ▐▀█▀▌ $ ¢ £ ¥ ₧
█████ ▐▄█▄▌ ◄►▲▼ ←→↑↓↕↨
⌠
│dx ≡ Σ √x²ⁿ·δx
⌡"#;
let cp437 = str_to_cp437(utf8);
let actual = cp437_to_str(&*cp437);
assert_eq!(utf8, actual)
}
#[test]
fn convert_invalid() {
assert_eq!(cp437_to_char(char_to_cp437('😜')), '?');
}
#[test]
fn str_to_char_grid() {
let original = "Hello\r\nWorld!\n...\n";
let grid = CharGrid::from(original);
assert_eq!(3, grid.height());
let actual = String::from(&grid);
assert_eq!("Hello\nWorld!\n...", actual);
}
}

View file

@ -76,15 +76,9 @@ pub trait Grid<T> {
///
/// When the specified position is out of bounds for this grid.
fn assert_in_bounds(&self, x: usize, y: usize) {
assert!(
x < self.width(),
"cannot access index [{x}, {y}] because x is outside of bounds 0..{}",
self.width() - 1
);
assert!(
y < self.height(),
"cannot access index [{x}, {y}] because y is outside of bounds 0..{}",
self.height() - 1
);
let width = self.width();
assert!(x < width, "cannot access index [{x}, {y}] because x is outside of bounds [0..{width})");
let height = self.height();
assert!(y < height, "cannot access index [{x}, {y}] because x is outside of bounds [0..{height})");
}
}

View file

@ -7,7 +7,7 @@
//! # Examples
//!
//! ```rust
//! use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
//! use servicepoint::{Command, CompressionCode, Grid, Bitmap};
//!
//! let connection = servicepoint::Connection::open("127.0.0.1:2342")
//! .expect("connection failed");
@ -18,15 +18,15 @@
//! ```
//!
//! ```rust
//! # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
//! # use servicepoint::{Command, CompressionCode, Grid, Bitmap};
//! # let connection = servicepoint::Connection::open("127.0.0.1:2342").expect("connection failed");
//! // turn on all pixels in a grid
//! let mut pixels = PixelGrid::max_sized();
//! let mut pixels = Bitmap::max_sized();
//! pixels.fill(true);
//!
//! // create command to send pixels
//! let command = Command::BitmapLinearWin(
//! servicepoint::Origin::new(0, 0),
//! servicepoint::Origin::ZERO,
//! pixels,
//! CompressionCode::Uncompressed
//! );
@ -40,6 +40,7 @@ use std::time::Duration;
pub use bitvec;
use bitvec::prelude::{BitVec, Msb0};
pub use crate::bitmap::Bitmap;
pub use crate::brightness::{Brightness, BrightnessGrid};
pub use crate::command::{Command, Offset};
pub use crate::compression_code::CompressionCode;
@ -48,23 +49,22 @@ pub use crate::cp437::{CharGrid, Cp437Grid};
pub use crate::data_ref::DataRef;
pub use crate::grid::Grid;
pub use crate::origin::{Origin, Pixels, Tiles};
pub use crate::pixel_grid::PixelGrid;
pub use crate::primitive_grid::PrimitiveGrid;
type SpBitVec = BitVec<u8, Msb0>;
mod bitmap;
mod brightness;
mod command;
mod command_code;
mod compression;
mod compression_code;
mod connection;
mod cp437;
pub mod cp437;
mod data_ref;
mod grid;
mod origin;
pub mod packet;
mod pixel_grid;
mod primitive_grid;
/// size of a single tile in one dimension
@ -95,8 +95,8 @@ pub const TILE_HEIGHT: usize = 20;
/// # Examples
///
/// ```rust
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
/// let grid = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
/// ```
pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
@ -105,8 +105,8 @@ pub const PIXEL_WIDTH: usize = TILE_WIDTH * TILE_SIZE;
/// # Examples
///
/// ```rust
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, PixelGrid};
/// let grid = PixelGrid::new(PIXEL_WIDTH, PIXEL_HEIGHT);
/// # use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH, Bitmap};
/// let grid = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
/// ```
pub const PIXEL_HEIGHT: usize = TILE_HEIGHT * TILE_SIZE;
@ -119,10 +119,10 @@ pub const PIXEL_COUNT: usize = PIXEL_WIDTH * PIXEL_HEIGHT;
///
/// ```rust
/// # use std::time::Instant;
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, PixelGrid};
/// # use servicepoint::{Command, CompressionCode, FRAME_PACING, Origin, Bitmap};
/// # let connection = servicepoint::Connection::open("172.23.42.29:2342")
/// # .expect("connection failed");
/// # let pixels = PixelGrid::max_sized();
/// # let pixels = Bitmap::max_sized();
/// loop {
/// let start = Instant::now();
///

View file

@ -12,7 +12,7 @@ pub struct Origin<Unit: DisplayUnit> {
}
impl<Unit: DisplayUnit> Origin<Unit> {
/// Top-left. Equivalent to `Origin::new(0, 0)`.
/// Top-left. Equivalent to `Origin::ZERO`.
pub const ZERO: Self = Self {
x: 0,
y: 0,

View file

@ -27,8 +27,8 @@ use std::mem::size_of;
use crate::compression::into_compressed;
use crate::{
command_code::CommandCode, Command, CompressionCode, Grid, Offset, Origin,
PixelGrid, Pixels, Tiles, TILE_SIZE,
command_code::CommandCode, Bitmap, Command, CompressionCode, Grid, Offset,
Origin, Pixels, Tiles, TILE_SIZE,
};
/// A raw header.
@ -214,7 +214,7 @@ impl From<Command> for Packet {
}
impl Packet {
/// Helper method for `BitMapLinear*`-Commands into [Packet]
/// Helper method for `BitmapLinear*`-Commands into [Packet]
#[allow(clippy::cast_possible_truncation)]
fn bitmap_linear_into_packet(
command: CommandCode,
@ -239,7 +239,7 @@ impl Packet {
#[allow(clippy::cast_possible_truncation)]
fn bitmap_win_into_packet(
origin: Origin<Pixels>,
pixels: PixelGrid,
pixels: Bitmap,
compression: CompressionCode,
) -> Packet {
debug_assert_eq!(origin.x % 8, 0);

View file

@ -110,6 +110,34 @@ impl<T: PrimitiveGridType> PrimitiveGrid<T> {
None
}
}
/// Convert between PrimitiveGrid types.
///
/// See also [Iterator::map].
///
/// # Examples
///
/// Use logic written for u8s and then convert to [Brightness] values for sending in a [Command].
/// ```
/// # fn foo(grid: &mut PrimitiveGrid<u8>) {}
/// # use servicepoint::{Brightness, BrightnessGrid, Command, Origin, PrimitiveGrid, TILE_HEIGHT, TILE_WIDTH};
/// let mut grid: PrimitiveGrid<u8> = PrimitiveGrid::new(TILE_WIDTH, TILE_HEIGHT);
/// foo(&mut grid);
/// let grid: BrightnessGrid = grid.map(Brightness::saturating_from);
/// let command = Command::CharBrightness(Origin::ZERO, grid);
/// ```
pub fn map<TConverted, F>(&self, f: F) -> PrimitiveGrid<TConverted>
where
TConverted: PrimitiveGridType,
F: Fn(T) -> TConverted,
{
let data = self
.data_ref()
.iter()
.map(|elem| f(*elem))
.collect::<Vec<_>>();
PrimitiveGrid::load(self.width(), self.height(), &data)
}
}
impl<T: PrimitiveGridType> Grid<T> for PrimitiveGrid<T> {

Some files were not shown because too many files have changed in this diff Show more