#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextOrientation { Horizontal, Vertical, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextJustification { Left, Center, Right, } #[derive(Debug, Clone)] pub struct TextEngineSpec { pub text: String, pub font_name: String, pub font_size: f64, pub color: [u8; 4], pub faux_bold: bool, pub faux_italic: bool, pub orientation: TextOrientation, pub justification: TextJustification, pub box_width: f64, pub box_height: f64, } #[derive(Debug, Clone)] enum EngineValue { Int(i32), Float(f64), Bool(bool), String(String), Array(Vec), Dict(Vec<(String, EngineValue)>), } pub fn encode_engine_data(spec: &TextEngineSpec) -> Vec { let text = normalize_text(&spec.text); let paragraph_lengths = paragraph_run_lengths(&text); let total_length = utf16_len(&text) as i32; let font_name = if spec.font_name.trim().is_empty() { "ArialMT" } else { spec.font_name.trim() }; let font_index = if font_name == "AdobeInvisFont" { 0 } else { 1 }; let writing_direction = match spec.orientation { TextOrientation::Horizontal => 0, TextOrientation::Vertical => 2, }; let procession = match spec.orientation { TextOrientation::Horizontal => 0, TextOrientation::Vertical => 1, }; let paragraph_properties = paragraph_properties(spec.justification); let base_style_sheet = base_style_sheet(font_index); let style_run_sheet = style_run_sheet(spec, font_index); let font_set = font_set(font_name); let root = EngineValue::Dict(vec![ ( "EngineDict".to_string(), EngineValue::Dict(vec![ ( "Editor".to_string(), EngineValue::Dict(vec![("Text".to_string(), EngineValue::String(text))]), ), ( "ParagraphRun".to_string(), EngineValue::Dict(vec![ ( "DefaultRunData".to_string(), EngineValue::Dict(vec![ ( "ParagraphSheet".to_string(), EngineValue::Dict(vec![ ("DefaultStyleSheet".to_string(), EngineValue::Int(0)), ("Properties".to_string(), EngineValue::Dict(Vec::new())), ]), ), ( "Adjustments".to_string(), EngineValue::Dict(vec![ ( "Axis".to_string(), EngineValue::Array(vec![ EngineValue::Float(1.0), EngineValue::Float(0.0), EngineValue::Float(1.0), ]), ), ( "XY".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(0.0), ]), ), ]), ), ]), ), ( "RunArray".to_string(), EngineValue::Array( paragraph_lengths .iter() .map(|_| { EngineValue::Dict(vec![ ( "ParagraphSheet".to_string(), EngineValue::Dict(vec![ ( "DefaultStyleSheet".to_string(), EngineValue::Int(0), ), ( "Properties".to_string(), EngineValue::Dict( paragraph_properties.clone(), ), ), ]), ), ( "Adjustments".to_string(), EngineValue::Dict(vec![ ( "Axis".to_string(), EngineValue::Array(vec![ EngineValue::Float(1.0), EngineValue::Float(0.0), EngineValue::Float(1.0), ]), ), ( "XY".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(0.0), ]), ), ]), ), ]) }) .collect(), ), ), ( "RunLengthArray".to_string(), EngineValue::Array( paragraph_lengths .iter() .copied() .map(EngineValue::Int) .collect(), ), ), ("IsJoinable".to_string(), EngineValue::Int(1)), ]), ), ( "StyleRun".to_string(), EngineValue::Dict(vec![ ( "DefaultRunData".to_string(), EngineValue::Dict(vec![( "StyleSheet".to_string(), EngineValue::Dict(vec![( "StyleSheetData".to_string(), EngineValue::Dict(Vec::new()), )]), )]), ), ( "RunArray".to_string(), EngineValue::Array(vec![EngineValue::Dict(vec![( "StyleSheet".to_string(), EngineValue::Dict(vec![( "StyleSheetData".to_string(), EngineValue::Dict(style_run_sheet.clone()), )]), )])]), ), ( "RunLengthArray".to_string(), EngineValue::Array(vec![EngineValue::Int(total_length)]), ), ("IsJoinable".to_string(), EngineValue::Int(2)), ]), ), ( "GridInfo".to_string(), EngineValue::Dict(vec![ ("GridIsOn".to_string(), EngineValue::Bool(false)), ("ShowGrid".to_string(), EngineValue::Bool(false)), ("GridSize".to_string(), EngineValue::Float(18.0)), ("GridLeading".to_string(), EngineValue::Float(22.0)), ( "GridColor".to_string(), EngineValue::Dict(color_type_values([0, 0, 255, 255])), ), ( "GridLeadingFillColor".to_string(), EngineValue::Dict(color_type_values([0, 0, 255, 255])), ), ( "AlignLineHeightToGridFlags".to_string(), EngineValue::Bool(false), ), ]), ), ("AntiAlias".to_string(), EngineValue::Int(4)), ( "UseFractionalGlyphWidths".to_string(), EngineValue::Bool(true), ), ( "Rendered".to_string(), EngineValue::Dict(vec![ ("Version".to_string(), EngineValue::Int(1)), ( "Shapes".to_string(), EngineValue::Dict(vec![ ( "WritingDirection".to_string(), EngineValue::Int(writing_direction), ), ( "Children".to_string(), EngineValue::Array(vec![EngineValue::Dict(vec![ ("ShapeType".to_string(), EngineValue::Int(1)), ("Procession".to_string(), EngineValue::Int(procession)), ( "Lines".to_string(), EngineValue::Dict(vec![ ( "WritingDirection".to_string(), EngineValue::Int(writing_direction), ), ( "Children".to_string(), EngineValue::Array(Vec::new()), ), ]), ), ( "Cookie".to_string(), EngineValue::Dict(vec![( "Photoshop".to_string(), EngineValue::Dict(vec![ ("ShapeType".to_string(), EngineValue::Int(1)), ( "BoxBounds".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(0.0), EngineValue::Float(spec.box_width), EngineValue::Float(spec.box_height), ]), ), ( "Base".to_string(), EngineValue::Dict(vec![ ( "ShapeType".to_string(), EngineValue::Int(1), ), ( "TransformPoint0".to_string(), EngineValue::Array(vec![ EngineValue::Float(1.0), EngineValue::Float(0.0), ]), ), ( "TransformPoint1".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(1.0), ]), ), ( "TransformPoint2".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(0.0), ]), ), ]), ), ]), )]), ), ])]), ), ]), ), ]), ), ]), ), ( "ResourceDict".to_string(), EngineValue::Dict(resource_dict( font_set.clone(), paragraph_properties.clone(), base_style_sheet.clone(), )), ), ( "DocumentResources".to_string(), EngineValue::Dict(resource_dict( font_set, paragraph_properties, base_style_sheet, )), ), ]); let mut out = Vec::new(); out.extend_from_slice(b"\n\n"); write_value(&mut out, &root, 0, false, None); out } fn normalize_text(text: &str) -> String { let normalized = text .replace("\r\n", "\n") .replace('\r', "\n") .replace('\n', "\r"); format!("{normalized}\r") } fn paragraph_run_lengths(text: &str) -> Vec { text.split_inclusive('\r') .map(|run| utf16_len(run) as i32) .collect() } fn utf16_len(text: &str) -> usize { text.encode_utf16().count() } fn paragraph_properties(justification: TextJustification) -> Vec<(String, EngineValue)> { vec![ ( "Justification".to_string(), EngineValue::Int(match justification { TextJustification::Left => 0, TextJustification::Right => 1, TextJustification::Center => 2, }), ), ("FirstLineIndent".to_string(), EngineValue::Float(0.0)), ("StartIndent".to_string(), EngineValue::Float(0.0)), ("EndIndent".to_string(), EngineValue::Float(0.0)), ("SpaceBefore".to_string(), EngineValue::Float(0.0)), ("SpaceAfter".to_string(), EngineValue::Float(0.0)), ("AutoHyphenate".to_string(), EngineValue::Bool(true)), ("HyphenatedWordSize".to_string(), EngineValue::Int(6)), ("PreHyphen".to_string(), EngineValue::Int(2)), ("PostHyphen".to_string(), EngineValue::Int(2)), ("ConsecutiveHyphens".to_string(), EngineValue::Int(8)), ("Zone".to_string(), EngineValue::Float(36.0)), ( "WordSpacing".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.8), EngineValue::Float(1.0), EngineValue::Float(1.33), ]), ), ( "LetterSpacing".to_string(), EngineValue::Array(vec![ EngineValue::Float(0.0), EngineValue::Float(0.0), EngineValue::Float(0.0), ]), ), ( "GlyphSpacing".to_string(), EngineValue::Array(vec![ EngineValue::Float(1.0), EngineValue::Float(1.0), EngineValue::Float(1.0), ]), ), ("AutoLeading".to_string(), EngineValue::Float(1.2)), ("LeadingType".to_string(), EngineValue::Int(0)), ("Hanging".to_string(), EngineValue::Bool(false)), ("Burasagari".to_string(), EngineValue::Bool(false)), ("KinsokuOrder".to_string(), EngineValue::Int(0)), ("EveryLineComposer".to_string(), EngineValue::Bool(false)), ] } fn base_style_sheet(font_index: i32) -> Vec<(String, EngineValue)> { vec![ ("Font".to_string(), EngineValue::Int(font_index)), ("FontSize".to_string(), EngineValue::Float(12.0)), ("FauxBold".to_string(), EngineValue::Bool(false)), ("FauxItalic".to_string(), EngineValue::Bool(false)), ("AutoLeading".to_string(), EngineValue::Bool(true)), ("Leading".to_string(), EngineValue::Float(0.0)), ("HorizontalScale".to_string(), EngineValue::Float(1.0)), ("VerticalScale".to_string(), EngineValue::Float(1.0)), ("Tracking".to_string(), EngineValue::Int(0)), ("AutoKerning".to_string(), EngineValue::Bool(true)), ("Kerning".to_string(), EngineValue::Int(0)), ("BaselineShift".to_string(), EngineValue::Float(0.0)), ("FontCaps".to_string(), EngineValue::Int(0)), ("FontBaseline".to_string(), EngineValue::Int(0)), ("Underline".to_string(), EngineValue::Bool(false)), ("Strikethrough".to_string(), EngineValue::Bool(false)), ("Ligatures".to_string(), EngineValue::Bool(true)), ("DLigatures".to_string(), EngineValue::Bool(false)), ("BaselineDirection".to_string(), EngineValue::Int(2)), ("Tsume".to_string(), EngineValue::Float(0.0)), ("StyleRunAlignment".to_string(), EngineValue::Int(2)), ("Language".to_string(), EngineValue::Int(0)), ("NoBreak".to_string(), EngineValue::Bool(false)), ( "FillColor".to_string(), EngineValue::Dict(color_type_values([0, 0, 0, 255])), ), ( "StrokeColor".to_string(), EngineValue::Dict(color_type_values([0, 0, 0, 255])), ), ("FillFlag".to_string(), EngineValue::Bool(true)), ("StrokeFlag".to_string(), EngineValue::Bool(false)), ("FillFirst".to_string(), EngineValue::Bool(true)), ("YUnderline".to_string(), EngineValue::Int(1)), ("OutlineWidth".to_string(), EngineValue::Float(1.0)), ("CharacterDirection".to_string(), EngineValue::Int(0)), ("HindiNumbers".to_string(), EngineValue::Bool(false)), ("Kashida".to_string(), EngineValue::Int(1)), ("DiacriticPos".to_string(), EngineValue::Int(2)), ] } fn style_run_sheet(spec: &TextEngineSpec, font_index: i32) -> Vec<(String, EngineValue)> { vec![ ("Font".to_string(), EngineValue::Int(font_index)), ("FontSize".to_string(), EngineValue::Float(spec.font_size)), ("FauxBold".to_string(), EngineValue::Bool(spec.faux_bold)), ( "FauxItalic".to_string(), EngineValue::Bool(spec.faux_italic), ), ("AutoKerning".to_string(), EngineValue::Bool(true)), ("Kerning".to_string(), EngineValue::Int(0)), ( "FillColor".to_string(), EngineValue::Dict(color_type_values(spec.color)), ), ] } fn resource_dict( font_set: Vec, paragraph_properties: Vec<(String, EngineValue)>, style_sheet: Vec<(String, EngineValue)>, ) -> Vec<(String, EngineValue)> { vec![ ( "KinsokuSet".to_string(), EngineValue::Array(vec![ EngineValue::Dict(vec![ ( "Name".to_string(), EngineValue::String("PhotoshopKinsokuHard".to_string()), ), ( "NoStart".to_string(), EngineValue::String( "\u{3001}\u{3002}\u{ff0c}\u{ff0e}\u{30fb}\u{ff1a}\u{ff1b}\u{ff1f}\u{ff01}\u{30fc}\u{2015}\u{2019}\u{201d}\u{ff09}\u{3015}\u{ff3d}\u{ff5d}\u{3009}\u{300b}\u{300d}\u{300f}\u{3011}\u{30fd}\u{30fe}\u{309d}\u{309e}\u{3005}\u{3041}\u{3043}\u{3045}\u{3047}\u{3049}\u{3063}\u{3083}\u{3085}\u{3087}\u{308e}\u{30a1}\u{30a3}\u{30a5}\u{30a7}\u{30a9}\u{30c3}\u{30e3}\u{30e5}\u{30e7}\u{30ee}\u{30f5}\u{30f6}\u{309b}\u{309c}?!)]},.:;\u{2103}\u{2109}\u{00a2}\u{ff05}\u{2030}".to_string(), ), ), ( "NoEnd".to_string(), EngineValue::String( "\u{2018}\u{201c}\u{ff08}\u{3014}\u{ff3b}\u{ff5b}\u{3008}\u{300a}\u{300c}\u{300e}\u{3010}([{\u{ffe5}\u{ff04}\u{00a3}\u{ff20}\u{00a7}\u{3012}\u{ff03}".to_string(), ), ), ( "Keep".to_string(), EngineValue::String("\u{2015}\u{2025}".to_string()), ), ( "Hanging".to_string(), EngineValue::String("\u{3001}\u{3002}.,".to_string()), ), ]), EngineValue::Dict(vec![ ( "Name".to_string(), EngineValue::String("PhotoshopKinsokuSoft".to_string()), ), ( "NoStart".to_string(), EngineValue::String( "\u{3001}\u{3002}\u{ff0c}\u{ff0e}\u{30fb}\u{ff1a}\u{ff1b}\u{ff1f}\u{ff01}\u{2019}\u{201d}\u{ff09}\u{3015}\u{ff3d}\u{ff5d}\u{3009}\u{300b}\u{300d}\u{300f}\u{3011}\u{30fd}\u{30fe}\u{309d}\u{309e}\u{3005}".to_string(), ), ), ( "NoEnd".to_string(), EngineValue::String( "\u{2018}\u{201c}\u{ff08}\u{3014}\u{ff3b}\u{ff5b}\u{3008}\u{300a}\u{300c}\u{300e}\u{3010}".to_string(), ), ), ( "Keep".to_string(), EngineValue::String("\u{2015}\u{2025}".to_string()), ), ( "Hanging".to_string(), EngineValue::String("\u{3001}\u{3002}.,".to_string()), ), ]), ]), ), ( "MojiKumiSet".to_string(), EngineValue::Array(vec![ EngineValue::Dict(vec![( "InternalName".to_string(), EngineValue::String("Photoshop6MojiKumiSet1".to_string()), )]), EngineValue::Dict(vec![( "InternalName".to_string(), EngineValue::String("Photoshop6MojiKumiSet2".to_string()), )]), EngineValue::Dict(vec![( "InternalName".to_string(), EngineValue::String("Photoshop6MojiKumiSet3".to_string()), )]), EngineValue::Dict(vec![( "InternalName".to_string(), EngineValue::String("Photoshop6MojiKumiSet4".to_string()), )]), ]), ), ("TheNormalStyleSheet".to_string(), EngineValue::Int(0)), ("TheNormalParagraphSheet".to_string(), EngineValue::Int(0)), ( "ParagraphSheetSet".to_string(), EngineValue::Array(vec![EngineValue::Dict(vec![ ( "Name".to_string(), EngineValue::String("Normal RGB".to_string()), ), ("DefaultStyleSheet".to_string(), EngineValue::Int(0)), ( "Properties".to_string(), EngineValue::Dict(paragraph_properties), ), ])]), ), ( "StyleSheetSet".to_string(), EngineValue::Array(vec![EngineValue::Dict(vec![ ( "Name".to_string(), EngineValue::String("Normal RGB".to_string()), ), ("StyleSheetData".to_string(), EngineValue::Dict(style_sheet)), ])]), ), ("FontSet".to_string(), EngineValue::Array(font_set)), ("SuperscriptSize".to_string(), EngineValue::Float(0.583)), ("SuperscriptPosition".to_string(), EngineValue::Float(0.333)), ("SubscriptSize".to_string(), EngineValue::Float(0.583)), ("SubscriptPosition".to_string(), EngineValue::Float(0.333)), ("SmallCapSize".to_string(), EngineValue::Float(0.7)), ] } fn font_set(font_name: &str) -> Vec { let mut fonts = vec![font_descriptor("AdobeInvisFont")]; if font_name != "AdobeInvisFont" { fonts.push(font_descriptor(font_name)); } fonts } fn font_descriptor(name: &str) -> EngineValue { EngineValue::Dict(vec![ ("Name".to_string(), EngineValue::String(name.to_string())), ("Script".to_string(), EngineValue::Int(0)), ("FontType".to_string(), EngineValue::Int(0)), ("Synthetic".to_string(), EngineValue::Int(0)), ]) } fn color_type_values(color: [u8; 4]) -> Vec<(String, EngineValue)> { vec![ ("Type".to_string(), EngineValue::Int(1)), ( "Values".to_string(), EngineValue::Array(vec![ EngineValue::Float(color[3] as f64 / 255.0), EngineValue::Float(color[0] as f64 / 255.0), EngineValue::Float(color[1] as f64 / 255.0), EngineValue::Float(color[2] as f64 / 255.0), ]), ), ] } fn write_value( out: &mut Vec, value: &EngineValue, indent: usize, in_property: bool, key: Option<&str>, ) { match value { EngineValue::Int(number) => { write_prefix(out, indent, in_property); out.extend_from_slice(number.to_string().as_bytes()); } EngineValue::Float(number) => { write_prefix(out, indent, in_property); out.extend_from_slice(serialize_float(*number, key).as_bytes()); } EngineValue::Bool(flag) => { write_prefix(out, indent, in_property); out.extend_from_slice(if *flag { b"true" } else { b"false" }); } EngineValue::String(text) => { write_prefix(out, indent, in_property); out.push(b'('); out.push(0xFE); out.push(0xFF); for unit in text.encode_utf16() { write_escaped_byte(out, (unit >> 8) as u8); write_escaped_byte(out, unit as u8); } out.push(b')'); } EngineValue::Array(items) => { write_prefix(out, indent, in_property); if items.iter().all(is_scalar) { out.extend_from_slice(b"["); for item in items { out.push(b' '); write_inline_value(out, item, key); } out.extend_from_slice(b" ]"); } else { out.extend_from_slice(b"[\n"); for item in items { write_value(out, item, indent + 1, false, key); out.push(b'\n'); } write_indent(out, indent); out.extend_from_slice(b"]"); } } EngineValue::Dict(entries) => { if in_property { out.push(b'\n'); } else { write_indent(out, indent); } out.extend_from_slice(b"<<\n"); for (entry_key, entry_value) in entries { write_indent(out, indent + 1); out.push(b'/'); out.extend_from_slice(entry_key.as_bytes()); write_value(out, entry_value, indent + 1, true, Some(entry_key)); out.push(b'\n'); } write_indent(out, indent); out.extend_from_slice(b">>"); } } } fn write_inline_value(out: &mut Vec, value: &EngineValue, key: Option<&str>) { match value { EngineValue::Int(number) => out.extend_from_slice(number.to_string().as_bytes()), EngineValue::Float(number) => { out.extend_from_slice(serialize_float(*number, key).as_bytes()) } EngineValue::Bool(flag) => out.extend_from_slice(if *flag { b"true" } else { b"false" }), _ => write_value(out, value, 0, false, key), } } fn write_prefix(out: &mut Vec, indent: usize, in_property: bool) { if in_property { out.push(b' '); } else { write_indent(out, indent); } } fn write_indent(out: &mut Vec, indent: usize) { for _ in 0..indent { out.push(b'\t'); } } fn write_escaped_byte(out: &mut Vec, byte: u8) { if matches!(byte, b'(' | b')' | b'\\') { out.push(b'\\'); } out.push(byte); } fn is_scalar(value: &EngineValue) -> bool { matches!( value, EngineValue::Int(_) | EngineValue::Float(_) | EngineValue::Bool(_) | EngineValue::String(_) ) } fn serialize_float(value: f64, key: Option<&str>) -> String { let is_float = matches!( key, Some( "Axis" | "XY" | "Zone" | "WordSpacing" | "FirstLineIndent" | "GlyphSpacing" | "StartIndent" | "EndIndent" | "SpaceBefore" | "SpaceAfter" | "LetterSpacing" | "Values" | "GridSize" | "GridLeading" | "PointBase" | "BoxBounds" | "TransformPoint0" | "TransformPoint1" | "TransformPoint2" | "FontSize" | "Leading" | "HorizontalScale" | "VerticalScale" | "BaselineShift" | "Tsume" | "OutlineWidth" | "AutoLeading" ) ) || value.fract() != 0.0; if !is_float { return (value as i32).to_string(); } let mut formatted = format!("{value:.5}"); if let Some(dot) = formatted.find('.') { while formatted.ends_with('0') && formatted.len() > dot + 2 { formatted.pop(); } } if formatted.starts_with("0.") && formatted .as_bytes() .get(2) .is_some_and(|digit| *digit != b'0') { formatted.remove(0); } else if formatted.starts_with("-0.0") && formatted .as_bytes() .get(4) .is_some_and(|digit| digit.is_ascii_digit() && *digit != b'0') { formatted.remove(1); } formatted } #[cfg(test)] mod tests { use super::{TextEngineSpec, TextJustification, TextOrientation, encode_engine_data}; #[test] fn engine_data_contains_expected_sections_and_utf16_text() { let bytes = encode_engine_data(&TextEngineSpec { text: "Hello".to_string(), font_name: "ArialMT".to_string(), font_size: 14.0, color: [1, 2, 3, 255], faux_bold: true, faux_italic: false, orientation: TextOrientation::Horizontal, justification: TextJustification::Center, box_width: 100.0, box_height: 32.0, }); assert!( bytes .windows("/EngineDict".len()) .any(|window| window == b"/EngineDict") ); assert!( bytes .windows("/FontSet".len()) .any(|window| window == b"/FontSet") ); assert!( bytes .windows("/RunLengthArray".len()) .any(|window| window == b"/RunLengthArray") ); assert!(bytes.windows(2).any(|window| window == [0xFE, 0xFF])); } #[test] fn engine_data_keeps_float_tokens_for_font_size_and_transforms() { let bytes = encode_engine_data(&TextEngineSpec { text: "Hello".to_string(), font_name: "ArialMT".to_string(), font_size: 14.0, color: [1, 2, 3, 255], faux_bold: true, faux_italic: false, orientation: TextOrientation::Horizontal, justification: TextJustification::Center, box_width: 100.0, box_height: 32.0, }); assert!( bytes .windows("/FontSize 14.0".len()) .any(|w| w == b"/FontSize 14.0") ); assert!( bytes .windows("/Axis [ 1.0 0.0 1.0 ]".len()) .any(|w| w == b"/Axis [ 1.0 0.0 1.0 ]") ); } }