@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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The API versions of avmplus' core/api-versions.h that hosts run ABCs at,
|
|
2
|
+
// in one place for the compiler and the runtime, which must agree: a
|
|
3
|
+
// version names a series of names, Flash Player's or AIR's, which
|
|
4
|
+
// alternate, and what code of that version sees in it.
|
|
5
|
+
|
|
6
|
+
/** Flash Player's latest, SWF_31: what a browser's SWFs, and avmshell's by default, run at. */
|
|
7
|
+
export const API_VERSION = 50;
|
|
8
|
+
|
|
9
|
+
/** AIR's latest, AIR_20_0: what an AIR host's SWFs run at. */
|
|
10
|
+
export const AIR_API_VERSION = 51;
|
|
11
|
+
|
|
12
|
+
/** VM_INTERNAL, which no ABC runs at: the first version past both series. */
|
|
13
|
+
const API_INTERNAL = 52;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `version` if ABCs can run at it, a version of either series, as
|
|
17
|
+
* avmshell's -api names them (1, AIR_1_0, up to AIR_API_VERSION); else a
|
|
18
|
+
* RangeError, so that the compiler, which keeps a byte, and the runtime
|
|
19
|
+
* never take two different versions for one.
|
|
20
|
+
*/
|
|
21
|
+
export function checkApiVersion(version: number): number {
|
|
22
|
+
if (!Number.isInteger(version) || version < 1 || version >= API_INTERNAL) {
|
|
23
|
+
throw new RangeError(`swf2es: no ABC runs at API version ${version}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return version;
|
|
27
|
+
}
|
package/src/bitmap.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// The SWF's bitmap tags. The lossless formats are read here to pixels; the
|
|
2
|
+
// JPEG tags hold an image (a JPEG, PNG or GIF) for the player to decode,
|
|
3
|
+
// with what Flash does to its bytes first already done.
|
|
4
|
+
import { zlibUncompress } from "./compression.js";
|
|
5
|
+
import type { Tag } from "./swf.js";
|
|
6
|
+
import {
|
|
7
|
+
DefineBits,
|
|
8
|
+
DefineBitsJPEG2,
|
|
9
|
+
DefineBitsJPEG3,
|
|
10
|
+
DefineBitsJPEG4,
|
|
11
|
+
DefineBitsLossless,
|
|
12
|
+
DefineBitsLossless2,
|
|
13
|
+
} from "./tags.js";
|
|
14
|
+
|
|
15
|
+
export type Bitmap =
|
|
16
|
+
/** Premultiplied ARGB, row by row. */
|
|
17
|
+
| {
|
|
18
|
+
id: number;
|
|
19
|
+
type: "pixels";
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
transparent: boolean;
|
|
23
|
+
pixels: Uint32Array;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
id: number;
|
|
27
|
+
type: "image";
|
|
28
|
+
format: "jpeg" | "png" | "gif";
|
|
29
|
+
/** The file, as a decoder takes it. */
|
|
30
|
+
data: Uint8Array;
|
|
31
|
+
/** DefineBitsJPEG3's alpha, a byte a pixel, for a JPEG; null for none. */
|
|
32
|
+
alpha: Uint8Array | null;
|
|
33
|
+
/** DefineBitsJPEG4: Flash shows it opaque though it reports it transparent. */
|
|
34
|
+
opaque: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** What Flash cannot read: a bitmap of 0 by 0. */
|
|
37
|
+
| { id: number; type: "invalid" };
|
|
38
|
+
|
|
39
|
+
/** Whether a tag defines a bitmap. */
|
|
40
|
+
export function isBitmapTag(code: number): boolean {
|
|
41
|
+
return (
|
|
42
|
+
code === DefineBits ||
|
|
43
|
+
code === DefineBitsJPEG2 ||
|
|
44
|
+
code === DefineBitsJPEG3 ||
|
|
45
|
+
code === DefineBitsJPEG4 ||
|
|
46
|
+
code === DefineBitsLossless ||
|
|
47
|
+
code === DefineBitsLossless2
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A bitmap tag's character. `tables` is the JPEGTables tag's data that
|
|
53
|
+
* DefineBits needs, null if there was none.
|
|
54
|
+
*/
|
|
55
|
+
export function readBitmap(bytes: Uint8Array, tag: Tag, tables: Uint8Array | null): Bitmap {
|
|
56
|
+
const body = bytes.subarray(tag.offset, tag.offset + tag.length);
|
|
57
|
+
const id = body.length >= 2 ? body[0] | (body[1] << 8) : 0;
|
|
58
|
+
// Flash refuses a bitmap tag written with the short header, whatever it holds.
|
|
59
|
+
if (!tag.long || body.length < 2) {
|
|
60
|
+
return { id, type: "invalid" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
switch (tag.code) {
|
|
65
|
+
case DefineBitsLossless:
|
|
66
|
+
case DefineBitsLossless2:
|
|
67
|
+
return lossless(id, body, tag.code === DefineBitsLossless2);
|
|
68
|
+
case DefineBits:
|
|
69
|
+
return tables ? image(id, concat(tables, body.subarray(2)), null, false) : invalid(id);
|
|
70
|
+
case DefineBitsJPEG2:
|
|
71
|
+
return image(id, body.subarray(2), null, false);
|
|
72
|
+
default: {
|
|
73
|
+
const size = u32(body, 2);
|
|
74
|
+
const start = tag.code === DefineBitsJPEG4 ? 8 : 6;
|
|
75
|
+
if (start + size > body.length) {
|
|
76
|
+
return invalid(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const alpha = zlibUncompress(body.subarray(start + size));
|
|
80
|
+
return image(id, body.subarray(start, start + size), alpha, tag.code === DefineBitsJPEG4);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Corrupt zlib data.
|
|
85
|
+
return invalid(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function invalid(id: number): Bitmap {
|
|
90
|
+
return { id, type: "invalid" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function u16(b: Uint8Array, at: number): number {
|
|
94
|
+
return b[at] | (b[at + 1] << 8);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function u32(b: Uint8Array, at: number): number {
|
|
98
|
+
return (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
102
|
+
const out = new Uint8Array(a.length + b.length);
|
|
103
|
+
out.set(a);
|
|
104
|
+
out.set(b, a.length);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function image(id: number, data: Uint8Array, alpha: Uint8Array | null, jpeg4: boolean): Bitmap {
|
|
109
|
+
if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
|
|
110
|
+
// A PNG or GIF keeps its own alpha; the tag's is ignored.
|
|
111
|
+
return { id, type: "image", format: "png", data, alpha: null, opaque: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x38) {
|
|
115
|
+
return { id, type: "image", format: "gif", data, alpha: null, opaque: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const jpeg = cleanJpeg(data);
|
|
119
|
+
if (!jpeg) {
|
|
120
|
+
return invalid(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
type: "image",
|
|
126
|
+
format: "jpeg",
|
|
127
|
+
data: jpeg,
|
|
128
|
+
alpha: jpeg4 ? null : alpha,
|
|
129
|
+
opaque: jpeg4,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A JPEG as a decoder takes it: one SOI, then its segments, with the EOI
|
|
135
|
+
* and SOI between tables and image that SWFs carry dropped. Old tools
|
|
136
|
+
* wrote a stray FF D9 FF D8 at the start, and DefineBits' tables come as
|
|
137
|
+
* a JPEG of their own; Flash reads both. Null if it is not a JPEG.
|
|
138
|
+
*/
|
|
139
|
+
function cleanJpeg(data: Uint8Array): Uint8Array | null {
|
|
140
|
+
const out: Uint8Array[] = [Uint8Array.of(0xff, 0xd8)];
|
|
141
|
+
let at = 0;
|
|
142
|
+
while (at + 1 < data.length) {
|
|
143
|
+
if (data[at] !== 0xff) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const marker = data[at + 1];
|
|
148
|
+
if (marker === 0xd8 || marker === 0xd9) {
|
|
149
|
+
at += 2;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (marker === 0xff) {
|
|
154
|
+
// Fill bytes before a marker.
|
|
155
|
+
at++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (marker === 0xda) {
|
|
160
|
+
// The scan, and everything after it, as it is.
|
|
161
|
+
out.push(data.subarray(at));
|
|
162
|
+
return concatAll(out);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (at + 3 >= data.length) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const end = at + 2 + ((data[at + 2] << 8) | data[at + 3]);
|
|
170
|
+
out.push(data.subarray(at, end));
|
|
171
|
+
at = end;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function concatAll(parts: Uint8Array[]): Uint8Array {
|
|
178
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
179
|
+
let at = 0;
|
|
180
|
+
for (const p of parts) {
|
|
181
|
+
out.set(p, at);
|
|
182
|
+
at += p.length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** DefineBitsLossless and 2: a palette, 15-bit or 32-bit pixels, zlib'd. */
|
|
189
|
+
function lossless(id: number, body: Uint8Array, alpha: boolean): Bitmap {
|
|
190
|
+
if (body.length < 7) {
|
|
191
|
+
return invalid(id);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const format = body[2];
|
|
195
|
+
const width = u16(body, 3);
|
|
196
|
+
const height = u16(body, 5);
|
|
197
|
+
if (width === 0 || height === 0 || (format !== 3 && format !== 4 && format !== 5)) {
|
|
198
|
+
return invalid(id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const colors = format === 3 ? body[7] + 1 : 0;
|
|
202
|
+
const data = zlibUncompress(body.subarray(format === 3 ? 8 : 7));
|
|
203
|
+
const pixels = new Uint32Array(width * height);
|
|
204
|
+
if (format === 3) {
|
|
205
|
+
const entry = alpha ? 4 : 3;
|
|
206
|
+
const palette = new Uint32Array(256);
|
|
207
|
+
for (let i = 0; i < colors; i++) {
|
|
208
|
+
const p = i * entry;
|
|
209
|
+
// Lossless2's palette is premultiplied ARGB, as its pixels are.
|
|
210
|
+
palette[i] = alpha
|
|
211
|
+
? ((data[p + 3] << 24) | (data[p] << 16) | (data[p + 1] << 8) | data[p + 2]) >>> 0
|
|
212
|
+
: (0xff000000 | (data[p] << 16) | (data[p + 1] << 8) | data[p + 2]) >>> 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const stride = (width + 3) & ~3;
|
|
216
|
+
const start = colors * entry;
|
|
217
|
+
if (start + stride * (height - 1) + width > data.length) {
|
|
218
|
+
return invalid(id);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (let y = 0; y < height; y++) {
|
|
222
|
+
for (let x = 0; x < width; x++) {
|
|
223
|
+
pixels[y * width + x] = palette[data[start + y * stride + x]];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else if (format === 4) {
|
|
227
|
+
// 15-bit RGB, rows padded to 4 bytes; a channel v is v * 8 + 7, 0 kept at 0.
|
|
228
|
+
const stride = (width * 2 + 3) & ~3;
|
|
229
|
+
if (stride * (height - 1) + width * 2 > data.length) {
|
|
230
|
+
return invalid(id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const widen = (v: number) => (v === 0 ? 0 : v * 8 + 7);
|
|
234
|
+
for (let y = 0; y < height; y++) {
|
|
235
|
+
for (let x = 0; x < width; x++) {
|
|
236
|
+
const p = y * stride + x * 2;
|
|
237
|
+
const v = (data[p] << 8) | data[p + 1];
|
|
238
|
+
pixels[y * width + x] =
|
|
239
|
+
(0xff000000 |
|
|
240
|
+
(widen((v >> 10) & 31) << 16) |
|
|
241
|
+
(widen((v >> 5) & 31) << 8) |
|
|
242
|
+
widen(v & 31)) >>>
|
|
243
|
+
0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
if (width * height * 4 > data.length) {
|
|
248
|
+
return invalid(id);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < width * height; i++) {
|
|
252
|
+
const p = i * 4;
|
|
253
|
+
const rgb = (data[p + 1] << 16) | (data[p + 2] << 8) | data[p + 3];
|
|
254
|
+
pixels[i] = alpha ? ((data[p] << 24) | rgb) >>> 0 : (0xff000000 | rgb) >>> 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { id, type: "pixels", width, height, transparent: alpha, pixels };
|
|
259
|
+
}
|
package/src/button.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// DefineButton and DefineButton2 (SWF specification, chapter 4): a
|
|
2
|
+
// button's records, each a character shown in some of its four states, and
|
|
3
|
+
// whether it tracks as a menu. Their actions are the SWF's action blocks
|
|
4
|
+
// (actions.ts), which a button gives by their indices.
|
|
5
|
+
import type { ActionList } from "./actions.js";
|
|
6
|
+
import { readFilterBytes } from "./display.js";
|
|
7
|
+
import { type ColorTransform, type Matrix, readColorTransform, readMatrix } from "./shape.js";
|
|
8
|
+
import { SwfReader, type Tag } from "./swf.js";
|
|
9
|
+
import { DefineButton2 } from "./tags.js";
|
|
10
|
+
|
|
11
|
+
/** A button's state, as BUTTONRECORD's flags have them. */
|
|
12
|
+
export const BUTTON_UP = 0x01;
|
|
13
|
+
export const BUTTON_OVER = 0x02;
|
|
14
|
+
export const BUTTON_DOWN = 0x04;
|
|
15
|
+
export const BUTTON_HIT_TEST = 0x08;
|
|
16
|
+
|
|
17
|
+
/** A character a button shows in the states `states` names (BUTTON_UP and the rest). */
|
|
18
|
+
export interface ButtonRecord {
|
|
19
|
+
states: number;
|
|
20
|
+
character: number;
|
|
21
|
+
depth: number;
|
|
22
|
+
matrix: Matrix;
|
|
23
|
+
/** DefineButton2's; null in a DefineButton. */
|
|
24
|
+
colorTransform: ColorTransform | null;
|
|
25
|
+
/** The filter list's bytes, undecoded, as a place keeps them; null without one. */
|
|
26
|
+
filters: Uint8Array | null;
|
|
27
|
+
blendMode: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Button {
|
|
31
|
+
id: number;
|
|
32
|
+
records: ButtonRecord[];
|
|
33
|
+
/** DefineButton2's TrackAsMenu: a press elsewhere can be released on it. */
|
|
34
|
+
trackAsMenu: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The indices of its AVM1 actions in the SWF's ActionList; null without
|
|
37
|
+
* them, or when read without the list.
|
|
38
|
+
*/
|
|
39
|
+
actions: number[] | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A DefineButton or DefineButton2; given the SWF's action list, with the indices of its actions. */
|
|
43
|
+
export function readButton(bytes: Uint8Array, tag: Tag, actions?: ActionList): Button {
|
|
44
|
+
const { button } = parseButton(bytes, tag);
|
|
45
|
+
button.actions = actions?.byTag.get(tag.offset) ?? null;
|
|
46
|
+
return button;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A button's id, and where its actions start: -1 for a DefineButton2 whose action offset is 0. */
|
|
50
|
+
export function buttonActionsOffset(bytes: Uint8Array, tag: Tag): { id: number; offset: number } {
|
|
51
|
+
const { button, actions } = parseButton(bytes, tag);
|
|
52
|
+
return { id: button.id, offset: actions };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseButton(bytes: Uint8Array, tag: Tag): { button: Button; actions: number } {
|
|
56
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
57
|
+
const two = tag.code === DefineButton2;
|
|
58
|
+
const button: Button = { id: r.u16(), records: [], trackAsMenu: false, actions: null };
|
|
59
|
+
// Only whether it is 0 counts: the actions start after the records' end, as Ruffle reads them.
|
|
60
|
+
let hasActions = true;
|
|
61
|
+
if (two) {
|
|
62
|
+
button.trackAsMenu = (r.u8() & 0x01) !== 0;
|
|
63
|
+
hasActions = r.u16() !== 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The records end at a flags byte of 0.
|
|
67
|
+
while (r.pos < r.end && !r.overrun) {
|
|
68
|
+
const flags = r.u8();
|
|
69
|
+
if (flags === 0) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const record: ButtonRecord = {
|
|
74
|
+
states: flags & 0x0f,
|
|
75
|
+
character: r.u16(),
|
|
76
|
+
depth: r.u16(),
|
|
77
|
+
matrix: readMatrix(r),
|
|
78
|
+
colorTransform: null,
|
|
79
|
+
filters: null,
|
|
80
|
+
blendMode: null,
|
|
81
|
+
};
|
|
82
|
+
if (two) {
|
|
83
|
+
record.colorTransform = readColorTransform(r, true);
|
|
84
|
+
if (flags & 0x10) {
|
|
85
|
+
record.filters = readFilterBytes(r);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (flags & 0x20) {
|
|
89
|
+
record.blendMode = r.u8();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!r.overrun) {
|
|
94
|
+
button.records.push(record);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { button, actions: hasActions ? Math.min(r.pos, r.end) : -1 };
|
|
99
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression as SWF files and ByteArray use it, synchronously and with no
|
|
3
|
+
* platform API, through @swf2es/native/compress: zlib itself for zlib and raw
|
|
4
|
+
* deflate, the LZMA SDK's encoder for LZMA, and an LZMA decoder after the
|
|
5
|
+
* SDK's specification.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
CORRUPT,
|
|
9
|
+
type Decoded,
|
|
10
|
+
DICTIONARY,
|
|
11
|
+
DONE,
|
|
12
|
+
deflate,
|
|
13
|
+
FULL,
|
|
14
|
+
inflate,
|
|
15
|
+
lzmaDecode,
|
|
16
|
+
lzmaEncode,
|
|
17
|
+
TRUNCATED,
|
|
18
|
+
} from "@swf2es/native/compress";
|
|
19
|
+
|
|
20
|
+
/** Data a decompressor cannot read: corrupt, truncated, or not what it says. */
|
|
21
|
+
export class CompressedDataError extends Error {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Data whose output does not fit: more than the decompressor's memory, or
|
|
25
|
+
* the page's, can hold. avmplus' ByteArray fails such an uncompress with
|
|
26
|
+
* MemoryError 1000, where it fails corrupt data with IOError 2058.
|
|
27
|
+
*/
|
|
28
|
+
export class CompressedDataTooLargeError extends CompressedDataError {}
|
|
29
|
+
|
|
30
|
+
/** A deflate level: 0 stores, 1 is fastest, 9 smallest. */
|
|
31
|
+
export type ZlibLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
32
|
+
|
|
33
|
+
/** `data` as a zlib stream, as zlib's deflate writes it at `level`, 9 by default, as avmplus' ByteArray. */
|
|
34
|
+
export function zlibCompress(data: Uint8Array, level: ZlibLevel = 9): Uint8Array {
|
|
35
|
+
return deflate(data, level, false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `data` as a raw deflate stream, with no zlib header or checksum. */
|
|
39
|
+
export function deflateCompress(data: Uint8Array): Uint8Array {
|
|
40
|
+
return deflate(data, 9, true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The most an inflate may write: as much as a wasm32 memory holds. */
|
|
44
|
+
const NO_LIMIT = 2 ** 32 - 1;
|
|
45
|
+
|
|
46
|
+
/** A failed decode's status as a CompressedDataError. */
|
|
47
|
+
function failure(result: Decoded): CompressedDataError {
|
|
48
|
+
switch (result.status) {
|
|
49
|
+
case TRUNCATED:
|
|
50
|
+
return new CompressedDataError("truncated");
|
|
51
|
+
case CORRUPT:
|
|
52
|
+
return new CompressedDataError(result.message ?? "corrupt");
|
|
53
|
+
case DICTIONARY:
|
|
54
|
+
return new CompressedDataError("needs a preset dictionary");
|
|
55
|
+
default:
|
|
56
|
+
return new CompressedDataTooLargeError("out of memory");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The bytes of a zlib stream (or, `raw`, of a raw deflate stream), up to its
|
|
62
|
+
* end: anything after is left, as zlib's inflate leaves it. The header is
|
|
63
|
+
* checked here, so that every way it can be wrong reads as avmplus' does.
|
|
64
|
+
*/
|
|
65
|
+
export function zlibUncompress(data: Uint8Array, raw = false): Uint8Array {
|
|
66
|
+
if (!raw) {
|
|
67
|
+
if (data.length < 2) {
|
|
68
|
+
throw new CompressedDataError("truncated");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cmf = data[0];
|
|
72
|
+
const flg = data[1];
|
|
73
|
+
if ((cmf & 0x0f) !== 8 || cmf >> 4 > 7 || ((cmf << 8) | flg) % 31 !== 0) {
|
|
74
|
+
throw new CompressedDataError("incorrect header check");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (flg & 0x20) {
|
|
78
|
+
throw new CompressedDataError("needs a preset dictionary");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = inflate(data, raw, NO_LIMIT);
|
|
83
|
+
if (result.status !== DONE) {
|
|
84
|
+
throw failure(result);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result.bytes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The size of an LZMA header: its 5 property bytes and the 8-byte length of what it holds. */
|
|
91
|
+
export const LZMA_HEADER = 13;
|
|
92
|
+
|
|
93
|
+
/** `data` as ByteArray's LZMA: the 5 property bytes, its length in 8 bytes, little-endian, and the stream. */
|
|
94
|
+
export function lzmaByteArrayCompress(data: Uint8Array): Uint8Array {
|
|
95
|
+
return lzmaEncode(data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ByteArray's LZMA, as avmplus reads it: the length its header gives must
|
|
100
|
+
* be what the stream holds. A length past 32 bits is not read here; the
|
|
101
|
+
* caller checks it first, as avmplus fails it before trying.
|
|
102
|
+
*/
|
|
103
|
+
export function lzmaByteArrayUncompress(data: Uint8Array): Uint8Array {
|
|
104
|
+
const length = (data[5] | (data[6] << 8) | (data[7] << 16) | (data[8] << 24)) >>> 0;
|
|
105
|
+
if (length > MAX_LZMA_RATIO * (data.length - LZMA_HEADER) + 4096) {
|
|
106
|
+
throw new CompressedDataError(`${length} bytes in a stream of ${data.length}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = lzmaDecode(data, 0, LZMA_HEADER, length);
|
|
110
|
+
if (result.status !== DONE) {
|
|
111
|
+
throw failure(result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (result.bytes.length !== length) {
|
|
115
|
+
throw new CompressedDataError("length mismatch");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.bytes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* More than LZMA can expand a byte to. Its best case, a long run of one
|
|
123
|
+
* byte, is matches of 273 bytes that each cost 14 decisions, each at least
|
|
124
|
+
* the 0.022 bits of LZMA's likeliest probability (2017/2048): 7090:1,
|
|
125
|
+
* which an encoder nears on 32 MB of zeros (6959:1). A header naming more
|
|
126
|
+
* than this of its stream is refused before decoding; one naming less is
|
|
127
|
+
* decoded, and a decoder that would read past the stream's end fails.
|
|
128
|
+
*/
|
|
129
|
+
const MAX_LZMA_RATIO = 8192;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The first `length` bytes of a SWF as an uncompressed one, or fewer if it
|
|
133
|
+
* has fewer, decompressing only as far as they need: the header and the
|
|
134
|
+
* first tags, without trusting the header's file length, which a SWF of
|
|
135
|
+
* 30 bytes may give as 4 GB. Whatever cannot be read ends it early.
|
|
136
|
+
*/
|
|
137
|
+
export function decompressSwfPrefix(swf: Uint8Array, length: number): Uint8Array {
|
|
138
|
+
const signature = String.fromCharCode(swf[0], swf[1], swf[2]);
|
|
139
|
+
if (swf.length < 8 || signature === "FWS") {
|
|
140
|
+
return swf.subarray(0, length);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const fileLength = new DataView(swf.buffer, swf.byteOffset, swf.byteLength).getUint32(4, true);
|
|
144
|
+
// No further than the header's length, where decompressSwf stops too.
|
|
145
|
+
const want = Math.max(0, Math.min(length, fileLength) - 8);
|
|
146
|
+
const options = { offset: 8, partial: true };
|
|
147
|
+
let out: Uint8Array = new Uint8Array(8);
|
|
148
|
+
// A kilobyte of the body first, as a header and the first tags need, and
|
|
149
|
+
// more only while the input runs out before the output: a large SWF is
|
|
150
|
+
// not copied whole for its first bytes. What decoded before any failure
|
|
151
|
+
// is kept, so a SWF cut short still has its start.
|
|
152
|
+
for (let input = 1024; ; input *= 16) {
|
|
153
|
+
const end = Math.min(swf.length, (signature === "ZWS" ? 17 : 8) + input);
|
|
154
|
+
let result: Decoded | null = null;
|
|
155
|
+
if (signature === "CWS") {
|
|
156
|
+
result = inflate(swf.subarray(8, end), false, want, options);
|
|
157
|
+
} else if (signature === "ZWS" && swf.length >= 17) {
|
|
158
|
+
const size = Math.min(want, Math.max(0, fileLength - 8));
|
|
159
|
+
result = lzmaDecode(swf.subarray(0, end), 12, 17, size, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result === null) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
out = result.bytes;
|
|
167
|
+
if (result.status !== TRUNCATED || end === swf.length) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
out.set(swf.subarray(0, 8), 0);
|
|
173
|
+
out[0] = 0x46; // F
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A SWF's bytes as an uncompressed one: the header as FWS, then the body,
|
|
179
|
+
* inflated for CWS, and for ZWS decoded from LZMA, whose header SWF writes
|
|
180
|
+
* differently (after the 8 bytes of the SWF header, a 4-byte compressed
|
|
181
|
+
* length and the 5 property bytes, and no 8-byte size).
|
|
182
|
+
*/
|
|
183
|
+
export function decompressSwf(swf: Uint8Array): Uint8Array {
|
|
184
|
+
const signature = String.fromCharCode(swf[0], swf[1], swf[2]);
|
|
185
|
+
if (signature === "FWS") {
|
|
186
|
+
return swf;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const fileLength = new DataView(swf.buffer, swf.byteOffset, swf.byteLength).getUint32(4, true);
|
|
190
|
+
if (fileLength < 8 && (signature === "CWS" || signature === "ZWS")) {
|
|
191
|
+
throw new CompressedDataError(`a file length of ${fileLength}, shorter than its header`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let result: Decoded;
|
|
195
|
+
if (signature === "CWS") {
|
|
196
|
+
// A byte past the header's length tells a longer body from one that
|
|
197
|
+
// fits, which then ends with its checksum checked.
|
|
198
|
+
result = inflate(swf.subarray(8), false, fileLength - 8 + 1, { offset: 8 });
|
|
199
|
+
if (result.status === TRUNCATED) {
|
|
200
|
+
// Some SWFs omit the Adler-32 trailer despite holding a complete deflate stream.
|
|
201
|
+
const raw = inflate(swf.subarray(10), true, fileLength - 8 + 1, { offset: 8 });
|
|
202
|
+
if ((raw.status === DONE || raw.status === FULL) && 10 + raw.used + 4 > swf.length) {
|
|
203
|
+
result = raw;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A longer body, as a packer may leave bytes after the End tag inside
|
|
208
|
+
// the stream, is read as far as the header's length and no further, as
|
|
209
|
+
// Flash Player plays it, and as a ZWS's decoding stops.
|
|
210
|
+
if (result.status === FULL) {
|
|
211
|
+
result = { ...result, status: DONE, bytes: result.bytes.subarray(0, fileLength) };
|
|
212
|
+
}
|
|
213
|
+
} else if (signature === "ZWS") {
|
|
214
|
+
if (swf.length < 17) {
|
|
215
|
+
throw new CompressedDataError("truncated");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A length no stream that short can hold is refused before decoding;
|
|
219
|
+
// within it, the decoder stops where the stream does.
|
|
220
|
+
if (fileLength - 8 > MAX_LZMA_RATIO * (swf.length - 17) + 4096) {
|
|
221
|
+
throw new CompressedDataError(`a body of ${fileLength - 8} bytes in ${swf.length} of SWF`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
result = lzmaDecode(swf, 12, 17, fileLength - 8, { offset: 8 });
|
|
225
|
+
} else {
|
|
226
|
+
throw new TypeError(`Not a SWF file (signature ${JSON.stringify(signature)})`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (result.status !== DONE) {
|
|
230
|
+
throw failure(result);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const file = result.bytes;
|
|
234
|
+
if (file.length !== fileLength) {
|
|
235
|
+
throw new CompressedDataError(
|
|
236
|
+
`body of ${file.length - 8} bytes where the header says ${fileLength - 8}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
file.set(swf.subarray(0, 8), 0);
|
|
241
|
+
file[0] = 0x46; // F
|
|
242
|
+
return file;
|
|
243
|
+
}
|