@swf2es/format 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/LICENSE.third-party +35 -0
- package/README.md +22 -2
- package/dist/abc-names.d.ts +3 -0
- package/dist/abc-names.d.ts.map +1 -0
- package/dist/abc-names.js +127 -0
- package/dist/abc-names.js.map +1 -0
- package/dist/actions.d.ts +106 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +223 -0
- package/dist/actions.js.map +1 -0
- package/dist/air-descriptor.d.ts +11 -0
- package/dist/air-descriptor.d.ts.map +1 -0
- package/dist/air-descriptor.js +64 -0
- package/dist/air-descriptor.js.map +1 -0
- package/dist/api-versions.d.ts +12 -0
- package/dist/api-versions.d.ts.map +1 -0
- package/dist/api-versions.js +23 -0
- package/dist/api-versions.js.map +1 -0
- package/dist/bitmap.d.ts +34 -0
- package/dist/bitmap.d.ts.map +1 -0
- package/dist/bitmap.js +200 -0
- package/dist/bitmap.js.map +1 -0
- package/dist/button.d.ts +39 -0
- package/dist/button.d.ts.map +1 -0
- package/dist/button.js +61 -0
- package/dist/button.js.map +1 -0
- package/dist/compression.d.ts +47 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +201 -0
- package/dist/compression.js.map +1 -0
- package/dist/display.d.ts +106 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +306 -0
- package/dist/display.js.map +1 -0
- package/dist/filters.d.ts +66 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +137 -0
- package/dist/filters.js.map +1 -0
- package/dist/font.d.ts +60 -0
- package/dist/font.d.ts.map +1 -0
- package/dist/font.js +169 -0
- package/dist/font.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/pbj.d.ts +178 -0
- package/dist/pbj.d.ts.map +1 -0
- package/dist/pbj.js +485 -0
- package/dist/pbj.js.map +1 -0
- package/dist/shape.d.ts +152 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +412 -0
- package/dist/shape.js.map +1 -0
- package/dist/sound.d.ts +71 -0
- package/dist/sound.d.ts.map +1 -0
- package/dist/sound.js +92 -0
- package/dist/sound.js.map +1 -0
- package/dist/swf.d.ts +77 -0
- package/dist/swf.d.ts.map +1 -0
- package/dist/swf.js +164 -0
- package/dist/swf.js.map +1 -0
- package/dist/tags.d.ts +57 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +59 -0
- package/dist/tags.js.map +1 -0
- package/dist/text.d.ts +75 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +136 -0
- package/dist/text.js.map +1 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +92 -0
- package/dist/zip.js.map +1 -0
- package/package.json +33 -3
- package/src/abc-names.ts +138 -0
- package/src/actions.ts +319 -0
- package/src/air-descriptor.ts +84 -0
- package/src/api-versions.ts +27 -0
- package/src/bitmap.ts +259 -0
- package/src/button.ts +99 -0
- package/src/compression.ts +243 -0
- package/src/display.ts +419 -0
- package/src/filters.ts +206 -0
- package/src/font.ts +238 -0
- package/src/index.ts +151 -0
- package/src/pbj.ts +651 -0
- package/src/shape.ts +618 -0
- package/src/sound.ts +173 -0
- package/src/swf.ts +234 -0
- package/src/tags.ts +58 -0
- package/src/text.ts +213 -0
- package/src/zip.ts +135 -0
package/src/shape.ts
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// Shapes as DefineShape, DefineShape2, DefineShape3 and DefineShape4 hold
|
|
2
|
+
// them (SWF specification, chapter 6): fill and line styles, then shape
|
|
3
|
+
// records, edges in twips relative to the pen and style changes that move
|
|
4
|
+
// it or switch styles, until an end record; and morph shapes, which hold
|
|
5
|
+
// two such shapes to blend. The player turns the edges into closed paths
|
|
6
|
+
// per fill; this file only decodes them.
|
|
7
|
+
import { type Rect, SwfReader } from "./swf.js";
|
|
8
|
+
import {
|
|
9
|
+
DefineMorphShape,
|
|
10
|
+
DefineMorphShape2,
|
|
11
|
+
DefineShape,
|
|
12
|
+
DefineShape2,
|
|
13
|
+
DefineShape3,
|
|
14
|
+
DefineShape4,
|
|
15
|
+
} from "./tags.js";
|
|
16
|
+
|
|
17
|
+
/** An affine transform as MATRIX holds one: a, b, c, d as numbers, tx and ty in twips. */
|
|
18
|
+
export interface Matrix {
|
|
19
|
+
a: number;
|
|
20
|
+
b: number;
|
|
21
|
+
c: number;
|
|
22
|
+
d: number;
|
|
23
|
+
tx: number;
|
|
24
|
+
ty: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const IDENTITY: Matrix = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
|
|
28
|
+
|
|
29
|
+
/** A colour transform, as CXFORM or, with alpha, CXFORMWITHALPHA: multipliers /256, offsets. */
|
|
30
|
+
export interface ColorTransform {
|
|
31
|
+
rMul: number;
|
|
32
|
+
gMul: number;
|
|
33
|
+
bMul: number;
|
|
34
|
+
aMul: number;
|
|
35
|
+
rAdd: number;
|
|
36
|
+
gAdd: number;
|
|
37
|
+
bAdd: number;
|
|
38
|
+
aAdd: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GradientStop {
|
|
42
|
+
ratio: number;
|
|
43
|
+
/** 0xAARRGGBB. */
|
|
44
|
+
color: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Gradient {
|
|
48
|
+
matrix: Matrix;
|
|
49
|
+
/** 0 pad, 1 reflect, 2 repeat. */
|
|
50
|
+
spread: number;
|
|
51
|
+
/** 0 normal RGB, 1 linear RGB. */
|
|
52
|
+
interpolation: number;
|
|
53
|
+
stops: GradientStop[];
|
|
54
|
+
/** A focal gradient's focal point, -1 to 1; 0 for any other. */
|
|
55
|
+
focal: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Fill =
|
|
59
|
+
| { type: "solid"; color: number }
|
|
60
|
+
| { type: "linear" | "radial" | "focal"; gradient: Gradient }
|
|
61
|
+
| { type: "bitmap"; bitmap: number; matrix: Matrix; repeat: boolean; smooth: boolean };
|
|
62
|
+
|
|
63
|
+
export interface Line {
|
|
64
|
+
/** Width in twips; 0 is a hairline. */
|
|
65
|
+
width: number;
|
|
66
|
+
/** 0xAARRGGBB, or the colour of a gradient's first stop for a filled line. */
|
|
67
|
+
color: number;
|
|
68
|
+
/** DefineShape4's LINESTYLE2 fields, else a round line. */
|
|
69
|
+
startCap: number;
|
|
70
|
+
endCap: number;
|
|
71
|
+
/** 0 round, 1 bevel, 2 miter. */
|
|
72
|
+
join: number;
|
|
73
|
+
miterLimit: number;
|
|
74
|
+
noHScale: boolean;
|
|
75
|
+
noVScale: boolean;
|
|
76
|
+
pixelHinting: boolean;
|
|
77
|
+
noClose: boolean;
|
|
78
|
+
fill: Fill | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A shape record: a style change, which may move the pen (moveTo, absolute),
|
|
83
|
+
* switch fills and line (0 for none, else 1-based into the styles in force),
|
|
84
|
+
* and bring new styles; or an edge from the pen, straight or quadratic,
|
|
85
|
+
* with its deltas in twips.
|
|
86
|
+
*/
|
|
87
|
+
export type ShapeRecord =
|
|
88
|
+
| {
|
|
89
|
+
type: "style";
|
|
90
|
+
moveTo: { x: number; y: number } | null;
|
|
91
|
+
fill0: number | null;
|
|
92
|
+
fill1: number | null;
|
|
93
|
+
line: number | null;
|
|
94
|
+
styles: { fills: Fill[]; lines: Line[] } | null;
|
|
95
|
+
}
|
|
96
|
+
| { type: "line"; dx: number; dy: number }
|
|
97
|
+
| { type: "curve"; cx: number; cy: number; ax: number; ay: number };
|
|
98
|
+
|
|
99
|
+
export interface Shape {
|
|
100
|
+
id: number;
|
|
101
|
+
bounds: Rect;
|
|
102
|
+
/** DefineShape4's bounds without the lines' widths. */
|
|
103
|
+
edgeBounds: Rect | null;
|
|
104
|
+
fills: Fill[];
|
|
105
|
+
lines: Line[];
|
|
106
|
+
records: ShapeRecord[];
|
|
107
|
+
/** Whether the records ran past the tag's end. */
|
|
108
|
+
truncated: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readMatrix(r: SwfReader): Matrix {
|
|
112
|
+
r.align();
|
|
113
|
+
const m = { ...IDENTITY };
|
|
114
|
+
|
|
115
|
+
if (r.ub(1)) {
|
|
116
|
+
const n = r.ub(5);
|
|
117
|
+
m.a = r.sb(n) / 65536;
|
|
118
|
+
m.d = r.sb(n) / 65536;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (r.ub(1)) {
|
|
122
|
+
const n = r.ub(5);
|
|
123
|
+
m.b = r.sb(n) / 65536;
|
|
124
|
+
m.c = r.sb(n) / 65536;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const n = r.ub(5);
|
|
128
|
+
m.tx = r.sb(n);
|
|
129
|
+
m.ty = r.sb(n);
|
|
130
|
+
r.align();
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readColorTransform(r: SwfReader, alpha: boolean): ColorTransform {
|
|
135
|
+
r.align();
|
|
136
|
+
const hasAdd = r.ub(1);
|
|
137
|
+
const hasMul = r.ub(1);
|
|
138
|
+
const n = r.ub(4);
|
|
139
|
+
const ct = { rMul: 1, gMul: 1, bMul: 1, aMul: 1, rAdd: 0, gAdd: 0, bAdd: 0, aAdd: 0 };
|
|
140
|
+
|
|
141
|
+
if (hasMul) {
|
|
142
|
+
ct.rMul = r.sb(n) / 256;
|
|
143
|
+
ct.gMul = r.sb(n) / 256;
|
|
144
|
+
ct.bMul = r.sb(n) / 256;
|
|
145
|
+
if (alpha) {
|
|
146
|
+
ct.aMul = r.sb(n) / 256;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (hasAdd) {
|
|
151
|
+
ct.rAdd = r.sb(n);
|
|
152
|
+
ct.gAdd = r.sb(n);
|
|
153
|
+
ct.bAdd = r.sb(n);
|
|
154
|
+
if (alpha) {
|
|
155
|
+
ct.aAdd = r.sb(n);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
r.align();
|
|
160
|
+
return ct;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** RGB, or RGBA with alpha, as 0xAARRGGBB. */
|
|
164
|
+
function readColor(r: SwfReader, alpha: boolean): number {
|
|
165
|
+
const red = r.u8();
|
|
166
|
+
const green = r.u8();
|
|
167
|
+
const blue = r.u8();
|
|
168
|
+
const a = alpha ? r.u8() : 255;
|
|
169
|
+
return ((a << 24) | (red << 16) | (green << 8) | blue) >>> 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readGradient(r: SwfReader, alpha: boolean, focal: boolean): Gradient {
|
|
173
|
+
const matrix = readMatrix(r);
|
|
174
|
+
r.align();
|
|
175
|
+
|
|
176
|
+
const spread = r.ub(2);
|
|
177
|
+
const interpolation = r.ub(2);
|
|
178
|
+
const count = r.ub(4);
|
|
179
|
+
const stops: GradientStop[] = [];
|
|
180
|
+
for (let i = 0; i < count; i++) {
|
|
181
|
+
stops.push({ ratio: r.u8(), color: readColor(r, alpha) });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const f = focal ? r.s16() : 0;
|
|
185
|
+
return { matrix, spread, interpolation, stops, focal: f / 256 };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readFill(r: SwfReader, version: number): Fill {
|
|
189
|
+
const alpha = version >= 3;
|
|
190
|
+
const type = r.u8();
|
|
191
|
+
if (type === 0x00) {
|
|
192
|
+
return { type: "solid", color: readColor(r, alpha) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (type === 0x10 || type === 0x12 || type === 0x13) {
|
|
196
|
+
const kind = type === 0x10 ? "linear" : type === 0x12 ? "radial" : "focal";
|
|
197
|
+
return { type: kind, gradient: readGradient(r, alpha, type === 0x13) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Bitmap fills 0x40 to 0x43: repeating or clipped, smoothed or not. Any
|
|
201
|
+
// other type byte reads as one too, rather than failing the shape.
|
|
202
|
+
const bitmap = r.u16();
|
|
203
|
+
const matrix = readMatrix(r);
|
|
204
|
+
return { type: "bitmap", bitmap, matrix, repeat: (type & 1) === 0, smooth: (type & 2) === 0 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readFills(r: SwfReader, version: number): Fill[] {
|
|
208
|
+
let count = r.u8();
|
|
209
|
+
if (count === 0xff && version >= 2) {
|
|
210
|
+
count = r.u16();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const fills: Fill[] = [];
|
|
214
|
+
for (let i = 0; i < count && !r.overrun; i++) {
|
|
215
|
+
fills.push(readFill(r, version));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return fills;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A line of DefineShape to DefineShape3, or a MORPHLINESTYLE's start or end: round, its colour only. */
|
|
222
|
+
function plainLine(width: number, color: number): Line {
|
|
223
|
+
return {
|
|
224
|
+
width,
|
|
225
|
+
color,
|
|
226
|
+
startCap: 0,
|
|
227
|
+
endCap: 0,
|
|
228
|
+
join: 0,
|
|
229
|
+
miterLimit: 3,
|
|
230
|
+
noHScale: false,
|
|
231
|
+
noVScale: false,
|
|
232
|
+
pixelHinting: false,
|
|
233
|
+
noClose: false,
|
|
234
|
+
fill: null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** LINESTYLE2's flags and miter limit, which MORPHLINESTYLE2 has too, before its fill or colour. */
|
|
239
|
+
function readLineFlags(
|
|
240
|
+
r: SwfReader,
|
|
241
|
+
): Omit<Line, "width" | "color" | "fill"> & { hasFill: boolean } {
|
|
242
|
+
const startCap = r.ub(2);
|
|
243
|
+
const join = r.ub(2);
|
|
244
|
+
const hasFill = r.ub(1) === 1;
|
|
245
|
+
const noHScale = r.ub(1) === 1;
|
|
246
|
+
const noVScale = r.ub(1) === 1;
|
|
247
|
+
const pixelHinting = r.ub(1) === 1;
|
|
248
|
+
|
|
249
|
+
r.ub(5); // reserved: the flags fill two bytes, must be zero
|
|
250
|
+
|
|
251
|
+
const noClose = r.ub(1) === 1;
|
|
252
|
+
const endCap = r.ub(2);
|
|
253
|
+
const miterLimit = join === 2 ? r.u16() / 256 : 3;
|
|
254
|
+
return { startCap, endCap, join, miterLimit, noHScale, noVScale, pixelHinting, noClose, hasFill };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The colour a filled line stands for where only a colour is read. */
|
|
258
|
+
function fillColor(fill: Fill): number {
|
|
259
|
+
return fill.type === "solid"
|
|
260
|
+
? fill.color
|
|
261
|
+
: fill.type === "bitmap"
|
|
262
|
+
? 0xff000000
|
|
263
|
+
: (fill.gradient.stops[0]?.color ?? 0xff000000);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readLines(r: SwfReader, version: number): Line[] {
|
|
267
|
+
let count = r.u8();
|
|
268
|
+
if (count === 0xff && version >= 2) {
|
|
269
|
+
count = r.u16();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lines: Line[] = [];
|
|
273
|
+
for (let i = 0; i < count && !r.overrun; i++) {
|
|
274
|
+
const width = r.u16();
|
|
275
|
+
if (version < 4) {
|
|
276
|
+
lines.push(plainLine(width, readColor(r, version >= 3)));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const { hasFill, ...flags } = readLineFlags(r);
|
|
281
|
+
const fill = hasFill ? readFill(r, version) : null;
|
|
282
|
+
const color = fill ? fillColor(fill) : readColor(r, true);
|
|
283
|
+
lines.push({ width, color, ...flags, fill });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The records from the reader's position: fill and line bit counts first, then records to the end record. */
|
|
290
|
+
export function readRecords(
|
|
291
|
+
r: SwfReader,
|
|
292
|
+
version: number,
|
|
293
|
+
): { records: ShapeRecord[]; truncated: boolean } {
|
|
294
|
+
r.align();
|
|
295
|
+
let fillBits = r.ub(4);
|
|
296
|
+
let lineBits = r.ub(4);
|
|
297
|
+
const records: ShapeRecord[] = [];
|
|
298
|
+
|
|
299
|
+
for (;;) {
|
|
300
|
+
if (r.overrun) {
|
|
301
|
+
return { records, truncated: true };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (r.ub(1)) {
|
|
305
|
+
// An edge.
|
|
306
|
+
const straight = r.ub(1);
|
|
307
|
+
const n = r.ub(4) + 2;
|
|
308
|
+
if (straight) {
|
|
309
|
+
const general = r.ub(1);
|
|
310
|
+
const vertical = general ? 0 : r.ub(1);
|
|
311
|
+
const dx = general || !vertical ? r.sb(n) : 0;
|
|
312
|
+
const dy = general || vertical ? r.sb(n) : 0;
|
|
313
|
+
records.push({ type: "line", dx, dy });
|
|
314
|
+
} else {
|
|
315
|
+
records.push({ type: "curve", cx: r.sb(n), cy: r.sb(n), ax: r.sb(n), ay: r.sb(n) });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const flags = r.ub(5);
|
|
322
|
+
if (flags === 0) {
|
|
323
|
+
return { records, truncated: false };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let moveTo: { x: number; y: number } | null = null;
|
|
327
|
+
if (flags & 1) {
|
|
328
|
+
const n = r.ub(5);
|
|
329
|
+
moveTo = { x: r.sb(n), y: r.sb(n) };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const fill0 = flags & 2 ? r.ub(fillBits) : null;
|
|
333
|
+
const fill1 = flags & 4 ? r.ub(fillBits) : null;
|
|
334
|
+
const line = flags & 8 ? r.ub(lineBits) : null;
|
|
335
|
+
let styles: { fills: Fill[]; lines: Line[] } | null = null;
|
|
336
|
+
|
|
337
|
+
if (flags & 16 && version >= 2) {
|
|
338
|
+
styles = { fills: readFills(r, version), lines: readLines(r, version) };
|
|
339
|
+
r.align();
|
|
340
|
+
fillBits = r.ub(4);
|
|
341
|
+
lineBits = r.ub(4);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
records.push({ type: "style", moveTo, fill0, fill1, line, styles });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* SWF 8 brought the unsmoothed bitmap fills (0x42, 0x43). In an older SWF
|
|
350
|
+
* Flash Player draws the others unsmoothed as well, still or moving, as
|
|
351
|
+
* Ruffle reads them; AIR's runtime smooths them (`avm1-bitmap-smoothing-swf7`).
|
|
352
|
+
*/
|
|
353
|
+
const SMOOTHED_FILLS_VERSION = 8;
|
|
354
|
+
|
|
355
|
+
function unsmooth(fill: Fill | null): void {
|
|
356
|
+
if (fill?.type === "bitmap") {
|
|
357
|
+
fill.smooth = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function unsmoothStyles(fills: Fill[], lines: Line[]): void {
|
|
362
|
+
for (let i = 0; i < fills.length; i++) {
|
|
363
|
+
unsmooth(fills[i]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < lines.length; i++) {
|
|
367
|
+
unsmooth(lines[i].fill);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A DefineShape tag's body, of the version its code gives, from a SWF of
|
|
373
|
+
* `swfVersion`, which decides whether its bitmap fills may be smoothed.
|
|
374
|
+
*/
|
|
375
|
+
export function readShape(
|
|
376
|
+
bytes: Uint8Array,
|
|
377
|
+
code: number,
|
|
378
|
+
offset: number,
|
|
379
|
+
length: number,
|
|
380
|
+
swfVersion = SMOOTHED_FILLS_VERSION,
|
|
381
|
+
): Shape {
|
|
382
|
+
const version =
|
|
383
|
+
code === DefineShape ? 1 : code === DefineShape2 ? 2 : code === DefineShape3 ? 3 : 4;
|
|
384
|
+
if (
|
|
385
|
+
code !== DefineShape &&
|
|
386
|
+
code !== DefineShape2 &&
|
|
387
|
+
code !== DefineShape3 &&
|
|
388
|
+
code !== DefineShape4
|
|
389
|
+
) {
|
|
390
|
+
throw new Error(`tag ${code} is not a shape`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const r = new SwfReader(bytes, offset, offset + length);
|
|
394
|
+
const id = r.u16();
|
|
395
|
+
const bounds = r.rect();
|
|
396
|
+
|
|
397
|
+
let edgeBounds: Rect | null = null;
|
|
398
|
+
if (version === 4) {
|
|
399
|
+
edgeBounds = r.rect();
|
|
400
|
+
r.u8();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const fills = readFills(r, version);
|
|
404
|
+
const lines = readLines(r, version);
|
|
405
|
+
const { records, truncated } = readRecords(r, version);
|
|
406
|
+
if (swfVersion < SMOOTHED_FILLS_VERSION) {
|
|
407
|
+
unsmoothStyles(fills, lines);
|
|
408
|
+
for (let i = 0; i < records.length; i++) {
|
|
409
|
+
const record = records[i];
|
|
410
|
+
if (record.type === "style" && record.styles) {
|
|
411
|
+
unsmoothStyles(record.styles.fills, record.styles.lines);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { id, bounds, edgeBounds, fills, lines, records, truncated };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* DefineMorphShape or DefineMorphShape2: a shape at two ends, which a
|
|
421
|
+
* placement's ratio blends. Each fill and line is a pair, its start and its
|
|
422
|
+
* end; the end's records have the start's edges in the same order and only
|
|
423
|
+
* moves for style changes.
|
|
424
|
+
*/
|
|
425
|
+
export interface MorphShape {
|
|
426
|
+
id: number;
|
|
427
|
+
startBounds: Rect;
|
|
428
|
+
endBounds: Rect;
|
|
429
|
+
/** DefineMorphShape2's bounds without the lines' widths. */
|
|
430
|
+
startEdgeBounds: Rect | null;
|
|
431
|
+
endEdgeBounds: Rect | null;
|
|
432
|
+
fills: { start: Fill; end: Fill }[];
|
|
433
|
+
lines: { start: Line; end: Line }[];
|
|
434
|
+
start: ShapeRecord[];
|
|
435
|
+
end: ShapeRecord[];
|
|
436
|
+
/** Whether either end's records ran past the tag's end. */
|
|
437
|
+
truncated: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** MORPHGRADIENT: both matrices, then each stop's start and end. */
|
|
441
|
+
function readMorphGradient(r: SwfReader, focal: boolean): { start: Gradient; end: Gradient } {
|
|
442
|
+
const startMatrix = readMatrix(r);
|
|
443
|
+
const endMatrix = readMatrix(r);
|
|
444
|
+
|
|
445
|
+
// The count's byte carries spread and interpolation as a shape's
|
|
446
|
+
// gradient's does, which Flash writes and reads alike.
|
|
447
|
+
const spread = r.ub(2);
|
|
448
|
+
const interpolation = r.ub(2);
|
|
449
|
+
const count = r.ub(4);
|
|
450
|
+
const startStops: GradientStop[] = [];
|
|
451
|
+
const endStops: GradientStop[] = [];
|
|
452
|
+
for (let i = 0; i < count; i++) {
|
|
453
|
+
startStops.push({ ratio: r.u8(), color: readColor(r, true) });
|
|
454
|
+
endStops.push({ ratio: r.u8(), color: readColor(r, true) });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const startFocal = focal ? r.s16() / 256 : 0;
|
|
458
|
+
const endFocal = focal ? r.s16() / 256 : 0;
|
|
459
|
+
return {
|
|
460
|
+
start: { matrix: startMatrix, spread, interpolation, stops: startStops, focal: startFocal },
|
|
461
|
+
end: { matrix: endMatrix, spread, interpolation, stops: endStops, focal: endFocal },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function readMorphFill(r: SwfReader): { start: Fill; end: Fill } {
|
|
466
|
+
const type = r.u8();
|
|
467
|
+
if (type === 0x00) {
|
|
468
|
+
return {
|
|
469
|
+
start: { type: "solid", color: readColor(r, true) },
|
|
470
|
+
end: { type: "solid", color: readColor(r, true) },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (type === 0x10 || type === 0x12 || type === 0x13) {
|
|
475
|
+
const kind = type === 0x10 ? "linear" : type === 0x12 ? "radial" : "focal";
|
|
476
|
+
const { start, end } = readMorphGradient(r, type === 0x13);
|
|
477
|
+
return { start: { type: kind, gradient: start }, end: { type: kind, gradient: end } };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const bitmap = r.u16();
|
|
481
|
+
const repeat = (type & 1) === 0;
|
|
482
|
+
const smooth = (type & 2) === 0;
|
|
483
|
+
return {
|
|
484
|
+
start: { type: "bitmap", bitmap, matrix: readMatrix(r), repeat, smooth },
|
|
485
|
+
end: { type: "bitmap", bitmap, matrix: readMatrix(r), repeat, smooth },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function readMorphFills(r: SwfReader): { start: Fill; end: Fill }[] {
|
|
490
|
+
let count = r.u8();
|
|
491
|
+
if (count === 0xff) {
|
|
492
|
+
count = r.u16();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const fills: { start: Fill; end: Fill }[] = [];
|
|
496
|
+
for (let i = 0; i < count && !r.overrun; i++) {
|
|
497
|
+
fills.push(readMorphFill(r));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return fills;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* MORPHLINESTYLE (version 1) or MORPHLINESTYLE2 (2): both widths, then colours, or flags and a
|
|
505
|
+
* fill.
|
|
506
|
+
*/
|
|
507
|
+
function readMorphLines(r: SwfReader, version: number): { start: Line; end: Line }[] {
|
|
508
|
+
let count = r.u8();
|
|
509
|
+
if (count === 0xff) {
|
|
510
|
+
count = r.u16();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const lines: { start: Line; end: Line }[] = [];
|
|
514
|
+
for (let i = 0; i < count && !r.overrun; i++) {
|
|
515
|
+
const startWidth = r.u16();
|
|
516
|
+
const endWidth = r.u16();
|
|
517
|
+
if (version < 2) {
|
|
518
|
+
const startColor = readColor(r, true);
|
|
519
|
+
lines.push({
|
|
520
|
+
start: plainLine(startWidth, startColor),
|
|
521
|
+
end: plainLine(endWidth, readColor(r, true)),
|
|
522
|
+
});
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const { hasFill, ...flags } = readLineFlags(r);
|
|
527
|
+
if (hasFill) {
|
|
528
|
+
const fill = readMorphFill(r);
|
|
529
|
+
lines.push({
|
|
530
|
+
start: { width: startWidth, color: fillColor(fill.start), ...flags, fill: fill.start },
|
|
531
|
+
end: { width: endWidth, color: fillColor(fill.end), ...flags, fill: fill.end },
|
|
532
|
+
});
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const startColor = readColor(r, true);
|
|
537
|
+
const endColor = readColor(r, true);
|
|
538
|
+
lines.push({
|
|
539
|
+
start: { width: startWidth, color: startColor, ...flags, fill: null },
|
|
540
|
+
end: { width: endWidth, color: endColor, ...flags, fill: null },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return lines;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* A DefineMorphShape or DefineMorphShape2 tag's body, of the version its
|
|
549
|
+
* code gives, from a SWF of `swfVersion` (as `readShape`).
|
|
550
|
+
*/
|
|
551
|
+
export function readMorphShape(
|
|
552
|
+
bytes: Uint8Array,
|
|
553
|
+
code: number,
|
|
554
|
+
offset: number,
|
|
555
|
+
length: number,
|
|
556
|
+
swfVersion = SMOOTHED_FILLS_VERSION,
|
|
557
|
+
): MorphShape {
|
|
558
|
+
if (code !== DefineMorphShape && code !== DefineMorphShape2) {
|
|
559
|
+
throw new Error(`tag ${code} is not a morph shape`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const version = code === DefineMorphShape ? 1 : 2;
|
|
563
|
+
const r = new SwfReader(bytes, offset, offset + length);
|
|
564
|
+
const id = r.u16();
|
|
565
|
+
const startBounds = r.rect();
|
|
566
|
+
const endBounds = r.rect();
|
|
567
|
+
|
|
568
|
+
let startEdgeBounds: Rect | null = null;
|
|
569
|
+
let endEdgeBounds: Rect | null = null;
|
|
570
|
+
if (version === 2) {
|
|
571
|
+
startEdgeBounds = r.rect();
|
|
572
|
+
endEdgeBounds = r.rect();
|
|
573
|
+
r.u8();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// The end's records are where the offset says, which may be past where
|
|
577
|
+
// the start's end when a tool pads them.
|
|
578
|
+
const endOffset = r.u32();
|
|
579
|
+
const endAt = r.pos + endOffset;
|
|
580
|
+
const fills = readMorphFills(r);
|
|
581
|
+
const lines = readMorphLines(r, version);
|
|
582
|
+
|
|
583
|
+
// The start's records stop at the end's, where the offset lies past the
|
|
584
|
+
// styles and in the tag: one missing its end record reads no further. A
|
|
585
|
+
// style change in either end's records brings no new styles: version 1
|
|
586
|
+
// reads none.
|
|
587
|
+
const offsetValid = endOffset !== 0 && endAt >= r.pos && endAt <= r.end;
|
|
588
|
+
const start = readRecords(offsetValid ? new SwfReader(r.bytes, r.pos, endAt) : r, 1);
|
|
589
|
+
if (offsetValid) {
|
|
590
|
+
r.pos = endAt;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const end = readRecords(r, 1);
|
|
594
|
+
if (swfVersion < SMOOTHED_FILLS_VERSION) {
|
|
595
|
+
for (let i = 0; i < fills.length; i++) {
|
|
596
|
+
unsmooth(fills[i].start);
|
|
597
|
+
unsmooth(fills[i].end);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (let i = 0; i < lines.length; i++) {
|
|
601
|
+
unsmooth(lines[i].start.fill);
|
|
602
|
+
unsmooth(lines[i].end.fill);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
id,
|
|
608
|
+
startBounds,
|
|
609
|
+
endBounds,
|
|
610
|
+
startEdgeBounds,
|
|
611
|
+
endEdgeBounds,
|
|
612
|
+
fills,
|
|
613
|
+
lines,
|
|
614
|
+
start: start.records,
|
|
615
|
+
end: end.records,
|
|
616
|
+
truncated: start.truncated || end.truncated,
|
|
617
|
+
};
|
|
618
|
+
}
|