binarygcode 0.0.3

A Rust implementation of libbgcode to serialise and deserialise binary gcode.
Documentation
use core::str;

use crate::components::common::{
    BinaryGcodeError, BlockKind, Checksum, CompressionAlgorithm, Encoding,
};
use crate::components::deserialiser::{DeserialisedResult, Deserialiser};
use crate::components::serialiser::{serialise_block, serialise_file_header};
use alloc::string::ToString;
use alloc::{borrow::ToOwned, boxed::Box, vec::Vec};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use regex::Regex;

/// Provide a reference to a u8 slice of the entire binary file
/// you would like to decode.
pub fn binary_to_ascii(
    binary: &[u8],
    with_block_comments: bool,
) -> Result<Box<str>, BinaryGcodeError> {
    let mut out = Vec::new();
    let mut deserialiser = Deserialiser::default();
    deserialiser.digest(binary);

    // Loop through running deserialise on the deserialisers inner
    // buffer with it returning either a header, block or request for more bytes.
    // Or an error when deserialising.
    loop {
        let r = deserialiser.deserialise()?;
        match r {
            DeserialisedResult::FileHeader(_) => {}
            DeserialisedResult::Block(mut b) => {
                b.to_ascii(&mut out, with_block_comments)?;
            }
            DeserialisedResult::MoreBytesRequired(_) => {
                break;
            }
        }
    }

    let gcode = str::from_utf8(&out).unwrap().to_owned().into_boxed_str();
    Ok(gcode)
}

/// Returns a bgcode from an ascii binary
///
/// Notes:
/// Maintains the comments `;` in the metadata lines that duplicates
/// on the deserialise. Could add a check if they exist on the deserialise side
/// and add them if not. And need to remove them on this side to save space??
pub fn ascii_to_binary(ascii: &str) -> Result<Box<[u8]>, BinaryGcodeError> {
    let mut binary: Vec<u8> = Vec::new();
    let header = serialise_file_header(1, Checksum::Crc32);
    binary.extend(header);

    // File metadata
    if let Some(start) = ascii.find("; generated by") {
        let needle = "\n\n";
        if let Some(end) = ascii[start..].find(needle) {
            let block_data = &ascii[start..start + end + needle.len()];
            let block = serialise_block(
                BlockKind::FileMetadata,
                CompressionAlgorithm::None,
                Encoding::Ini,
                Checksum::Crc32,
                &[],
                block_data.as_bytes(),
            )?;
            binary.extend(block);
        } else {
            return Err(BinaryGcodeError::SerialiseError("file_metadata"));
        }
    }

    // Printer Metadata
    if let Some(start) = ascii.find("; printer_model") {
        let needle = "\n\n";
        if let Some(end) = ascii[start..].find(needle) {
            let block_data = &ascii[start..start + end + needle.len()];
            let block = serialise_block(
                BlockKind::PrinterMetadata,
                CompressionAlgorithm::None,
                Encoding::Ini,
                Checksum::Crc32,
                &[],
                block_data.as_bytes(),
            )?;
            binary.extend(block);
        } else {
            return Err(BinaryGcodeError::SerialiseError("printer_metadata"));
        }
    }

    // Thumbnails
    let mut inner = ascii;
    while let Some(start) = inner.find("thumbnail begin") {
        let needle = "; thumbnail end";
        if let Some(end) = inner[start..].find("; thumbnail end") {
            let block =
                thumbnail_block(&inner[start..start + end + needle.len()])?;
            binary.extend(block);
            // continue along the str
            inner = &inner[start + end + needle.len()..];
        } else {
            return Err(BinaryGcodeError::SerialiseError("thumbnail"));
        }
    }

    // Gcode
    if let Some(start) = ascii.find("M73 P0") {
        let needle = "M73 P100 R0\n";
        if let Some(end) = ascii[start..].find(needle) {
            let gcode = &ascii[start..start + end + needle.len()];
            // Need to chunk it up to account for the u16 slice input buffer.
            let mut chunk: Vec<u8> = Vec::new();
            for b in gcode.as_bytes() {
                chunk.push(*b);
                // If the chunk is nearing max u16 and
                // we reach a new line then encode it.
                // TODO: decide what is a reasonable size gcode chunk
                // and check against the libgcode reference.
                if u16::MAX - (chunk.len() as u16) < 100 && *b == 10 {
                    let block = serialise_block(
                        BlockKind::GCode,
                        CompressionAlgorithm::Heatshrink11_4,
                        Encoding::Ascii,
                        Checksum::Crc32,
                        &[],
                        &chunk,
                    )?;
                    binary.extend(block);
                    chunk.clear();
                }
            }

            // One remaining chunk
            if !chunk.is_empty() {
                let block = serialise_block(
                    BlockKind::GCode,
                    CompressionAlgorithm::Heatshrink11_4,
                    Encoding::Ascii,
                    Checksum::Crc32,
                    &[],
                    &chunk,
                )?;
                binary.extend(block);
                chunk.clear();
            }
        }
    }

    // Slicer config (prusa slicer only atm)
    if let Some(start) = ascii.find("; prusaslicer_config = begin") {
        let needle = "; prusaslicer_config = end";
        if let Some(end) = ascii[start..].find(needle) {
            let block_data = &ascii[start..start + end + needle.len()];
            let block = serialise_block(
                BlockKind::SlicerMetadata,
                CompressionAlgorithm::Deflate,
                Encoding::Ini,
                Checksum::Crc32,
                &[],
                block_data.as_bytes(),
            )?;
            binary.extend(block);
        } else {
            return Err(BinaryGcodeError::SerialiseError("slicer_config"));
        }
    }

    Ok(binary.into_boxed_slice())
}

