sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use super::*;
use crate::{TableEdgeStyle, fonts::FontBook};
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,
    widget_library_sha256: String,
    text_library_sha256: String,
    update_geometry_address: String,
    minimum_height_address: String,
    measurement_inputs: String,
    cases: Vec<Case>,
}

#[derive(Deserialize)]
struct Case {
    name: String,
    height_limit: Option<HeightLimit>,
    heights: Vec<f32>,
    widths: Vec<f32>,
    spans: Vec<[u32; 2]>,
    native_defaults: bool,
    outer_border: Option<[CapturedEdge; 4]>,
    default_border: Option<[CapturedEdge; 4]>,
    borders: Vec<Option<[CapturedEdge; 4]>>,
    frames: Vec<[f32; 4]>,
    metrics: Vec<Metrics>,
    drawn_widths: [f32; 4],
    content_bbox: [f32; 4],
    measured_bbox: [f32; 4],
    minimum_height_bits: Vec<[u32; 2]>,
}

#[derive(Deserialize)]
pub(super) struct HeightLimit {
    enabled: bool,
    maximum: f32,
}

impl HeightLimit {
    pub fn apply(&self, table: &mut RichTextTable) {
        table.style.max_height_enabled = self.enabled;
        table.style.max_height = Some(self.maximum);
    }
}

#[derive(Clone, Copy, Deserialize)]
struct CapturedEdge {
    color: u32,
    width: f32,
    start_radius: f32,
    end_radius: f32,
}

impl From<CapturedEdge> for TableEdgeStyle {
    fn from(edge: CapturedEdge) -> Self {
        Self {
            color: edge.color,
            width: edge.width,
            start_radius: edge.start_radius,
            end_radius: edge.end_radius,
        }
    }
}

fn border(edges: [CapturedEdge; 4]) -> TableBorder {
    let [left, top, right, bottom] = edges.map(TableEdgeStyle::from);
    TableBorder {
        left,
        top,
        right,
        bottom,
        metadata: crate::TableRecordMetadata::default(),
    }
}

#[derive(Deserialize)]
struct Metrics {
    has_text_layout: bool,
    has_text: bool,
    first_line_height: f32,
    top_margin: f32,
    measured_height: f32,
}

fn capture() -> Capture {
    let bytes = include_bytes!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../../conformance/table-measured-geometry.json"
    ));
    assert_eq!(
        format!("{:x}", Sha256::digest(bytes)),
        "6b210cfa59fa00971addb39da7d7576eeb3238214fa560d31a9dcdf7f4039ed4"
    );
    let capture: Capture = serde_json::from_slice(bytes).unwrap();
    assert_eq!(capture.apk_version, "4.4.45.37");
    assert_eq!(capture.allocation_fills, [0, 165, 255]);
    assert_eq!(
        capture.apk_sha256,
        "daed1eff8c8ee9dfb8afe2771e39e893a8808f3230d6d522a8aa647db09b8667"
    );
    assert_eq!(
        capture.model_library_sha256,
        "4fbcf6d4213e929f1535d32abb487743643fd5d0dfc366e50dfeb2e7d8015b7a"
    );
    assert_eq!(
        capture.drawing_library_sha256,
        "788bf413ddeb0b9d352062c5f1b7b8ed11babca911df72691da58ff1a0a5a4bd"
    );
    assert_eq!(
        capture.base_library_sha256,
        "e10da0116946691cf68302437ef261282e1dfe0eec15bf2dfa66093286985deb"
    );
    assert_eq!(
        capture.widget_library_sha256,
        "cfaaccbfd62763f0e514271cc372c0de7b6df41f0d2f991887b8b9584abd1ec9"
    );
    assert_eq!(
        capture.text_library_sha256,
        "5483711673a499743625eb3275e34b37a006919af346212b46b8d8857834308b"
    );
    assert_eq!(capture.update_geometry_address, "0xab168");
    assert_eq!(capture.minimum_height_address, "0xac9f0");
    assert_eq!(
        capture.measurement_inputs,
        "supplied cached frames and text metrics, not native text shaping"
    );
    assert_eq!(capture.cases.len(), 154);
    capture
}

