sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::shape::{NativePathCommand, visit_path};
use crate::{BoundingBox, NativeShape};

use super::ShapeTextFrameIssue;

#[derive(Clone, Copy)]
pub(super) struct NativeRect {
    pub left: f32,
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
    pub width: f32,
    pub height: f32,
    pub center: [f32; 2],
}

impl NativeRect {
    pub fn new(bounds: BoundingBox) -> Result<Self, ShapeTextFrameIssue> {
        let left = bounds.x_min as f32;
        let top = bounds.y_min as f32;
        let right = bounds.x_max as f32;
        let bottom = bounds.y_max as f32;
        let width = right - left;
        let height = bottom - top;
        let center = [(left + right) * 0.5, (top + bottom) * 0.5];
        if ![
            left, top, right, bottom, width, height, center[0], center[1],
        ]
        .into_iter()
        .all(f32::is_finite)
            || width <= 0.0
            || height <= 0.0
        {
            return Err(ShapeTextFrameIssue::InvalidGeometry);
        }
        Ok(Self {
            left,
            top,
            right,
            bottom,
            width,
            height,
            center,
        })
    }

    pub fn bounds(self) -> BoundingBox {
        BoundingBox {
            x_min: f64::from(self.left),
            y_min: f64::from(self.top),
            x_max: f64::from(self.right),
            y_max: f64::from(self.bottom),
        }
    }

    pub fn restore_point(
        &self,
        point: [f64; 2],
        degrees: f32,
    ) -> Result<[f32; 2], ShapeTextFrameIssue> {
        self.rotate_point(point.map(|value| value as f32), -degrees)
    }

    pub fn rotate_point(
        &self,
        point: [f32; 2],
        degrees: f32,
    ) -> Result<[f32; 2], ShapeTextFrameIssue> {
        if !point.into_iter().chain([degrees]).all(f32::is_finite) {
            return Err(ShapeTextFrameIssue::InvalidGeometry);
        }
        if degrees == 0.0 {
            return Ok(point);
        }
        let radians = (f64::from(degrees) / 180.0) * std::f64::consts::PI;
        let (sin, cos) = radians.sin_cos();
        let dx = f64::from(point[0] - self.center[0]);
        let dy = f64::from(point[1] - self.center[1]);
        let result = [
            self.center[0] + dx.mul_add(cos, -(sin * dy)) as f32,
            self.center[1] + dx.mul_add(sin, cos * dy) as f32,
        ];
        result
            .into_iter()
            .all(f32::is_finite)
            .then_some(result)
            .ok_or(ShapeTextFrameIssue::InvalidGeometry)
    }
}

pub(super) fn clamp_horizontal(
    point: [f32; 2],
    start: [f32; 2],
    end: [f32; 2],
) -> Result<[f32; 2], ShapeTextFrameIssue> {
    if !point
        .into_iter()
        .chain(start)
        .chain(end)
        .all(f32::is_finite)
        || start[1] != end[1]
    {
        return Err(ShapeTextFrameIssue::InvalidGeometry);
    }
    let dx = start[0] - end[0];
    let dy = start[1] - end[1];
    let length = dx.mul_add(dx, dy * dy).sqrt();
    let divisor = if length == 0.0 { 1.0 } else { length };
    let term = (point[1] - start[1]) * (end[1] - start[1]);
    let projection = (point[0] - start[0]).mul_add(end[0] - start[0], term) / divisor;
    Ok(if projection <= 0.0 {
        start
    } else if projection >= divisor {
        end
    } else {
        [point[0], start[1]]
    })
}

pub(super) struct TemplatePath {
    commands: [NativePathCommand<f32>; 10],
    length: usize,
}

impl TemplatePath {
    pub fn read(shape: &NativeShape, rect: &NativeRect) -> Result<Self, ShapeTextFrameIssue> {
        if shape.path_data.is_empty() {
            return Err(ShapeTextFrameIssue::UnsupportedTemplate);
        }
        let mut raw = [NativePathCommand::Close; 10];
        let mut length = 0usize;
        let (consumed, supported) = visit_path(&shape.path_data, |command| {
            if let Some(slot) = raw.get_mut(length) {
                *slot = command;
            }
            length = length.saturating_add(1);
        })
        .map_err(|_| ShapeTextFrameIssue::InvalidGeometry)?;
        if !supported || consumed != shape.path_data.len() || length > raw.len() {
            return Err(ShapeTextFrameIssue::UnsupportedTemplate);
        }
        let mut commands = [NativePathCommand::Close; 10];
        let restore = |point| rect.restore_point(point, shape.rotation_degrees);
        for (slot, command) in commands.iter_mut().zip(raw).take(length) {
            *slot = match command {
                NativePathCommand::Move(point) => NativePathCommand::Move(restore(point)?),
                NativePathCommand::Line(point) => NativePathCommand::Line(restore(point)?),
                NativePathCommand::Quadratic([first, end]) => {
                    NativePathCommand::Quadratic([restore(first)?, restore(end)?])
                }
                NativePathCommand::Cubic([first, second, end]) => {
                    NativePathCommand::Cubic([restore(first)?, restore(second)?, restore(end)?])
                }
                NativePathCommand::Close => NativePathCommand::Close,
                NativePathCommand::Arc(_) | NativePathCommand::Oval(_) => {
                    return Err(ShapeTextFrameIssue::UnsupportedTemplate);
                }
            };
        }
        Ok(Self { commands, length })
    }