fn thumbnail_block(thumb: &str) -> Result<Box<[u8]>, BinaryGcodeError> {
    // TODO: Add checks to the &str input

    let (left, right) = thumb.split_once(";").unwrap();

    // Left is the header and will be used to construct
    // the parameter bytes that come before the body.
    let mut encoding = Encoding::Png;
    if left.contains("_QOI") {
        encoding = Encoding::Qoi;
    } else if left.contains("_JPG") {
        encoding = Encoding::Jpg;
    }

    let re = Regex::new(r"\d+x\d+").unwrap();
    let m = re.find(left);
    if m.is_none() {
        return Err(BinaryGcodeError::DevError(left.to_string()));
    }
    let m = m.unwrap().as_str();
    let (w, h) = m.split_once("x").unwrap();
    let w = w.parse::<u16>();
    if w.is_err() {
        return Err(BinaryGcodeError::DevError("width_error".to_string()));
    }
    let w = w.unwrap();
    let h = h.parse::<u16>();
    if h.is_err() {
        return Err(BinaryGcodeError::DevError("height_error".to_string()));
    }
    let h = h.unwrap();

    let mut parameters: Vec<u8> = Vec::new();
    // parameters beyond the encoding
    parameters.extend(w.to_le_bytes());
    parameters.extend(h.to_le_bytes());

    let mut right = right.to_string();
    right = right.replace("\n; ", "");
    right = right.replace("thumbnail end", "");
    let right = right.trim();
    let data = BASE64_STANDARD.decode(right);
    if data.is_err() {
        return Err(BinaryGcodeError::DevError(right.to_string()));
    }
    let data = data.unwrap();

    serialise_block(
        BlockKind::Thumbnail,
        CompressionAlgorithm::None,
        encoding,
        Checksum::Crc32,
        &parameters,
        &data,
    )
}

#[cfg(test)]
mod tests {
    use super::thumbnail_block;

    #[test]
    fn convert_thumbnail_block() {
        let thumb = "thumbnail begin 16x16 616
; iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABlElEQVR4AY2SP0/CUBTF76KLLg66Ob
; i5+CchMQYH458ogghIabEUWkoLGBQ1UQYXEzVxkLg56OrgLF/ARBeNLn6iI/eRV2kAYTjJ7c05v56+
; Pkomk5BSFMWn2+yckKHGfPv2DMllKpXylNXiaDjj+NknIZ7ddMTnkTmSC1VVha6zC16wXZ/lIdRz85
; 5P5ogfNE0TerKnuobb1XAmYKZ3hZ+zxENJj+KtNNI33N6mZqwKCOXzeVQTATwrhO/SYIDHKMHdmoFl
; WSDDMHCWDuJmnXAfIbzbvYOvFuFyhXC8SKjEAigUCiBd11GNBwSAVd8gvOx1tuGGHJQqhmfB2RYg8Q
; eQemjW/HAIX0XC3aY/zOJP+Bcg29SWOsM+QCaT6QlgdQtLAGcFoKCFcREaGxhwsDwGUwm3AFyDh5yu
; 4nRnui+gFJoG/znOeGfADwKSy+HEiuEqNNoBOAoO4zC9JjzS7wPwpahUKjBNE2azzXlk0gNw5Wxzx2
; 92XVd4PQAPjuOgXC571aT4cJ3tgG8nzqx5gWzbFv5fUBP7TVgxxNgAAAAASUVORK5CYII=
; thumbnail end";

        let _ = thumbnail_block(thumb).expect("Error making thumbnail");
    }
}