impl Case {
    fn plan(&self) -> (RichTextTable, PreparedTable) {
        let mut source = tests::grid(&self.heights, &self.widths);
        if let Some(limit) = &self.height_limit {
            limit.apply(&mut source);
        }
        source.style.border = if self.native_defaults {
            None
        } else {
            self.outer_border.map(border)
        };
        source.style.default_cell_border = self.default_border.map(border);
        for (slot, cell) in source
            .rows
            .iter_mut()
            .flat_map(|row| &mut row.cells)
            .enumerate()
        {
            [cell.row_span, cell.column_span] = self.spans[slot];
            cell.border = self.borders[slot].map(border);
            cell.content.text = if self.metrics[slot].has_text {
                "x".to_owned()
            } else {
                String::new()
            };
            cell.content.margins = Some([0.0, self.metrics[slot].top_margin, 0.0, 0.0]);
        }
        let half_border = drawable_half_border(&source);
        let mut rows = initialize_rows(&source, half_border).unwrap();
        for (slot, cell) in rows.iter_mut().flat_map(|row| &mut row.cells).enumerate() {
            let [x_min, y_min, x_max, y_max] = self.frames[slot].map(f64::from);
            cell.frame = BoundingBox {
                x_min,
                y_min,
                x_max,
                y_max,
            };
            cell.metrics.first_line_height = self.metrics[slot]
                .has_text_layout
                .then_some(f64::from(self.metrics[slot].first_line_height));
            cell.metrics.measured_height = f64::from(self.metrics[slot].measured_height);
        }
        let plan = PreparedTable {
            callback_top: 0.0,
            content_bbox: BoundingBox::default(),
            measured_bbox: BoundingBox {
                x_min: 0.0,
                y_min: 0.0,
                x_max: 0.0,
                y_max: 0.0,
            },
            topology: TableGrid::new(&source).unwrap(),
            pending_gaps: vec![0.0; rows.len()],
            rows,
            min_first_page_height: 0.0,
            constraint: ObjectSpanLayoutConstraint::OverPages,
            bands: BandList::default(),
            half_border,
        };
        (source, plan)
    }
}

fn rect_bits(rect: BoundingBox) -> [u32; 4] {
    [rect.x_min, rect.y_min, rect.x_max, rect.y_max].map(|value| (value as f32).to_bits())
}

#[test]
fn native_measured_geometry_uses_endpoint_owner_frames() {
    let fonts = FontBook::default();
    let renderer = TextRenderer::new(super::super::text::TextSettings::default(), &fonts);
    let mut slots = 0;
    for case in capture().cases {
        let (source, mut plan) = case.plan();
        assert_eq!(
            table_border_widths(&source).map(|value| (value as f32).to_bits()),
            case.drawn_widths.map(f32::to_bits),
            "{}, border widths",
            case.name
        );
        plan.update_geometry(&source, &renderer).unwrap();
        assert_eq!(
            rect_bits(plan.measured_bbox),
            case.measured_bbox.map(f32::to_bits),
            "{}, measured bounds",
            case.name
        );
        assert_eq!(
            rect_bits(plan.content_bbox),
            case.content_bbox.map(f32::to_bits),
            "{}, content bounds",
            case.name
        );
        assert_eq!(
            (plan.min_first_page_height as f32).to_bits(),
            case.minimum_height_bits[0][1],
            "{}, first-page offset",
            case.name
        );
        slots += case.frames.len();
    }
    assert_eq!(slots, 1281);
}

#[test]
fn native_first_page_minima_use_owner_metrics_and_first_row_fallback() {
    let fonts = FontBook::default();
    let renderer = TextRenderer::new(super::super::text::TextSettings::default(), &fonts);
    let mut queries = 0;
    for case in capture().cases {
        let (source, plan) = case.plan();
        for (row, minimum) in case.minimum_height_bits.iter().enumerate() {
            assert_eq!(
                (plan.first_line_minimum(row, &source, &renderer).unwrap() as f32).to_bits(),
                minimum[0],
                "{}, row {row}",
                case.name
            );
            assert_eq!(
                (plan.first_page_minimum(row, &source, &renderer).unwrap() as f32).to_bits(),
                minimum[1],
                "{}, row {row} with origin",
                case.name,
            );
            queries += 2;
        }
    }
    assert_eq!(queries, 914);
}