@flighthq/swf 0.3.0 → 0.3.1-next.1041.35b950c

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 (61) hide show
  1. package/dist/contract.d.ts +2 -0
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +2 -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/swfBitmap.d.ts +2 -1
  10. package/dist/swfBitmap.d.ts.map +1 -1
  11. package/dist/swfBitmap.js +30 -3
  12. package/dist/swfBitmap.js.map +1 -1
  13. package/dist/swfDocument.d.ts +5 -5
  14. package/dist/swfDocument.d.ts.map +1 -1
  15. package/dist/swfDocument.js +404 -108
  16. package/dist/swfDocument.js.map +1 -1
  17. package/dist/swfEditText.d.ts.map +1 -1
  18. package/dist/swfEditText.js +6 -4
  19. package/dist/swfEditText.js.map +1 -1
  20. package/dist/swfFilter.d.ts +2 -2
  21. package/dist/swfFilter.d.ts.map +1 -1
  22. package/dist/swfFilter.js +24 -2
  23. package/dist/swfFilter.js.map +1 -1
  24. package/dist/swfFrameAction.d.ts +2 -2
  25. package/dist/swfFrameAction.d.ts.map +1 -1
  26. package/dist/swfFrameAction.js +13 -6
  27. package/dist/swfFrameAction.js.map +1 -1
  28. package/dist/swfFrameActionTestHelper.d.ts +2 -0
  29. package/dist/swfFrameActionTestHelper.d.ts.map +1 -0
  30. package/dist/swfFrameActionTestHelper.js +99 -0
  31. package/dist/swfFrameActionTestHelper.js.map +1 -0
  32. package/dist/swfImageDecoder.d.ts +4 -0
  33. package/dist/swfImageDecoder.d.ts.map +1 -0
  34. package/dist/swfImageDecoder.js +21 -0
  35. package/dist/swfImageDecoder.js.map +1 -0
  36. package/dist/swfMorphShape.d.ts +2 -1
  37. package/dist/swfMorphShape.d.ts.map +1 -1
  38. package/dist/swfMorphShape.js +18 -7
  39. package/dist/swfMorphShape.js.map +1 -1
  40. package/dist/swfShape.d.ts +2 -2
  41. package/dist/swfShape.d.ts.map +1 -1
  42. package/dist/swfShape.js +26 -7
  43. package/dist/swfShape.js.map +1 -1
  44. package/dist/swfShapeTestHelper.d.ts +1 -0
  45. package/dist/swfShapeTestHelper.d.ts.map +1 -1
  46. package/dist/swfShapeTestHelper.js +14 -0
  47. package/dist/swfShapeTestHelper.js.map +1 -1
  48. package/dist/swfText.d.ts +2 -2
  49. package/dist/swfText.d.ts.map +1 -1
  50. package/dist/swfText.js +15 -2
  51. package/dist/swfText.js.map +1 -1
  52. package/package.json +25 -22
  53. package/src/swfBitmap.test.ts +106 -5
  54. package/src/swfDocument.test.ts +1639 -125
  55. package/src/swfEditText.test.ts +45 -5
  56. package/src/swfFilter.test.ts +53 -0
  57. package/src/swfFrameAction.test.ts +26 -97
  58. package/src/swfImageDecoder.test.ts +73 -0
  59. package/src/swfImageResources.test.ts +14 -17
  60. package/src/swfMorphShape.test.ts +18 -0
  61. package/src/swfShape.test.ts +47 -0
@@ -42,6 +42,34 @@ describe('readSwfEditTextFactory', () => {
42
42
  expect(field.data.wordWrap).toBe(true);
43
43
  expect(field.data.border).toBe(true);
44
44
  expect(field.data.selectable).toBe(false);
45
+ expect(field.data.maxChars).toBe(-1);
46
+ });
47
+
48
+ it('preserves an authored maximum character count', () => {
49
+ const field = build({ maxChars: 12, text: 'limited' })(() => '');
50
+
51
+ expect(field.data.maxChars).toBe(12);
52
+ });
53
+
54
+ it('preserves the authored RGBA text color', () => {
55
+ const field = build({ color: 0x11223344, text: 'Color' })(() => '');
56
+
57
+ expect(field.data.defaultTextFormat.color).toBe(0x11223344);
58
+ expect(field.data.textColor).toBe(0x11223344);
59
+ });
60
+
61
+ it('preserves an authored zero alpha instead of making it opaque', () => {
62
+ const field = build({ color: 0x11223300, text: 'Transparent' })(() => '');
63
+
64
+ expect(field.data.defaultTextFormat.color).toBe(0x11223300);
65
+ expect(field.data.textColor).toBe(0x11223300);
66
+ });
67
+
68
+ it('uses the opaque black field default when the tag omits text color', () => {
69
+ const field = build({ color: null, text: 'Default' })(() => '');
70
+
71
+ expect(field.data.defaultTextFormat.color).toBeUndefined();
72
+ expect(field.data.textColor).toBe(0x000000ff);
45
73
  });
46
74
 