    pub fn commands(&self) -> &[NativePathCommand<f32>] {
        &self.commands[..self.length]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rect() -> NativeRect {
        NativeRect::new(BoundingBox {
            x_min: 0.0,
            y_min: 0.0,
            x_max: 200.0,
            y_max: 100.0,
        })
        .unwrap()
    }

    #[test]
    fn inverse_rotation_recovers_world_points_about_the_original_center() {
        assert_eq!(
            rect().restore_point([100.0, 90.0], 90.0).unwrap(),
            [140.0, 50.0]
        );
        assert_eq!(
            rect().restore_point([60.0, 50.0], 180.0).unwrap(),
            [140.0, 50.0]
        );
        assert_eq!(
            rect().restore_point([100.0, 10.0], 270.0).unwrap(),
            [140.0, 50.0]
        );
    }

    #[test]
    fn noncardinal_rotation_preserves_native_narrowing_and_center_rounding() {
        for (bounds, point, angle, expected) in [
            (
                BoundingBox {
                    x_min: 1868.2598,
                    y_min: 33.549805,
                    x_max: 2305.0388,
                    y_max: 1003.1478,
                },
                [2_646.705_749_634_719, 116.720_810_392_523_43],
                102.409_91,
                [0x44c4_c180, 0x4266_c230],
            ),
            (
                BoundingBox {
                    x_min: -470.1499,
                    y_min: -4018.32,
                    x_max: 42.617096,
                    y_max: -3168.339,
                },
                [-69.716_888_304_734_37, -3_734.565_063_020_562_3],
                251.150_02,
                [0xc2fd_4b3e, 0xc555_35fb],
            ),
        ] {
            let rect = NativeRect::new(bounds).unwrap();
            assert_eq!(
                rect.restore_point(point, angle).unwrap().map(f32::to_bits),
                expected
            );
        }
    }

    #[test]
    fn zero_rotation_narrows_storage_without_subtracting_the_center() {
        let point = [f64::from(f32::MAX), f64::from(f32::MIN_POSITIVE)];
        assert_eq!(
            rect().restore_point(point, -0.0).unwrap(),
            [f32::MAX, f32::MIN_POSITIVE]
        );
        assert_eq!(
            rect().restore_point([f64::MAX, 0.0], 0.0),
            Err(ShapeTextFrameIssue::InvalidGeometry)
        );
    }

    #[test]
    fn horizontal_projection_clamps_in_both_directions_and_preserves_interior_x() {
        for (start, end) in [([20.0, 7.0], [100.0, 7.0]), ([100.0, 7.0], [20.0, 7.0])] {
            assert_eq!(
                clamp_horizontal([65.25, -50.0], start, end).unwrap(),
                [65.25, 7.0]
            );
            assert_eq!(
                clamp_horizontal([0.0, -50.0], start, end).unwrap(),
                [20.0, 7.0]
            );
            assert_eq!(
                clamp_horizontal([200.0, -50.0], start, end).unwrap(),
                [100.0, 7.0]
            );
        }
        assert_eq!(
            clamp_horizontal([65.0, 0.0], [20.0, 7.0], [20.0, 7.0]).unwrap(),
            [20.0, 7.0]
        );
    }

    #[test]
    fn finite_controls_keep_native_infinite_and_unordered_projection_branches() {
        assert_eq!(
            clamp_horizontal([f32::MAX, 0.0], [0.0, 0.0], [200.0, 0.0]).unwrap(),
            [200.0, 0.0],
        );
        assert_eq!(
            clamp_horizontal([-f32::MAX, 0.0], [0.0, 0.0], [200.0, 0.0]).unwrap(),
            [0.0, 0.0],
        );
        assert_eq!(
            clamp_horizontal([1e19, 0.0], [0.0, 0.0], [4e19, 0.0]).unwrap(),
            [1e19, 0.0],
        );
        assert_eq!(
            clamp_horizontal([65.25, f32::MAX], [0.0, -f32::MAX], [200.0, -f32::MAX]).unwrap(),
            [65.25, -f32::MAX],
        );
    }
}