@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.
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/swfBitmap.d.ts +2 -1
- package/dist/swfBitmap.d.ts.map +1 -1
- package/dist/swfBitmap.js +30 -3
- package/dist/swfBitmap.js.map +1 -1
- package/dist/swfDocument.d.ts +5 -5
- package/dist/swfDocument.d.ts.map +1 -1
- package/dist/swfDocument.js +404 -108
- package/dist/swfDocument.js.map +1 -1
- package/dist/swfEditText.d.ts.map +1 -1
- package/dist/swfEditText.js +6 -4
- package/dist/swfEditText.js.map +1 -1
- package/dist/swfFilter.d.ts +2 -2
- package/dist/swfFilter.d.ts.map +1 -1
- package/dist/swfFilter.js +24 -2
- package/dist/swfFilter.js.map +1 -1
- package/dist/swfFrameAction.d.ts +2 -2
- package/dist/swfFrameAction.d.ts.map +1 -1
- package/dist/swfFrameAction.js +13 -6
- package/dist/swfFrameAction.js.map +1 -1
- package/dist/swfFrameActionTestHelper.d.ts +2 -0
- package/dist/swfFrameActionTestHelper.d.ts.map +1 -0
- package/dist/swfFrameActionTestHelper.js +99 -0
- package/dist/swfFrameActionTestHelper.js.map +1 -0
- package/dist/swfImageDecoder.d.ts +4 -0
- package/dist/swfImageDecoder.d.ts.map +1 -0
- package/dist/swfImageDecoder.js +21 -0
- package/dist/swfImageDecoder.js.map +1 -0
- package/dist/swfMorphShape.d.ts +2 -1
- package/dist/swfMorphShape.d.ts.map +1 -1
- package/dist/swfMorphShape.js +18 -7
- package/dist/swfMorphShape.js.map +1 -1
- package/dist/swfShape.d.ts +2 -2
- package/dist/swfShape.d.ts.map +1 -1
- package/dist/swfShape.js +26 -7
- package/dist/swfShape.js.map +1 -1
- package/dist/swfShapeTestHelper.d.ts +1 -0
- package/dist/swfShapeTestHelper.d.ts.map +1 -1
- package/dist/swfShapeTestHelper.js +14 -0
- package/dist/swfShapeTestHelper.js.map +1 -1
- package/dist/swfText.d.ts +2 -2
- package/dist/swfText.d.ts.map +1 -1
- package/dist/swfText.js +15 -2
- package/dist/swfText.js.map +1 -1
- package/package.json +25 -22
- package/src/swfBitmap.test.ts +106 -5
- package/src/swfDocument.test.ts +1639 -125
- package/src/swfEditText.test.ts +45 -5
- package/src/swfFilter.test.ts +53 -0
- package/src/swfFrameAction.test.ts +26 -97
- package/src/swfImageDecoder.test.ts +73 -0
- package/src/swfImageResources.test.ts +14 -17
- package/src/swfMorphShape.test.ts +18 -0
- package/src/swfShape.test.ts +47 -0
package/src/swfEditText.test.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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);
|
package/src/swfFilter.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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(
|
|
57
|
-
if (!
|
|
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.
|
|
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
|
|
67
|
-
return source?.kind ===
|
|
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();
|
package/src/swfShape.test.ts
CHANGED
|
@@ -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);
|