sdocx 1.0.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use super::*;
use crate::render::table::{TableBorderGeometry, TableGrid, tests::grid};
use crate::{TableBorder, TableEdgeStyle, TableRecordMetadata};
use serde::Deserialize;
use sha2::{Digest, Sha256};

#[derive(Deserialize)]
struct Capture {
    cases: Vec<Case>,
}

#[derive(Deserialize)]
struct Case {
    name: String,
    constraint: u32,
    source_table_rect: [f32; 4],
    saved_cell_rects: Vec<[f32; 4]>,
    drawing_x: f32,
    display_rect: [f32; 4],
    prepared_frames: Vec<[f32; 4]>,
    pending_gaps: Vec<f32>,
    spans: Vec<[u32; 2]>,
    heights: Vec<f32>,
    widths: Vec<f32>,
    borders: Vec<[CapturedStyle; 4]>,
    outline_color: u32,
    outline: [f32; 3],
    canvas_scale: f32,
    selected_paths: Vec<CapturedPath>,
    commands: Vec<Command>,
}

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

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

#[derive(Deserialize)]
struct CapturedPath {
    position: [usize; 2],
    edge: u32,
    color: u32,
    width: f32,
    endpoint_bits: [u32; 4],
}

#[derive(Debug, PartialEq, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum Command {
    Line {
        position: [usize; 2],
        endpoint_bits: [u32; 4],
        color: u32,
        width_bits: u32,
    },
    Rect {
        position: [usize; 2],
        bounds_bits: [u32; 4],
        radii: [f32; 2],
        color: u32,
    },
    RoundRect {
        position: [usize; 2],
        bounds_bits: [u32; 4],
        radii: [f32; 2],
        color: u32,
    },
}

fn rect_bits(bounds: BoundingBox) -> [u32; 4] {
    native_rect(bounds).map(f32::to_bits)
}

#[test]
fn complete_native_cell_artwork_matches_all_frozen_drawing_cases() {
    let bytes = include_bytes!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../../conformance/table-cell-drawing.json"
    ));
    assert_eq!(
        format!("{:x}", Sha256::digest(bytes)),
        "cee7c9dd9d06c5ce623c474183e058ada95af6de0d5a2c2834ea32cdc73a0b3d"
    );
    let capture: Capture = serde_json::from_slice(bytes).unwrap();
    assert_eq!(capture.cases.len(), 60);
    let mut total_paths = 0;
    let mut total_commands = 0;
    for case in capture.cases {
        let mut table = grid(&case.heights, &case.widths);
        table.bbox = rect(case.source_table_rect);
        table.style.content_bbox = Some(table.bbox);
        for (index, cell) in table
            .rows
            .iter_mut()
            .flat_map(|row| &mut row.cells)
            .enumerate()
        {
            cell.bbox = rect(case.saved_cell_rects[index]);
            [cell.row_span, cell.column_span] = case.spans[index];
            let [left, top, right, bottom] = case.borders[index].map(Into::into);
            cell.border = Some(TableBorder {
                left,
                top,
                right,
                bottom,
                metadata: TableRecordMetadata::default(),
            });
        }
        let grid = TableGrid::new(&table).unwrap();
        let borders = TableBorderGeometry::new(&table, &grid).unwrap();
        let mode = match case.constraint {
            0 => ObjectSpanLayoutConstraint::Normal,
            1 => ObjectSpanLayoutConstraint::OverPages,
            2 => ObjectSpanLayoutConstraint::OverPagesOverlapPadding,
            _ => panic!("uncaptured constraint"),
        };
        let artwork =
            TableArtworkGeometry::native_drawing(mode, table.bbox, case.drawing_x).unwrap();
        let mut commands = Vec::new();
        let mut paths = Vec::new();
        for &position in grid.visible_cells() {
            let index = position.row * case.widths.len() + position.column;
            let cached = offset_frame(
                rect(case.prepared_frames[index]),
                [f64::from(case.drawing_x), 0.0],
            )
            .unwrap();
            let frame =
                artwork.background(table.rows[position.row].cells[position.column].bbox, cached);
            let [left, top, right, bottom] = native_rect(frame);
            let [dl, dt, dr, db] = case.display_rect;
            if dl < dr && dt < db && !(left < dr && right > dl && top < db && bottom > dt) {
                continue;
            }
            let color = 0xff20_3040 + index as u32;
            let location = [position.row, position.column];
            let background = CellBackgroundGeometry::new(
                frame,
                position,
                case.spans[index],
                [case.heights.len(), case.widths.len()],
                [case.outline[1], case.outline[2]],
            );
            commands.push(match background.radii {
                Some(radii) => Command::RoundRect {
                    position: location,
                    bounds_bits: rect_bits(background.bounds),
                    radii,
                    color,
                },
                None => Command::Rect {
                    position: location,
                    bounds_bits: rect_bits(background.bounds),
                    radii: [0.0; 2],
                    color,
                },
            });
            for bounds in background.square_corners.into_iter().flatten() {
                commands.push(Command::Rect {
                    position: location,
                    bounds_bits: rect_bits(bounds),
                    radii: [0.0; 2],
                    color,
                });
            }
            for path in borders.cell_paths(position) {
                let Some(width) = path.paint_width() else {
                    continue;
                };
                let gap = case
                    .pending_gaps
                    .get(position.row + 1)
                    .is_some_and(|gap| gap.abs() > 0.001);
                if !path.selected(
                    position,
                    [case.heights.len(), case.widths.len()],
                    case.outline_color != 0 && case.outline[0] > 0.0,
                    gap,
                ) {
                    continue;
                }
                let drawn = artwork.border(path, frame);
                let selected = if artwork.cached() { drawn } else { path };
                paths.push((
                    location,
                    selected.edge as u32,
                    selected.style.color,
                    width,
                    selected.endpoints.map(f32::to_bits),
                ));
                let width = if width * case.canvas_scale < 1.0 {
                    1.0 / case.canvas_scale
                } else {
                    width
                };
                commands.push(Command::Line {
                    position: location,
                    endpoint_bits: drawn.endpoints.map(f32::to_bits),
                    color: drawn.style.color,
                    width_bits: width.to_bits(),
                });
            }
        }
        assert_eq!(
            paths.len(),
            case.selected_paths.len(),
            "{} paths",
            case.name
        );
        for (actual, expected) in paths.iter().zip(&case.selected_paths) {
            assert_eq!(
                *actual,
                (
                    expected.position,
                    expected.edge,
                    expected.color,
                    expected.width,
                    expected.endpoint_bits
                ),
                "{} selected path",
                case.name
            );
        }
        assert_eq!(commands, case.commands, "{} canvas commands", case.name);
        total_paths += paths.len();
        total_commands += commands.len();
    }
    assert_eq!(total_paths, 501);
    assert_eq!(total_commands, 738);
}