sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::{BoundingBox, DocumentMetadata, Page};

#[derive(Clone, Copy)]
pub(in crate::render) struct TableExportPage {
    body_bounds: [f32; 4],
    origin: [f64; 2],
}

impl TableExportPage {
    fn new(size: [i32; 2], margins: [f32; 2]) -> Self {
        Self {
            body_bounds: [0.0, margins[0], size[0] as f32, size[1] as f32 - margins[1]],
            origin: [0.0; 2],
        }
    }

    pub fn for_page(page: &Page, metadata: &DocumentMetadata) -> Option<Self> {
        let size = [
            i32::try_from(page.width).ok()?,
            i32::try_from(page.height).ok()?,
        ];
        let margins = if metadata.page_mode == Some(0) {
            let stored = metadata
                .note_text
                .as_ref()
                .and_then(|text| text.margins)
                .unwrap_or([0.0; 4]);
            [stored[1], stored[3]].map(|margin| margin * metadata.document_density())
        } else {
            [0.0; 2]
        };
        margins
            .into_iter()
            .all(f32::is_finite)
            .then(|| Self::new(size, margins))
    }

    pub fn translated(mut self, dx: f64, dy: f64) -> Self {
        self.origin[0] += dx;
        self.origin[1] += dy;
        self
    }

    pub fn has_zero_origin(self) -> bool {
        self.origin == [0.0; 2]
    }

    pub fn artwork_bounds(self, measured: BoundingBox) -> BoundingBox {
        let local = [
            (measured.x_min - self.origin[0]) as f32,
            (measured.y_min - self.origin[1]) as f32,
            (measured.x_max - self.origin[0]) as f32,
            (measured.y_max - self.origin[1]) as f32,
        ];
        let mut bounds = self.body_bounds;
        if bounds[0] < local[2]
            && local[0] < bounds[2]
            && bounds[1] < local[3]
            && local[1] < bounds[3]
        {
            bounds = [
                bounds[0].max(local[0]),
                bounds[1].max(local[1]),
                bounds[2].min(local[2]),
                bounds[3].min(local[3]),
            ];
        }
        let bounds = expanded_bounds(bounds);
        BoundingBox {
            x_min: bounds.x_min + self.origin[0],
            y_min: bounds.y_min + self.origin[1],
            x_max: bounds.x_max + self.origin[0],
            y_max: bounds.y_max + self.origin[1],
        }
    }
}

pub(in crate::render) fn artwork_bounds(
    measured: BoundingBox,
    page: Option<TableExportPage>,
) -> BoundingBox {
    page.map_or_else(
        || {
            expanded_bounds(
                [
                    measured.x_min,
                    measured.y_min,
                    measured.x_max,
                    measured.y_max,
                ]
                .map(|value| value as f32),
            )
        },
        |page| page.artwork_bounds(measured),
    )
}

