msla_format 0.2.0

Library for encoding and decoding various MSLA file formats: Elegoo (.goo), Chitu Encrypted (.ctb), NanoDLP (.nanodlp).
Documentation
use std::{
    io::{Cursor, Read, Seek, Write},
    time::{SystemTime, UNIX_EPOCH},
};

use anyhow::Result;
use crate::{
    container::{Image, Run},
    progress::Progress,
    serde::{DynamicSerializer, Serializer},
    slice::{Format, SliceInfo, SliceResult, SlicedFile},
};
use image::{DynamicImage, RgbaImage};
use nalgebra::{Vector2, Vector3};
use serde::Serialize;
use zip::{ZipArchive, ZipWriter, write::FileOptions};

use crate::nanodlp::{
    Layer, LayerDecoder, LayerEncoder, decode_png, encode_png,
    layer::layers_bounds,
    read_to_bytes,
    types::{
        Color, LayerInfo, Meta, Options, Plate, Profile, SHIELD_AFTER_LAYER, SHIELD_BEFORE_LAYER,
    },
};

/// NanoDLP file.
pub struct File {
    pub meta: Meta,
    pub plate: Plate,
    pub options: Options,
    pub profile: Profile,
    pub preview: DynamicImage,

    pub layer_info: Vec<LayerInfo>,
    pub layers: Vec<Vec<u8>>, // layers encoded as png
}

impl File {
    pub fn from_slice_result(result: SliceResult<Layer>) -> Self {
        let (layers, layer_info): (Vec<_>, Vec<_>) =
            result.layers.into_iter().map(|x| (x.inner, x.info)).unzip();

        let config = result.slice_config;
        let pixel_size = Vector2::new(
            config.platform_size.x / config.platform_resolution.x as f32,
            config.platform_size.y / config.platform_resolution.y as f32,
        );
        let voxel_volume = pixel_size.x * pixel_size.y * config.slice_height;

        let timestamp = (SystemTime::now().duration_since(UNIX_EPOCH).unwrap()).as_secs();
        let (min, max) = layers_bounds(config, &layer_info);

        Self {
            meta: Default::default(),
            plate: Plate {
                processed: true,
                total_solid_area: voxel_volume.convert() * result.voxels as f32,
                layers_count: layers.len() as u32,
                x_min: min.x,
                x_max: max.x,
                y_min: min.y,
                y_max: max.y,
                z_min: min.z,
                z_max: max.z,
                ..Default::default()
            },
            options: Options {
                p_width: config.platform_resolution.x,
                p_height: config.platform_resolution.y,
                thickness: config.slice_height.convert(),
                x_offset: config.platform_resolution.x / 2,
                y_offset: config.platform_resolution.y / 2,
                x_pixel_size: pixel_size.x,
                y_pixel_size: pixel_size.y,
                x_res: pixel_size.x.convert(),
                y_res: pixel_size.y.convert(),
                ignore_mask: 1,
                image_mirror: 1,
                display_controller: 1,
                support_layer_number: config.first_layers,
                fill_color: "#ffffff".into(),
                blank_color: "#000000".into(),
                fill_color_rgb: Color::repeat(255),
                blank_color_rgb: Color::repeat(0),
                ..Default::default()
            },
            profile: Profile {
                title: "msla_format Config".into(),
                depth: config.slice_height.convert(),
                support_depth: config.slice_height.convert(),
                transitional_layer: config.transition_layers,
                updated: timestamp as u32,
                cure_time: config.exposure_config.exposure_time,
                support_cure_time: config.first_exposure_config.exposure_time,
                fill_color: "#ffffff".into(),
                blank_color: "#000000".into(),
                ignore_mask: 1,
                shield_before_layer: SHIELD_BEFORE_LAYER.into(),
                shield_after_layer: SHIELD_AFTER_LAYER.into(),
                ..Default::default()
            },
            preview: Default::default(), // overwritten later

            layer_info,
            layers,
        }
    }

