sdocx 1.1.0

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

use super::native::{NativeRect, TemplatePath, clamp_horizontal};
use super::{ShapeTextFrameIssue, TextInsets};

pub(super) fn insets(
    shape: &NativeShape,
    rect: &NativeRect,
) -> Result<TextInsets, ShapeTextFrameIssue> {
    let path = TemplatePath::read(shape, rect)?;
    let mut corners = Corners::read(path.commands())?;
    for &point in &shape.control_points {
        let point = rect.restore_point(point, shape.rotation_degrees)?;
        corners.apply_control(point, rect, shape.horizontal_flip, shape.vertical_flip)?;
    }
    let margin = corners.margin(rect)?;
    Ok(TextInsets::symmetric(margin, margin))
}

struct Corners {
    first: [f32; 2],
    line3_x: f32,
    opposite_y: f32,
    line7_x: f32,
}

impl Corners {
    fn read(commands: &[NativePathCommand<f32>]) -> Result<Self, ShapeTextFrameIssue> {
        use NativePathCommand::{Close, Cubic, Line, Move};
        let [
            Move(first),
            Line(_),
            Cubic(_),
            Line(line3),
            Cubic(_),
            Line(opposite),
            Cubic(_),
            Line(line7),
            Cubic(_),
            Close,
        ] = commands
        else {
            return Err(ShapeTextFrameIssue::UnsupportedTemplate);
        };
        Ok(Self {
            first: *first,
            line3_x: line3[0],
            opposite_y: opposite[1],
            line7_x: line7[0],
        })
    }

    fn radius_at(&self, x: f32, rect: &NativeRect) -> Result<f32, ShapeTextFrameIssue> {
        let radius = if self.line7_x > self.line3_x {
            rect.right - x
        } else {
            x - rect.left
        };
        if radius.is_finite() {
            Ok(radius)
        } else {
            Err(ShapeTextFrameIssue::InvalidGeometry)
        }
    }

    fn apply_control(
        &mut self,
        point: [f32; 2],
        rect: &NativeRect,
        horizontal_flip: bool,
        vertical_flip: bool,
    ) -> Result<(), ShapeTextFrameIssue> {
        let mirrored = self.line7_x >= self.line3_x;
        let start_x = if mirrored { rect.right } else { rect.left };
        let half_height = if rect.height == 0.0 {
            0.0
        } else {
            rect.height * 0.5
        };
        let end_x = if rect.height >= rect.width {
            rect.center[0]
        } else if mirrored {
            rect.right - half_height
        } else {
            rect.left + half_height
        };
        let y = if self.first[1] < self.opposite_y {
            rect.top
        } else {
            rect.bottom
        };
        let point = clamp_horizontal(point, [start_x, y], [end_x, y])?;
        let radius = self.radius_at(point[0], rect)?;
        self.reconstruct(radius, rect, horizontal_flip, vertical_flip)
    }

    fn reconstruct(
        &mut self,
        radius: f32,
        rect: &NativeRect,
        horizontal_flip: bool,
        vertical_flip: bool,
    ) -> Result<(), ShapeTextFrameIssue> {
        let first_x = if horizontal_flip {
            rect.right - radius
        } else {
            rect.left + radius
        };
        if !first_x.is_finite() {
            return Err(ShapeTextFrameIssue::InvalidGeometry);
        }
        self.first = [first_x, if vertical_flip { rect.bottom } else { rect.top }];
        self.line3_x = if horizontal_flip {
            rect.left
        } else {
            rect.right
        };
        self.line7_x = if horizontal_flip {
            rect.right
        } else {
            rect.left
        };
        self.opposite_y = if vertical_flip { rect.top } else { rect.bottom };
        Ok(())
    }