fn expanded_bounds([left, top, right, bottom]: [f32; 4]) -> BoundingBox {
    let [x_min, y_min, x_max, y_max] = [
        (left - 1.0).floor(),
        (top - 1.0).floor(),
        (right + 1.0).ceil(),
        (bottom + 1.0).ceil(),
    ]
    .map(f64::from);
    BoundingBox {
        x_min,
        y_min,
        x_max,
        y_max,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;
    use sha2::{Digest, Sha256};

    #[derive(Deserialize)]
    struct Capture {
        apk_version: String,
        apk_sha256: String,
        allocation_fills: Vec<u8>,
        model_library_sha256: String,
        drawing_library_sha256: String,
        base_library_sha256: String,
        composer_library_sha256: String,
        constructor_address: String,
        crop_start: String,
        crop_end: String,
        measurement_inputs: String,
        cases: Vec<Case>,
    }

    #[derive(Deserialize)]
    struct Case {
        name: String,
        page_size: [i32; 2],
        scaled_margins: [f32; 2],
        measured_bbox: [f32; 4],
        background_crop: [f32; 4],
        default_focus: [f32; 4],
        default_clip_counts: [usize; 3],
    }

    fn bounds([left, top, right, bottom]: [f32; 4]) -> BoundingBox {
        BoundingBox {
            x_min: f64::from(left),
            y_min: f64::from(top),
            x_max: f64::from(right),
            y_max: f64::from(bottom),
        }
    }

    #[test]
    fn native_export_crops_match_every_coordinate_bit() {
        let bytes = include_bytes!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../conformance/table-export-clipping.json"
        ));
        assert_eq!(
            format!("{:x}", Sha256::digest(bytes)),
            "872b937dedb795d3b995249c4ab26ae421d3f6f0eaec909ccf396038cf7d38b8"
        );
        let capture: Capture = serde_json::from_slice(bytes).unwrap();
        assert_eq!(capture.apk_version, "4.4.45.37");
        assert_eq!(
            capture.apk_sha256,
            "daed1eff8c8ee9dfb8afe2771e39e893a8808f3230d6d522a8aa647db09b8667"
        );
        assert_eq!(capture.allocation_fills, [0, 165, 255]);
        assert_eq!(
            capture.model_library_sha256,
            "4fbcf6d4213e929f1535d32abb487743643fd5d0dfc366e50dfeb2e7d8015b7a"
        );
        assert_eq!(
            capture.drawing_library_sha256,
            "788bf413ddeb0b9d352062c5f1b7b8ed11babca911df72691da58ff1a0a5a4bd"
        );
        assert_eq!(
            capture.base_library_sha256,
            "e10da0116946691cf68302437ef261282e1dfe0eec15bf2dfa66093286985deb"
        );
        assert_eq!(
            capture.composer_library_sha256,
            "52b83157198368da3a3855a721bfc7d3aafde4e644ce25b5d6eab3b6b510d39f"
        );
        assert_eq!(capture.constructor_address, "0xa5634");
        assert_eq!(capture.crop_start, "0x37e534");
        assert_eq!(capture.crop_end, "0x37e56c");
        assert!(
            capture
                .measurement_inputs
                .starts_with("Supplied world measured bounds")
        );
        assert_eq!(capture.cases.len(), 78);
        for case in capture.cases {
            let actual = TableExportPage::new(case.page_size, case.scaled_margins)
                .artwork_bounds(bounds(case.measured_bbox));
            assert_eq!(
                [actual.x_min, actual.y_min, actual.x_max, actual.y_max]
                    .map(|value| (value as f32).to_bits()),
                case.background_crop.map(f32::to_bits),
                "{}",
                case.name,
            );
            assert_eq!(
                case.default_focus.map(f32::to_bits),
                [0; 4],
                "{}",
                case.name
            );
            assert_eq!(case.default_clip_counts, [0; 3], "{}", case.name);
        }
    }

    #[test]
    fn translated_page_crops_stay_in_the_active_paint_coordinates() {
        let page = TableExportPage::new([100, 200], [10.0, 20.0]).translated(-5.25, 200.0);
        assert_eq!(
            page.artwork_bounds(bounds([-20.25, 170.0, 120.0, 421.0])),
            bounds([-6.25, 209.0, 95.75, 381.0]),
        );
    }

    #[test]
    fn oversized_public_pages_use_the_measured_artwork_fallback() {
        let mut page = Page {
            uuid: String::new(),
            width: 100,
            height: 200,
            content_bbox: BoundingBox::default(),
            background_color: None,
            template: None,
            background: Default::default(),
            objects: Vec::new(),
        };
        let metadata = DocumentMetadata::default();
        let measured = bounds([10.5, 20.5, 30.5, 40.5]);
        let expected = bounds([9.0, 19.0, 32.0, 42.0]);
        for size in [[1 << 31, 200], [100, 1 << 31], [u32::MAX; 2]] {
            [page.width, page.height] = size;
            let export = TableExportPage::for_page(&page, &metadata);
            assert!(export.is_none(), "{size:?}");
            assert_eq!(artwork_bounds(measured, export), expected, "{size:?}");
        }
        [page.width, page.height] = [i32::MAX as u32; 2];
        assert!(TableExportPage::for_page(&page, &metadata).is_some());
    }

    #[test]
    fn artwork_visibility_includes_border_ink_beyond_the_cell_frame() {
        use super::super::{CellFill, CellPosition, TableBorderGeometry, TableGrid, tests::grid};
        use crate::render::{RenderTheme, TableCellPaint, viewport::Viewport};

        let table = grid(&[100.0], &[100.0]);
        let grid = TableGrid::new(&table).unwrap();
        let borders = TableBorderGeometry::new(&table, &grid).unwrap();
        let cell = &table.rows[0].cells[0];
        let mut paint = TableCellPaint {
            cell,
            fill: CellFill::resolve(&table.style, 0, cell, RenderTheme::for_canvas(false)),
            frame: bounds([0.0, -100.0, 100.0, -0.25]),
            artwork_frame: bounds([0.0, -100.0, 100.0, -0.25]),
            layout: None,
            position: CellPosition { row: 0, column: 0 },
            gap: false,
            native_text_placement: Ok(
                super::super::native_placement::NativeCellTextPlacement::Unknown,
            ),
        };
        let viewport = Viewport::new(bounds([0.0, 0.0, 100.0, 100.0]));
        assert!(!viewport.intersects(paint.frame));
        assert!(viewport.intersects(paint.artwork_bounds(
            Some(&borders),
            0.0,
            true,
            super::super::TableArtworkGeometry::Cached
        )));
        paint.artwork_frame.y_max = -0.5;
        assert!(!viewport.intersects(paint.artwork_bounds(
            Some(&borders),
            0.0,
            true,
            super::super::TableArtworkGeometry::Cached
        )));
    }
}