sdocx 1.0.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use super::*;
use crate::{
    BoundingBox, Document, DocumentMetadata, ObjectSpanLayoutOption, Page, PageElement,
    RichTextObjectContent, RichTextObjectSpan,
};

#[path = "../../../tests/support/binary_records.rs"]
mod binary_records;

fn text_object(value: &str, margins: [f32; 4]) -> Vec<u8> {
    let units = value.encode_utf16().collect::<Vec<_>>();
    let mut common = (units.len() as u32).to_le_bytes().to_vec();
    for unit in units {
        common.extend(unit.to_le_bytes());
    }
    common.extend(1_u32.to_le_bytes());
    common.extend(20_u16.to_le_bytes());
    for field in [3_u32, 0, value.encode_utf16().count() as u32, 0] {
        common.extend(field.to_le_bytes());
    }
    common.extend(17.0_f32.to_le_bytes());
    common.extend(1_u32.to_le_bytes());
    common.extend(16_u16.to_le_bytes());
    for field in [3_u32, 0, 1, 2] {
        common.extend(field.to_le_bytes());
    }
    for margin in margins {
        common.extend(margin.to_le_bytes());
    }
    common.push(0);
    common.extend(0_u16.to_le_bytes());
    common.extend([0; 8]);
    binary_records::text_object(&object_base_frame(), true, &binary_records::sized(&common))
}

fn parsed_table(value: &str, column_width: f32, margins: [f32; 4]) -> crate::RichTextTable {
    let text = text_object(value, margins);
    let bytes = binary_records::single_cell_table(&object_base_frame(), &text, column_width, 100.0);
    parse_table_object(
        &bytes,
        &ParseLimits::default(),
        "measurement-width table",
        0,
    )
    .unwrap()
}

fn document(table: crate::RichTextTable) -> Document {
    let mut parent = table.rows[0].cells[0].content.clone();
    parent.text = "\u{fffc}".into();
    parent.spans.clear();
    parent.paragraphs.clear();
    parent.margins = None;
    parent.gravity = None;
    parent.object_spans = vec![RichTextObjectSpan {
        object_type: ObjectType::Table,
        object_data: Vec::new(),
        content: Some(RichTextObjectContent::Table(Box::new(table))),
        text_index_utf16: 0,
        layout_option: ObjectSpanLayoutOption::Inline,
        layout_constraint: ObjectSpanLayoutConstraint::OverPages,
    }];
    let bounds = BoundingBox {
        x_min: 0.0,
        y_min: 0.0,
        x_max: 360.0,
        y_max: 400.0,
    };
    Document {
        pages: vec![Page {
            uuid: "measurement-width".into(),
            width: 360,
            height: 400,
            content_bbox: bounds,
            background_color: None,
            template: None,
            background: Default::default(),
            objects: vec![PageElement::TextBox(parent).into()],
        }],
        metadata: DocumentMetadata {
            default_page_dimensions: Some((360, 400)),
            flow_page_padding: Some((0, 0)),
            ..Default::default()
        },
    }
}

#[test]
fn parsed_subpixel_table_width_resolves_native_automatic_measurement_for_vector_exports() {
    for (value, width, margins, expected_lines) in [
        ("AV", 0.75, [0.0; 4], 1),
        ("AV", 1.0, [0.0; 4], 2),
        ("AV abc", 80.9, [0.0; 4], 1),
        ("AV\nToTo", 0.75, [0.0; 4], 2),
        ("AV abc", 0.75, [1.25, 2.5, 3.75, 4.5], 1),
    ] {
        let table = parsed_table(value, width, margins);
        assert_eq!(table.column_widths, [width]);
        assert_eq!(table.rows[0].cells[0].content.font_size, Some(17.0));
        let doc = document(table);
        let layout = crate::layout_document(&doc);
        let page = crate::render_layout_page_svg(&doc, &layout, 0, &Default::default()).unwrap();
        assert!(
            page.text_diagnostics.is_empty(),
            "{value:?} {width}: {:?}",
            page.text_diagnostics
        );
        let xml = roxmltree::Document::parse(&page.svg).unwrap();
        assert_eq!(
            xml.descendants()
                .filter(|node| node.has_tag_name("text"))
                .count(),
            expected_lines,
            "{value:?} {width}: {}",
            page.svg
        );
        assert!(!xml.descendants().any(|node| node.has_tag_name("image")));
        let retained = xml
            .descendants()
            .filter(|node| node.has_tag_name("tspan"))
            .filter_map(|node| node.text())
            .collect::<String>();
        assert_eq!(retained, value.replace('\n', ""));
        let replay =
            crate::render_layout_page_replay_svg(&doc, &layout, 0, &Default::default()).unwrap();
        assert_eq!(page.svg, replay.svg);
        #[cfg(feature = "pdf")]
        {
            let pdf =
                crate::render_document_pdf(&doc, &Default::default(), &Default::default()).unwrap();
            let pdf = lopdf::Document::load_mem(&pdf).unwrap();
            assert_eq!(
                pdf.extract_text(&[1])
                    .unwrap()
                    .split_whitespace()
                    .collect::<String>(),
                value.split_whitespace().collect::<String>()
            );
            assert!(
                !pdf.objects
                    .values()
                    .any(|object| object
                        .as_stream()
                        .is_ok_and(|stream| stream.dict.get(b"Subtype").is_ok_and(|value| value
                            .as_name()
                            .is_ok_and(|name| name == b"Image"))))
            );
        }
    }
}