47
75
  it('parses the markup of a field flagged as html into text and format ranges', () => {
@@ -70,12 +98,14 @@ describe('readSwfEditTextFactory', () => {
70
98
 
71
99
  interface FieldOptions {
72
100
  border?: boolean;
101
+ color?: number | null;
73
102
  fontHeight?: number;
74
103
  fontId?: number;
75
104
  html?: boolean;
76
105
  indent?: number;
77
106
  leading?: number;
78
107
  leftMargin?: number;
108
+ maxChars?: number;
79
109
  multiline?: boolean;
80
110
  noSelect?: boolean;
81
111
  rightMargin?: number;
@@ -102,7 +132,13 @@ function bytesFor(options: Readonly<FieldOptions>): Uint8Array {
102
132
  u8(0);
103
133
  };
104
134
 
105
- const flags = 0x80 | 0x04 | 0x01 | (options.wordWrap === true ? 0x40 : 0) | (options.multiline === true ? 0x20 : 0);
135
+ const flags =
136
+ 0x80 |
137
+ 0x01 |
138
+ (options.color === null ? 0 : 0x04) |
139
+ (options.wordWrap === true ? 0x40 : 0) |
140
+ (options.multiline === true ? 0x20 : 0) |
141
+ (options.maxChars === undefined ? 0 : 0x02);
106
142
  const layoutFlags =
107
143
  0x20 |
108
144
  (options.border === true ? 0x08 : 0) |
@@ -112,10 +148,14 @@ function bytesFor(options: Readonly<FieldOptions>): Uint8Array {
112
148
  u8(layoutFlags);
113
149
  u16(options.fontId ?? 1);
114
150
  u16(options.fontHeight ?? 200);
115
- u8(0x11);
116
- u8(0x22);
117
- u8(0x33);
118
- u8(0xff);
151
+ if (options.color !== null) {
152
+ const color = options.color ?? 0x112233ff;
153
+ u8(color >>> 24);
154
+ u8(color >>> 16);
155
+ u8(color >>> 8);
156
+ u8(color);
157
+ }
158
+ if (options.maxChars !== undefined) u16(options.maxChars);
119
159
  u8(0); // align: left
120
160
  u16(options.leftMargin ?? 0);
121
161
  u16(options.rightMargin ?? 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,73 @@
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
+ let first = 1;
53
+ let second = 0;
54
+ for (const byte of bytes) {
55
+ first = (first + byte) % 65_521;
56
+ second = (second + first) % 65_521;
57
+ }
58
+ const adler = ((second << 16) | first) >>> 0;
59
+ return [
60
+ 0x78,
61
+ 0x01,
62
+ 0x01,
63
+ length & 0xff,
64
+ length >> 8,
65
+ ~length & 0xff,
66
+ (~length >> 8) & 0xff,
67
+ ...bytes,
68
+ adler >>> 24,
69
+ (adler >>> 16) & 0xff,
70
+ (adler >>> 8) & 0xff,
71
+ adler & 0xff,
72
+ ];
73
+ }
@@ -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();
@@ -1,4 +1,5 @@
1
1
  import { createTexture } from '@flighthq/texture/contract';
2
+ import type { ImportDiagnostic } from '@flighthq/types/contract';
2
3
 
3
4
  import { SwfReader } from './swfReader';
4
5
  import { createSwfGlyphShape, createSwfShape, readSwfMorphShapePaths } from './swfShape';
@@ -270,6 +271,52 @@ describe('createSwfShape', () => {
270
271
  expect(shape?.data.commands[7]).toBe('pad');
271
272
  });
272
273
 
274
+ // The fill matrix is file data, so a collapsed one is a plain thing to encounter. Validated where it
275
+ // enters rather than at the renderer, which would otherwise invert it into a defined-but-wrong matrix
276
+ // (a/b/c/d zeroed, tx/ty negated) and paint wrong pixels with nothing raised and nothing to grep for.
277
+ it('drops a singular bitmap fill matrix and names the character, keeping the fill', () => {
278
+ const writer = new ShapeWriter();
279
+ writer.writeFillStyleCount(1);
280
+ writer.writeByte(0x41);
281
+ writer.writeUint16(7);
282
+ writer.writeZeroScaleMatrix();
283
+ writer.writeLineStyleCount(0);
284
+ writer.writeStyleBits(1, 0);
285
+ writer.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
286
+ writer.writeStraightEdge(2000, 0);
287
+ writer.writeEndShape();
288
+
289
+ const diagnostics: ImportDiagnostic[] = [];
290
+ const shape = createSwfShape(writer.toReader(), 1, () => createTexture(), diagnostics);
291
+
292
+ const matches = diagnostics.filter((d) => d.kind === 'swf.fill-matrix-singular');
293
+ expect(matches).toHaveLength(1);
294
+ expect(matches[0].severity).toBe('Recover');
295
+ expect(matches[0].detail).toMatchObject({ character: 7 });
296
+ // Recovered, not rejected: the fill is still emitted, just untransformed.
297
+ expect(shape?.data.commands[0]).toBe('beginTextureFill');
298
+ expect(shape?.data.commands[3]).toBeNull();
299
+ });
300
+
301
+ it('keeps an invertible bitmap fill matrix and reports nothing', () => {
302
+ const writer = new ShapeWriter();
303
+ writer.writeFillStyleCount(1);
304
+ writer.writeByte(0x41);
305
+ writer.writeUint16(7);
306
+ writer.writeIdentityMatrix(0, 0);
307
+ writer.writeLineStyleCount(0);
308
+ writer.writeStyleBits(1, 0);
309
+ writer.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
310
+ writer.writeStraightEdge(2000, 0);
311
+ writer.writeEndShape();
312
+
313
+ const diagnostics: ImportDiagnostic[] = [];
314
+ const shape = createSwfShape(writer.toReader(), 1, () => createTexture(), diagnostics);
315
+
316
+ expect(diagnostics.filter((d) => d.kind === 'swf.fill-matrix-singular')).toHaveLength(0);
317
+ expect(shape?.data.commands[3]).not.toBeNull();
318
+ });
319
+
273
320
  it('emits geometry for a bitmap fill, with a texture its pixels arrive into later', () => {
274
321
  const writer = new ShapeWriter();
275
322
  writer.writeFillStyleCount(1);