tetratto_core/model/
carp.rs

1use serde::{Serialize, Deserialize};
2
3/// Starting at the beginning of the file, the header details specific information
4/// about the file.
5///
6/// 1. `CG` tag (2 bytes)
7/// 2. version number (2 bytes)
8/// 3. width of graph (4 bytes)
9/// 4. height of graph (4 bytes)
10/// 5. `END_OF_HEADER`
11///
12/// The header has a total of 13 bytes. (12 of info, 1 of `END_OF_HEADER)
13///
14/// Everything after `END_OF_HEADER` should be another command and its parameters.
15pub const END_OF_HEADER: u8 = 0x1a;
16/// The color command marks the beginning of a hex-encoded color **string**.
17///
18/// The hastag character should **not** be included.
19pub const COLOR: u8 = 0x1b;
20/// The size command marks the beginning of a integer brush size.
21pub const SIZE: u8 = 0x2b;
22/// Marks the beginning of a new line.
23pub const LINE: u8 = 0x3b;
24/// A point marks the coordinates (relative to the previous `DELTA_ORIGIN`, or `(0, 0)`)
25/// in which a point should be drawn.
26///
27/// The size and color are that of the previous `COLOR` and `SIZE` commands.
28///
29/// Points are two `u32`s (or 8 bytes in length).
30pub const POINT: u8 = 0x4b;
31/// An end-of-file marker.
32pub const EOF: u8 = 0x1f;
33
34/// A type of [`Command`].
35#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
36#[repr(u8)]
37pub enum CommandType {
38    /// [`END_OF_HEADER`]
39    EndOfHeader = END_OF_HEADER,
40    /// [`COLOR`]
41    Color = COLOR,
42    /// [`SIZE`]
43    Size = SIZE,
44    /// [`LINE`]
45    Line = LINE,
46    /// [`POINT`]
47    Point = POINT,
48    /// [`EOF`]
49    Eof = EOF,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone)]
53pub struct Command {
54    /// The type of the command.
55    pub r#type: CommandType,
56    /// Raw data as bytes.
57    pub data: Vec<u8>,
58}
59
60impl From<Command> for Vec<u8> {
61    fn from(val: Command) -> Self {
62        let mut d = val.data;
63        d.insert(0, val.r#type as u8);
64        d
65    }
66}
67
68/// A graph is CarpGraph's representation of an image. It's essentially just a
69/// reproducable series of commands which a renderer can traverse to reconstruct
70/// an image.
71#[derive(Serialize, Deserialize, Debug)]
72pub struct CarpGraph {
73    pub header: Vec<u8>,
74    pub dimensions: (u32, u32),
75    pub commands: Vec<Command>,
76}
77
78macro_rules! select_bytes {
79    ($count:literal, $from:ident) => {{
80        let mut data: Vec<u8> = Vec::new();
81        let mut seen_bytes = 0;
82
83        while let Some((_, byte)) = $from.next() {
84            seen_bytes += 1;
85            data.push(byte.to_owned());
86
87            if seen_bytes == $count {
88                // we only need <count> bytes, stop just before we eat the next byte
89                break;
90            }
91        }
92
93        data
94    }};
95}
96
97macro_rules! spread {
98    ($into:ident, $from:expr) => {
99        for byte in &$from {
100            $into.push(byte.to_owned())
101        }
102    };
103}
104
105impl CarpGraph {
106    pub fn to_bytes(&self) -> Vec<u8> {
107        let mut out: Vec<u8> = Vec::new();
108
109        // reconstruct header
110        spread!(out, self.header);
111        spread!(out, self.dimensions.0.to_be_bytes()); // width
112        spread!(out, self.dimensions.1.to_be_bytes()); // height
113        out.push(END_OF_HEADER);
114
115        // reconstruct commands
116        for command in &self.commands {
117            out.push(command.r#type as u8);
118            spread!(out, command.data);
119        }
120
121        // ...
122        out.push(EOF);
123        out
124    }
125
126    pub fn from_bytes(bytes: Vec<u8>) -> Self {
127        let mut header: Vec<u8> = Vec::new();
128        let mut dimensions: (u32, u32) = (0, 0);
129        let mut commands: Vec<Command> = Vec::new();
130
131        let mut in_header: bool = true;
132        let mut byte_buffer: Vec<u8> = Vec::new(); // storage for bytes which need to construct a bigger type (like `u32`)
133
134        let mut bytes_iter = bytes.iter().enumerate();
135        while let Some((i, byte)) = bytes_iter.next() {
136            let byte = byte.to_owned();
137            match byte {
138                END_OF_HEADER => in_header = false,
139                COLOR => {
140                    let data = select_bytes!(6, bytes_iter);
141                    commands.push(Command {
142                        r#type: CommandType::Color,
143                        data,
144                    });
145                }
146                SIZE => {
147                    let data = select_bytes!(2, bytes_iter);
148                    commands.push(Command {
149                        r#type: CommandType::Size,
150                        data,
151                    });
152                }
153                POINT => {
154                    let data = select_bytes!(8, bytes_iter);
155                    commands.push(Command {
156                        r#type: CommandType::Point,
157                        data,
158                    });
159                }
160                LINE => commands.push(Command {
161                    r#type: CommandType::Line,
162                    data: Vec::new(),
163                }),
164                EOF => break,
165                _ => {
166                    if in_header {
167                        if (0..2).contains(&i) {
168                            // tag
169                            header.push(byte);
170                        } else if (2..4).contains(&i) {
171                            // version
172                            header.push(byte);
173                        } else if (4..8).contains(&i) {
174                            // width
175                            byte_buffer.push(byte);
176
177                            if i == 7 {
178                                // end, construct from byte buffer
179                                let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
180                                dimensions.0 = u32::from_be_bytes(bytes.try_into().unwrap());
181                                byte_buffer = Vec::new();
182                            }
183                        } else if (8..12).contains(&i) {
184                            // height
185                            byte_buffer.push(byte);
186
187                            if i == 11 {
188                                // end, construct from byte buffer
189                                let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
190                                dimensions.1 = u32::from_be_bytes(bytes.try_into().unwrap());
191                                byte_buffer = Vec::new();
192                            }
193                        }
194                    } else {
195                        // misc byte
196                        println!("extraneous byte at {i}");
197                    }
198                }
199            }
200        }
201
202        Self {
203            header,
204            dimensions,
205            commands,
206        }
207    }
208
209    pub fn to_svg(&self) -> String {
210        let mut out: String = String::new();
211        out.push_str(&format!(
212            "<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" style=\"background: white; width: {}px; height: {}px\" class=\"carpgraph\">",
213            self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1
214        ));
215
216        // add lines
217        let mut stroke_size: u16 = 2;
218        let mut stroke_color: String = "000000".to_string();
219
220        let mut previous_x_y: Option<(u32, u32)> = None;
221        let mut line_path = String::new();
222
223        for command in &self.commands {
224            match command.r#type {
225                CommandType::Size => {
226                    let (bytes, _) = command.data.split_at(size_of::<u16>());
227                    stroke_size = u16::from_be_bytes(bytes.try_into().unwrap_or([0, 0]));
228                }
229                CommandType::Color => {
230                    stroke_color =
231                        String::from_utf8(command.data.to_owned()).unwrap_or("#000000".to_string())
232                }
233                CommandType::Line => {
234                    if !line_path.is_empty() {
235                        out.push_str(&format!(
236                            "<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
237                        ));
238                    }
239
240                    previous_x_y = None;
241                    line_path = String::new();
242                }
243                CommandType::Point => {
244                    let (x, y) = command.data.split_at(size_of::<u32>());
245                    let point = ({ u32::from_be_bytes(x.try_into().unwrap()) }, {
246                        u32::from_be_bytes(y.try_into().unwrap())
247                    });
248
249                    // add to path string
250                    line_path.push_str(&format!(
251                        " M{} {}{}",
252                        point.0,
253                        point.1,
254                        if let Some(pxy) = previous_x_y {
255                            // line to there
256                            format!(" L{} {}", pxy.0, pxy.1)
257                        } else {
258                            String::new()
259                        }
260                    ));
261
262                    previous_x_y = Some((point.0, point.1));
263
264                    // add circular point
265                    out.push_str(&format!(
266                        "<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"#{stroke_color}\" />",
267                        point.0,
268                        point.1,
269                        stroke_size / 2 // the size is technically the diameter of the circle
270                    ));
271                }
272                _ => unreachable!("never pushed to commands"),
273            }
274        }
275
276        if !line_path.is_empty() {
277            out.push_str(&format!(
278                "<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
279            ));
280        }
281
282        // return
283        format!("{out}</svg>")
284    }
285}