340 lines
9.8 KiB
Rust
340 lines
9.8 KiB
Rust
use crate::{
|
|
command_code::{CommandCode, InvalidCommandCodeError},
|
|
commands::errors::{TryFromPacketError, TryIntoPacketError},
|
|
compression::{compress, decompress},
|
|
Bitmap, CompressionCode, DataRef, Grid, Header, LoadableFromPacket, Origin,
|
|
Packet, Pixels, TypedCommand, TILE_SIZE,
|
|
};
|
|
|
|
/// Overwrites a rectangular region of pixels.
|
|
///
|
|
/// Origin coordinates must be divisible by 8.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use servicepoint::*;
|
|
/// # let connection = FakeConnection;
|
|
/// #
|
|
/// let mut bitmap = Bitmap::max_sized();
|
|
/// // draw something to the pixels here
|
|
/// # bitmap.set(2, 5, true);
|
|
///
|
|
/// // create command to send pixels
|
|
/// let command = BitmapCommand {
|
|
/// bitmap,
|
|
/// origin: Origin::ZERO,
|
|
/// compression: CompressionCode::Uncompressed
|
|
/// };
|
|
///
|
|
/// connection.send_command(command).expect("send failed");
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub struct BitmapCommand {
|
|
/// the pixels to send
|
|
pub bitmap: Bitmap,
|
|
/// where to start drawing the pixels
|
|
pub origin: Origin<Pixels>,
|
|
/// how to compress the command when converting to packet
|
|
pub compression: CompressionCode,
|
|
}
|
|
|
|
impl TryFrom<&BitmapCommand> for Packet {
|
|
type Error = TryIntoPacketError;
|
|
|
|
fn try_from(value: &BitmapCommand) -> Result<Self, Self::Error> {
|
|
let tile_x = (value.origin.x / TILE_SIZE).try_into()?;
|
|
let tile_w = (value.bitmap.width() / TILE_SIZE).try_into()?;
|
|
let pixel_h = value.bitmap.height().try_into()?;
|
|
let command =
|
|
BitmapCommand::command_code_for_compression(value.compression);
|
|
let data_ref = value.bitmap.data_ref();
|
|
let payload = compress(value.compression, data_ref)?;
|
|
|
|
Ok(Packet {
|
|
header: Header {
|
|
command_code: command.into(),
|
|
a: tile_x,
|
|
b: value.origin.y.try_into()?,
|
|
c: tile_w,
|
|
d: pixel_h,
|
|
},
|
|
payload: Some(payload),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TryFrom<BitmapCommand> for Packet {
|
|
type Error = TryIntoPacketError;
|
|
|
|
fn try_from(value: BitmapCommand) -> Result<Self, Self::Error> {
|
|
Packet::try_from(&value)
|
|
}
|
|
}
|
|
|
|
impl LoadableFromPacket for BitmapCommand {
|
|
fn load<'t>(
|
|
header: &Header,
|
|
payload: &'t [u8],
|
|
) -> Result<(Self, &'t [u8]), TryFromPacketError> {
|
|
let code = CommandCode::try_from(header.command_code)?;
|
|
let compression = BitmapCommand::compression_for_command_code(code)
|
|
.ok_or(InvalidCommandCodeError(header.command_code))?;
|
|
|
|
let Header {
|
|
command_code: _,
|
|
a: tiles_x,
|
|
b: pixels_y,
|
|
c: tile_w,
|
|
d: pixel_h,
|
|
} = *header;
|
|
let tile_w = tile_w as usize;
|
|
let pixel_h = pixel_h as usize;
|
|
let tiles_x = tiles_x as usize;
|
|
let pixels_y = pixels_y as usize;
|
|
|
|
let expected_bytes = tile_w * pixel_h;
|
|
let (payload, read_payload_bytes) =
|
|
decompress(compression, &payload, expected_bytes)
|
|
.map_err(|_| TryFromPacketError::DecompressionFailed)?;
|
|
if expected_bytes < payload.len() {
|
|
return Err(TryFromPacketError::UnexpectedPayloadSize {
|
|
expected: expected_bytes,
|
|
actual: payload.len(),
|
|
});
|
|
}
|
|
let bitmap = Bitmap::load(
|
|
tile_w * TILE_SIZE,
|
|
pixel_h,
|
|
&payload[..expected_bytes],
|
|
)?;
|
|
let origin = Origin::new(tiles_x * TILE_SIZE, pixels_y);
|
|
|
|
Ok((
|
|
Self {
|
|
bitmap,
|
|
origin,
|
|
compression,
|
|
},
|
|
read_payload_bytes,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl From<BitmapCommand> for TypedCommand {
|
|
fn from(command: BitmapCommand) -> Self {
|
|
Self::Bitmap(command)
|
|
}
|
|
}
|
|
|
|
impl From<Bitmap> for BitmapCommand {
|
|
fn from(bitmap: Bitmap) -> Self {
|
|
Self {
|
|
bitmap,
|
|
origin: Origin::default(),
|
|
compression: CompressionCode::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BitmapCommand {
|
|
fn command_code_for_compression(
|
|
compression_code: CompressionCode,
|
|
) -> CommandCode {
|
|
match compression_code {
|
|
CompressionCode::Uncompressed => {
|
|
CommandCode::BitmapLinearWinUncompressed
|
|
}
|
|
#[cfg(feature = "compression_zlib")]
|
|
CompressionCode::Zlib => CommandCode::BitmapLinearWinZlib,
|
|
#[cfg(feature = "compression_bzip2")]
|
|
CompressionCode::Bzip2 => CommandCode::BitmapLinearWinBzip2,
|
|
#[cfg(feature = "compression_lzma")]
|
|
CompressionCode::Lzma => CommandCode::BitmapLinearWinLzma,
|
|
#[cfg(feature = "compression_zstd")]
|
|
CompressionCode::Zstd => CommandCode::BitmapLinearWinZstd,
|
|
}
|
|
}
|
|
|
|
fn compression_for_command_code(
|
|
command_code: CommandCode,
|
|
) -> Option<CompressionCode> {
|
|
Some(match command_code {
|
|
CommandCode::BitmapLinearWinUncompressed => {
|
|
CompressionCode::Uncompressed
|
|
}
|
|
#[cfg(feature = "compression_zlib")]
|
|
CommandCode::BitmapLinearWinZlib => CompressionCode::Zlib,
|
|
#[cfg(feature = "compression_bzip2")]
|
|
CommandCode::BitmapLinearWinBzip2 => CompressionCode::Bzip2,
|
|
#[cfg(feature = "compression_lzma")]
|
|
CommandCode::BitmapLinearWinLzma => CompressionCode::Lzma,
|
|
#[cfg(feature = "compression_zstd")]
|
|
CommandCode::BitmapLinearWinZstd => CompressionCode::Zstd,
|
|
_ => return None,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
command_code::CommandCode, commands::tests::TestImplementsCommand,
|
|
GridMut,
|
|
};
|
|
|
|
impl TestImplementsCommand for BitmapCommand {}
|
|
|
|
#[test]
|
|
fn command_code() {
|
|
assert_eq!(
|
|
BitmapCommand::try_from(Packet {
|
|
payload: None,
|
|
header: Header {
|
|
command_code: CommandCode::Brightness.into(),
|
|
..Default::default()
|
|
}
|
|
}),
|
|
Err(InvalidCommandCodeError(CommandCode::Brightness.into()).into())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn error_decompression_failed_win() {
|
|
for compression in CompressionCode::ALL.to_owned() {
|
|
let p: Packet = BitmapCommand {
|
|
origin: Origin::new(16, 8),
|
|
bitmap: Bitmap::new(8, 8).unwrap(),
|
|
compression: compression,
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
let Packet { header, payload } = p;
|
|
let mut payload = payload.unwrap();
|
|
|
|
// mangle it
|
|
for byte in &mut payload {
|
|
*byte -= *byte / 2;
|
|
}
|
|
|
|
let p = Packet {
|
|
header,
|
|
payload: Some(payload),
|
|
};
|
|
let result = TypedCommand::try_from(p);
|
|
if compression != CompressionCode::Uncompressed {
|
|
assert_eq!(
|
|
result,
|
|
Err(TryFromPacketError::DecompressionFailed)
|
|
);
|
|
} else {
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn into_command() {
|
|
let mut bitmap = Bitmap::max_sized();
|
|
bitmap.fill(true);
|
|
|
|
assert_eq!(
|
|
BitmapCommand::from(bitmap.clone()),
|
|
BitmapCommand {
|
|
bitmap,
|
|
origin: Origin::default(),
|
|
compression: CompressionCode::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn into_packet_out_of_range() {
|
|
let mut cmd = BitmapCommand::from(Bitmap::max_sized());
|
|
cmd.origin.x = usize::MAX;
|
|
assert!(matches!(
|
|
Packet::try_from(cmd),
|
|
Err(TryIntoPacketError::ConversionError(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn into_packet_invalid_alignment() {
|
|
let cmd = BitmapCommand {
|
|
bitmap: Bitmap::max_sized(),
|
|
compression: CompressionCode::Uncompressed,
|
|
origin: Origin::new(5, 0),
|
|
};
|
|
let packet = Packet::try_from(cmd).unwrap();
|
|
assert_eq!(
|
|
packet.header,
|
|
Header {
|
|
command_code: 19,
|
|
a: 0,
|
|
b: 0,
|
|
c: 56,
|
|
d: 160
|
|
}
|
|
);
|
|
|
|
let cmd = BitmapCommand {
|
|
bitmap: Bitmap::max_sized(),
|
|
compression: CompressionCode::Uncompressed,
|
|
origin: Origin::new(11, 0),
|
|
};
|
|
let packet = Packet::try_from(cmd).unwrap();
|
|
assert_eq!(
|
|
packet.header,
|
|
Header {
|
|
command_code: 19,
|
|
a: 1,
|
|
b: 0,
|
|
c: 56,
|
|
d: 160
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn round_trip() {
|
|
for compression in CompressionCode::ALL {
|
|
crate::commands::tests::round_trip(
|
|
BitmapCommand {
|
|
origin: Origin::ZERO,
|
|
bitmap: Bitmap::new(8, 2).unwrap(),
|
|
compression: *compression,
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn round_trip_ref() {
|
|
for compression in CompressionCode::ALL {
|
|
crate::commands::tests::round_trip_ref(
|
|
&BitmapCommand {
|
|
origin: Origin::ZERO,
|
|
bitmap: Bitmap::max_sized(),
|
|
compression: *compression,
|
|
}
|
|
.into(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_y() {
|
|
let command = BitmapCommand {
|
|
bitmap: Bitmap::new(8, 3).unwrap(),
|
|
origin: Origin::new(4, u16::MAX as usize + 1),
|
|
compression: CompressionCode::Uncompressed,
|
|
};
|
|
assert!(matches!(
|
|
Packet::try_from(command),
|
|
Err(TryIntoPacketError::ConversionError(_)),
|
|
))
|
|
}
|
|
}
|