sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::render::text::native_cell_clip::{NativeCellClipError, NativeCellRunClip};
use crate::render::vector::{Clip, ClipPath, Definitions, Group, Rectangle, Scene, Styled};

#[cfg(all(test, feature = "pdf"))]
mod pdf_tests;

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct NativeRunClip {
    rectangle: Option<ClipRectangle>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct ClipRectangle {
    origin: [f64; 2],
    size: [f64; 2],
}

impl NativeRunClip {
    pub fn new(
        clip: NativeCellRunClip,
        output_translation: [f64; 2],
    ) -> Result<Self, NativeCellClipError> {
        if !output_translation.into_iter().all(f64::is_finite) {
            return Err(NativeCellClipError);
        }
        let NativeCellRunClip::Clipped { world_rect, .. } = clip else {
            return Ok(Self { rectangle: None });
        };
        let [left, top, right, bottom] = world_rect.map(f64::from);
        if !world_rect.into_iter().all(f32::is_finite) {
            return Err(NativeCellClipError);
        }
        if right <= left || bottom <= top {
            return Ok(Self { rectangle: None });
        }
        let rectangle = ClipRectangle {
            origin: [left + output_translation[0], top + output_translation[1]],
            size: [right - left, bottom - top],
        };
        if (0..2).any(|axis| {
            let far_edge = rectangle.origin[axis] + rectangle.size[axis];
            !far_edge.is_finite() || far_edge <= rectangle.origin[axis]
        }) {
            return Err(NativeCellClipError);
        }
        Ok(Self {
            rectangle: Some(rectangle),
        })
    }

    pub fn paint(self, scene: &mut Scene, draw: impl FnOnce(&mut Scene)) {
        let Some(rectangle) = self.rectangle else {
            draw(scene);
            return;
        };
        let id = scene.definition::<Clip>();
        let node = Rectangle::new()
            .x(rectangle.origin[0])
            .y(rectangle.origin[1])
            .width(rectangle.size[0])
            .height(rectangle.size[1]);
        scene.push(Definitions::new().add(ClipPath::new(&id).add(node)));
        scene.scope(Group::new().clipped(&id), draw);
    }
}

#[cfg(all(test, feature = "serde"))]
mod tests {
    use super::*;
    use crate::render::vector::{Svg, Text};
    use serde::Deserialize;
    use sha2::{Digest, Sha256};

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

    #[derive(Deserialize)]
    struct Case {
        name: String,
        world_clip: Option<[f32; 4]>,
        backend_clip: bool,
    }

    #[test]
    fn captured_clip_transport_preserves_text_and_restores_neighbor_scopes() {
        let bytes = include_bytes!("../../../../../../../conformance/table-text-clip-paths.json");
        assert_eq!(
            format!("{:x}", Sha256::digest(bytes)),
            "e5f6d442e39aedf995d807a276d86ab7ff877c31d044a5597b620adb4eaa6bdc"
        );
        let capture: Capture = serde_json::from_slice(bytes).unwrap();
        assert_eq!(capture.cases.len(), 132);
        for case in capture.cases {
            let mut scene = Scene::new(Svg::new());
            scene.push(Text::new("before"));
            let clip = case
                .world_clip
                .map_or(NativeCellRunClip::Unclipped, |world_rect| {
                    NativeCellRunClip::Clipped {
                        world_rect,
                        intersects_source: false,
                    }
                });
            let shift = [11.125, 23.375];
            NativeRunClip::new(clip, shift)
                .unwrap()
                .paint(&mut scene, |scene| scene.push(Text::new("source")));
            scene.push(Text::new("after"));
            let svg = scene.finish();
            let xml = roxmltree::Document::parse(&svg).unwrap();
            let text = xml
                .descendants()
                .filter(|node| node.has_tag_name("text"))
                .collect::<Vec<_>>();
            assert_eq!(
                text.iter()
                    .map(|node| node.text().unwrap())
                    .collect::<Vec<_>>(),
                ["before", "source", "after"],
                "{}",
                case.name
            );
            for node in [text[0], text[2]] {
                assert!(
                    node.ancestors()
                        .all(|ancestor| ancestor.attribute("clip-path").is_none()),
                    "{}",
                    case.name
                );
            }
            assert_eq!(
                text[1]
                    .ancestors()
                    .any(|ancestor| ancestor.attribute("clip-path").is_some()),
                case.backend_clip,
                "{}",
                case.name
            );
            let rectangle = xml.descendants().find(|node| node.has_tag_name("rect"));
            assert_eq!(rectangle.is_some(), case.backend_clip, "{}", case.name);
            if let Some(rectangle) = rectangle {
                let [left, top, right, bottom] = case.world_clip.unwrap().map(f64::from);
                for (attribute, expected) in [
                    ("x", left + shift[0]),
                    ("y", top + shift[1]),
                    ("width", right - left),
                    ("height", bottom - top),
                ] {
                    assert_eq!(
                        rectangle
                            .attribute(attribute)
                            .unwrap()
                            .parse::<f64>()
                            .unwrap(),
                        expected,
                        "{} {attribute}",
                        case.name
                    );
                }
            }
        }
    }

    #[test]
    fn invalid_output_offsets_fail_before_any_text_scope_is_painted() {
        let clip = NativeCellRunClip::Clipped {
            world_rect: [0.0, 0.0, 10.0, 10.0],
            intersects_source: false,
        };
        for offset in [f64::NAN, f64::INFINITY, f64::MAX] {
            assert!(NativeRunClip::new(clip, [offset; 2]).is_err());
        }
    }
}