sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use std::{ops::Range, sync::Arc};

use super::*;
use crate::fonts::FontBook;
use crate::render::text::{ObjectMeasurementContext, TextContext, TextSettings, wrap_paragraph};
use crate::render::vector::Svg;
use crate::{BoundingBox, RichTextBox, RichTextSpan, RichTextSpanType, SpanIntervalType};

#[derive(Debug, PartialEq)]
struct RetainedGlyph {
    id: u32,
    origin: [f64; 2],
    advance: [f64; 2],
    source: Range<usize>,
}

struct RetainedLine {
    source: String,
    runs: Vec<Vec<RetainedGlyph>>,
    paints: Vec<super::super::native::NativeTextPaint>,
    measured_groups: usize,
}

impl RetainedLine {
    fn glyphs(&self) -> Vec<&RetainedGlyph> {
        self.runs.iter().flatten().collect()
    }

    fn assert_geometry(&self, other: &Self) {
        assert_eq!(self.source, other.source);
        assert_eq!(self.glyphs(), other.glyphs());
    }
}

fn text_box(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(45.0),
        runs: Vec::new(),
        spans: Vec::new(),
        paragraphs: Vec::new(),
        object_spans: Vec::new(),
        text_sections: Vec::new(),
        margins: None,
        gravity: None,
    }
}

fn span(kind: RichTextSpanType, range: Range<u32>, payload: Vec<u8>) -> RichTextSpan {
    RichTextSpan {
        kind,
        start_utf16: range.start,
        end_utf16: range.end,
        interval_type: SpanIntervalType::ClosedOpen,
        payload,
    }
}

fn observe(content: &RichTextBox) -> RetainedLine {
    let fonts = FontBook::default();
    observe_with_fonts(content, &fonts)
}

fn observe_with_fonts(content: &RichTextBox, fonts: &FontBook) -> RetainedLine {
    let settings = TextSettings::default();
    let renderer = TextRenderer::new(settings, fonts);
    let styled = StyledText::new(content, TextContext::Placed, settings);
    let theme = RenderTheme::for_canvas(false);
    let lines = wrap_paragraph(
        &styled,
        0..styled.index.len(),
        1000.0,
        theme,
        None,
        &renderer,
        ObjectMeasurementContext::Frame,
    )
    .unwrap();
    assert_eq!(lines.len(), 1);
    let line = &lines[0];
    let measured_groups = usize::from(!line.placements.is_empty())
        + line
            .placements
            .windows(2)
            .filter(|pair| !Arc::ptr_eq(&pair[0].cluster.run, &pair[1].cluster.run))
            .count();
    let mut scene = Scene::new(Svg::new());
    scene.retain_text();
    render_retained_line(
        &mut scene,
        &styled,
        line,
        [20.0, 1000.0, 100.0],
        None,
        theme,
        None,
        &renderer,
    )
    .unwrap();
    let blocks: Vec<_> = scene.native_text().iter().collect();
    assert_eq!(
        blocks.len(),
        1,
        "the line keeps one selectable source block"
    );
    let block = blocks[0].1;
    RetainedLine {
        source: block.source.to_string(),
        runs: block
            .runs
            .iter()
            .map(|run| {
                run.glyphs
                    .iter()
                    .map(|glyph| RetainedGlyph {
                        id: glyph.glyph_id,
                        origin: glyph.origin,
                        advance: glyph.advance,
                        source: glyph.source.bytes().clone(),
                    })
                    .collect()
            })
            .collect(),
        paints: block.runs.iter().map(|run| run.paint).collect(),
        measured_groups,
    }
}

#[test]
fn native_draw_fields_split_retained_runs_without_reshaping_equal_paint() {
    let baseline = observe(&text_box("AV"));
    assert_eq!(baseline.measured_groups, 1);
    assert_eq!(baseline.runs.len(), 1);
    for (name, attribute) in [
        (
            "ordinary background",
            span(
                RichTextSpanType::BackgroundColor,
                1..2,
                0x0011_2233_u32.to_le_bytes().to_vec(),
            ),
        ),
        (
            "composing background",
            span(
                RichTextSpanType::ComposingBackgroundColor,
                1..2,
                [0x0011_2233_u32.to_le_bytes(), [0; 4]].concat(),
            ),
        ),
        (
            "underline",
            span(RichTextSpanType::Underline, 1..2, vec![1, 0]),
        ),
        (
            "strike",
            span(RichTextSpanType::Strikethrough, 1..2, vec![1, 0]),
        ),
        (
            "suggestion underline",
            span(
                RichTextSpanType::Suggestion,
                1..2,
                [
                    0_u32.to_le_bytes(),
                    0xff11_2233_u32.to_le_bytes(),
                    0_u32.to_le_bytes(),
                ]
                .concat(),
            ),
        ),
    ] {
        let mut content = text_box("AV");
        content.spans.push(attribute);
        let retained = observe(&content);
        assert_eq!(retained.measured_groups, 1, "{name}: shared shaping");
        assert_eq!(retained.runs.len(), 2, "{name}: native draw boundary");
        assert_eq!(
            retained.paints,
            vec![baseline.paints[0]; 2],
            "{name}: equal paint"
        );
        retained.assert_geometry(&baseline);
    }
}

