@flighthq/swf 0.3.0 → 0.3.1-next.320.fe56fc2

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.
Files changed (43) hide show
  1. package/dist/contract.d.ts +1 -0
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +1 -0
  4. package/dist/contract.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/swfDocument.d.ts +5 -5
  10. package/dist/swfDocument.d.ts.map +1 -1
  11. package/dist/swfDocument.js +359 -102
  12. package/dist/swfDocument.js.map +1 -1
  13. package/dist/swfFilter.d.ts +2 -2
  14. package/dist/swfFilter.d.ts.map +1 -1
  15. package/dist/swfFilter.js +24 -2
  16. package/dist/swfFilter.js.map +1 -1
  17. package/dist/swfFrameAction.d.ts +2 -2
  18. package/dist/swfFrameAction.d.ts.map +1 -1
  19. package/dist/swfFrameAction.js +13 -6
  20. package/dist/swfFrameAction.js.map +1 -1
  21. package/dist/swfFrameActionTestHelper.d.ts +2 -0
  22. package/dist/swfFrameActionTestHelper.d.ts.map +1 -0
  23. package/dist/swfFrameActionTestHelper.js +99 -0
  24. package/dist/swfFrameActionTestHelper.js.map +1 -0
  25. package/dist/swfImageDecoder.d.ts +4 -0
  26. package/dist/swfImageDecoder.d.ts.map +1 -0
  27. package/dist/swfImageDecoder.js +21 -0
  28. package/dist/swfImageDecoder.js.map +1 -0
  29. package/dist/swfMorphShape.d.ts +2 -1
  30. package/dist/swfMorphShape.d.ts.map +1 -1
  31. package/dist/swfMorphShape.js +18 -7
  32. package/dist/swfMorphShape.js.map +1 -1
  33. package/dist/swfText.d.ts +2 -2
  34. package/dist/swfText.d.ts.map +1 -1
  35. package/dist/swfText.js +15 -2
  36. package/dist/swfText.js.map +1 -1
  37. package/package.json +24 -22
  38. package/src/swfDocument.test.ts +1527 -163
  39. package/src/swfFilter.test.ts +53 -0
  40. package/src/swfFrameAction.test.ts +26 -97
  41. package/src/swfImageDecoder.test.ts +53 -0
  42. package/src/swfImageResources.test.ts +14 -17
  43. package/src/swfMorphShape.test.ts +18 -0
