sdocx 1.0.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
#![cfg(all(feature = "render", feature = "serde"))]

use sdocx::{
    BoundingBox, Document, DocumentMetadata, ObjectSpanLayoutConstraint, ObjectSpanLayoutOption,
    ObjectType, Page, RichTextBox, RichTextObjectContent, RichTextObjectSpan, RichTextParagraph,
    RichTextParagraphType, RichTextSection, RichTextTable, RichTextTableCell, RichTextTableRow,
};

fn text(source: &str) -> RichTextBox {
    RichTextBox {
        text_area_type: None,
        bbox: BoundingBox::default(),
        rotation_degrees: None,
        text: source.into(),
        color: None,
        highlight_color: None,
        underline: false,
        font_size: Some(10.0),
        runs: vec![],
        spans: vec![],
        paragraphs: vec![],
        object_spans: vec![],
        text_sections: vec![],
        margins: None,
        gravity: None,
    }
}

fn document(constraint: ObjectSpanLayoutConstraint, percent: f32) -> Document {
    let bounds = BoundingBox {
        x_min: 0.0,
        y_min: 0.0,
        x_max: 200.0,
        y_max: 100.0,
    };
    let mut empty = text("");
    empty.font_size = Some(100.0);
    empty.margins = Some([0.0, 2.0, 0.0, 3.0]);
    empty.paragraphs.push(RichTextParagraph {
        kind: RichTextParagraphType::LineSpacing,
        start_paragraph: 0,
        end_paragraph: 1,
        payload: [1_u32.to_le_bytes(), percent.to_le_bytes()].concat(),
    });
    let table = RichTextTable {
        style: serde_json::from_str(
            r#"{"heading_column_enabled":false,"heading_row_enabled":false,
                "max_height_enabled":false,"metadata":{"property_mask":[],
                "field_mask":[],"fixed_trailing_data":[],"flexible_trailing_data":[]}}"#,
        )
        .unwrap(),
        bbox: bounds,
        rotation_degrees: None,
        column_widths: vec![200.0],
        rows: vec![RichTextTableRow {
            max_height: None,
            min_height: None,
            metadata: Default::default(),
            index: 0,
            height: 100.0,
            cells: vec![RichTextTableCell {
                border: None,
                metadata: Default::default(),
                column_index: 0,
                row_span: 1,
                column_span: 1,
                background_color: 0,
                has_own_background_color: false,
                bbox: bounds,
                editable: false,
                content: empty,
            }],
        }],
    };
    let mut body = text("\u{fffc}\nEnd");
    body.text_sections.push(RichTextSection {
        start_utf16: 0,
        length_utf16: 5,
    });
    body.object_spans.push(RichTextObjectSpan {
        object_type: ObjectType::Table,
        object_data: vec![],
        content: Some(RichTextObjectContent::Table(Box::new(table))),
        text_index_utf16: 0,
        layout_option: ObjectSpanLayoutOption::Inline,
        layout_constraint: constraint,
    });
    Document {
        pages: vec![Page {
            uuid: "empty-table".into(),
            width: 360,
            height: 500,
            content_bbox: BoundingBox::default(),
            background_color: None,
            template: None,
            background: Default::default(),
            objects: vec![],
        }],
        metadata: DocumentMetadata {
            note_text: Some(body),
            page_mode: Some(0),
            default_page_dimensions: Some((360, 500)),
            orientation: Some(0),
            flow_page_padding: Some((0, 0)),
            ..Default::default()
        },
    }
}

#[test]
fn empty_table_cells_reserve_default_cursor_height_without_emitting_text() {
    for constraint in [
        ObjectSpanLayoutConstraint::OverPages,
        ObjectSpanLayoutConstraint::OverPagesOverlapPadding,
    ] {
        for (percent, baseline) in [(1.0, 129.50101), (1.6, 189.50101)] {
            let doc = document(constraint, percent);
            let layout = sdocx::layout_document(&doc);
            for replay in [false, true] {
                let page = if replay {
                    sdocx::render_layout_page_replay_svg(&doc, &layout, 0, &Default::default())
                } else {
                    sdocx::render_layout_page_svg(&doc, &layout, 0, &Default::default())
                }
                .unwrap();
                assert!(
                    page.text_diagnostics.is_empty(),
                    "{:?}",
                    page.text_diagnostics
                );
                assert!(
                    page.object_diagnostics.is_empty(),
                    "{:?}",
                    page.object_diagnostics
                );
                let xml = roxmltree::Document::parse(&page.svg).unwrap();
                let spans = xml
                    .descendants()
                    .filter(|node| node.has_tag_name("tspan"))
                    .collect::<Vec<_>>();
                assert_eq!(spans.len(), 1);
                assert_eq!(spans[0].text(), Some("End"));
                let y = spans[0]
                    .attribute("y")
                    .unwrap()
                    .split_whitespace()
                    .next()
                    .unwrap();
                assert!(
                    (y.parse::<f64>().unwrap() - baseline).abs() < 0.00001,
                    "{y}"
                );
                assert!(!xml.descendants().any(|node| node.has_tag_name("image")));
            }
        }
    }
}