#[test]
fn hyperlink_flags_split_equal_blue_retained_paint_without_reshaping() {
    let mut content = text_box("AV");
    content.color = Some(crate::Color {
        r: 0,
        g: 84,
        b: 255,
    });
    content.underline = true;
    let baseline = observe(&content);
    content.spans.push(span(
        RichTextSpanType::Hyperlink,
        1..2,
        [9_u32.to_le_bytes(), [0; 4], [0; 4]].concat(),
    ));
    let retained = observe(&content);
    assert_eq!(retained.measured_groups, 1);
    assert_eq!(retained.runs.len(), 2);
    assert_eq!(retained.paints, vec![baseline.paints[0]; 2]);
    retained.assert_geometry(&baseline);
}

#[test]
fn transparent_foreground_alpha_splits_shaping_with_equal_retained_rgb() {
    let mut content = text_box("AV");
    content.spans.push(span(
        RichTextSpanType::ForegroundColor,
        0..1,
        0x0026_2626_u32.to_le_bytes().to_vec(),
    ));
    let retained = observe(&content);
    assert_eq!(retained.measured_groups, 2);
    assert_eq!(retained.runs.len(), 2);
    assert_eq!(retained.paints[0], retained.paints[1]);
    let a = observe(&text_box("A"));
    let v = observe(&text_box("V"));
    let glyphs = retained.glyphs();
    assert_eq!(retained.source, "AV");
    assert_eq!(glyphs[0].id, a.glyphs()[0].id);
    assert_eq!(glyphs[1].id, v.glyphs()[0].id);
    assert_eq!(
        glyphs[1].origin[0],
        glyphs[0].origin[0] + a.glyphs()[0].advance[0]
    );
}

#[test]
fn equivalent_font_names_and_no_op_decorations_keep_one_retained_run() {
    let font = |padding: u8| {
        [
            vec![padding; 8],
            7_u16.to_le_bytes().to_vec(),
            b"Roboto\0".to_vec(),
        ]
        .concat()
    };
    let mut content = text_box("AV");
    content.spans.extend([
        span(RichTextSpanType::FontName, 0..1, font(0)),
        span(RichTextSpanType::FontName, 1..2, font(0xa5)),
        span(RichTextSpanType::Underline, 1..2, vec![0, 0]),
    ]);
    let retained = observe(&content);
    assert_eq!(retained.measured_groups, 1);
    assert_eq!(retained.runs.len(), 1);
    retained.assert_geometry(&observe(&text_box("AV")));
}

#[test]
fn unavailable_correction_identity_does_not_coalesce_adjacent_clusters() {
    let mut content = text_box("AVX");
    let compatibility_fonts = FontBook::new(FontBook::default().database());
    let baseline = observe_with_fonts(&content, &compatibility_fonts);
    content
        .spans
        .push(span(RichTextSpanType::SpellCorrection, 0..3, Vec::new()));
    let retained = observe(&content);
    assert_eq!(retained.measured_groups, 1);
    assert_eq!(retained.runs.len(), 3);
    assert!(retained.runs.iter().all(|run| run.len() == 1));
    retained.assert_geometry(&baseline);
}

#[test]
fn an_intra_cluster_identity_boundary_keeps_the_shaped_source_cluster_intact() {
    let mut content = text_box("e\u{301}");
    let baseline = observe(&content);
    content.spans.push(span(
        RichTextSpanType::BackgroundColor,
        1..2,
        0xff11_2233_u32.to_le_bytes().to_vec(),
    ));
    let retained = observe(&content);
    assert_eq!(retained.measured_groups, 1);
    assert_eq!(retained.runs.len(), 1);
    assert_eq!(retained.glyphs().len(), 1);
    assert_eq!(retained.glyphs()[0].source, 0.."e\u{301}".len());
    retained.assert_geometry(&baseline);
}