boox-note-parser 0.2.0

A Rust library for parsing .note files from Onyx Boox e-ink devices.
Documentation
use zip::ZipArchive;

use crate::{
    id::{PointsUuid, ShapeGroupUuid, StrokeUuid},
    json::Dimensions,
    shape::json::{DisplayScale, LineStyle, LineStyleContainer},
    utils::{convert_timestamp_to_datetime, parse_json},
};

#[derive(Debug, Clone)]
pub struct ShapeGroup {
    shapes: Vec<Shape>,
}

impl ShapeGroup {
    pub fn read(mut reader: impl std::io::Read) -> crate::error::Result<Self> {
        let mut buf = Vec::new();
        reader.read_to_end(&mut buf)?;

        let reader = std::io::Cursor::new(buf);

        let mut archive = ZipArchive::new(reader)?;

        let mut reader = archive.by_index(0)?;

        let container = protobuf::ShapeContainer::read(&mut reader)?;
        let shapes = container
            .shapes
            .iter()
            .map(Shape::from_protobuf)
            .collect::<crate::error::Result<_>>()?;
        Ok(Self { shapes })
    }

    pub fn shapes(&self) -> &[Shape] {
        &self.shapes
    }
}

#[derive(Debug, Clone)]
pub struct Shape {
    pub stroke_id: StrokeUuid,
    pub created: chrono::DateTime<chrono::Utc>,
    pub modified: chrono::DateTime<chrono::Utc>,
    pub unknown: i64,
    pub stroke_width: f32,
    pub unknown_6: u32,
    pub bbox: Dimensions,
    pub render_scale: DisplayScale,
    pub z_order: i64,
    pub unknown_15: u32,
    pub points_id: Option<PointsUuid>,
    pub line_style: Option<LineStyle>,
    pub shape_group_id: ShapeGroupUuid,
    pub points_json: String,
}

impl Shape {
    fn from_protobuf(shape: &protobuf::Shape) -> crate::error::Result<Self> {
        Ok(Self {
            stroke_id: StrokeUuid::from_str(&shape.stroke_uuid)?,
            created: convert_timestamp_to_datetime(shape.created)?,
            modified: convert_timestamp_to_datetime(shape.modified)?,
            unknown: shape.unknown,
            stroke_width: shape.stroke_width,
            unknown_6: shape.unknown_6,
            bbox: parse_json(&shape.bbox_json)?,
            render_scale: parse_json(&shape.render_scale_json)?,
            z_order: shape.z_order,
            unknown_15: shape.unknown_15,
            points_id: if shape.points_uuid.is_empty() {
                None
            } else {
                Some(PointsUuid::from_str(&shape.points_uuid)?)
            },
            line_style: if shape.line_style_json.is_empty() {
                None
            } else {
                let line_style_container: LineStyleContainer = parse_json(&shape.line_style_json)?;
                Some(line_style_container.line_style)
            },
            shape_group_id: ShapeGroupUuid::from_str(&shape.shape_group_uuid)?,
            points_json: shape.empty_array_json.clone(),
        })
    }
}

mod json {
    use serde::Deserialize;

    #[derive(Debug, Clone, Deserialize)]
    // Retained for future render-scale fidelity work even when not yet consumed.
    #[allow(dead_code)]
    #[serde(rename_all = "camelCase")]
    pub struct DisplayScale {
        display_scale: f32,
        max_pressure: f32,
        #[serde(default)]
        revised_display_scale: f32,
        source: u32,
    }

    #[derive(Debug, Clone, Deserialize)]
    #[serde(rename_all = "camelCase")]
    pub struct LineStyleContainer {
        pub line_style: LineStyle,
    }

    #[derive(Debug, Clone, Deserialize)]
    #[serde(rename_all = "camelCase")]
    pub struct LineStyle {
        pub phase: f32,
        pub type_: u8,
    }
}

mod protobuf {
    use prost::Message;

    use crate::error::Result;

    #[derive(Clone, PartialEq, Message)]
    pub struct ShapeContainer {
        #[prost(message, repeated, tag = "1")]
        pub shapes: Vec<Shape>,
    }

    impl ShapeContainer {
        pub fn read(mut reader: impl std::io::Read) -> Result<Self> {
            let mut buf = Vec::new();
            reader.read_to_end(&mut buf)?;
            Ok(ShapeContainer::decode(&buf[..])?)
        }
    }

    #[derive(Clone, PartialEq, Message)]
    pub struct Shape {
        // Confirmed
        #[prost(string, tag = "1")]
        pub stroke_uuid: String,
        // Confirmed
        #[prost(uint64, tag = "2")]
        pub created: u64,
        // Confirmed
        #[prost(uint64, tag = "3")]
        pub modified: u64,
        // Observed as ARGB color encoded as signed integer value
        #[prost(int64, tag = "4")]
        pub unknown: i64,
        // Uncertain
        #[prost(float, tag = "5")]
        pub stroke_width: f32,
        // Observed on multi-layer pages; likely layer/state metadata.
        #[prost(uint32, tag = "6")]
        pub unknown_6: u32,
        // Confirmed
        #[prost(string, tag = "7")]
        pub bbox_json: String,
        // Confirmed
        #[prost(string, tag = "11")]
        pub render_scale_json: String,
        // Observed as pen/style type key (non-zigzag integer)
        #[prost(int64, tag = "12")]
        pub z_order: i64,
        // Observed on entries missing points UUID; likely tombstone/state marker.
        #[prost(uint32, tag = "15")]
        pub unknown_15: u32,
        // Confirmed
        #[prost(string, tag = "16")]
        pub points_uuid: String,
        // Confirmed
        #[prost(string, tag = "17")]
        pub line_style_json: String,
        // Confirmed
        #[prost(string, tag = "18")]
        pub shape_group_uuid: String,
        // Uncertain
        #[prost(string, tag = "21")]
        pub empty_array_json: String,
    }
}