ag_psd/engine_data2.rs
1/*
2File: crates/ag-psd/src/engine_data2.rs
3
4Purpose:
5вариант Engine Data v2 (альтернативное/структурное представление текстовых данных).
6
7Source compatibility:
8- порт upstream-файла `test/ag-psd/src/engineData2.ts` (разбиение 1:1).
9
10Main responsibilities:
11- зеркалировать соответствующий upstream-модуль при портировании;
12- держать публичный контракт этого участка в одном месте.
13
14Mapping TS -> Rust:
15- `decodeEngineData2(data)` -> `decode_engine_data2(data: &EngineValue) -> EngineValue`
16- внутренняя `decodeObj` -> `decode_obj(obj, keys) -> EngineValue`
17- словари переименования ключей (`keysRoot`, `keysStyleSheet`, ...) -> статически
18 построенные `KeysDict` (lazy через `OnceLock`).
19
20Модель:
21Upstream `decodeObj` принимает нетипизированный распарсенный EngineData (числовые ключи)
22и переименовывает ключи по словарю `KeysDict`, опционально «поднимая» (uproot) вложенный
23узел на уровень выше и проставляя `_type` из ключа `99`. Здесь вход и выход —
24`crate::engine_data::EngineValue`; результат содержит переименованные `Dict`-ключи,
25а `_type` записывается как обычный ключ `"_type"`.
26
27TS-тип `GlobalEngineData` — это loose-структура (`any`/TODO в upstream'е), поэтому
28типобезопасной обёртки нет: `decode_engine_data2` возвращает `EngineValue`.
29*/
30
31use crate::engine_data::EngineValue;
32use std::collections::HashMap;
33use std::sync::OnceLock;
34
35/// Запись в словаре переименования ключей (зеркало `KeysDict[key]`).
36#[derive(Debug, Clone, Default)]
37pub struct KeyEntry {
38 pub name: Option<&'static str>,
39 pub uproot: bool,
40 pub children: Option<KeysDict>,
41}
42
43/// Словарь переименования: ключ EngineData -> правило (`KeysDict`).
44pub type KeysDict = HashMap<&'static str, KeyEntry>;
45
46fn entry_name(name: &'static str) -> KeyEntry {
47 KeyEntry { name: Some(name), uproot: false, children: None }
48}
49
50fn entry(name: Option<&'static str>, uproot: bool, children: Option<KeysDict>) -> KeyEntry {
51 KeyEntry { name, uproot, children }
52}
53
54fn dict(pairs: Vec<(&'static str, KeyEntry)>) -> KeysDict {
55 pairs.into_iter().collect()
56}
57
58fn keys_color() -> KeysDict {
59 dict(vec![(
60 "0",
61 entry(
62 None,
63 true,
64 Some(dict(vec![
65 ("0", entry_name("Type")),
66 ("1", entry_name("Values")),
67 ])),
68 ),
69 )])
70}
71
72fn keys_style_sheet() -> KeysDict {
73 dict(vec![
74 ("0", entry_name("Font")),
75 ("1", entry_name("FontSize")),
76 ("2", entry_name("FauxBold")),
77 ("3", entry_name("FauxItalic")),
78 ("4", entry_name("AutoLeading")),
79 ("5", entry_name("Leading")),
80 ("6", entry_name("HorizontalScale")),
81 ("7", entry_name("VerticalScale")),
82 ("8", entry_name("Tracking")),
83 ("9", entry_name("BaselineShift")),
84 ("11", entry_name("Kerning?")),
85 ("12", entry_name("FontCaps")),
86 ("13", entry_name("FontBaseline")),
87 ("15", entry_name("Strikethrough?")),
88 ("16", entry_name("Underline?")),
89 ("18", entry_name("Ligatures")),
90 ("19", entry_name("DLigatures")),
91 ("23", entry_name("Fractions")),
92 ("24", entry_name("Ordinals")),
93 ("28", entry_name("StylisticAlternates")),
94 ("30", entry_name("OldStyle?")),
95 ("35", entry_name("BaselineDirection")),
96 ("38", entry_name("Language")),
97 ("52", entry_name("NoBreak")),
98 ("53", entry(Some("FillColor"), false, Some(keys_color()))),
99 ("54", entry(Some("StrokeColor"), false, Some(keys_color()))),
100 (
101 "55",
102 entry(
103 None,
104 false,
105 Some(dict(vec![("99", entry(None, true, None))])),
106 ),
107 ),
108 ("79", entry(None, false, Some(keys_color()))),
109 ])
110}
111
112fn keys_paragraph() -> KeysDict {
113 dict(vec![
114 ("0", entry_name("Justification")),
115 ("1", entry_name("FirstLineIndent")),
116 ("2", entry_name("StartIndent")),
117 ("3", entry_name("EndIndent")),
118 ("4", entry_name("SpaceBefore")),
119 ("5", entry_name("SpaceAfter")),
120 ("7", entry_name("AutoLeading")),
121 ("9", entry_name("AutoHyphenate")),
122 ("10", entry_name("HyphenatedWordSize")),
123 ("11", entry_name("PreHyphen")),
124 ("12", entry_name("PostHyphen")),
125 ("13", entry_name("ConsecutiveHyphens?")),
126 ("14", entry_name("Zone")),
127 ("15", entry_name("HypenateCapitalizedWords")),
128 ("17", entry_name("WordSpacing")),
129 ("18", entry_name("LetterSpacing")),
130 ("19", entry_name("GlyphSpacing")),
131 ("32", entry(Some("StyleSheet"), false, Some(keys_style_sheet()))),
132 ])
133}
134
135fn keys_style_sheet_data() -> KeyEntry {
136 entry(Some("StyleSheetData"), false, Some(keys_style_sheet()))
137}
138
139fn keys_root() -> KeysDict {
140 dict(vec![
141 (
142 "0",
143 entry(
144 Some("ResourceDict"),
145 false,
146 Some(dict(vec![
147 (
148 "1",
149 entry(
150 Some("FontSet"),
151 false,
152 Some(dict(vec![(
153 "0",
154 entry(
155 None,
156 true,
157 Some(dict(vec![(
158 "0",
159 entry(
160 None,
161 true,
162 Some(dict(vec![(
163 "0",
164 entry(
165 None,
166 true,
167 Some(dict(vec![
168 ("0", entry_name("Name")),
169 ("2", entry_name("FontType")),
170 ])),
171 ),
172 )])),
173 ),
174 )])),
175 ),
176 )])),
177 ),
178 ),
179 ("2", entry(Some("2"), false, Some(dict(vec![])))),
180 (
181 "3",
182 entry(
183 Some("MojiKumiSet"),
184 false,
185 Some(dict(vec![(
186 "0",
187 entry(
188 None,
189 true,
190 Some(dict(vec![(
191 "0",
192 entry(
193 None,
194 true,
195 Some(dict(vec![("0", entry_name("InternalName"))])),
196 ),
197 )])),
198 ),
199 )])),
200 ),
201 ),
202 (
203 "4",
204 entry(
205 Some("KinsokuSet"),
206 false,
207 Some(dict(vec![(
208 "0",
209 entry(
210 None,
211 true,
212 Some(dict(vec![(
213 "0",
214 entry(
215 None,
216 true,
217 Some(dict(vec![
218 ("0", entry_name("Name")),
219 (
220 "5",
221 entry(
222 None,
223 true,
224 Some(dict(vec![
225 ("0", entry_name("NoStart")),
226 ("1", entry_name("NoEnd")),
227 ("2", entry_name("Keep")),
228 ("3", entry_name("Hanging")),
229 ("4", entry_name("Name")),
230 ])),
231 ),
232 ),
233 ])),
234 ),
235 )])),
236 ),
237 )])),
238 ),
239 ),
240 (
241 "5",
242 entry(
243 Some("StyleSheetSet"),
244 false,
245 Some(dict(vec![(
246 "0",
247 entry(
248 None,
249 true,
250 Some(dict(vec![(
251 "0",
252 entry(
253 None,
254 true,
255 Some(dict(vec![
256 ("0", entry_name("Name")),
257 ("6", keys_style_sheet_data()),
258 ])),
259 ),
260 )])),
261 ),
262 )])),
263 ),
264 ),
265 (
266 "6",
267 entry(
268 Some("ParagraphSheetSet"),
269 false,
270 Some(dict(vec![(
271 "0",
272 entry(
273 None,
274 true,
275 Some(dict(vec![(
276 "0",
277 entry(
278 None,
279 true,
280 Some(dict(vec![
281 ("0", entry_name("Name")),
282 (
283 "5",
284 entry(
285 Some("Properties"),
286 false,
287 Some(keys_paragraph()),
288 ),
289 ),
290 ("6", entry_name("DefaultStyleSheet")),
291 ])),
292 ),
293 )])),
294 ),
295 )])),
296 ),
297 ),
298 (
299 "8",
300 entry(
301 Some("TextFrameSet"),
302 false,
303 Some(dict(vec![(
304 "0",
305 entry(
306 None,
307 true,
308 Some(dict(vec![(
309 "0",
310 entry(
311 Some("path"),
312 false,
313 Some(dict(vec![
314 ("0", entry_name("name")),
315 (
316 "1",
317 entry(
318 Some("bezierCurve"),
319 false,
320 Some(dict(vec![(
321 "0",
322 entry_name("controlPoints"),
323 )])),
324 ),
325 ),
326 (
327 "2",
328 entry(
329 Some("data"),
330 false,
331 Some(dict(vec![
332 ("0", entry_name("type")),
333 ("1", entry_name("orientation")),
334 ("2", entry_name("frameMatrix")),
335 ("4", entry_name("4")),
336 ("6", entry_name("textRange")),
337 ("7", entry_name("rowGutter")),
338 ("8", entry_name("columnGutter")),
339 ("9", entry_name("9")),
340 (
341 "10",
342 entry(
343 Some("baselineAlignment"),
344 false,
345 Some(dict(vec![
346 ("0", entry_name("flag")),
347 ("1", entry_name("min")),
348 ])),
349 ),
350 ),
351 (
352 "11",
353 entry(
354 Some("pathData"),
355 false,
356 Some(dict(vec![
357 ("1", entry_name("1")),
358 ("0", entry_name("reversed")),
359 ("2", entry_name("2")),
360 ("3", entry_name("3")),
361 ("4", entry_name("spacing")),
362 ("5", entry_name("5")),
363 ("6", entry_name("6")),
364 ("7", entry_name("7")),
365 ("18", entry_name("18")),
366 ])),
367 ),
368 ),
369 ("12", entry_name("12")),
370 ("13", entry_name("13")),
371 ])),
372 ),
373 ),
374 ("3", entry_name("3")),
375 ("97", entry_name("uuid")),
376 ])),
377 ),
378 )])),
379 ),
380 )])),
381 ),
382 ),
383 (
384 "9",
385 entry(
386 Some("Predefined"),
387 false,
388 Some(dict(vec![
389 (
390 "0",
391 entry(
392 None,
393 false,
394 Some(dict(vec![("0", entry(None, true, None))])),
395 ),
396 ),
397 (
398 "1",
399 entry(
400 None,
401 false,
402 Some(dict(vec![("0", entry(None, true, None))])),
403 ),
404 ),
405 ])),
406 ),
407 ),
408 ])),
409 ),
410 ),
411 (
412 "1",
413 entry(
414 Some("EngineDict"),
415 false,
416 Some(dict(vec![
417 (
418 "0",
419 entry(
420 Some("0"),
421 false,
422 Some(dict(vec![
423 ("3", entry_name("SuperscriptSize")),
424 ("4", entry_name("SuperscriptPosition")),
425 ("5", entry_name("SubscriptSize")),
426 ("6", entry_name("SubscriptPosition")),
427 ("7", entry_name("SmallCapSize")),
428 ("8", entry_name("UseFractionalGlyphWidths")),
429 (
430 "15",
431 entry(
432 None,
433 false,
434 Some(dict(vec![("0", entry(None, true, None))])),
435 ),
436 ),
437 ])),
438 ),
439 ),
440 (
441 "1",
442 entry(
443 Some("Editors?"),
444 false,
445 Some(dict(vec![
446 (
447 "0",
448 entry(
449 Some("Editor"),
450 false,
451 Some(dict(vec![
452 ("0", entry_name("Text")),
453 (
454 "5",
455 entry(
456 Some("ParagraphRun"),
457 false,
458 Some(dict(vec![(
459 "0",
460 entry(
461 Some("RunArray"),
462 false,
463 Some(dict(vec![
464 (
465 "0",
466 entry(
467 Some("ParagraphSheet"),
468 false,
469 Some(dict(vec![(
470 "0",
471 entry(
472 None,
473 true,
474 Some(dict(vec![
475 ("0", entry_name("0")),
476 (
477 "5",
478 entry(
479 Some("5"),
480 false,
481 Some(keys_paragraph()),
482 ),
483 ),
484 ("6", entry_name("6")),
485 ])),
486 ),
487 )])),
488 ),
489 ),
490 ("1", entry_name("RunLength")),
491 ])),
492 ),
493 )])),
494 ),
495 ),
496 (
497 "6",
498 entry(
499 Some("StyleRun"),
500 false,
501 Some(dict(vec![(
502 "0",
503 entry(
504 Some("RunArray"),
505 false,
506 Some(dict(vec![
507 (
508 "0",
509 entry(
510 Some("StyleSheet"),
511 false,
512 Some(dict(vec![(
513 "0",
514 entry(
515 None,
516 true,
517 Some(dict(vec![(
518 "6",
519 keys_style_sheet_data(),
520 )])),
521 ),
522 )])),
523 ),
524 ),
525 ("1", entry_name("RunLength")),
526 ])),
527 ),
528 )])),
529 ),
530 ),
531 ])),
532 ),
533 ),
534 ("1", entry_name("FontVectorData ???")),
535 ])),
536 ),
537 ),
538 ("2", entry(Some("StyleSheet"), false, Some(keys_style_sheet()))),
539 ("3", entry(Some("ParagraphSheet"), false, Some(keys_paragraph()))),
540 ])),
541 ),
542 ),
543 ])
544}
545
546fn keys_root_static() -> &'static KeysDict {
547 static ROOT: OnceLock<KeysDict> = OnceLock::new();
548 ROOT.get_or_init(keys_root)
549}
550
551static EMPTY_KEYS: OnceLock<KeysDict> = OnceLock::new();
552
553fn empty_keys() -> &'static KeysDict {
554 EMPTY_KEYS.get_or_init(HashMap::new)
555}
556
557// Зеркало `decodeObj`. Переименовывает ключи по `keys`, обрабатывает uproot и `_type`.
558fn decode_obj(obj: &EngineValue, keys: &KeysDict) -> EngineValue {
559 match obj {
560 EngineValue::Null => EngineValue::Null,
561 EngineValue::Array(arr) => {
562 EngineValue::Array(arr.iter().map(|x| decode_obj(x, keys)).collect())
563 }
564 // typeof obj !== 'object' -> вернуть как есть (числа, булевы, строки).
565 EngineValue::Number(_) | EngineValue::Bool(_) | EngineValue::Str(_) => obj.clone(),
566 EngineValue::Dict(map) => {
567 let mut result: Vec<(String, EngineValue)> = Vec::new();
568
569 for (key, value) in map {
570 if let Some(entry) = keys.get(key.as_str()) {
571 if entry.uproot {
572 // result = decodeObj(obj[key], children) — только если ключ != '99'.
573 if key != "99" {
574 let children = entry.children.as_ref().unwrap_or_else(|| empty_keys());
575 let decoded = decode_obj(value, children);
576 // result := decoded (перезаписываем целиком).
577 result = match decoded {
578 EngineValue::Dict(m) => m,
579 // decodeObj uproot-узла всегда даёт объект, но на случай
580 // не-объекта сохраняем под пустым ключом не требуется —
581 // в upstream'е это присвоение `result = <не-объект>` с
582 // последующим `result._type`/`result[...]`; для не-объекта
583 // ветка недостижима в реальных данных. Используем пусто.
584 other => vec![("".to_string(), other)],
585 };
586 }
587 // if (obj['99']) result._type = obj['99'];
588 if let Some(t) = map.iter().find(|(k, _)| k == "99").map(|(_, v)| v) {
589 if is_truthy(t) {
590 set_kv(&mut result, "_type", t.clone());
591 }
592 }
593 break;
594 } else {
595 let name = entry.name.unwrap_or(key.as_str());
596 let children = entry.children.as_ref().unwrap_or_else(|| empty_keys());
597 set_kv(&mut result, name, decode_obj(value, children));
598 }
599 } else if key == "99" {
600 set_kv(&mut result, "_type", value.clone());
601 } else {
602 set_kv(&mut result, key, decode_obj(value, empty_keys()));
603 }
604 }
605
606 EngineValue::Dict(result)
607 }
608 }
609}
610
611// JS-истинность значения для `if (obj['99'])`: null/false/0/"" -> false.
612fn is_truthy(v: &EngineValue) -> bool {
613 match v {
614 EngineValue::Null => false,
615 EngineValue::Bool(b) => *b,
616 EngineValue::Number(n) => *n != 0.0 && !n.is_nan(),
617 EngineValue::Str(s) => !s.is_empty(),
618 EngineValue::Array(_) | EngineValue::Dict(_) => true,
619 }
620}
621
622fn set_kv(map: &mut Vec<(String, EngineValue)>, key: &str, value: EngineValue) {
623 if let Some(slot) = map.iter_mut().find(|(k, _)| k == key) {
624 slot.1 = value;
625 } else {
626 map.push((key.to_string(), value));
627 }
628}
629
630/// Порт `decodeEngineData2`. Возвращает декодированную (переименованную) структуру.
631pub fn decode_engine_data2(data: &EngineValue) -> EngineValue {
632 decode_obj(data, keys_root_static())
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use crate::engine_data::EngineValue;
639
640 fn d(pairs: Vec<(&str, EngineValue)>) -> EngineValue {
641 EngineValue::Dict(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
642 }
643
644 fn get<'a>(v: &'a EngineValue, key: &str) -> Option<&'a EngineValue> {
645 if let EngineValue::Dict(m) = v {
646 m.iter().find(|(k, _)| k == key).map(|(_, v)| v)
647 } else {
648 None
649 }
650 }
651
652 #[test]
653 fn renames_top_level_keys() {
654 // { "0": {...}, "1": {...} } -> { ResourceDict, EngineDict }
655 let input = d(vec![
656 ("0", d(vec![])),
657 ("1", d(vec![])),
658 ]);
659 let out = decode_engine_data2(&input);
660 assert!(get(&out, "ResourceDict").is_some());
661 assert!(get(&out, "EngineDict").is_some());
662 }
663
664 #[test]
665 fn renames_style_sheet_keys() {
666 // EngineDict.StyleSheet (key "2") uses keysStyleSheet: "1" -> FontSize.
667 let input = d(vec![(
668 "1",
669 d(vec![(
670 "2",
671 d(vec![("1", EngineValue::Number(12.0))]),
672 )]),
673 )]);
674 let out = decode_engine_data2(&input);
675 let engine = get(&out, "EngineDict").unwrap();
676 let style = get(engine, "StyleSheet").unwrap();
677 assert_eq!(get(style, "FontSize"), Some(&EngineValue::Number(12.0)));
678 }
679
680 #[test]
681 fn uproot_and_type() {
682 // keysColor: "0" uproot with children {0:Type,1:Values}; "99" -> _type.
683 // Build a FillColor (StyleSheet key "53").
684 let input = d(vec![(
685 "1", // EngineDict
686 d(vec![(
687 "2", // EngineDict.StyleSheet
688 d(vec![(
689 "53", // FillColor, children = keysColor
690 d(vec![
691 ("99", EngineValue::Str("/SimplePaint".to_string())),
692 (
693 "0",
694 d(vec![
695 ("0", EngineValue::Number(1.0)),
696 ("1", EngineValue::Array(vec![EngineValue::Number(0.0)])),
697 ]),
698 ),
699 ]),
700 )]),
701 )]),
702 )]);
703 let out = decode_engine_data2(&input);
704 let style = get(get(&out, "EngineDict").unwrap(), "StyleSheet").unwrap();
705 let fill = get(style, "FillColor").unwrap();
706 // uproot of "0" pulls Type/Values up; "_type" from "99".
707 assert_eq!(get(fill, "Type"), Some(&EngineValue::Number(1.0)));
708 assert_eq!(get(fill, "_type"), Some(&EngineValue::Str("/SimplePaint".to_string())));
709 assert!(get(fill, "Values").is_some());
710 }
711
712 #[test]
713 fn unknown_keys_passthrough() {
714 let input = d(vec![("zzz", EngineValue::Number(7.0))]);
715 let out = decode_engine_data2(&input);
716 assert_eq!(get(&out, "zzz"), Some(&EngineValue::Number(7.0)));
717 }
718}