File size: 35,086 Bytes
0b969ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
#[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<EngineValue>),
    Dict(Vec<(String, EngineValue)>),
}

pub fn encode_engine_data(spec: &TextEngineSpec) -> Vec<u8> {
    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<i32> {
    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<EngineValue>,
    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<EngineValue> {
    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<u8>,
    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<u8>, 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<u8>, indent: usize, in_property: bool) {
    if in_property {
        out.push(b' ');
    } else {
        write_indent(out, indent);
    }
}

fn write_indent(out: &mut Vec<u8>, indent: usize) {
    for _ in 0..indent {
        out.push(b'\t');
    }
}

fn write_escaped_byte(out: &mut Vec<u8>, 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 ]")
        );
    }
}