use serde::{Serialize, Deserialize};
pub const END_OF_HEADER: u8 = 0x1a;
pub const COLOR: u8 = 0x1b;
pub const SIZE: u8 = 0x2b;
pub const LINE: u8 = 0x3b;
pub const POINT: u8 = 0x4b;
pub const EOF: u8 = 0x1f;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CommandType {
EndOfHeader = END_OF_HEADER,
Color = COLOR,
Size = SIZE,
Line = LINE,
Point = POINT,
Eof = EOF,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Command {
pub r#type: CommandType,
pub data: Vec<u8>,
}
impl From<Command> for Vec<u8> {
fn from(val: Command) -> Self {
let mut d = val.data;
d.insert(0, val.r#type as u8);
d
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CarpGraph {
pub header: Vec<u8>,
pub dimensions: (u32, u32),
pub commands: Vec<Command>,
}
macro_rules! select_bytes {
($count:literal, $from:ident) => {{
let mut data: Vec<u8> = Vec::new();
let mut seen_bytes = 0;
while let Some((_, byte)) = $from.next() {
seen_bytes += 1;
data.push(byte.to_owned());
if seen_bytes == $count {
break;
}
}
data
}};
}
macro_rules! spread {
($into:ident, $from:expr) => {
for byte in &$from {
$into.push(byte.to_owned())
}
};
}
impl CarpGraph {
pub fn to_bytes(&self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
spread!(out, self.header);
spread!(out, self.dimensions.0.to_be_bytes()); spread!(out, self.dimensions.1.to_be_bytes()); out.push(END_OF_HEADER);
for command in &self.commands {
out.push(command.r#type as u8);
spread!(out, command.data);
}
out.push(EOF);
out
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
let mut header: Vec<u8> = Vec::new();
let mut dimensions: (u32, u32) = (0, 0);
let mut commands: Vec<Command> = Vec::new();
let mut in_header: bool = true;
let mut byte_buffer: Vec<u8> = Vec::new();
let mut bytes_iter = bytes.iter().enumerate();
while let Some((i, byte)) = bytes_iter.next() {
let byte = byte.to_owned();
match byte {
END_OF_HEADER => in_header = false,
COLOR => {
let data = select_bytes!(6, bytes_iter);
commands.push(Command {
r#type: CommandType::Color,
data,
});
}
SIZE => {
let data = select_bytes!(2, bytes_iter);
commands.push(Command {
r#type: CommandType::Size,
data,
});
}
POINT => {
let data = select_bytes!(8, bytes_iter);
commands.push(Command {
r#type: CommandType::Point,
data,
});
}
LINE => commands.push(Command {
r#type: CommandType::Line,
data: Vec::new(),
}),
EOF => break,
_ => {
if in_header {
if (0..2).contains(&i) {
header.push(byte);
} else if (2..4).contains(&i) {
header.push(byte);
} else if (4..8).contains(&i) {
byte_buffer.push(byte);
if i == 7 {
let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
dimensions.0 = u32::from_be_bytes(bytes.try_into().unwrap());
byte_buffer = Vec::new();
}
} else if (8..12).contains(&i) {
byte_buffer.push(byte);
if i == 11 {
let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
dimensions.1 = u32::from_be_bytes(bytes.try_into().unwrap());
byte_buffer = Vec::new();
}
}
} else {
println!("extraneous byte at {i}");
}
}
}
}
Self {
header,
dimensions,
commands,
}
}
pub fn to_svg(&self) -> String {
let mut out: String = String::new();
out.push_str(&format!(
"<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" style=\"background: white; width: {}px; height: {}px\" class=\"carpgraph\">",
self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1
));
let mut stroke_size: u16 = 2;
let mut stroke_color: String = "000000".to_string();
let mut previous_x_y: Option<(u32, u32)> = None;
let mut line_path = String::new();
for command in &self.commands {
match command.r#type {
CommandType::Size => {
let (bytes, _) = command.data.split_at(size_of::<u16>());
stroke_size = u16::from_be_bytes(bytes.try_into().unwrap_or([0, 0]));
}
CommandType::Color => {
stroke_color =
String::from_utf8(command.data.to_owned()).unwrap_or("#000000".to_string())
}
CommandType::Line => {
if !line_path.is_empty() {
out.push_str(&format!(
"<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
));
}
previous_x_y = None;
line_path = String::new();
}
CommandType::Point => {
let (x, y) = command.data.split_at(size_of::<u32>());
let point = ({ u32::from_be_bytes(x.try_into().unwrap()) }, {
u32::from_be_bytes(y.try_into().unwrap())
});
line_path.push_str(&format!(
" M{} {}{}",
point.0,
point.1,
if let Some(pxy) = previous_x_y {
format!(" L{} {}", pxy.0, pxy.1)
} else {
String::new()
}
));
previous_x_y = Some((point.0, point.1));
out.push_str(&format!(
"<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"#{stroke_color}\" />",
point.0,
point.1,
stroke_size / 2 ));
}
_ => unreachable!("never pushed to commands"),
}
}
if !line_path.is_empty() {
out.push_str(&format!(
"<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
));
}
format!("{out}</svg>")
}
}