//! Photoshop's `EngineData`: the PostScript-like text engine document stored in `TySh`. //! //! Syntax (from observation of real files and public notes): dictionaries `<< /Key value … >>`, //! arrays `[ … ]`, strings `( … )` holding UTF-16BE with a `FE FF` byte-order mark and `\` //! escapes, numbers (`12`, `-1.5`, `.5`), `true`/`false`, and `/Name` values. use std::fmt::Write as _; #[derive(Clone, Debug, PartialEq)] pub enum Value { Dict(Vec<(String, Value)>), Array(Vec), String(String), Int(i64), Real(f64), Bool(bool), Name(String), } impl Value { pub fn get(&self, key: &str) -> Option<&Value> { match self { Value::Dict(items) => items.iter().find(|(k, _)| k == key).map(|(_, v)| v), _ => None, } } /// Follows a path of dictionary keys. pub fn path(&self, keys: &[&str]) -> Option<&Value> { keys.iter().try_fold(self, |v, k| v.get(k)) } pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { match self { Value::Dict(items) => items.iter_mut().find(|(k, _)| k == key).map(|(_, v)| v), _ => None, } } /// Sets (or appends) a dictionary entry. pub fn set(&mut self, key: &str, v: Value) { if let Value::Dict(items) = self { match items.iter_mut().find(|(k, _)| k == key) { Some(e) => e.1 = v, None => items.push((key.to_string(), v)), } } } pub fn as_f64(&self) -> Option { match self { Value::Int(i) => Some(*i as f64), // `inf`, `nan` and `1e999` parse as reals but are never a valid style number. Value::Real(r) if r.is_finite() => Some(*r), _ => None, } } pub fn as_i64(&self) -> Option { match self { Value::Int(i) => Some(*i), Value::Real(r) => Some(*r as i64), _ => None, } } pub fn as_bool(&self) -> Option { match self { Value::Bool(b) => Some(*b), Value::Int(i) => Some(*i != 0), _ => None, } } pub fn as_str(&self) -> Option<&str> { match self { Value::String(s) | Value::Name(s) => Some(s), _ => None, } } pub fn as_array(&self) -> Option<&[Value]> { match self { Value::Array(a) => Some(a), _ => None, } } pub fn dict() -> Value { Value::Dict(Vec::new()) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParseError(pub String); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "EngineData: {}", self.0) } } impl std::error::Error for ParseError {} const MAX_DEPTH: usize = 64; struct Parser<'a> { b: &'a [u8], i: usize, } impl Parser<'_> { fn ws(&mut self) { while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\r' | b'\n' | 0) { self.i += 1; } } fn err(&self, m: &str) -> Result { Err(ParseError(format!("{m} at byte {}", self.i))) } fn value(&mut self, depth: usize) -> Result { if depth > MAX_DEPTH { return self.err("nesting too deep"); } self.ws(); let b = self.b; match b.get(self.i) { None => self.err("unexpected end"), Some(b'<') if b.get(self.i + 1) == Some(&b'<') => { self.i += 2; let mut items = Vec::new(); loop { self.ws(); match b.get(self.i) { Some(b'>') if b.get(self.i + 1) == Some(&b'>') => { self.i += 2; return Ok(Value::Dict(items)); } Some(b'/') => { let k = self.name(); let v = self.value(depth + 1)?; items.push((k, v)); } None => return self.err("unterminated dictionary"), _ => return self.err("expected key"), } } } Some(b'[') => { self.i += 1; let mut items = Vec::new(); loop { self.ws(); match b.get(self.i) { Some(b']') => { self.i += 1; return Ok(Value::Array(items)); } None => return self.err("unterminated array"), _ => items.push(self.value(depth + 1)?), } } } Some(b'(') => { self.i += 1; let mut raw = Vec::new(); loop { match b.get(self.i) { None => return self.err("unterminated string"), Some(b'\\') => { if let Some(&c) = b.get(self.i + 1) { raw.push(c); } self.i += 2; } Some(b')') => { self.i += 1; break; } Some(&c) => { raw.push(c); self.i += 1; } } } Ok(Value::String(decode_string(&raw))) } Some(b'/') => Ok(Value::Name(self.name())), Some(_) => { let start = self.i; while self.i < b.len() && !matches!(b[self.i], b' ' | b'\t' | b'\r' | b'\n' | b'[' | b']' | b'<' | b'>' | b'(' | b'/') { self.i += 1; } let tok = std::str::from_utf8(&b[start..self.i]).unwrap_or(""); match tok { "true" => Ok(Value::Bool(true)), "false" => Ok(Value::Bool(false)), "" => { self.i += 1; self.err("unexpected byte") } t if t.contains('.') || t.contains('e') || t.contains('E') => t.parse().map(Value::Real).or_else(|_| self.err("bad number")), t => t.parse().map(Value::Int).or_else(|_| t.parse().map(Value::Real).or_else(|_| self.err("bad token"))), } } } } fn name(&mut self) -> String { self.i += 1; // '/' let start = self.i; while self.i < self.b.len() && !matches!(self.b[self.i], b' ' | b'\t' | b'\r' | b'\n' | b'[' | b']' | b'<' | b'>' | b'(' | b'/') { self.i += 1; } String::from_utf8_lossy(&self.b[start..self.i]).into_owned() } } fn decode_string(raw: &[u8]) -> String { if raw.len() >= 2 && raw[0] == 0xFE && raw[1] == 0xFF { let u: Vec = raw[2..].as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).collect(); String::from_utf16_lossy(&u) } else { raw.iter().map(|&c| c as char).collect() } } /// Parses an EngineData document (the top-level dictionary). pub fn parse(data: &[u8]) -> Result { let mut p = Parser { b: data, i: 0 }; p.value(0) } /// Serializes in Photoshop's layout (tab-indented, one key per line). pub fn write(v: &Value) -> Vec { let mut out = b"\n\n".to_vec(); write_value(v, 0, &mut out); out.push(b'\n'); out } /// Serializes `/key value` pairs without an enclosing dictionary: the layout of the document's /// `Txt2` block (see [`crate::psd::build_txt2`]), which `parse_txt2` wraps in `<< >>` to parse. pub fn write_bare(items: &[(String, Value)]) -> Vec { let mut out = b"\n\n".to_vec(); for (k, v) in items { out.push(b'/'); out.extend_from_slice(k.as_bytes()); if is_container(v) { out.push(b'\n'); indent(&mut out, 1); } else { out.push(b' '); } write_value(v, 1, &mut out); out.push(b'\n'); } out } fn indent(out: &mut Vec, n: usize) { out.extend(std::iter::repeat_n(b'\t', n)); } fn is_container(v: &Value) -> bool { matches!(v, Value::Dict(_)) || matches!(v, Value::Array(a) if a.iter().any(|x| matches!(x, Value::Dict(_) | Value::Array(_)))) } fn write_value(v: &Value, depth: usize, out: &mut Vec) { match v { Value::Dict(items) => { out.extend_from_slice(b"<<\n"); for (k, v) in items { indent(out, depth + 1); out.push(b'/'); out.extend_from_slice(k.as_bytes()); if is_container(v) { out.push(b'\n'); indent(out, depth + 1); } else { out.push(b' '); } write_value(v, depth + 1, out); out.push(b'\n'); } indent(out, depth); out.extend_from_slice(b">>"); } Value::Array(items) if is_container(v) => { out.extend_from_slice(b"[\n"); for x in items { indent(out, depth); write_value(x, depth, out); out.push(b'\n'); } indent(out, depth); out.push(b']'); } Value::Array(items) => { out.push(b'['); for x in items { out.push(b' '); write_value(x, depth, out); } out.extend_from_slice(b" ]"); } Value::String(s) => { out.push(b'('); let mut raw = vec![0xFE, 0xFF]; for u in s.encode_utf16() { raw.extend_from_slice(&u.to_be_bytes()); } for c in raw { if matches!(c, b'(' | b')' | b'\\') { out.push(b'\\'); } out.push(c); } out.push(b')'); } Value::Int(i) => { let _ = write!(Wrap(out), "{i}"); } Value::Real(r) => out.extend_from_slice(format_real(*r).as_bytes()), Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }), Value::Name(n) => { out.push(b'/'); out.extend_from_slice(n.as_bytes()); } } } struct Wrap<'a>(&'a mut Vec); impl std::fmt::Write for Wrap<'_> { fn write_str(&mut self, s: &str) -> std::fmt::Result { self.0.extend_from_slice(s.as_bytes()); Ok(()) } } /// Photoshop style: `1.0`, `.5`, `-.25`. fn format_real(r: f64) -> String { if !r.is_finite() { return "0.0".into(); } if r.fract() == 0.0 && r.abs() < 1e15 { return format!("{r:.1}"); } let mut s = format!("{r:.5}"); while s.ends_with('0') { s.pop(); } if matches!(s.as_str(), "0." | "-0.") { return "0.0".into(); } if let Some(rest) = s.strip_prefix("0.") { s = format!(".{rest}"); } else if let Some(rest) = s.strip_prefix("-0.") { s = format!("-.{rest}"); } s } #[cfg(test)] mod tests { use super::*; const SAMPLE: &[u8] = b"\n\n<<\n\t/EngineDict\n\t<<\n\t\t/Editor\n\t\t<<\n\t\t\t/Text (\xFE\xFF\x00H\x00i\x00\\)\x00\r)\n\t\t>>\n\t\t/Nums [ 1.0 .5 -2 0 ]\n\t\t/Flag true\n\t\t/Kind /Roman\n\t\t/Runs [\n\t\t<<\n\t\t\t/A 1\n\t\t>>\n\t\t]\n\t>>\n>>"; #[test] fn parses_sample() { let v = parse(SAMPLE).unwrap(); assert_eq!(v.path(&["EngineDict", "Editor", "Text"]).and_then(Value::as_str), Some("Hi)\r")); let nums: Vec = v.path(&["EngineDict", "Nums"]).unwrap().as_array().unwrap().iter().filter_map(Value::as_f64).collect(); assert_eq!(nums, [1.0, 0.5, -2.0, 0.0]); assert_eq!(v.path(&["EngineDict", "Flag"]).and_then(Value::as_bool), Some(true)); assert_eq!(v.path(&["EngineDict", "Kind"]), Some(&Value::Name("Roman".into()))); assert_eq!(v.path(&["EngineDict", "Runs"]).unwrap().as_array().unwrap()[0].get("A").and_then(Value::as_i64), Some(1)); } #[test] fn write_parse_roundtrip() { let v = parse(SAMPLE).unwrap(); let bytes = write(&v); assert_eq!(parse(&bytes).unwrap(), v); // Stable: writing twice gives identical bytes. assert_eq!(write(&parse(&bytes).unwrap()), bytes); } #[test] fn reals_format_like_photoshop() { assert_eq!(format_real(1.0), "1.0"); assert_eq!(format_real(0.5), ".5"); assert_eq!(format_real(-0.25), "-.25"); assert_eq!(format_real(12.125), "12.125"); } #[test] fn tiny_reals_round_trip_as_numbers() { for value in [0.000004, -0.000004] { assert_eq!(format_real(value), "0.0"); let source = Value::Dict(vec![( "EngineDict".into(), Value::Dict(vec![("StyleRun".into(), Value::Array(vec![Value::Dict(vec![("BaselineShift".into(), Value::Real(value))])]))]), )]); let encoded = write(&source); let parsed = parse(&encoded).expect("formatted tiny real should parse"); let baseline_shift = parsed .path(&["EngineDict", "StyleRun"]) .and_then(Value::as_array) .and_then(|runs| runs.first()) .and_then(|run| run.get("BaselineShift")) .and_then(Value::as_f64); assert_eq!(baseline_shift, Some(0.0)); } } #[test] fn malformed_input_never_panics() { for cut in 0..SAMPLE.len() { let _ = parse(&SAMPLE[..cut]); } assert!(parse(b"<< /A").is_err()); assert!(parse(&[b'['; 200]).is_err()); } }