sdocx 1.0.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::binary::Reader;
use crate::frame::Frame;
use crate::math::{checked_payload, read_count, read_latex};
use crate::{
    BoundingBox, Error, MathAngleType, ObjectMetadata, ObjectType, ParseLimits, Result,
    StoredObject, Stroke,
};

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FormulaMetadata {
    pub base: ObjectMetadata,
    pub has_trigonometry_calculation: bool,
    pub plottable: bool,
    pub latex: Vec<String>,
    pub latex_result_rect: Option<BoundingBox>,
    pub nine_patch_rect: Option<[i32; 4]>,
    pub latex_image_media_id: Option<i32>,
    pub latex_result: Vec<String>,
    pub angle_type: Option<MathAngleType>,
    pub font_size: Option<f32>,
    pub strokes: Vec<FormulaStroke>,
    pub answer_strokes: Vec<FormulaStroke>,
    pub answer: Option<String>,
    pub answer_stroke_color: Option<u32>,
    pub relative_original_formula_rect: Option<BoundingBox>,
    pub relative_original_answer_rect: Option<BoundingBox>,
    pub expression_type_raw: Option<u32>,
    pub label_graphs: Vec<FormulaLabelGraph>,
    pub substitution_latex: Vec<String>,
    pub property_mask: Vec<u8>,
    pub field_mask: Vec<u8>,
    pub fixed_trailing_data: Vec<u8>,
    pub flexible_trailing_data: Vec<u8>,
    pub trailing_data: Vec<u8>,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FormulaStroke {
    pub base: ObjectMetadata,
    pub stroke: Stroke,
    pub object_data: Vec<u8>,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FormulaLabelGraph {
    pub labels: Vec<FormulaLabel>,
    pub relations: Vec<FormulaLabelRelation>,
    pub start_label: u32,
    pub end_label: u32,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FormulaLabel {
    pub text: String,
    pub bbox: BoundingBox,
    pub stroke_indices: Vec<u32>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FormulaLabelRelation {
    pub from_label: u32,
    pub to_label: u32,
    pub kind_raw: u32,
}

impl FormulaLabelRelation {
    pub fn kind(&self) -> FormulaLabelRelationKind {
        self.kind_raw.into()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum FormulaLabelRelationKind {
    Unknown,
    Right,
    Subscript,
    Superscript,
    Inside,
    Below,
    Above,
    Index,
    Other(u32),
}

impl From<u32> for FormulaLabelRelationKind {
    fn from(value: u32) -> Self {
        match value {
            0 => Self::Unknown,
            1 => Self::Right,
            2 => Self::Subscript,
            3 => Self::Superscript,
            4 => Self::Inside,
            5 => Self::Below,
            6 => Self::Above,
            7 => Self::Index,
            value => Self::Other(value),
        }
    }
}

impl FormulaMetadata {
    pub fn parse_bytes(bytes: &[u8]) -> Result<Self> {
        Self::parse_bytes_with_limits(bytes, &ParseLimits::default())
    }

    pub fn parse_bytes_with_limits(bytes: &[u8], limits: &ParseLimits) -> Result<Self> {
        if bytes.len() as u64 > limits.max_entry_size {
            return Err(Error::LimitExceeded {
                resource: "formula payload size",
                limit: limits.max_entry_size,
                actual: bytes.len() as u64,
            });
        }
        FormulaDecoder {
            limits,
            entries: 0,
            strokes: 0,
        }
        .parse(bytes)
    }
}

impl StoredObject {
    pub fn formula_metadata(&self, page_bytes: &[u8]) -> Result<FormulaMetadata> {
        self.formula_metadata_with_limits(page_bytes, &ParseLimits::default())
    }

    pub fn formula_metadata_with_limits(
        &self,
        page_bytes: &[u8],
        limits: &ParseLimits,
    ) -> Result<FormulaMetadata> {
        let payload = checked_payload(self, page_bytes, ObjectType::Formula, limits)?;
        FormulaMetadata::parse_bytes_with_limits(payload, limits)
    }
}

struct FormulaDecoder<'a> {
    limits: &'a ParseLimits,
    entries: u64,
    strokes: usize,
}

impl FormulaDecoder<'_> {
    fn parse(mut self, payload: &[u8]) -> Result<FormulaMetadata> {
        let mut reader = Reader::new(payload, "formula object");
        let base = ObjectMetadata::read(&mut reader)?;
        let frame = Frame::read(&mut reader)?;
        frame.expect_kind(11)?;
        let mut flexible = Reader::new(frame.flexible, "formula flexible fields");
        let latex = if frame.fields.contains(0) {
            self.latex_list(&mut flexible)?
        } else {
            Vec::new()
        };
        let latex_result_rect = frame
            .fields
            .contains(1)
            .then(|| crate::object::read_bbox(&mut flexible))
            .transpose()?;
        let nine_patch_rect = frame
            .fields
            .contains(3)
            .then(|| {
                Ok::<_, Error>([
                    flexible.read_i32("nine-patch left")?,
                    flexible.read_i32("nine-patch top")?,
                    flexible.read_i32("nine-patch right")?,
                    flexible.read_i32("nine-patch bottom")?,
                ])
            })
            .transpose()?;
        let latex_image_media_id = frame
            .fields
            .contains(2)
            .then(|| flexible.read_i32("formula image media ID"))
            .transpose()?;
        let latex_result = if frame.fields.contains(4) {
            self.latex_list(&mut flexible)?
        } else {
            Vec::new()
        };
        let angle_type = frame
            .fields
            .contains(5)
            .then(|| flexible.read_u32("formula angle type").map(Into::into))
            .transpose()?;
        let font_size = frame
            .fields
            .contains(6)
            .then(|| flexible.read_f32("formula font size"))
            .transpose()?;
        let strokes = if frame.fields.contains(7) {
            self.stroke_list(&mut flexible)?
        } else {
            Vec::new()
        };
        let answer_strokes = if frame.fields.contains(8) {
            self.stroke_list(&mut flexible)?
        } else {
            Vec::new()
        };
        let answer = frame
            .fields
            .contains(9)
            .then(|| {
                flexible.read_utf16_u16_without_null_sentinel(
                    "formula answer",
                    self.limits.max_text_characters,
                )
            })
            .transpose()?;
        let answer_stroke_color = frame
            .fields
            .contains(10)
            .then(|| flexible.read_u32("formula answer stroke color"))
            .transpose()?;
        let relative_original_formula_rect = frame
            .fields
            .contains(11)
            .then(|| crate::object::read_bbox(&mut flexible))
            .transpose()?;
        let relative_original_answer_rect = frame
            .fields
            .contains(12)
            .then(|| crate::object::read_bbox(&mut flexible))
            .transpose()?;
        let expression_type_raw = frame
            .fields
            .contains(13)
            .then(|| flexible.read_u32("formula expression type"))
            .transpose()?;
        let label_graphs = if frame.fields.contains(14) {
            self.label_graphs(&mut flexible)?
        } else {
            Vec::new()
        };
        let substitution_latex = if frame.fields.contains(15) {
            self.latex_list(&mut flexible)?
        } else {
            Vec::new()
        };
        Ok(FormulaMetadata {
            base,
            has_trigonometry_calculation: frame.properties.contains(0),
            plottable: frame.properties.contains(1),
            latex,
            latex_result_rect,
            nine_patch_rect,
            latex_image_media_id,
            latex_result,
            angle_type,
            font_size,
            strokes,
            answer_strokes,
            answer,
            answer_stroke_color,
            relative_original_formula_rect,
            relative_original_answer_rect,
            expression_type_raw,
            label_graphs,
            substitution_latex,
            property_mask: frame.properties.bytes().to_vec(),
            field_mask: frame.fields.bytes().to_vec(),
            fixed_trailing_data: frame.fixed.to_vec(),
            flexible_trailing_data: flexible
                .read_bytes(flexible.remaining(), "formula flexible trailing data")?
                .to_vec(),
            trailing_data: reader
                .read_bytes(reader.remaining(), "formula trailing data")?
                .to_vec(),
        })
    }

    fn count(&mut self, reader: &mut Reader<'_>, minimum_record_size: usize) -> Result<usize> {
        read_count(reader, minimum_record_size, &mut self.entries, self.limits)
    }

    fn latex_list(&mut self, reader: &mut Reader<'_>) -> Result<Vec<String>> {
        let count = self.count(reader, 2)?;
        let mut values = Vec::with_capacity(count);
        for _ in 0..count {
            values.push(read_latex(reader, self.limits)?);
        }
        Ok(values)
    }

    fn stroke_list(&mut self, reader: &mut Reader<'_>) -> Result<Vec<FormulaStroke>> {
        let count = self.count(reader, 4)?;
        self.strokes = self
            .strokes
            .checked_add(count)
            .ok_or(Error::LimitExceeded {
                resource: "formula strokes",
                limit: self.limits.max_strokes_per_page as u64,
                actual: u64::MAX,
            })?;
        if self.strokes > self.limits.max_strokes_per_page {
            return Err(Error::LimitExceeded {
                resource: "formula strokes",
                limit: self.limits.max_strokes_per_page as u64,
                actual: self.strokes as u64,
            });
        }
        let mut strokes = Vec::with_capacity(count);
        for _ in 0..count {
            let size = reader.read_u32("formula stroke object size")? as usize;
            let data = reader.read_bytes(size, "formula stroke object")?;
            strokes.push(FormulaStroke {
                base: ObjectMetadata::read(&mut Reader::new(data, "formula stroke"))?,
                stroke: crate::decode::decode_stroke(data, self.limits)?,
                object_data: data.to_vec(),
            });
        }
        Ok(strokes)
    }

    fn label_graphs(&mut self, reader: &mut Reader<'_>) -> Result<Vec<FormulaLabelGraph>> {
        let count = self.count(reader, 16)?;
        let mut graphs = Vec::with_capacity(count);
        for _ in 0..count {
            let count = self.count(reader, 40)?;
            let mut labels = Vec::with_capacity(count);
            for _ in 0..count {
                let byte_count = reader.read_u32("formula label byte count")? as usize;
                let bytes = reader.read_bytes(byte_count, "formula label text")?;
                let text = std::str::from_utf8(bytes)
                    .map_err(|_| Error::Format("invalid UTF-8 in formula label".into()))?;
                let units = text.encode_utf16().count();
                if units > self.limits.max_text_characters {
                    return Err(Error::LimitExceeded {
                        resource: "text characters",
                        limit: self.limits.max_text_characters as u64,
                        actual: units as u64,
                    });
                }
                let bbox = crate::object::read_bbox(reader)?;
                let count = self.count(reader, 4)?;
                let mut stroke_indices = Vec::with_capacity(count);
                for _ in 0..count {
                    stroke_indices.push(reader.read_u32("formula label stroke index")?);
                }
                labels.push(FormulaLabel {
                    text: text.to_owned(),
                    bbox,
                    stroke_indices,
                });
            }
            let count = self.count(reader, 12)?;
            let mut relations = Vec::with_capacity(count);
            for _ in 0..count {
                relations.push(FormulaLabelRelation {
                    from_label: reader.read_u32("relation source label")?,
                    to_label: reader.read_u32("relation target label")?,
                    kind_raw: reader.read_u32("relation type")?,
                });
            }
            graphs.push(FormulaLabelGraph {
                labels,
                relations,
                start_label: reader.read_u32("label graph start label")?,
                end_label: reader.read_u32("label graph end label")?,
            });
        }
        Ok(graphs)
    }
}