sdocx 1.0.0

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

fn scene() -> crate::render::RenderedScene {
    let bounds = BoundingBox {
        x_min: 10.0,
        y_min: 10.0,
        x_max: 90.0,
        y_max: 90.0,
    };
    let content = RichTextBox {
        text_area_type: None,
        bbox: bounds,
        rotation_degrees: None,
        text: "W".into(),
        color: None,
        highlight_color: None,
        underline: false,
        font_size: Some(20.0),
        runs: Vec::new(),
        spans: Vec::new(),
        paragraphs: Vec::new(),
        object_spans: Vec::new(),
        text_sections: Vec::new(),
        margins: None,
        gravity: None,
    };
    let document = Document {
        metadata: DocumentMetadata {
            default_page_dimensions: Some((100, 100)),
            ..Default::default()
        },
        pages: vec![Page {
            uuid: "font-identity".into(),
            width: 100,
            height: 100,
            content_bbox: bounds,
            background_color: None,
            template: None,
            background: Default::default(),
            objects: vec![PageElement::TextBox(content).into()],
        }],
    };
    let layout = crate::layout_document(&document);
    let fonts = FontBook::default();
    let scene = DocumentTextCache::default()
        .render_layout_page_scenes(&document, &[&layout.pages[0]], &Default::default(), &fonts)
        .remove(0);
    assert!(scene.text_error.is_none());
    assert_eq!(scene.text.len(), 1);
    scene
}

fn different_fonts() -> PdfOptions {
    let mut database = fontdb::Database::new();
    database.load_font_data(include_bytes!("../../assets/fonts/RobotoMono-Regular.ttf").to_vec());
    database.set_sans_serif_family("Roboto Mono");
    database.set_serif_family("Roboto Mono");
    PdfOptions::new(Arc::new(database))
}

#[test]
fn raw_svg_rejects_a_missing_selected_physical_font() {
    let scene = scene();
    assert!(matches!(
        render_svg_pages_pdf(&[scene.page], &different_fonts()),
        Err(PdfError::UnsupportedText { page_index: 0, .. })
    ));
}

#[test]
fn retained_text_keeps_its_font_when_svg_parsing_uses_a_different_database() {
    let scene = scene();
    let bytes = render_pages_pdf(
        [PdfPage {
            page: &scene.page,
            text: Some(&scene.text),
            text_error: None,
        }],
        &different_fonts(),
    )
    .unwrap();
    let pdf = lopdf::Document::load_mem(&bytes).unwrap();
    assert!(pdf.extract_text(&[1]).unwrap().contains('W'));
    let regular = FontBook::default().resolve("Roboto", false, false).unwrap();
    let regular = rustybuzz::ttf_parser::Face::parse(regular.bytes(), regular.index).unwrap();
    let expected = regular
        .glyph_bounding_box(regular.glyph_index('W').unwrap())
        .unwrap();
    let mut bounds = Vec::new();
    for object in pdf.objects.values() {
        let Ok(dictionary) = object.as_dict() else {
            continue;
        };
        let Ok(font) = dictionary.get(b"FontFile2") else {
            continue;
        };
        let stream = pdf.dereference(font).unwrap().1.as_stream().unwrap();
        let bytes = stream
            .decompressed_content()
            .unwrap_or_else(|_| stream.content.clone());
        let face = rustybuzz::ttf_parser::Face::parse(&bytes, 0).unwrap();
        bounds.extend(
            (0..face.number_of_glyphs())
                .filter_map(|id| face.glyph_bounding_box(rustybuzz::ttf_parser::GlyphId(id))),
        );
    }
    assert!(bounds.contains(&expected), "{bounds:?}");
}