@@ -1,3 +1,4 @@
1
+ import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
1
2
  import type {
2
3
  Adjustment,
3
4
  BevelEffect,
@@ -6,11 +7,13 @@ import type {
6
7
  ConvolutionEffect,
7
8
  DropShadowEffect,
8
9
  GradientGlowEffect,
10
+ ImportDiagnostic,
9
11
  InnerGlowEffect,
10
12
  InnerShadowEffect,
11
13
  OuterGlowEffect,
12
14
  RenderEffect,
13
15
  } from '@flighthq/types/contract';
16
+ import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
14
17
 
15
18
  import { readSwfFilterList, setSwfFilterListGuard } from './swfFilter';
16
19
  import { SwfReader } from './swfReader';
@@ -124,6 +127,35 @@ describe('readSwfFilterList', () => {
124
127
  expect(glow.ratios).toEqual([0, 255]);
125
128
  });
126
129
 
130
+ it('reports a gradient glow angle and distance it cannot represent, and stays silent without them', () => {
131
+ // GradientGlowEffect has no angle, distance, or inner/on-top member, so a glow authored with any of
132
+ // them loses that placement. The zero case is the load-bearing half: no crumb when nothing is lost.
133
+ const withPlacement = collectImportDiagnostics((sink) => {
134
+ readGlow(fixed(45), fixed(4), sink);
135
+ });
136
+ expect(withPlacement.map((entry) => entry.kind)).toEqual(['swf.filter-field-unrepresentable']);
137
+ expect(withPlacement[0].severity).toBe(ImportDiagnosticSeverity.Skip);
138
+ expect(withPlacement[0].detail).toEqual({
139
+ capability: 'swf.placement.filter-list',
140
+ field: 'placement',
141
+ filterId: 4,
142
+ });
143
+
144
+ expect(collectImportDiagnostics((sink) => readGlow(fixed(0), fixed(0), sink))).toEqual([]);
145
+ });
146
+
147
+ it('reports a blur pass count it cannot represent, and stays silent at one pass', () => {
148
+ // Found by auditing the CLAIM against the code: the filter-list wire covered gradient glow only,
149
+ // while BlurEffect silently discards an authored pass count. Same kind, distinguished by `field`.
150
+ const multi = collectImportDiagnostics((sink) => {
151
+ readBlur(3, sink);
152
+ });
153
+ expect(multi.map((entry) => entry.kind)).toEqual(['swf.filter-field-unrepresentable']);
154
+ expect(multi[0].detail).toEqual({ capability: 'swf.placement.filter-list', field: 'passes', filterId: 1 });
155
+
156
+ expect(collectImportDiagnostics((sink) => readBlur(1, sink))).toEqual([]);
157
+ });
158
+
127
159
  it('reads a convolution kernel with its flags', () => {
128
160
  const { effects } = read(
129
161
  joinBytes(
@@ -225,6 +257,27 @@ describe('setSwfFilterListGuard', () => {
225
257
  });
226
258
  });
227
259
 
260
+ function readBlur(passes: number, diagnostics: ImportDiagnostic[]): void {
261
+ const bytes = joinBytes(new Uint8Array([1, 1]), fixed(4), fixed(4), new Uint8Array([passes]));
262
+ readSwfFilterList(new SwfReader(bytes, 0, bytes.length), [], [], diagnostics);
263
+ }
264
+
265
+ function readGlow(angle: Uint8Array, distance: Uint8Array, diagnostics: ImportDiagnostic[]): void {
266
+ const bytes = joinBytes(
267
+ new Uint8Array([1, 4, 2]),
268
+ rgba(0xff, 0, 0, 0),
269
+ rgba(0, 0, 0xff, 0xff),
270
+ new Uint8Array([0, 255]),
271
+ fixed(6),
272
+ fixed(6),
273
+ angle,
274
+ distance,
275
+ fixed8(1),
276
+ new Uint8Array([0]),
277
+ );
278
+ readSwfFilterList(new SwfReader(bytes, 0, bytes.length), [], [], diagnostics);
279
+ }
280
+
228
281
  function read(bytes: Uint8Array): { adjustments: Adjustment[]; complete: boolean; effects: RenderEffect[] } {
229
282
  const adjustments: Adjustment[] = [];
230
283
  const effects: RenderEffect[] = [];
@@ -1,3 +1,4 @@
1
+ import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
1
2
  import {
2
3
  createMovieClip,
3
4
  getMovieClipCurrentFrame,
@@ -6,8 +7,10 @@ import {
6
7
  setMovieClipSource,
7
8
  } from '@flighthq/movieclip/contract';
8
9
  import type { MovieClip, Node2D } from '@flighthq/types/contract';
10
+ import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
9
11
 
10
12
  import { readSwfAbcFrameScripts, readSwfFrameActions } from './swfFrameAction';
13
+ import { buildFrameScriptAbc } from './swfFrameActionTestHelper';
11
14
  import { SwfReader } from './swfReader';
12
15
 
13
16
  describe('readSwfAbcFrameScripts', () => {
@@ -25,6 +28,29 @@ describe('readSwfAbcFrameScripts', () => {
25
28
  expect(isMovieClipPlaying(clip)).toBe(false);
26
29
  });
27
30
 
31
+ it('reports a frame script whose body is not a command this importer obeys', () => {
32
+ // The handler calls a name that is not one of the recognized playback commands, so the body parses
33
+ // and is then declined. The file bound a script to frame 1 and the document has none.
34
+ const diagnostics = collectImportDiagnostics((sink) => {
35
+ expect(readSwfAbcFrameScripts(buildFrameScriptAbc(1), sink)).not.toBeNull();
36
+ });
37
+
38
+ const dropped = diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-script-declined');
39
+ expect(dropped).toHaveLength(1);
40
+ expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
41
+ expect(dropped[0].detail).toEqual({ frame: 1, reason: 'commands-declined' });
42
+ });
43
+
44
+ it('stays silent about a frame script it does obey, so the drop entry carries information', () => {
45
+ const diagnostics = collectImportDiagnostics((sink) => {
46
+ const scripts = readSwfAbcFrameScripts(buildFrameScriptAbc(), sink);
47
+ // Non-vacuous: a run that bound no frame script would be silent for the wrong reason.
48
+ expect([...scripts!.get('Main')!.keys()]).toEqual([1]);
49
+ });
50
+
51
+ expect(diagnostics).toEqual([]);
52
+ });
53
+
28
54
  it('reports nothing for bytecode that is not a readable container', () => {
29
55
  expect(readSwfAbcFrameScripts(new Uint8Array([1, 2, 3]))).toBeNull();
30
56
  });
@@ -86,103 +112,6 @@ describe('readSwfFrameActions', () => {
86
112
 
87
113
  // An ABC file shaped the way a compiler writes one: a class whose constructor calls
88
114
  // `addFrameScript(0, this.frame1)`, and a `frame1` method whose whole body is `stop()`.
89
- function buildFrameScriptAbc(): Uint8Array {
90
- const bytes: number[] = [];
91
- const u8 = (v: number): void => void bytes.push(v & 0xff);
92
- const u16 = (v: number): void => {
93
- u8(v);
94
- u8(v >> 8);
95
- };
96
- const u30 = (v: number): void => {
97
- let r = v;
98
- do {
99
- const b = r % 0x80;
100
- r = Math.floor(r / 0x80);
101
- u8(r > 0 ? b | 0x80 : b);
102
- } while (r > 0);
103
- };
104
- const str = (v: string): void => {
105
- const e = new TextEncoder().encode(v);
106
- u30(e.length);
107
- for (const b of e) u8(b);
108
- };
109
-
110
- u16(16);
111
- u16(46);
112
- u30(1); // no integers
113
- u30(1); // no unsigned integers
114
- u30(1); // no doubles
115
- u30(6); // strings: Main, addFrameScript, stop, frame1, ''
116
- str('Main');
117
- str('addFrameScript');
118
- str('stop');
119
- str('frame1');
120
- str('');
121
- u30(2); // one namespace: public, named by the empty string
122
- u8(0x16);
123
- u30(5);
124
- u30(1); // no namespace sets
125
- u30(5); // four qualified names, one per string above
126
- for (let i = 1; i <= 4; i++) {
127
- u8(0x07);
128
- u30(1);
129
- u30(i);
130
- }
131
-
132
- u30(2); // two methods: the constructor and frame1
133
- for (let i = 0; i < 2; i++) {
134
- u30(0);
135
- u30(0);
136
- u30(0);
137
- u8(0);
138
- }
139
- u30(0); // no metadata
140
-
141
- u30(1); // one class
142
- u30(1); // instance name -> Main
143
- u30(1); // super name
144
- u8(0);
145
- u30(0); // no interfaces
146
- u30(0); // instance initializer is method 0
147
- u30(1); // one trait: the frame1 method
148
- u30(4);
149
- u8(0x01); // method trait
150
- u30(0); // dispatch id
151
- u30(1); // method index
152
- u30(0); // class initializer
153
- u30(0); // no class traits
154
-
155
- u30(1); // one script
156
- u30(0);
157
- u30(0);
158
-
159
- u30(2); // two method bodies
160
- // The constructor: addFrameScript(0, this.frame1)
161
- const constructorCode = [0xd0, 0x30, 0x5d, 0x02, 0x24, 0x00, 0xd0, 0x66, 0x04, 0x4f, 0x02, 0x02, 0x47];
162
- u30(0);
163
- u30(3);
164
- u30(1);
165
- u30(1);
166
- u30(2);
167
- u30(constructorCode.length);
168
- for (const b of constructorCode) u8(b);
169
- u30(0);
170
- u30(0);
171
- // frame1: stop()
172
- const handlerCode = [0xd0, 0x30, 0x5d, 0x03, 0x4f, 0x03, 0x00, 0x47];
173
- u30(1);
174
- u30(2);
175
- u30(1);
176
- u30(1);
177
- u30(2);
178
- u30(handlerCode.length);
179
- for (const b of handlerCode) u8(b);
180
- u30(0);
181
- u30(0);
182
-
183
- return new Uint8Array(bytes);
184
- }
185
-
186
115
  function actions(bytes: readonly number[]): SwfReader {
187
116
  const source = new Uint8Array(bytes);
188
117
  return new SwfReader(source, 0, source.length);
@@ -0,0 +1,53 @@
1
+ import { registerDeflateDecompressor, unregisterDecompressor } from '@flighthq/compression/contract';
2
+ import {
3
+ clearImageDecoders,
4
+ decodeImage,
5
+ decodeImagePremultiplied,
6
+ hasImageDecoder,
7
+ } from '@flighthq/image-codec/contract';
8
+ import { Compression } from '@flighthq/types/contract';
9
+
10
+ import { registerSwfImageDecoders, SWF_LOSSLESS_ALPHA_MIME_TYPE, SWF_LOSSLESS_MIME_TYPE } from './swfImageDecoder';
11
+
12
+ beforeEach(() => {
13
+ clearImageDecoders();
14
+ unregisterDecompressor(Compression.Deflate);
15
+ });
16
+
17
+ afterEach(() => {
18
+ clearImageDecoders();
19
+ unregisterDecompressor(Compression.Deflate);
20
+ });
21
+
22
+ describe('registerSwfImageDecoders', () => {
23
+ it('explicitly registers both container-native lossless MIME types', () => {
24
+ expect(hasImageDecoder(SWF_LOSSLESS_MIME_TYPE)).toBe(false);
25
+ expect(hasImageDecoder(SWF_LOSSLESS_ALPHA_MIME_TYPE)).toBe(false);
26
+
27
+ registerSwfImageDecoders();
28
+
29
+ expect(hasImageDecoder(SWF_LOSSLESS_MIME_TYPE)).toBe(true);
30
+ expect(hasImageDecoder(SWF_LOSSLESS_ALPHA_MIME_TYPE)).toBe(true);
31
+ });
32
+
33
+ it('retains authored premultiplied pixels when requested and normalizes the default to straight', async () => {
34
+ registerDeflateDecompressor();
35
+ registerSwfImageDecoders();
36
+ const payload = losslessPayload(5, 1, 1, stored([0x80, 0x40, 0x20, 0x10]));
37
+
38
+ const premultiplied = await decodeImagePremultiplied(payload, SWF_LOSSLESS_ALPHA_MIME_TYPE);
39
+ const straight = await decodeImage(payload, SWF_LOSSLESS_ALPHA_MIME_TYPE);
40
+
41
+ expect([...premultiplied!.data]).toEqual([0x40, 0x20, 0x10, 0x80]);
42
+ expect([...straight!.data]).toEqual([0x80, 0x40, 0x20, 0x80]);
43
+ });
44
+ });
45
+
46
+ function losslessPayload(format: number, width: number, height: number, pixels: readonly number[]): Uint8Array {
47
+ return new Uint8Array([format, width & 0xff, width >> 8, height & 0xff, height >> 8, ...pixels]);
48
+ }
49
+
50
+ function stored(bytes: readonly number[]): number[] {
51
+ const length = bytes.length;
52
+ return [0x78, 0x01, 0x01, length & 0xff, length >> 8, ~length & 0xff, (~length >> 8) & 0xff, ...bytes, 0, 0, 0, 0];
53
+ }
@@ -1,11 +1,12 @@
1
1
  // @vitest-environment jsdom
2
+ import { clearImageDecoders, registerImageDecoder } from '@flighthq/image-codec/contract';
2
3
  import { getNodeChildren } from '@flighthq/node/contract';
3
4
  import { loadScene2DImageResources } from '@flighthq/scene2d-resources/contract';
4
5
  import { getTextureSource } from '@flighthq/texture/contract';
5
- import type { Image, Shape, Texture2D, TextureSource } from '@flighthq/types/contract';
6
+ import type { Bitmap, Shape, Texture2D, TextureSource } from '@flighthq/types/contract';
6
7
  import {
8
+ BitmapTextureSourceKind,
7
9
  ImageResourceReferenceKind,
8
- ImageTextureSourceKind,
9
10
  ResourceResolutionState,
10
11
  ShapeKind,
11
12
  } from '@flighthq/types/contract';
@@ -14,18 +15,15 @@ import { createScene2DFromSwf } from './swfDocument';
14
15
  import { ShapeWriter } from './swfShapeTestHelper';
15
16
 
16
17
  beforeEach(() => {
17
- // jsdom has the browser image surface but does not decode image bytes. Keep the production Blob ->
18
- // HTMLImageElement path intact and model the dimensions a real decode of this 1x1 PNG supplies.
19
- HTMLImageElement.prototype.decode = vi.fn(function (this: HTMLImageElement) {
20
- this.width = 1;
21
- this.height = 1;
22
- return Promise.resolve();
23
- });
18
+ registerImageDecoder('image/png', async () => ({
19
+ data: new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]),
20
+ height: 1,
21
+ width: 1,
22
+ }));
24
23
  });
25
24
 
26
25
  afterEach(() => {
27
- vi.restoreAllMocks();
28
- delete (HTMLImageElement.prototype as Partial<HTMLImageElement>).decode;
26
+ clearImageDecoders();
29
27
  });
30
28
 
31
29
  describe('SWF image resources', () => {
@@ -53,18 +51,17 @@ describe('SWF image resources', () => {
53
51
  expect(resources.resolved).toEqual([reference]);
54
52
  expect(resources.unresolved).toEqual([]);
55
53
  expect(reference.state).toBe(ResourceResolutionState.Resolved);
56
- expect(image?.kind).toBe(ImageTextureSourceKind);
57
- if (!isImageTextureSource(image)) throw new Error('Expected decoded image source');
54
+ expect(image?.kind).toBe(BitmapTextureSourceKind);
55
+ if (!isBitmapTextureSource(image)) throw new Error('Expected decoded bitmap source');
58
56
  expect(image.width).toBe(1);
59
57
  expect(image.height).toBe(1);
60
- expect(image.source).toBeInstanceOf(HTMLImageElement);
61
- expect(HTMLImageElement.prototype.decode).toHaveBeenCalledOnce();
58
+ expect([...image.data]).toEqual([0x11, 0x22, 0x33, 0xff]);
62
59
  expect(getTextureSource(reference.textures![0])).toBe(image);
63
60
  });
64
61
  });
65
62
 
66
- function isImageTextureSource(source: Readonly<TextureSource> | null): source is Image {
67
- return source?.kind === ImageTextureSourceKind;
63
+ function isBitmapTextureSource(source: Readonly<TextureSource> | null): source is Bitmap {
64
+ return source?.kind === BitmapTextureSourceKind;
68
65
  }
69
66
 
70
67
  function createBitmapFillSwf(imageBytes: Uint8Array): Uint8Array {
@@ -1,5 +1,7 @@
1
+ import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
1
2
  import { setMorphShapeProgress } from '@flighthq/shape/contract';
2
3
  import { createTexture } from '@flighthq/texture/contract';
4
+ import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
3
5
  import type { Texture2D } from '@flighthq/types/contract';
4
6
 
5
7
  import { createSwfMorphShape } from './swfMorphShape';
@@ -61,6 +63,22 @@ describe('createSwfMorphShape', () => {
61
63
  expect(shape.data.commands[index + 2]).toBe(texture);
62
64
  });
63
65
 
66
+ // No fire proof: the two edge streams are read in lockstep by readSwfMorphShapePaths, which breaks the
67
+ // moment either runs out, so both paths of a pair are built with identical structure and the mismatches
68
+ // createPathMorph declines on cannot arise from SWF bytes. The wire stays as a guard; the absence of a
69
+ // fire proof is recorded rather than papered over with a fixture that does not reach it.
70
+ it('stays silent when every path pair morphs, so the declined count carries information', () => {
71
+ const body = buildMorph({ endWidth: 400, startWidth: 200 });
72
+ let shape: ReturnType<typeof createSwfMorphShape> = null;
73
+ const diagnostics = collectImportDiagnostics((sink) => {
74
+ shape = createSwfMorphShape(new SwfReader(body, 0, body.length), 1, null, sink);
75
+ });
76
+
77
+ // Non-vacuous: a run that produced no morph at all would be silent for the wrong reason.
78
+ expect(shape).not.toBeNull();
79
+ expect(diagnostics).toEqual([]);
80
+ });
81
+
64
82
  it('returns null for a body that does not decode rather than throwing', () => {
65
83
  expect(readMorph(new Uint8Array())).toBeNull();
66
84
  expect(readMorph(new Uint8Array([1, 2, 3, 4, 5]))).toBeNull();