sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use std::collections::HashMap;

use crate::render::harfrust;

use harfrust::font::{BuiltinFontFuncs, FontFuncs};
use harfrust::{
    BufferClusterLevel, BufferFlags, Direction, Feature, FontRef, GlyphId, IntegerScalingRounding,
    Script, ShapeOptions, Shaper, ShaperData, Tag, UnicodeBuffer,
};
use serde::Deserialize;
use sha2::{Digest, Sha256};

use super::{FontBook, ResolvedFace};

#[derive(Deserialize)]
struct Capture {
    font_sha256: String,
    font_face_index: u32,
    memory_fills: [u8; 3],
    repeat_zero_fill: bool,
    context_order: ContextOrder,
    cases: Vec<Case>,
}

#[derive(Deserialize)]
struct ContextOrder {
    pre: String,
    post: String,
}

#[derive(Deserialize)]
struct Case {
    name: String,
    callbacks: Callbacks,
    hb_calls: Vec<ShapeCall>,
}

#[derive(Deserialize)]
struct Callbacks {
    vector_glyphs: Vec<Vec<u32>>,
    vector_quantized: Vec<i32>,
    scalar_glyphs: Vec<u32>,
}

#[derive(Deserialize)]
struct ShapeCall {
    nativefont_scale: [i32; 2],
    input: SourceInput,
    output: ShapeOutput,
}

#[derive(Deserialize)]
struct SourceInput {
    infos: Vec<UnicodeInfo>,
    direction: u32,
    script: u32,
    language: Option<String>,
    flags: u32,
    cluster_level: u32,
    content_type: u32,
    pre_context: Vec<u32>,
    post_context: Vec<u32>,
    features: Vec<SourceFeature>,
}

#[derive(Deserialize)]
struct UnicodeInfo {
    codepoint: u32,
    cluster: u32,
}

#[derive(Deserialize)]
struct SourceFeature {
    tag: u32,
    value: u32,
    start: u32,
    end: u32,
}

#[derive(Deserialize)]
struct ShapeOutput {
    infos: Vec<GlyphInfo>,
    positions: Vec<[i32; 4]>,
    content_type: u32,
}

#[derive(Debug, PartialEq, Eq, Deserialize)]
struct GlyphInfo {
    glyph_id: u32,
    cluster: u32,
}

impl SourceInput {
    fn buffer(&self) -> UnicodeBuffer {
        assert_eq!(self.content_type, 1);
        let mut buffer = UnicodeBuffer::new();
        for info in &self.infos {
            buffer.add(char::from_u32(info.codepoint).unwrap(), info.cluster);
        }
        buffer.set_direction(match self.direction {
            4 => Direction::LeftToRight,
            5 => Direction::RightToLeft,
            other => panic!("unsupported captured horizontal direction {other}"),
        });
        buffer.set_script(Script::from_iso15924_tag(tag(self.script)).unwrap());
        if let Some(language) = &self.language {
            buffer.set_language(language.parse().unwrap());
        }
        buffer.set_flags(BufferFlags::from_bits(self.flags).unwrap());
        buffer.set_cluster_level(match self.cluster_level {
            0 => BufferClusterLevel::MonotoneGraphemes,
            1 => BufferClusterLevel::MonotoneCharacters,
            2 => BufferClusterLevel::Characters,
            other => panic!("unsupported captured cluster level {other}"),
        });
        assert!(self.pre_context.len() <= 5 && self.post_context.len() <= 5);
        buffer.set_pre_context_codepoints(&self.pre_context);
        buffer.set_post_context_codepoints(&self.post_context);
        buffer
    }

    fn features(&self) -> Vec<Feature> {
        self.features
            .iter()
            .map(|feature| Feature {
                tag: tag(feature.tag),
                value: feature.value,
                start: feature.start,
                end: feature.end,
            })
            .collect()
    }
}

fn tag(value: u32) -> Tag {
    Tag::new(&value.to_be_bytes())
}

struct CapturedAdvances(HashMap<u32, i32>);

impl CapturedAdvances {
    fn new(callbacks: &Callbacks) -> Self {
        assert!(callbacks.scalar_glyphs.is_empty());
        let glyphs: Vec<_> = callbacks.vector_glyphs.iter().flatten().copied().collect();
        assert_eq!(glyphs.len(), callbacks.vector_quantized.len());
        let mut advances = HashMap::new();
        for (glyph, &advance) in glyphs.into_iter().zip(&callbacks.vector_quantized) {
            if let Some(previous) = advances.insert(glyph, advance) {
                assert_eq!(advance, previous, "consistent per-case native glyph metric");
            }
        }
        Self(advances)
    }
}

impl FontFuncs for CapturedAdvances {
    fn advance_width(&mut self, _: &BuiltinFontFuncs, glyph: GlyphId) -> i32 {
        *self
            .0
            .get(&glyph.to_u32())
            .expect("shaping requested a captured native glyph advance")
    }

    fn advance_height(&mut self, _: &BuiltinFontFuncs, _: GlyphId) -> i32 {
        panic!("horizontal fixtures require no vertical metric callback")
    }

    fn vertical_origin(&mut self, _: &BuiltinFontFuncs, _: GlyphId) -> (i32, i32) {
        panic!("horizontal fixtures require no vertical origin callback")
    }

    fn extents(&mut self, _: &BuiltinFontFuncs, _: GlyphId) -> Option<harfrust::GlyphExtents> {
        panic!("these fixtures require no uncaptured glyph extents callback")
    }
}