    fn margin(&self, rect: &NativeRect) -> Result<f32, ShapeTextFrameIssue> {
        let radius = self.radius_at(self.first[0], rect)?;
        let margin = radius + ((radius * f32::from_bits(0xbfb5_04f3)) * 0.5);
        if margin.is_finite() {
            Ok(margin)
        } else {
            Err(ShapeTextFrameIssue::InvalidGeometry)
        }
    }
}

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

    const PATH: [NativePathCommand<f32>; 10] = [
        NativePathCommand::Move([10.0, 0.0]),
        NativePathCommand::Line([70.0, 0.0]),
        NativePathCommand::Cubic([[75.0, 0.0], [80.0, 5.0], [80.0, 10.0]]),
        NativePathCommand::Line([80.0, 30.0]),
        NativePathCommand::Cubic([[80.0, 35.0], [75.0, 40.0], [70.0, 40.0]]),
        NativePathCommand::Line([10.0, 40.0]),
        NativePathCommand::Cubic([[5.0, 40.0], [0.0, 35.0], [0.0, 30.0]]),
        NativePathCommand::Line([0.0, 10.0]),
        NativePathCommand::Cubic([[0.0, 5.0], [5.0, 0.0], [10.0, 0.0]]),
        NativePathCommand::Close,
    ];

    fn rect() -> NativeRect {
        NativeRect {
            left: 0.0,
            top: 0.0,
            right: 80.0,
            bottom: 40.0,
            width: 80.0,
            height: 40.0,
            center: [40.0, 20.0],
        }
    }

    #[test]
    fn canonical_saved_radius_uses_native_operation_order() {
        let corners = Corners::read(&PATH).unwrap();
        assert_eq!(
            f64::from(corners.margin(&rect()).unwrap()),
            2.9289321899414062
        );
        let corners = Corners {
            first: [6.666_666_5, 0.0],
            ..corners
        };
        assert_eq!(
            f64::from(corners.margin(&rect()).unwrap()),
            1.9526214599609375
        );
    }

    #[test]
    fn empty_and_noncanonical_paths_do_not_guess_a_default_radius() {
        assert!(matches!(
            Corners::read(&[]),
            Err(ShapeTextFrameIssue::UnsupportedTemplate)
        ));
        let mut path = PATH;
        path[2] = NativePathCommand::Quadratic([[80.0, 0.0], [80.0, 10.0]]);
        assert!(matches!(
            Corners::read(&path),
            Err(ShapeTextFrameIssue::UnsupportedTemplate)
        ));
        assert!(matches!(
            Corners::read(&PATH[..9]),
            Err(ShapeTextFrameIssue::UnsupportedTemplate)
        ));
    }

    #[test]
    fn controls_clamp_to_corner_range_and_replay_in_order() {
        let rect = rect();
        let mut corners = Corners::read(&PATH).unwrap();
        corners
            .apply_control([100.0, -100.0], &rect, false, false)
            .unwrap();
        assert_eq!(corners.first, [20.0, 0.0]);
        assert_eq!(
            f64::from(corners.margin(&rect).unwrap()),
            5.8578643798828125
        );
        corners
            .apply_control([-100.0, 100.0], &rect, false, false)
            .unwrap();
        assert_eq!(corners.margin(&rect).unwrap(), 0.0);
        corners
            .apply_control([5.0, 100.0], &rect, false, false)
            .unwrap();
        assert_eq!(
            f64::from(corners.margin(&rect).unwrap()),
            1.4644660949707031
        );
    }

    #[test]
    fn stored_flips_reconstruct_the_path_before_the_next_control() {
        let rect = rect();
        let mut corners = Corners::read(&PATH).unwrap();
        corners
            .apply_control([12.0, 0.0], &rect, true, true)
            .unwrap();
        assert_eq!(corners.first, [68.0, 40.0]);
        assert_eq!(corners.line3_x, 0.0);
        assert_eq!(corners.line7_x, 80.0);
        assert_eq!(corners.opposite_y, 0.0);
        corners
            .apply_control([60.0, -100.0], &rect, true, true)
            .unwrap();
        assert_eq!(corners.first, [60.0, 40.0]);
        assert_eq!(
            f64::from(corners.margin(&rect).unwrap()),
            5.8578643798828125
        );
    }

    #[test]
    fn equal_side_coordinates_use_different_clamp_and_radius_predicates() {
        let rect = rect();
        let mut corners = Corners::read(&PATH).unwrap();
        corners.line7_x = 80.0;
        corners
            .apply_control([70.0, 0.0], &rect, false, false)
            .unwrap();
        assert_eq!(corners.first, [70.0, 0.0]);
        assert_eq!(corners.radius_at(corners.first[0], &rect).unwrap(), 70.0);
    }

    #[test]
    fn radius_is_recovered_from_the_reconstructed_native_coordinate() {
        let rect = NativeRect::new(crate::BoundingBox {
            x_min: 393_669.656_25,
            y_min: 0.0,
            x_max: 2_000_000.0,
            y_max: 2_000_000.0,
        })
        .unwrap();
        let mut corners = Corners::read(&PATH).unwrap();
        corners
            .reconstruct(f32::from_bits(0x4904_9623), &rect, false, false)
            .unwrap();
        assert_eq!(f64::from(corners.first[0]), 936_743.875);
        assert_eq!(
            f64::from(corners.radius_at(corners.first[0], &rect).unwrap()),
            543_074.25
        );
    }

    #[test]
    fn overflowing_derived_radius_is_invalid_geometry() {
        let mut rect = rect();
        rect.left = -f32::MAX;
        let mut corners = Corners::read(&PATH).unwrap();
        corners.first[0] = f32::MAX;
        assert_eq!(
            corners.margin(&rect),
            Err(ShapeTextFrameIssue::InvalidGeometry)
        );
    }
}