    pub fn serialize<T: Serializer>(&self, ser: &mut T, progress: Progress) -> Result<()> {
        let mut bytes = Vec::new();
        let mut zip = ZipWriter::new(Cursor::new(&mut bytes));

        fn serialize_file<W, T>(zip: &mut ZipWriter<W>, name: &str, value: &T) -> Result<()>
        where
            W: Write + Seek,
            T: Serialize,
        {
            zip.start_file(name, FileOptions::DEFAULT)?;
            serde_json::to_writer_pretty(zip, &value)?;
            Ok(())
        }

        serialize_file(&mut zip, "meta.json", &self.meta)?;
        serialize_file(&mut zip, "info.json", &self.layer_info)?;
        serialize_file(&mut zip, "plate.json", &self.plate)?;
        serialize_file(&mut zip, "profile.json", &self.profile)?;

        // The actual NanoDLP software seems to work with options.json, but this
        // is not currently recognized by UVTools like slicer.json is. Not sure
        // whats going on here, but to maximize compatibility Ill just output
        // both?
        serialize_file(&mut zip, "options.json", &self.options)?;
        serialize_file(&mut zip, "slicer.json", &self.options)?;

        zip.start_file("3d.png", FileOptions::DEFAULT)?;
        zip.write_all(&encode_png(&self.preview)?)?;

        progress.set_total(self.layers.len() as u64);
        for (i, layer) in self.layers.iter().enumerate() {
            progress.set_complete(i as u64);
            zip.start_file(format!("{}.png", i + 1), FileOptions::DEFAULT)?;
            zip.write_all(layer)?;
        }

        drop(zip);
        ser.write_bytes(&bytes);
        Ok(())
    }

    pub fn deserialize<T: Read + Seek>(reader: T) -> Result<Self> {
        let mut zip = ZipArchive::new(reader)?;

        let meta = (zip.by_name("meta.json").ok())
            .map(serde_json::from_reader::<_, Meta>)
            .transpose()?
            .unwrap_or_default();
        let layer_info = serde_json::from_reader::<_, Vec<LayerInfo>>(zip.by_name("info.json")?)?;
        let plate = serde_json::from_reader::<_, Plate>(zip.by_name("plate.json")?)?;
        let profile = serde_json::from_reader::<_, Profile>(zip.by_name("profile.json")?)?;

        let mut options = zip.by_name("options.json");
        if options.is_err() {
            drop(options);
            options = zip.by_name("slicer.json");
        }

        let options = serde_json::from_reader::<_, Options>(options?)?;

        let preview = decode_png(&read_to_bytes(zip.by_name("3d.png")?)?)?;
        let layers = (0..layer_info.len())
            .map(|i| read_to_bytes(zip.by_name(&format!("{}.png", i + 1))?))
            .collect::<Result<Vec<_>>>()?;

        Ok(File {
            meta,
            plate,
            options,
            profile,
            preview,

            layer_info,
            layers,
        })
    }
}

impl SlicedFile for File {
    fn serialize(&self, ser: &mut DynamicSerializer, progress: Progress) {
        self.serialize(ser, progress).unwrap();
    }

    fn set_preview(&mut self, preview: &RgbaImage) {
        self.preview = preview.to_owned().into();
    }

    fn info(&self) -> SliceInfo {
        SliceInfo {
            layers: self.layer_info.len() as u32,
            resolution: Vector2::new(self.options.p_width, self.options.p_height),
            size: Vector3::default(), // todo: this
            bottom_layers: self.profile.support_layer_number,
        }
    }

    fn format(&self) -> Format {
        Format::NanoDLP
    }

    fn runs(&self, layer: usize) -> Box<dyn Iterator<Item = Run> + '_> {
        let decoder = LayerDecoder::new(&self.layers[layer]);
        Box::new(decoder.runs().collect::<Vec<_>>().into_iter())
    }

    fn overwrite_layer(&mut self, layer: usize, image: Image) {
        let encoder = LayerEncoder::from_image(image);
        self.layers[layer] = encoder.image_data();
    }
}