fn captures() -> [(Capture, [usize; 3]); 3] {
    [
        (
            include_bytes!(concat!(
                env!("CARGO_MANIFEST_DIR"),
                "/../../conformance/table-text-shaping.json"
            ))
            .as_slice(),
            "a4c58481cb1e36bb7de8783bea49a800b10b4227177684c48f5b5c356d7a425b",
            [16, 16, 48],
        ),
        (
            include_bytes!(concat!(
                env!("CARGO_MANIFEST_DIR"),
                "/../../conformance/table-text-shaping-numeric.json"
            ))
            .as_slice(),
            "1e476f7fc8254b2a6c7316c6ff18b9c436f8ab57707d536daa07e11eca2af455",
            [17, 19, 182],
        ),
        (
            include_bytes!(concat!(
                env!("CARGO_MANIFEST_DIR"),
                "/../../conformance/table-text-shaping-gpos.json"
            ))
            .as_slice(),
            "9280953ac5de0b6b745baedcda73cf4d88a95a7b52cbfbc99188e0b66a71efb2",
            [4, 4, 12],
        ),
    ]
    .map(|(bytes, hash, counts)| {
        assert_eq!(format!("{:x}", Sha256::digest(bytes)), hash);
        (serde_json::from_slice(bytes).unwrap(), counts)
    })
}

fn captured_face(capture: &Capture) -> ResolvedFace {
    let face = FontBook::default().resolve("Roboto", false, false).unwrap();
    assert_eq!(
        format!("{:x}", Sha256::digest(face.bytes())),
        capture.font_sha256
    );
    assert_eq!(face.index, capture.font_face_index);
    assert_eq!(capture.memory_fills, [0, 0xa5, 0xff]);
    assert!(capture.repeat_zero_fill);
    assert_eq!(capture.context_order.pre, "nearest_first");
    assert_eq!(capture.context_order.post, "source_order");
    face
}

fn shape(
    shaper: &Shaper<'_>,
    call: &ShapeCall,
    advances: &mut CapturedAdvances,
    rounding: Option<IntegerScalingRounding>,
) -> (Vec<GlyphInfo>, Vec<[i32; 4]>) {
    let features = call.input.features();
    let mut options = ShapeOptions::new()
        .features(&features)
        .scale_separate(Some((call.nativefont_scale[0], call.nativefont_scale[1])))
        .font_funcs(Some(advances));
    if let Some(rounding) = rounding {
        options = options.integer_scaling_rounding(rounding);
    }
    let output = shaper.shape(call.input.buffer(), options);
    let infos = output
        .glyph_infos()
        .iter()
        .map(|glyph| GlyphInfo {
            glyph_id: glyph.glyph_id,
            cluster: glyph.cluster,
        })
        .collect();
    let positions = output
        .glyph_positions()
        .iter()
        .map(|glyph| {
            [
                glyph.x_advance,
                glyph.y_advance,
                glyph.x_offset,
                glyph.y_offset,
            ]
        })
        .collect();
    (infos, positions)
}

#[test]
fn explicit_floor_shaping_matches_all_242_captured_positions_with_supplied_metrics() {
    let mut total = 0;
    for (capture, counts) in captures() {
        let face = captured_face(&capture);
        let font = FontRef::from_index(face.bytes(), face.index).unwrap();
        let data = ShaperData::new(&font);
        let shaper = data.shaper(&font).build();
        assert_eq!(capture.cases.len(), counts[0]);
        let mut calls = 0;
        let mut glyphs = 0;
        for case in &capture.cases {
            let mut advances = CapturedAdvances::new(&case.callbacks);
            for call in &case.hb_calls {
                assert_eq!(call.output.content_type, 2);
                let (infos, positions) = shape(
                    &shaper,
                    call,
                    &mut advances,
                    Some(IntegerScalingRounding::Floor),
                );
                assert_eq!(
                    infos, call.output.infos,
                    "{} glyphs and UTF-16 owners",
                    case.name
                );
                assert_eq!(
                    positions, call.output.positions,
                    "{} native scaled positions",
                    case.name
                );
                calls += 1;
                glyphs += positions.len();
            }
        }
        assert_eq!([calls, glyphs], [counts[1], counts[2]]);
        total += glyphs;
    }
    assert_eq!(total, 242);
}

#[test]
fn default_rounding_is_preserved_and_differs_from_actual_native_pairs() {
    let (capture, _) = captures().into_iter().next().unwrap();
    let face = captured_face(&capture);
    let font = FontRef::from_index(face.bytes(), face.index).unwrap();
    let data = ShaperData::new(&font);
    let shaper = data.shaper(&font).build();
    let case = capture
        .cases
        .iter()
        .find(|case| case.name == "av_default")
        .unwrap();
    let call = &case.hb_calls[0];
    let mut advances = CapturedAdvances::new(&case.callbacks);
    let implicit = shape(&shaper, call, &mut advances, None);
    let nearest = shape(
        &shaper,
        call,
        &mut advances,
        Some(IntegerScalingRounding::Nearest),
    );
    let floor = shape(
        &shaper,
        call,
        &mut advances,
        Some(IntegerScalingRounding::Floor),
    );
    assert_eq!(implicit, nearest);
    assert_eq!(nearest.0, floor.0);
    assert_eq!(nearest.1[0][0], 265417);
    assert_eq!(floor.1, call.output.positions);
    assert_eq!(floor.1[0][0], 265416);
    assert_ne!(nearest.1, call.output.positions);
}