@flighthq/swf 0.3.0-next.1162.e90e518

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,919 @@
1
+ import {
2
+ getNodeLocalBoundsRectangle,
3
+ getNodeLocalMatrix,
4
+ getNodeParent,
5
+ getNodeWorldMatrix,
6
+ } from '@flighthq/node/contract';
7
+ import {
8
+ createScene2DDocumentFromBytes,
9
+ createScene2DDocumentImporterRegistry,
10
+ } from '@flighthq/scene2d-resources/contract';
11
+
12
+ import { createScene2DFromSwf, registerSwfScene2DDocumentImporter } from './swfDocument';
13
+
14
+ describe('createScene2DFromSwf', () => {
15
+ it('imports a named PlaceObject2 as a transformed slot with SymbolClass linkage', () => {
16
+ const document = createScene2DFromSwf(
17
+ createSwf([
18
+ createTag(TAG_FILE_ATTRIBUTES, new Uint8Array(4)),
19
+ createTag(TAG_SYMBOL_CLASS, joinBytes(uint16(1), uint16(7), swfString('Game.Avatar'))),
20
+ createTag(
21
+ TAG_PLACE_OBJECT_2,
22
+ joinBytes(
23
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER]),
24
+ uint16(3),
25
+ uint16(7),
26
+ createMatrix(1.5, 0.25, -0.125, 0.5, 200, -40),
27
+ swfString('avatarSlot'),
28
+ ),
29
+ ),
30
+ createTag(TAG_SHOW_FRAME),
31
+ createTag(TAG_END),
32
+ ]),
33
+ );
34
+
35
+ expect(document?.sourceKind).toBe('swf');
36
+ expect(document?.references).toHaveLength(1);
37
+ expect(getNodeLocalBoundsRectangle(document!.root)).toMatchObject({ height: 50, width: 100, x: 0, y: 0 });
38
+ const reference = document!.references[0];
39
+ expect(reference.kind).toBe('Slot');
40
+ expect(reference.name).toBe('avatarSlot');
41
+ expect(reference.kind === 'Slot' ? reference.linkage : null).toBe('Game.Avatar');
42
+ expect(reference.target.name).toBe('avatarSlot');
43
+
44
+ const matrix = getNodeLocalMatrix(reference.target);
45
+ expect(matrix.a).toBeCloseTo(1.5);
46
+ expect(matrix.b).toBeCloseTo(0.25);
47
+ expect(matrix.c).toBeCloseTo(-0.125);
48
+ expect(matrix.d).toBeCloseTo(0.5);
49
+ expect(matrix.tx).toBeCloseTo(10);
50
+ expect(matrix.ty).toBeCloseTo(-2);
51
+ });
52
+
53
+ it('imports a named empty shape with zero-bit RECT bounds', () => {
54
+ const document = createScene2DFromSwf(
55
+ createSwf([
56
+ createTag(TAG_DEFINE_SHAPE_3, joinBytes(uint16(1), new Uint8Array([0]))),
57
+ createTag(
58
+ TAG_PLACE_OBJECT_2,
59
+ joinBytes(new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]), uint16(1), uint16(1), swfString('empty')),
60
+ ),
61
+ createTag(TAG_SHOW_FRAME),
62
+ createTag(TAG_END),
63
+ ]),
64
+ );
65
+
66
+ expect(document?.references).toHaveLength(1);
67
+ expect(document?.references[0].name).toBe('empty');
68
+ expect(getNodeLocalBoundsRectangle(document!.references[0].target)).toMatchObject({
69
+ height: 0,
70
+ width: 0,
71
+ x: 0,
72
+ y: 0,
73
+ });
74
+ });
75
+
76
+ it('uses a PlaceObject3 class name as direct slot linkage', () => {
77
+ const document = createScene2DFromSwf(
78
+ createSwf([
79
+ createTag(
80
+ TAG_PLACE_OBJECT_3,
81
+ joinBytes(
82
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER, PLACE_HAS_CLASS_NAME]),
83
+ uint16(1),
84
+ swfString('Game.ExternalAvatar'),
85
+ uint16(9),
86
+ createMatrix(1, 0, 0, 1, 0, 0),
87
+ swfString('externalSlot'),
88
+ ),
89
+ ),
90
+ createTag(TAG_SHOW_FRAME),
91
+ createTag(TAG_END),
92
+ ]),
93
+ );
94
+
95
+ const reference = document?.references[0];
96
+ expect(reference?.kind).toBe('Slot');
97
+ expect(reference?.kind === 'Slot' ? reference.linkage : null).toBe('Game.ExternalAvatar');
98
+ });
99
+
100
+ it('updates an existing PlaceObject2 when the move flag is set', () => {
101
+ const document = createScene2DFromSwf(
102
+ createSwf([
103
+ createTag(TAG_SYMBOL_CLASS, joinBytes(uint16(1), uint16(7), swfString('Game.Avatar'))),
104
+ createTag(
105
+ TAG_PLACE_OBJECT_2,
106
+ joinBytes(
107
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER]),
108
+ uint16(3),
109
+ uint16(7),
110
+ createMatrix(1, 0, 0, 1, 0, 0),
111
+ swfString('avatarSlot'),
112
+ ),
113
+ ),
114
+ createTag(
115
+ TAG_PLACE_OBJECT_2,
116
+ joinBytes(new Uint8Array([PLACE_MOVE | PLACE_HAS_MATRIX]), uint16(3), createMatrix(2, 0, 0, 3, 40, -60)),
117
+ ),
118
+ createTag(TAG_SHOW_FRAME),
119
+ createTag(TAG_END),
120
+ ]),
121
+ );
122
+
123
+ const reference = document?.references[0];
124
+ expect(reference?.name).toBe('avatarSlot');
125
+ expect(reference?.kind === 'Slot' ? reference.linkage : null).toBe('Game.Avatar');
126
+ expect(getNodeLocalMatrix(reference!.target)).toMatchObject({ a: 2, d: 3, tx: 2, ty: -3 });
127
+ });
128
+
129
+ it('does not inherit stale fields for a fresh placement at an occupied depth', () => {
130
+ const document = createScene2DFromSwf(
131
+ createSwf([
132
+ createTag(
133
+ TAG_PLACE_OBJECT_2,
134
+ joinBytes(
135
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
136
+ uint16(3),
137
+ uint16(7),
138
+ swfString('staleSlot'),
139
+ ),
140
+ ),
141
+ createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(3), uint16(8))),
142
+ createTag(TAG_SHOW_FRAME),
143
+ createTag(TAG_END),
144
+ ]),
145
+ );
146
+
147
+ expect(document?.references).toEqual([]);
148
+ });
149
+
150
+ it('ignores a PlaceObject3 move with no display-list target', () => {
151
+ const document = createScene2DFromSwf(
152
+ createSwf([
153
+ createTag(
154
+ TAG_PLACE_OBJECT_3,
155
+ joinBytes(
156
+ new Uint8Array([PLACE_MOVE | PLACE_HAS_NAME, PLACE_HAS_CLASS_NAME]),
157
+ uint16(1),
158
+ swfString('Game.Ghost'),
159
+ swfString('ghostSlot'),
160
+ ),
161
+ ),
162
+ createTag(TAG_SHOW_FRAME),
163
+ createTag(TAG_END),
164
+ ]),
165
+ );
166
+
167
+ expect(document?.references).toEqual([]);
168
+ });
169
+
170
+ it('keeps the first-frame snapshot isolated from later mutations', () => {
171
+ const document = createScene2DFromSwf(
172
+ createSwf([
173
+ createTag(
174
+ TAG_PLACE_OBJECT_2,
175
+ joinBytes(
176
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
177
+ uint16(1),
178
+ uint16(7),
179
+ swfString('firstFrameSlot'),
180
+ ),
181
+ ),
182
+ createTag(TAG_SHOW_FRAME),
183
+ createTag(TAG_REMOVE_OBJECT_2, uint16(1)),
184
+ createTag(
185
+ TAG_PLACE_OBJECT_2,
186
+ joinBytes(
187
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
188
+ uint16(2),
189
+ uint16(8),
190
+ swfString('secondFrameSlot'),
191
+ ),
192
+ ),
193
+ createTag(TAG_SHOW_FRAME),
194
+ createTag(TAG_END),
195
+ ]),
196
+ );
197
+
198
+ expect(document?.references.map((reference) => reference.name)).toEqual(['firstFrameSlot']);
199
+ });
200
+
201
+ it('uses a PlaceObject4 class name as direct slot linkage while ignoring later metadata', () => {
202
+ const document = createScene2DFromSwf(
203
+ createSwf([
204
+ createTag(
205
+ TAG_PLACE_OBJECT_4,
206
+ joinBytes(
207
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER, PLACE_HAS_CLASS_NAME]),
208
+ uint16(1),
209
+ swfString('Game.MetadataAvatar'),
210
+ uint16(9),
211
+ createMatrix(1, 0, 0, 1, 60, 80),
212
+ swfString('metadataSlot'),
213
+ new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
214
+ ),
215
+ ),
216
+ createTag(TAG_SHOW_FRAME),
217
+ createTag(TAG_END),
218
+ ]),
219
+ );
220
+
221
+ const reference = document?.references[0];
222
+ expect(reference?.name).toBe('metadataSlot');
223
+ expect(reference?.kind === 'Slot' ? reference.linkage : null).toBe('Game.MetadataAvatar');
224
+ expect(getNodeLocalMatrix(reference!.target)).toMatchObject({ tx: 3, ty: 4 });
225
+ });
226
+
227
+ it('uses legacy PlaceObject transforms to import named descendants from a sprite', () => {
228
+ const document = createScene2DFromSwf(
229
+ createSwf([
230
+ createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(7), createRectangle(0, 200, 0, 100))),
231
+ createTag(
232
+ TAG_DEFINE_SPRITE,
233
+ joinBytes(
234
+ uint16(20),
235
+ uint16(1),
236
+ createTag(
237
+ TAG_PLACE_OBJECT_2,
238
+ joinBytes(
239
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
240
+ uint16(2),
241
+ uint16(7),
242
+ swfString('legacyChild'),
243
+ ),
244
+ ),
245
+ createTag(TAG_SHOW_FRAME),
246
+ createTag(TAG_END),
247
+ ),
248
+ ),
249
+ createTag(TAG_SYMBOL_CLASS, joinBytes(uint16(1), uint16(7), swfString('Game.LegacyChild'))),
250
+ createTag(
251
+ TAG_PLACE_OBJECT,
252
+ joinBytes(uint16(20), uint16(1), createMatrix(2, 0, 0, 3, 100, -40), createLegacyColorTransform()),
253
+ ),
254
+ createTag(TAG_SHOW_FRAME),
255
+ createTag(TAG_END),
256
+ ]),
257
+ );
258
+
259
+ const reference = document?.references[0];
260
+ expect(reference?.name).toBe('legacyChild');
261
+ expect(reference?.kind === 'Slot' ? reference.linkage : null).toBe('Game.LegacyChild');
262
+ expect(getNodeWorldMatrix(reference!.target)).toMatchObject({ a: 2, d: 3, tx: 5, ty: -2 });
263
+ });
264
+
265
+ it('applies legacy RemoveObject before freezing the first frame', () => {
266
+ const document = createScene2DFromSwf(
267
+ createSwf([
268
+ createTag(
269
+ TAG_DEFINE_SPRITE,
270
+ joinBytes(
271
+ uint16(20),
272
+ uint16(1),
273
+ createTag(
274
+ TAG_PLACE_OBJECT_2,
275
+ joinBytes(
276
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
277
+ uint16(1),
278
+ uint16(7),
279
+ swfString('removedChild'),
280
+ ),
281
+ ),
282
+ createTag(TAG_SHOW_FRAME),
283
+ createTag(TAG_END),
284
+ ),
285
+ ),
286
+ createTag(TAG_PLACE_OBJECT, joinBytes(uint16(20), uint16(4), createMatrix(1, 0, 0, 1, 0, 0))),
287
+ createTag(TAG_REMOVE_OBJECT, joinBytes(uint16(20), uint16(4))),
288
+ createTag(TAG_SHOW_FRAME),
289
+ createTag(TAG_END),
290
+ ]),
291
+ );
292
+
293
+ expect(document?.references).toEqual([]);
294
+ });
295
+
296
+ it('applies RemoveObject2 before freezing the first frame', () => {
297
+ const document = createScene2DFromSwf(
298
+ createSwf([
299
+ createTag(
300
+ TAG_PLACE_OBJECT_2,
301
+ joinBytes(
302
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
303
+ uint16(4),
304
+ uint16(7),
305
+ swfString('removedSlot'),
306
+ ),
307
+ ),
308
+ createTag(TAG_REMOVE_OBJECT_2, uint16(4)),
309
+ createTag(TAG_SHOW_FRAME),
310
+ createTag(TAG_END),
311
+ ]),
312
+ );
313
+
314
+ expect(document?.references).toEqual([]);
315
+ });
316
+
317
+ it('imports named slots from nested DefineSprite timelines', () => {
318
+ const document = createScene2DFromSwf(
319
+ createSwf([
320
+ createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(7), createRectangle(-20, 180, -40, 160))),
321
+ createTag(
322
+ TAG_DEFINE_SPRITE,
323
+ joinBytes(
324
+ uint16(20),
325
+ uint16(1),
326
+ createTag(
327
+ TAG_PLACE_OBJECT_2,
328
+ joinBytes(
329
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER]),
330
+ uint16(2),
331
+ uint16(7),
332
+ createMatrix(1, 0, 0, 1, 40, 60),
333
+ swfString('avatarSlot'),
334
+ ),
335
+ ),
336
+ createTag(TAG_SHOW_FRAME),
337
+ createTag(TAG_END),
338
+ ),
339
+ ),
340
+ createTag(
341
+ TAG_DEFINE_SPRITE,
342
+ joinBytes(
343
+ uint16(30),
344
+ uint16(1),
345
+ createTag(
346
+ TAG_PLACE_OBJECT_2,
347
+ joinBytes(
348
+ new Uint8Array([PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER]),
349
+ uint16(1),
350
+ uint16(20),
351
+ createMatrix(2, 0, 0, 3, 20, 20),
352
+ ),
353
+ ),
354
+ createTag(TAG_SHOW_FRAME),
355
+ createTag(TAG_END),
356
+ ),
357
+ ),
358
+ createTag(
359
+ TAG_SYMBOL_CLASS,
360
+ joinBytes(uint16(2), uint16(30), swfString('Game.Panel'), uint16(7), swfString('Game.Avatar')),
361
+ ),
362
+ createTag(
363
+ TAG_PLACE_OBJECT_2,
364
+ joinBytes(
365
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_MATRIX | PLACE_HAS_CHARACTER]),
366
+ uint16(1),
367
+ uint16(30),
368
+ createMatrix(2, 0, 0, 3, 100, 40),
369
+ swfString('panelSlot'),
370
+ ),
371
+ ),
372
+ createTag(TAG_SHOW_FRAME),
373
+ createTag(TAG_END),
374
+ ]),
375
+ );
376
+
377
+ expect(document?.references).toHaveLength(2);
378
+ const panel = document!.references[0];
379
+ const avatar = document!.references[1];
380
+ expect(panel.name).toBe('panelSlot');
381
+ expect(panel.kind === 'Slot' ? panel.linkage : null).toBe('Game.Panel');
382
+ expect(avatar.name).toBe('avatarSlot');
383
+ expect(avatar.kind === 'Slot' ? avatar.linkage : null).toBe('Game.Avatar');
384
+ expect(getNodeLocalBoundsRectangle(panel.target)).toMatchObject({ height: 30, width: 20, x: 3, y: 4 });
385
+ expect(getNodeLocalBoundsRectangle(avatar.target)).toMatchObject({ height: 10, width: 10, x: -1, y: -2 });
386
+ const intermediate = getNodeParent(avatar.target);
387
+ expect(intermediate).not.toBeNull();
388
+ expect(getNodeParent(intermediate!)).toBe(panel.target);
389
+
390
+ const world = getNodeWorldMatrix(avatar.target);
391
+ expect(world.a).toBeCloseTo(4);
392
+ expect(world.d).toBeCloseTo(9);
393
+ expect(world.tx).toBeCloseTo(15);
394
+ expect(world.ty).toBeCloseTo(32);
395
+ });
396
+
397
+ it('preserves authored dimensions from lossless bitmap definitions', () => {
398
+ const document = createScene2DFromSwf(
399
+ createSwf([
400
+ createTag(
401
+ TAG_DEFINE_BITS_LOSSLESS,
402
+ joinBytes(uint16(11), new Uint8Array([LOSSLESS_BITMAP_FORMAT_32_BIT]), uint16(32), uint16(16)),
403
+ ),
404
+ createTag(
405
+ TAG_DEFINE_BITS_LOSSLESS_2,
406
+ joinBytes(
407
+ uint16(12),
408
+ new Uint8Array([LOSSLESS_BITMAP_FORMAT_COLORMAPPED]),
409
+ uint16(8),
410
+ uint16(4),
411
+ new Uint8Array([0]),
412
+ ),
413
+ ),
414
+ createTag(
415
+ TAG_PLACE_OBJECT_2,
416
+ joinBytes(
417
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
418
+ uint16(1),
419
+ uint16(11),
420
+ swfString('rgbBitmap'),
421
+ ),
422
+ ),
423
+ createTag(
424
+ TAG_PLACE_OBJECT_2,
425
+ joinBytes(
426
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
427
+ uint16(2),
428
+ uint16(12),
429
+ swfString('alphaBitmap'),
430
+ ),
431
+ ),
432
+ createTag(TAG_SHOW_FRAME),
433
+ createTag(TAG_END),
434
+ ]),
435
+ );
436
+
437
+ expect(getNodeLocalBoundsRectangle(document!.references[0].target)).toMatchObject({
438
+ height: 16,
439
+ width: 32,
440
+ x: 0,
441
+ y: 0,
442
+ });
443
+ expect(getNodeLocalBoundsRectangle(document!.references[1].target)).toMatchObject({
444
+ height: 4,
445
+ width: 8,
446
+ x: 0,
447
+ y: 0,
448
+ });
449
+ });
450
+
451
+ it('preserves authored dimensions from a video stream definition', () => {
452
+ const document = createScene2DFromSwf(
453
+ createSwf([
454
+ createTag(
455
+ TAG_DEFINE_VIDEO_STREAM,
456
+ joinBytes(uint16(13), uint16(10), uint16(320), uint16(180), new Uint8Array([0, 2])),
457
+ ),
458
+ createTag(
459
+ TAG_PLACE_OBJECT_2,
460
+ joinBytes(
461
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
462
+ uint16(1),
463
+ uint16(13),
464
+ swfString('videoSlot'),
465
+ ),
466
+ ),
467
+ createTag(TAG_SHOW_FRAME),
468
+ createTag(TAG_END),
469
+ ]),
470
+ );
471
+
472
+ expect(getNodeLocalBoundsRectangle(document!.references[0].target)).toMatchObject({
473
+ height: 180,
474
+ width: 320,
475
+ x: 0,
476
+ y: 0,
477
+ });
478
+ });
479
+
480
+ it('preserves PNG and GIF dimensions embedded by DefineBitsJPEG2', () => {
481
+ const document = createScene2DFromSwf(
482
+ createSwf([
483
+ createTag(TAG_DEFINE_BITS_JPEG_2, joinBytes(uint16(14), createPngHeader(48, 24))),
484
+ createTag(TAG_DEFINE_BITS_JPEG_2, joinBytes(uint16(15), createGifHeader(17, 9))),
485
+ createTag(
486
+ TAG_PLACE_OBJECT_2,
487
+ joinBytes(
488
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
489
+ uint16(1),
490
+ uint16(14),
491
+ swfString('pngBitmap'),
492
+ ),
493
+ ),
494
+ createTag(
495
+ TAG_PLACE_OBJECT_2,
496
+ joinBytes(
497
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
498
+ uint16(2),
499
+ uint16(15),
500
+ swfString('gifBitmap'),
501
+ ),
502
+ ),
503
+ createTag(TAG_SHOW_FRAME),
504
+ createTag(TAG_END),
505
+ ]),
506
+ );
507
+
508
+ expect(getNodeLocalBoundsRectangle(document!.references[0].target)).toMatchObject({
509
+ height: 24,
510
+ width: 48,
511
+ x: 0,
512
+ y: 0,
513
+ });
514
+ expect(getNodeLocalBoundsRectangle(document!.references[1].target)).toMatchObject({
515
+ height: 9,
516
+ width: 17,
517
+ x: 0,
518
+ y: 0,
519
+ });
520
+ });
521
+
522
+ it('bounds DefineBitsJPEG3 and DefineBitsJPEG4 images before their alpha payloads', () => {
523
+ const jpeg3 = createJpegHeader(64, 32);
524
+ const jpeg4 = createJpegHeader(31, 19);
525
+ const document = createScene2DFromSwf(
526
+ createSwf([
527
+ createTag(
528
+ TAG_DEFINE_BITS_JPEG_3,
529
+ joinBytes(uint16(16), uint32(jpeg3.length), jpeg3, new Uint8Array([1, 2, 3])),
530
+ ),
531
+ createTag(
532
+ TAG_DEFINE_BITS_JPEG_4,
533
+ joinBytes(uint16(17), uint32(jpeg4.length + 2), uint16(0), jpeg4, new Uint8Array([4, 5, 6])),
534
+ ),
535
+ createTag(
536
+ TAG_PLACE_OBJECT_2,
537
+ joinBytes(
538
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
539
+ uint16(1),
540
+ uint16(16),
541
+ swfString('jpeg3Bitmap'),
542
+ ),
543
+ ),
544
+ createTag(
545
+ TAG_PLACE_OBJECT_2,
546
+ joinBytes(
547
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
548
+ uint16(2),
549
+ uint16(17),
550
+ swfString('jpeg4Bitmap'),
551
+ ),
552
+ ),
553
+ createTag(TAG_SHOW_FRAME),
554
+ createTag(TAG_END),
555
+ ]),
556
+ );
557
+
558
+ expect(getNodeLocalBoundsRectangle(document!.references[0].target)).toMatchObject({
559
+ height: 32,
560
+ width: 64,
561
+ x: 0,
562
+ y: 0,
563
+ });
564
+ expect(getNodeLocalBoundsRectangle(document!.references[1].target)).toMatchObject({
565
+ height: 19,
566
+ width: 31,
567
+ x: 0,
568
+ y: 0,
569
+ });
570
+ });
571
+
572
+ it('rejects compressed and truncated inputs without throwing', () => {
573
+ const compressed = createSwf([createTag(TAG_END)]);
574
+ compressed[0] = 0x43;
575
+ expect(createScene2DFromSwf(compressed)).toBeNull();
576
+ expect(createScene2DFromSwf(new Uint8Array([0x46, 0x57, 0x53]))).toBeNull();
577
+ expect(createScene2DFromSwf(createSwf([]))).toBeNull();
578
+ expect(
579
+ createScene2DFromSwf(
580
+ createSwf([
581
+ createTag(TAG_DEFINE_SPRITE, joinBytes(uint16(1), uint16(1), createTag(TAG_SHOW_FRAME))),
582
+ createTag(TAG_END),
583
+ ]),
584
+ ),
585
+ ).toBeNull();
586
+ expect(
587
+ createScene2DFromSwf(
588
+ createSwf([
589
+ createTag(
590
+ TAG_DEFINE_VIDEO_STREAM,
591
+ joinBytes(uint16(1), uint16(10), uint16(320), uint16(180), new Uint8Array([0])),
592
+ ),
593
+ createTag(TAG_END),
594
+ ]),
595
+ ),
596
+ ).toBeNull();
597
+ expect(
598
+ createScene2DFromSwf(
599
+ createSwf([createTag(TAG_PLACE_OBJECT, joinBytes(uint16(7), uint16(1))), createTag(TAG_END)]),
600
+ ),
601
+ ).toBeNull();
602
+ expect(createScene2DFromSwf(createSwf([createTag(TAG_REMOVE_OBJECT, uint16(7)), createTag(TAG_END)]))).toBeNull();
603
+ expect(
604
+ createScene2DFromSwf(
605
+ createSwf([
606
+ createTag(TAG_PLACE_OBJECT_4, new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE_HAS_CLASS_NAME])),
607
+ createTag(TAG_END),
608
+ ]),
609
+ ),
610
+ ).toBeNull();
611
+ expect(
612
+ createScene2DFromSwf(
613
+ createSwf([
614
+ createTag(
615
+ TAG_DEFINE_BITS_LOSSLESS,
616
+ joinBytes(new Uint8Array([1, 0, LOSSLESS_BITMAP_FORMAT_32_BIT]), uint16(32)),
617
+ ),
618
+ createTag(TAG_END),
619
+ ]),
620
+ ),
621
+ ).toBeNull();
622
+ expect(
623
+ createScene2DFromSwf(
624
+ createSwf([
625
+ createTag(
626
+ TAG_DEFINE_BITS_LOSSLESS_2,
627
+ joinBytes(uint16(1), new Uint8Array([LOSSLESS_BITMAP_FORMAT_15_BIT]), uint16(8), uint16(4)),
628
+ ),
629
+ createTag(TAG_END),
630
+ ]),
631
+ ),
632
+ ).toBeNull();
633
+ expect(
634
+ createScene2DFromSwf(
635
+ createSwf([
636
+ createTag(
637
+ TAG_DEFINE_BITS_LOSSLESS,
638
+ joinBytes(uint16(1), new Uint8Array([LOSSLESS_BITMAP_FORMAT_32_BIT]), uint16(8), uint16(4)),
639
+ ),
640
+ createTag(
641
+ TAG_DEFINE_VIDEO_STREAM,
642
+ joinBytes(uint16(1), uint16(10), uint16(320), uint16(180), new Uint8Array([0, 2])),
643
+ ),
644
+ createTag(TAG_END),
645
+ ]),
646
+ ),
647
+ ).toBeNull();
648
+ expect(
649
+ createScene2DFromSwf(
650
+ createSwf([createTag(TAG_DEFINE_BITS_JPEG_2, joinBytes(uint16(1), createPngHeader(0, 8))), createTag(TAG_END)]),
651
+ ),
652
+ ).toBeNull();
653
+ expect(
654
+ createScene2DFromSwf(
655
+ createSwf([
656
+ createTag(
657
+ TAG_DEFINE_BITS_JPEG_3,
658
+ joinBytes(uint16(1), uint32(100), createJpegHeader(8, 8), new Uint8Array([1])),
659
+ ),
660
+ createTag(TAG_END),
661
+ ]),
662
+ ),
663
+ ).toBeNull();
664
+ expect(
665
+ createScene2DFromSwf(
666
+ createSwf([
667
+ createTag(TAG_DEFINE_BITS_JPEG_4, joinBytes(uint16(1), uint32(1), uint16(0), createJpegHeader(8, 8))),
668
+ createTag(TAG_END),
669
+ ]),
670
+ ),
671
+ ).toBeNull();
672
+ });
673
+
674
+ it('rejects recursive sprite definitions without overflowing the graph walk', () => {
675
+ expect(
676
+ createScene2DFromSwf(
677
+ createSwf([
678
+ createTag(
679
+ TAG_DEFINE_SPRITE,
680
+ joinBytes(
681
+ uint16(1),
682
+ uint16(1),
683
+ createTag(
684
+ TAG_PLACE_OBJECT_2,
685
+ joinBytes(
686
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
687
+ uint16(1),
688
+ uint16(1),
689
+ swfString('recursiveChild'),
690
+ ),
691
+ ),
692
+ createTag(TAG_SHOW_FRAME),
693
+ createTag(TAG_END),
694
+ ),
695
+ ),
696
+ createTag(
697
+ TAG_PLACE_OBJECT_2,
698
+ joinBytes(
699
+ new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER]),
700
+ uint16(1),
701
+ uint16(1),
702
+ swfString('recursiveRoot'),
703
+ ),
704
+ ),
705
+ createTag(TAG_SHOW_FRAME),
706
+ createTag(TAG_END),
707
+ ]),
708
+ ),
709
+ ).toBeNull();
710
+ });
711
+ });
712
+
713
+ describe('registerSwfScene2DDocumentImporter', () => {
714
+ it('adds an opt-in SWF codec without global registration', () => {
715
+ const registry = createScene2DDocumentImporterRegistry();
716
+ const source = createSwf([createTag(TAG_END)]);
717
+ expect(createScene2DDocumentFromBytes(source, registry)).toBeNull();
718
+
719
+ registerSwfScene2DDocumentImporter(registry);
720
+
721
+ expect(createScene2DDocumentFromBytes(source, registry)?.sourceKind).toBe('swf');
722
+ });
723
+ });
724
+
725
+ class BitWriter {
726
+ private readonly bits: number[] = [];
727
+
728
+ toBytes(): Uint8Array {
729
+ const bytes = new Uint8Array(Math.ceil(this.bits.length / 8));
730
+ for (let i = 0; i < this.bits.length; i++) {
731
+ bytes[Math.floor(i / 8)] |= this.bits[i] << (7 - (i % 8));
732
+ }
733
+ return bytes;
734
+ }
735
+
736
+ writeSigned(value: number, count: number): void {
737
+ this.writeUnsigned(value < 0 ? value + 2 ** count : value, count);
738
+ }
739
+
740
+ writeUnsigned(value: number, count: number): void {
741
+ for (let i = count - 1; i >= 0; i--) this.bits.push(Math.floor(value / 2 ** i) & 1);
742
+ }
743
+ }
744
+
745
+ function createMatrix(a: number, b: number, c: number, d: number, tx: number, ty: number): Uint8Array {
746
+ const writer = new BitWriter();
747
+ const scales = [Math.round(a * FIXED_16_ONE), Math.round(d * FIXED_16_ONE)];
748
+ const rotates = [Math.round(b * FIXED_16_ONE), Math.round(c * FIXED_16_ONE)];
749
+ const scaleBits = signedBitCount(scales);
750
+ const rotateBits = signedBitCount(rotates);
751
+ const translateBits = signedBitCount([tx, ty]);
752
+
753
+ writer.writeUnsigned(1, 1);
754
+ writer.writeUnsigned(scaleBits, 5);
755
+ for (const value of scales) writer.writeSigned(value, scaleBits);
756
+ writer.writeUnsigned(1, 1);
757
+ writer.writeUnsigned(rotateBits, 5);
758
+ for (const value of rotates) writer.writeSigned(value, rotateBits);
759
+ writer.writeUnsigned(translateBits, 5);
760
+ writer.writeSigned(tx, translateBits);
761
+ writer.writeSigned(ty, translateBits);
762
+ return writer.toBytes();
763
+ }
764
+
765
+ function createLegacyColorTransform(): Uint8Array {
766
+ const writer = new BitWriter();
767
+ writer.writeUnsigned(1, 1);
768
+ writer.writeUnsigned(1, 1);
769
+ writer.writeUnsigned(2, 4);
770
+ for (let i = 0; i < 3; i++) writer.writeSigned(1, 2);
771
+ for (let i = 0; i < 3; i++) writer.writeSigned(-1, 2);
772
+ return writer.toBytes();
773
+ }
774
+
775
+ function createGifHeader(width: number, height: number): Uint8Array {
776
+ return joinBytes(_encoder.encode('GIF89a'), uint16(width), uint16(height));
777
+ }
778
+
779
+ function createJpegHeader(width: number, height: number): Uint8Array {
780
+ return new Uint8Array([
781
+ 0xff,
782
+ 0xd8,
783
+ 0xff,
784
+ 0xc0,
785
+ 0,
786
+ 17,
787
+ 8,
788
+ (height >> 8) & 0xff,
789
+ height & 0xff,
790
+ (width >> 8) & 0xff,
791
+ width & 0xff,
792
+ 3,
793
+ 1,
794
+ 0x11,
795
+ 0,
796
+ 2,
797
+ 0x11,
798
+ 0,
799
+ 3,
800
+ 0x11,
801
+ 0,
802
+ 0xff,
803
+ 0xd9,
804
+ ]);
805
+ }
806
+
807
+ function createPngHeader(width: number, height: number): Uint8Array {
808
+ return new Uint8Array([
809
+ 0x89,
810
+ 0x50,
811
+ 0x4e,
812
+ 0x47,
813
+ 0x0d,
814
+ 0x0a,
815
+ 0x1a,
816
+ 0x0a,
817
+ 0,
818
+ 0,
819
+ 0,
820
+ 13,
821
+ 0x49,
822
+ 0x48,
823
+ 0x44,
824
+ 0x52,
825
+ (width >>> 24) & 0xff,
826
+ (width >>> 16) & 0xff,
827
+ (width >>> 8) & 0xff,
828
+ width & 0xff,
829
+ (height >>> 24) & 0xff,
830
+ (height >>> 16) & 0xff,
831
+ (height >>> 8) & 0xff,
832
+ height & 0xff,
833
+ ]);
834
+ }
835
+
836
+ function createRectangle(xMin: number, xMax: number, yMin: number, yMax: number): Uint8Array {
837
+ const writer = new BitWriter();
838
+ const values = [xMin, xMax, yMin, yMax];
839
+ const bits = signedBitCount(values);
840
+ writer.writeUnsigned(bits, 5);
841
+ for (const value of values) writer.writeSigned(value, bits);
842
+ return writer.toBytes();
843
+ }
844
+
845
+ function createSwf(tags: ReadonlyArray<Uint8Array>): Uint8Array {
846
+ const body = joinBytes(createRectangle(0, 2000, 0, 1000), uint16(24 * 256), uint16(1), ...tags);
847
+ const fileLength = SWF_PREFIX_LENGTH + body.length;
848
+ return joinBytes(new Uint8Array([0x46, 0x57, 0x53, 9]), uint32(fileLength), body);
849
+ }
850
+
851
+ function createTag(code: number, body: Uint8Array = new Uint8Array()): Uint8Array {
852
+ const shortLength = body.length < 0x3f ? body.length : 0x3f;
853
+ const header = uint16((code << 6) | shortLength);
854
+ return shortLength === 0x3f ? joinBytes(header, uint32(body.length), body) : joinBytes(header, body);
855
+ }
856
+
857
+ function joinBytes(...parts: ReadonlyArray<Uint8Array>): Uint8Array {
858
+ let length = 0;
859
+ for (const part of parts) length += part.length;
860
+ const result = new Uint8Array(length);
861
+ let offset = 0;
862
+ for (const part of parts) {
863
+ result.set(part, offset);
864
+ offset += part.length;
865
+ }
866
+ return result;
867
+ }
868
+
869
+ function signedBitCount(values: ReadonlyArray<number>): number {
870
+ for (let bits = 1; bits < 32; bits++) {
871
+ const minimum = -(2 ** (bits - 1));
872
+ const maximum = 2 ** (bits - 1) - 1;
873
+ if (values.every((value) => value >= minimum && value <= maximum)) return bits;
874
+ }
875
+ return 32;
876
+ }
877
+
878
+ function swfString(value: string): Uint8Array {
879
+ return joinBytes(_encoder.encode(value), new Uint8Array([0]));
880
+ }
881
+
882
+ function uint16(value: number): Uint8Array {
883
+ return new Uint8Array([value & 0xff, (value >> 8) & 0xff]);
884
+ }
885
+
886
+ function uint32(value: number): Uint8Array {
887
+ return new Uint8Array([value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff]);
888
+ }
889
+
890
+ const FIXED_16_ONE = 0x10000;
891
+ const LOSSLESS_BITMAP_FORMAT_15_BIT = 4;
892
+ const LOSSLESS_BITMAP_FORMAT_32_BIT = 5;
893
+ const LOSSLESS_BITMAP_FORMAT_COLORMAPPED = 3;
894
+ const PLACE_HAS_CHARACTER = 0x02;
895
+ const PLACE_HAS_CLASS_NAME = 0x08;
896
+ const PLACE_HAS_MATRIX = 0x04;
897
+ const PLACE_HAS_NAME = 0x20;
898
+ const PLACE_MOVE = 0x01;
899
+ const SWF_PREFIX_LENGTH = 8;
900
+ const TAG_END = 0;
901
+ const TAG_DEFINE_BITS_JPEG_2 = 21;
902
+ const TAG_DEFINE_BITS_JPEG_3 = 35;
903
+ const TAG_DEFINE_BITS_JPEG_4 = 90;
904
+ const TAG_DEFINE_BITS_LOSSLESS = 20;
905
+ const TAG_DEFINE_BITS_LOSSLESS_2 = 36;
906
+ const TAG_DEFINE_SHAPE = 2;
907
+ const TAG_DEFINE_SHAPE_3 = 32;
908
+ const TAG_DEFINE_SPRITE = 39;
909
+ const TAG_DEFINE_VIDEO_STREAM = 60;
910
+ const TAG_FILE_ATTRIBUTES = 69;
911
+ const TAG_PLACE_OBJECT = 4;
912
+ const TAG_PLACE_OBJECT_2 = 26;
913
+ const TAG_PLACE_OBJECT_3 = 70;
914
+ const TAG_PLACE_OBJECT_4 = 94;
915
+ const TAG_REMOVE_OBJECT = 5;
916
+ const TAG_REMOVE_OBJECT_2 = 28;
917
+ const TAG_SHOW_FRAME = 1;
918
+ const TAG_SYMBOL_CLASS = 76;
919
+ const _encoder = new TextEncoder();