@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/sound.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// DefineSound's header and encoded samples, and the tags that play sounds
|
|
2
|
+
// from a timeline: StartSound and StartSound2, the stream of
|
|
3
|
+
// SoundStreamHead and SoundStreamBlock, and DefineButtonSound. Decoding
|
|
4
|
+
// belongs to the player, where a host can supply an audio device.
|
|
5
|
+
import { readString } from "./display.js";
|
|
6
|
+
import { SwfReader, type Tag } from "./swf.js";
|
|
7
|
+
import { StartSound2 } from "./tags.js";
|
|
8
|
+
|
|
9
|
+
export interface Sound {
|
|
10
|
+
id: number;
|
|
11
|
+
format: number;
|
|
12
|
+
sampleRate: number;
|
|
13
|
+
sampleSize: 8 | 16;
|
|
14
|
+
channels: 1 | 2;
|
|
15
|
+
sampleCount: number;
|
|
16
|
+
/** For MP3, the signed sample offset at the front of SoundData. */
|
|
17
|
+
seekSamples: number;
|
|
18
|
+
data: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RATES = [5512.5, 11025, 22050, 44100];
|
|
22
|
+
|
|
23
|
+
export function readSound(bytes: Uint8Array, tag: Tag): Sound {
|
|
24
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
25
|
+
const id = r.u16();
|
|
26
|
+
const flags = r.u8();
|
|
27
|
+
const format = flags >> 4;
|
|
28
|
+
const sampleRate = RATES[(flags >> 2) & 3];
|
|
29
|
+
const sampleSize = (flags & 2) === 0 ? 8 : 16;
|
|
30
|
+
const channels = (flags & 1) === 0 ? 1 : 2;
|
|
31
|
+
const sampleCount = r.u32();
|
|
32
|
+
const seekSamples = format === 2 ? r.s16() : 0;
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
format,
|
|
36
|
+
sampleRate,
|
|
37
|
+
sampleSize,
|
|
38
|
+
channels,
|
|
39
|
+
sampleCount,
|
|
40
|
+
seekSamples,
|
|
41
|
+
data: bytes.subarray(r.pos, tag.offset + tag.length),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A point of SOUNDINFO's envelope: its sample at 44.1 kHz from the start, and each channel's level, 0 to 1. */
|
|
46
|
+
export interface SoundEnvelopePoint {
|
|
47
|
+
sample: number;
|
|
48
|
+
left: number;
|
|
49
|
+
right: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** SOUNDINFO: how StartSound, StartSound2 and DefineButtonSound play a sound. */
|
|
53
|
+
export interface SoundInfo {
|
|
54
|
+
/** SyncStop: stop the sound wherever it plays, and start none. */
|
|
55
|
+
stop: boolean;
|
|
56
|
+
/** SyncNoMultiple: start none while the sound plays. */
|
|
57
|
+
noMultiple: boolean;
|
|
58
|
+
/** Where each loop starts and ends, in samples at 44.1 kHz; null for the sound's own. */
|
|
59
|
+
inPoint: number | null;
|
|
60
|
+
outPoint: number | null;
|
|
61
|
+
/** 1 without HasLoops. */
|
|
62
|
+
loops: number;
|
|
63
|
+
envelope: SoundEnvelopePoint[] | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readSoundInfo(r: SwfReader): SoundInfo {
|
|
67
|
+
const flags = r.u8();
|
|
68
|
+
const inPoint = flags & 0x01 ? r.u32() : null;
|
|
69
|
+
const outPoint = flags & 0x02 ? r.u32() : null;
|
|
70
|
+
const loops = flags & 0x04 ? r.u16() : 1;
|
|
71
|
+
let envelope: SoundEnvelopePoint[] | null = null;
|
|
72
|
+
if (flags & 0x08) {
|
|
73
|
+
envelope = [];
|
|
74
|
+
for (let n = r.u8(); n > 0 && !r.overrun; n--) {
|
|
75
|
+
envelope.push({ sample: r.u32(), left: r.u16() / 32768, right: r.u16() / 32768 });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
stop: (flags & 0x20) !== 0,
|
|
81
|
+
noMultiple: (flags & 0x10) !== 0,
|
|
82
|
+
inPoint,
|
|
83
|
+
outPoint,
|
|
84
|
+
loops,
|
|
85
|
+
envelope,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** StartSound names its sound by id, StartSound2 by the class bound to it. */
|
|
90
|
+
export interface StartSound {
|
|
91
|
+
id: number | null;
|
|
92
|
+
className: string | null;
|
|
93
|
+
info: SoundInfo;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readStartSound(bytes: Uint8Array, tag: Tag): StartSound {
|
|
97
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
98
|
+
if (tag.code === StartSound2) {
|
|
99
|
+
const className = readString(r);
|
|
100
|
+
return { id: null, className, info: readSoundInfo(r) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const id = r.u16();
|
|
104
|
+
return { id, className: null, info: readSoundInfo(r) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** SoundStreamHead or SoundStreamHead2: the format of a timeline's SoundStreamBlocks. */
|
|
108
|
+
export interface SoundStreamHead {
|
|
109
|
+
format: number;
|
|
110
|
+
sampleRate: number;
|
|
111
|
+
sampleSize: 8 | 16;
|
|
112
|
+
channels: 1 | 2;
|
|
113
|
+
/** The samples each frame's block holds, on average. */
|
|
114
|
+
samplesPerBlock: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readSoundStreamHead(bytes: Uint8Array, tag: Tag): SoundStreamHead {
|
|
118
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
119
|
+
// The playback format is only a hint; the stream's own follows it.
|
|
120
|
+
r.u8();
|
|
121
|
+
const flags = r.u8();
|
|
122
|
+
return {
|
|
123
|
+
format: flags >> 4,
|
|
124
|
+
sampleRate: RATES[(flags >> 2) & 3],
|
|
125
|
+
sampleSize: (flags & 2) === 0 ? 8 : 16,
|
|
126
|
+
channels: (flags & 1) === 0 ? 1 : 2,
|
|
127
|
+
samplesPerBlock: r.u16(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A SoundStreamBlock's samples: for MP3, the frames after its sample count and seek. */
|
|
132
|
+
export interface SoundStreamBlock {
|
|
133
|
+
/** MP3's own count; null for the other formats, whose bytes tell it. */
|
|
134
|
+
sampleCount: number | null;
|
|
135
|
+
data: Uint8Array;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readSoundStreamBlock(
|
|
139
|
+
bytes: Uint8Array,
|
|
140
|
+
tag: Tag,
|
|
141
|
+
format: number,
|
|
142
|
+
): SoundStreamBlock {
|
|
143
|
+
const end = tag.offset + tag.length;
|
|
144
|
+
if (format !== 2) {
|
|
145
|
+
return { sampleCount: null, data: bytes.subarray(tag.offset, end) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const r = new SwfReader(bytes, tag.offset, end);
|
|
149
|
+
const sampleCount = r.u16();
|
|
150
|
+
return { sampleCount, data: bytes.subarray(Math.min(end, tag.offset + 4), end) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* DefineButtonSound: the sounds a button plays as its state changes, in
|
|
155
|
+
* the tag's order: over to up, up to over, over to down, down to over.
|
|
156
|
+
* Some tools write fewer than four, as Ruffle notes; the missing are none.
|
|
157
|
+
*/
|
|
158
|
+
export interface ButtonSounds {
|
|
159
|
+
id: number;
|
|
160
|
+
sounds: ({ id: number; info: SoundInfo } | null)[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function readButtonSound(bytes: Uint8Array, tag: Tag): ButtonSounds {
|
|
164
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
165
|
+
const id = r.u16();
|
|
166
|
+
const sounds: ButtonSounds["sounds"] = [];
|
|
167
|
+
for (let i = 0; i < 4; i++) {
|
|
168
|
+
const sound = r.pos + 2 <= r.end ? r.u16() : 0;
|
|
169
|
+
sounds.push(sound === 0 ? null : { id: sound, info: readSoundInfo(r) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { id, sounds };
|
|
173
|
+
}
|
package/src/swf.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// A SWF's movie header and its tags, as the SWF file format specification
|
|
2
|
+
// (version 19) lays them out: after the 8-byte file header and the body's
|
|
3
|
+
// decompression, the frame size as a RECT, the frame rate as 8.8 fixed
|
|
4
|
+
// point, the frame count, then tags to an End tag or the body's end.
|
|
5
|
+
import { decompressSwf, decompressSwfPrefix } from "./compression.js";
|
|
6
|
+
import { End, FileAttributes, SetBackgroundColor } from "./tags.js";
|
|
7
|
+
|
|
8
|
+
export type SwfCompression = "none" | "zlib" | "lzma";
|
|
9
|
+
|
|
10
|
+
export interface SwfHeader {
|
|
11
|
+
compression: SwfCompression;
|
|
12
|
+
/** SWF version byte (e.g. 10 for Flash Player 10). */
|
|
13
|
+
version: number;
|
|
14
|
+
/** Uncompressed length of the whole file, header included. */
|
|
15
|
+
fileLength: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A rectangle in twips (1/20 pixel), as a RECT holds one. */
|
|
19
|
+
export interface Rect {
|
|
20
|
+
xMin: number;
|
|
21
|
+
xMax: number;
|
|
22
|
+
yMin: number;
|
|
23
|
+
yMax: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A tag: its code, and where its body is in the decompressed file. */
|
|
27
|
+
export interface Tag {
|
|
28
|
+
code: number;
|
|
29
|
+
/** Offset of the body in the decompressed file, after the tag's header. */
|
|
30
|
+
offset: number;
|
|
31
|
+
length: number;
|
|
32
|
+
/** Whether the header was the long form, which Flash requires of a bitmap tag. */
|
|
33
|
+
long: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Swf {
|
|
37
|
+
header: SwfHeader;
|
|
38
|
+
/** The whole file, decompressed, header included; tags' offsets are into it. */
|
|
39
|
+
bytes: Uint8Array;
|
|
40
|
+
frameSize: Rect;
|
|
41
|
+
/** Frames per second: the 8.8 fixed-point value as a number. */
|
|
42
|
+
frameRate: number;
|
|
43
|
+
frameCount: number;
|
|
44
|
+
tags: Tag[];
|
|
45
|
+
/** Whether the tags ran past the end of the file, and were cut there. */
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SIGNATURES: Record<string, SwfCompression> = { FWS: "none", CWS: "zlib", ZWS: "lzma" };
|
|
50
|
+
|
|
51
|
+
/** Read the 8-byte header every SWF starts with. */
|
|
52
|
+
export function readSwfHeader(bytes: Uint8Array): SwfHeader {
|
|
53
|
+
if (bytes.length < 8) {
|
|
54
|
+
throw new RangeError(`SWF header needs 8 bytes, got ${bytes.length}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const signature = String.fromCharCode(bytes[0], bytes[1], bytes[2]);
|
|
58
|
+
const compression = SIGNATURES[signature];
|
|
59
|
+
if (!compression) {
|
|
60
|
+
throw new TypeError(`Not a SWF file (signature ${JSON.stringify(signature)})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
64
|
+
return { compression, version: bytes[3], fileLength: view.getUint32(4, true) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reads bits and bytes of a SWF body, bits most significant first, as the format packs them. */
|
|
68
|
+
export class SwfReader {
|
|
69
|
+
private bit = 0;
|
|
70
|
+
private byte = 0;
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
readonly bytes: Uint8Array,
|
|
74
|
+
public pos = 0,
|
|
75
|
+
readonly end = bytes.length,
|
|
76
|
+
) {}
|
|
77
|
+
|
|
78
|
+
/** Past the end: every read after it gives 0. */
|
|
79
|
+
get overrun(): boolean {
|
|
80
|
+
return this.pos > this.end;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
align(): void {
|
|
84
|
+
this.bit = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ub(n: number): number {
|
|
88
|
+
let v = 0;
|
|
89
|
+
for (let i = 0; i < n; i++) {
|
|
90
|
+
if (this.bit === 0) {
|
|
91
|
+
this.byte = this.pos < this.end ? this.bytes[this.pos] : 0;
|
|
92
|
+
this.pos++;
|
|
93
|
+
this.bit = 8;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.bit--;
|
|
97
|
+
v = v * 2 + ((this.byte >> this.bit) & 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return v;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
sb(n: number): number {
|
|
104
|
+
if (n > 0 && n <= 32 && n === (n | 0)) {
|
|
105
|
+
const shift = 32 - n;
|
|
106
|
+
return (this.ub(n) << shift) >> shift;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (n === 0) {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const v = this.ub(n);
|
|
114
|
+
return v >= 2 ** (n - 1) ? v - 2 ** n : v;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
u8(): number {
|
|
118
|
+
this.align();
|
|
119
|
+
const v = this.pos < this.end ? this.bytes[this.pos] : 0;
|
|
120
|
+
this.pos++;
|
|
121
|
+
return v;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
u16(): number {
|
|
125
|
+
return this.u8() | (this.u8() << 8);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
u32(): number {
|
|
129
|
+
return (this.u16() | (this.u16() << 16)) >>> 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
s16(): number {
|
|
133
|
+
return (this.u16() << 16) >> 16;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
rect(): Rect {
|
|
137
|
+
this.align();
|
|
138
|
+
const n = this.ub(5);
|
|
139
|
+
const rect = { xMin: this.sb(n), xMax: this.sb(n), yMin: this.sb(n), yMax: this.sb(n) };
|
|
140
|
+
this.align();
|
|
141
|
+
return rect;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A SWF's header and tags, its body decompressed. */
|
|
146
|
+
export function readSwf(file: Uint8Array): Swf {
|
|
147
|
+
const header = readSwfHeader(file);
|
|
148
|
+
const bytes = decompressSwf(file);
|
|
149
|
+
const r = new SwfReader(bytes, 8);
|
|
150
|
+
const frameSize = r.rect();
|
|
151
|
+
const frameRate = r.u16() / 256;
|
|
152
|
+
const frameCount = r.u16();
|
|
153
|
+
const { tags, truncated } = readTags(bytes, r.pos, bytes.length);
|
|
154
|
+
return { header, bytes, frameSize, frameRate, frameCount, tags, truncated };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The tags from `start` to an End tag or `end`, as a sprite's or the file's. */
|
|
158
|
+
export function readTags(
|
|
159
|
+
bytes: Uint8Array,
|
|
160
|
+
start: number,
|
|
161
|
+
end: number,
|
|
162
|
+
): { tags: Tag[]; truncated: boolean } {
|
|
163
|
+
const tags: Tag[] = [];
|
|
164
|
+
const r = new SwfReader(bytes, start, end);
|
|
165
|
+
while (r.pos + 2 <= end) {
|
|
166
|
+
const codeAndLength = r.u16();
|
|
167
|
+
const code = codeAndLength >> 6;
|
|
168
|
+
let length = codeAndLength & 0x3f;
|
|
169
|
+
const long = length === 0x3f;
|
|
170
|
+
if (long) {
|
|
171
|
+
length = r.u32();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (r.pos + length > end) {
|
|
175
|
+
tags.push({ code, offset: r.pos, length: Math.max(0, end - r.pos), long });
|
|
176
|
+
return { tags, truncated: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
tags.push({ code, offset: r.pos, length, long });
|
|
180
|
+
r.pos += length;
|
|
181
|
+
if (code === End) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { tags, truncated: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The background colour SetBackgroundColor sets, 0xRRGGBB, or white without one. */
|
|
190
|
+
export function backgroundColor(swf: Swf): number {
|
|
191
|
+
const tag = swf.tags.find((t) => t.code === SetBackgroundColor);
|
|
192
|
+
if (!tag || tag.length < 3) {
|
|
193
|
+
return 0xffffff;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const b = swf.bytes;
|
|
197
|
+
return (b[tag.offset] << 16) | (b[tag.offset + 1] << 8) | b[tag.offset + 2];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The flags of a SWF file's FileAttributes, from the start of the file
|
|
202
|
+
* alone, as Flash Player reads them, FileAttributes being the first tag:
|
|
203
|
+
* 0 for a SWF whose first tag is another. For a host that must not
|
|
204
|
+
* decompress the whole of a SWF it does not trust, as a desktop shell
|
|
205
|
+
* choosing a local SWF's sandbox; ActionScript3 is 0x08, UseNetwork 0x01.
|
|
206
|
+
*/
|
|
207
|
+
export function fileAttributes(file: Uint8Array): number {
|
|
208
|
+
// The header's rect is 17 bytes at most, then the rate, count, and a long tag header.
|
|
209
|
+
const bytes = decompressSwfPrefix(file, 64);
|
|
210
|
+
try {
|
|
211
|
+
const r = new SwfReader(bytes, 8);
|
|
212
|
+
r.rect();
|
|
213
|
+
r.u16();
|
|
214
|
+
r.u16();
|
|
215
|
+
const head = r.u16();
|
|
216
|
+
if (head >> 6 !== FileAttributes) {
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ((head & 0x3f) === 0x3f) {
|
|
221
|
+
r.u32();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return r.u8();
|
|
225
|
+
} catch {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Whether FileAttributes marks the SWF as ActionScript 3; without the bit it is an AVM1 movie. */
|
|
231
|
+
export function isAs3(swf: Swf): boolean {
|
|
232
|
+
const tag = swf.tags.find((t) => t.code === FileAttributes);
|
|
233
|
+
return !!tag && tag.length >= 1 && (swf.bytes[tag.offset] & 0x08) !== 0;
|
|
234
|
+
}
|
package/src/tags.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SWF tag codes, as the specification numbers them. The player reads these
|
|
2
|
+
// first; any other tag is kept by its code.
|
|
3
|
+
export const End = 0;
|
|
4
|
+
export const ShowFrame = 1;
|
|
5
|
+
export const DefineShape = 2;
|
|
6
|
+
export const PlaceObject = 4;
|
|
7
|
+
export const RemoveObject = 5;
|
|
8
|
+
export const DefineBits = 6;
|
|
9
|
+
export const DefineButton = 7;
|
|
10
|
+
export const JPEGTables = 8;
|
|
11
|
+
export const SetBackgroundColor = 9;
|
|
12
|
+
export const DefineFont = 10;
|
|
13
|
+
export const DefineText = 11;
|
|
14
|
+
export const DoAction = 12;
|
|
15
|
+
export const DefineFontInfo = 13;
|
|
16
|
+
export const DefineSound = 14;
|
|
17
|
+
export const StartSound = 15;
|
|
18
|
+
export const DefineButtonSound = 17;
|
|
19
|
+
export const SoundStreamHead = 18;
|
|
20
|
+
export const SoundStreamBlock = 19;
|
|
21
|
+
export const DefineBitsLossless = 20;
|
|
22
|
+
export const DefineBitsJPEG2 = 21;
|
|
23
|
+
export const DefineShape2 = 22;
|
|
24
|
+
export const DefineButtonCxform = 23;
|
|
25
|
+
export const PlaceObject2 = 26;
|
|
26
|
+
export const RemoveObject2 = 28;
|
|
27
|
+
export const DefineShape3 = 32;
|
|
28
|
+
export const DefineText2 = 33;
|
|
29
|
+
export const DefineButton2 = 34;
|
|
30
|
+
export const DefineBitsJPEG3 = 35;
|
|
31
|
+
export const DefineBitsLossless2 = 36;
|
|
32
|
+
export const DefineEditText = 37;
|
|
33
|
+
export const DefineSprite = 39;
|
|
34
|
+
export const FrameLabel = 43;
|
|
35
|
+
export const SoundStreamHead2 = 45;
|
|
36
|
+
export const DefineMorphShape = 46;
|
|
37
|
+
export const DefineFont2 = 48;
|
|
38
|
+
export const ExportAssets = 56;
|
|
39
|
+
export const ScriptLimits = 65;
|
|
40
|
+
export const DoInitAction = 59;
|
|
41
|
+
export const DefineFontInfo2 = 62;
|
|
42
|
+
export const FileAttributes = 69;
|
|
43
|
+
export const PlaceObject3 = 70;
|
|
44
|
+
export const DoABC = 72;
|
|
45
|
+
export const DefineFontAlignZones = 73;
|
|
46
|
+
export const CSMTextSettings = 74;
|
|
47
|
+
export const DefineFont3 = 75;
|
|
48
|
+
export const SymbolClass = 76;
|
|
49
|
+
export const DefineScalingGrid = 78;
|
|
50
|
+
export const DoABC2 = 82;
|
|
51
|
+
export const DefineShape4 = 83;
|
|
52
|
+
export const DefineMorphShape2 = 84;
|
|
53
|
+
export const DefineSceneAndFrameLabelData = 86;
|
|
54
|
+
export const DefineBinaryData = 87;
|
|
55
|
+
export const StartSound2 = 89;
|
|
56
|
+
export const DefineBitsJPEG4 = 90;
|
|
57
|
+
export const DefineFont4 = 91;
|
|
58
|
+
export const PlaceObject4 = 94;
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// The text characters a SWF places on its timeline: editable text
|
|
2
|
+
// (DefineEditText) and static text (DefineText, DefineText2).
|
|
3
|
+
import { readString, utf8 } from "./display.js";
|
|
4
|
+
import { type Matrix, readMatrix } from "./shape.js";
|
|
5
|
+
import { type Rect, SwfReader, type Tag } from "./swf.js";
|
|
6
|
+
import { DefineText2 } from "./tags.js";
|
|
7
|
+
|
|
8
|
+
export interface EditText {
|
|
9
|
+
id: number;
|
|
10
|
+
bounds: Rect;
|
|
11
|
+
variable: string;
|
|
12
|
+
text: string;
|
|
13
|
+
color: number | null;
|
|
14
|
+
fontId: number | null;
|
|
15
|
+
fontClass: string | null;
|
|
16
|
+
fontHeight: number | null;
|
|
17
|
+
align: number;
|
|
18
|
+
leftMargin: number;
|
|
19
|
+
rightMargin: number;
|
|
20
|
+
indent: number;
|
|
21
|
+
leading: number;
|
|
22
|
+
maxLength: number | null;
|
|
23
|
+
wordWrap: boolean;
|
|
24
|
+
multiline: boolean;
|
|
25
|
+
password: boolean;
|
|
26
|
+
readOnly: boolean;
|
|
27
|
+
autoSize: boolean;
|
|
28
|
+
selectable: boolean;
|
|
29
|
+
border: boolean;
|
|
30
|
+
html: boolean;
|
|
31
|
+
useOutlines: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** DefineEditText (tag 37), including every conditional field before its variable and text. */
|
|
35
|
+
export function readEditText(bytes: Uint8Array, tag: Tag): EditText {
|
|
36
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
37
|
+
const id = r.u16();
|
|
38
|
+
const bounds = r.rect();
|
|
39
|
+
const flags = r.u8();
|
|
40
|
+
const flags2 = r.u8();
|
|
41
|
+
const hasText = (flags & 0x80) !== 0;
|
|
42
|
+
const hasFont = (flags & 0x01) !== 0;
|
|
43
|
+
const hasFontClass = (flags2 & 0x80) !== 0;
|
|
44
|
+
const fontId = hasFont ? r.u16() : null;
|
|
45
|
+
const fontClass = hasFontClass ? readString(r) : null;
|
|
46
|
+
const fontHeight = hasFont || hasFontClass ? r.u16() : null;
|
|
47
|
+
const color = flags & 0x04 ? r.u32() : null;
|
|
48
|
+
const maxLength = flags & 0x02 ? r.u16() : null;
|
|
49
|
+
let align = 0;
|
|
50
|
+
let leftMargin = 0;
|
|
51
|
+
let rightMargin = 0;
|
|
52
|
+
let indent = 0;
|
|
53
|
+
let leading = 0;
|
|
54
|
+
if (flags2 & 0x20) {
|
|
55
|
+
align = r.u8();
|
|
56
|
+
leftMargin = r.u16();
|
|
57
|
+
rightMargin = r.u16();
|
|
58
|
+
// Signed, as Flash reads it, though the specification says UI16: a hanging indent is negative.
|
|
59
|
+
indent = r.s16();
|
|
60
|
+
leading = r.s16();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
bounds,
|
|
66
|
+
variable: readString(r),
|
|
67
|
+
text: hasText ? readString(r) : "",
|
|
68
|
+
color,
|
|
69
|
+
fontId,
|
|
70
|
+
fontClass,
|
|
71
|
+
fontHeight,
|
|
72
|
+
align,
|
|
73
|
+
leftMargin,
|
|
74
|
+
rightMargin,
|
|
75
|
+
indent,
|
|
76
|
+
leading,
|
|
77
|
+
maxLength,
|
|
78
|
+
wordWrap: (flags & 0x40) !== 0,
|
|
79
|
+
multiline: (flags & 0x20) !== 0,
|
|
80
|
+
password: (flags & 0x10) !== 0,
|
|
81
|
+
readOnly: (flags & 0x08) !== 0,
|
|
82
|
+
autoSize: (flags2 & 0x40) !== 0,
|
|
83
|
+
selectable: (flags2 & 0x10) === 0,
|
|
84
|
+
border: (flags2 & 0x08) !== 0,
|
|
85
|
+
html: (flags2 & 0x02) !== 0,
|
|
86
|
+
useOutlines: (flags2 & 0x01) !== 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A DefineFont2 or DefineFont3's name and style: what a field in it shows its text in, the glyphs left. */
|
|
91
|
+
export interface FontName {
|
|
92
|
+
id: number;
|
|
93
|
+
name: string;
|
|
94
|
+
bold: boolean;
|
|
95
|
+
italic: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** DefineFont2 (48) or DefineFont3 (75): the id, flags, language, then the name, a length-prefixed string. */
|
|
99
|
+
export function readFontName(bytes: Uint8Array, tag: Tag): FontName {
|
|
100
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
101
|
+
const id = r.u16();
|
|
102
|
+
const flags = r.u8();
|
|
103
|
+
r.u8();
|
|
104
|
+
const length = r.u8();
|
|
105
|
+
const start = r.pos;
|
|
106
|
+
let end = Math.min(start + length, r.end);
|
|
107
|
+
// A trailing NUL, which some tools write, is no part of the name.
|
|
108
|
+
while (end > start && bytes[end - 1] === 0) {
|
|
109
|
+
end--;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const name = utf8(bytes.subarray(start, end));
|
|
113
|
+
return { id, name, bold: (flags & 0x01) !== 0, italic: (flags & 0x02) !== 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A run of DefineText's glyphs: what its record sets, null where it keeps the run before's. */
|
|
117
|
+
export interface TextRecord {
|
|
118
|
+
font: number | null;
|
|
119
|
+
/** 0xAARRGGBB; DefineText's opaque. */
|
|
120
|
+
color: number | null;
|
|
121
|
+
/** The pen's place, in twips. */
|
|
122
|
+
x: number | null;
|
|
123
|
+
y: number | null;
|
|
124
|
+
/** The glyphs' height, in twips; set with the font. */
|
|
125
|
+
height: number | null;
|
|
126
|
+
/** Each glyph's index into the font's glyphs, and the pen's advance after it, in twips. */
|
|
127
|
+
glyphs: { index: number; advance: number }[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** DefineText or DefineText2: static text, glyphs of embedded fonts placed by the authoring tool. */
|
|
131
|
+
export interface StaticText {
|
|
132
|
+
id: number;
|
|
133
|
+
bounds: Rect;
|
|
134
|
+
matrix: Matrix;
|
|
135
|
+
records: TextRecord[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** DefineText (11) or DefineText2 (33, whose colours have alpha), up to its end record or the tag's end. */
|
|
139
|
+
export function readStaticText(bytes: Uint8Array, tag: Tag): StaticText {
|
|
140
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
141
|
+
const alpha = tag.code === DefineText2;
|
|
142
|
+
const id = r.u16();
|
|
143
|
+
const bounds = r.rect();
|
|
144
|
+
const matrix = readMatrix(r);
|
|
145
|
+
const glyphBits = r.u8();
|
|
146
|
+
const advanceBits = r.u8();
|
|
147
|
+
const records: TextRecord[] = [];
|
|
148
|
+
for (;;) {
|
|
149
|
+
const flags = r.u8();
|
|
150
|
+
if (flags === 0 || r.overrun) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const font = flags & 0x08 ? r.u16() : null;
|
|
155
|
+
let color: number | null = null;
|
|
156
|
+
if (flags & 0x04) {
|
|
157
|
+
const red = r.u8();
|
|
158
|
+
const green = r.u8();
|
|
159
|
+
const blue = r.u8();
|
|
160
|
+
const a = alpha ? r.u8() : 255;
|
|
161
|
+
color = ((a << 24) | (red << 16) | (green << 8) | blue) >>> 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const x = flags & 0x01 ? r.s16() : null;
|
|
165
|
+
const y = flags & 0x02 ? r.s16() : null;
|
|
166
|
+
const height = font !== null ? r.u16() : null;
|
|
167
|
+
const count = r.u8();
|
|
168
|
+
const glyphs: { index: number; advance: number }[] = [];
|
|
169
|
+
for (let i = 0; i < count && !r.overrun; i++) {
|
|
170
|
+
glyphs.push({ index: r.ub(glyphBits), advance: r.sb(advanceBits) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A record the tag's end cuts off is left out, not read as zeros.
|
|
174
|
+
if (r.overrun) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
r.align();
|
|
179
|
+
records.push({ font, color, x, y, height, glyphs });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { id, bounds, matrix, records };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** CSMTextSettings (74): how a text character is anti-aliased, as TextField's settings of the same names. */
|
|
186
|
+
export interface CsmTextSettings {
|
|
187
|
+
id: number;
|
|
188
|
+
/** Advanced anti-aliasing (Saffron) rather than normal. */
|
|
189
|
+
useFlashType: boolean;
|
|
190
|
+
/** 0 none, 1 pixel, 2 subpixel. */
|
|
191
|
+
gridFit: number;
|
|
192
|
+
thickness: number;
|
|
193
|
+
sharpness: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function readCsmTextSettings(bytes: Uint8Array, tag: Tag): CsmTextSettings {
|
|
197
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
198
|
+
const id = r.u16();
|
|
199
|
+
const flags = r.u8();
|
|
200
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
201
|
+
const float = () => {
|
|
202
|
+
const at = r.pos;
|
|
203
|
+
r.pos += 4;
|
|
204
|
+
return at + 4 <= r.end ? view.getFloat32(at, true) : 0;
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
id,
|
|
208
|
+
useFlashType: flags >> 6 === 1,
|
|
209
|
+
gridFit: (flags >> 3) & 7,
|
|
210
|
+
thickness: float(),
|
|
211
|
+
sharpness: float(),
|
|
212
|
+
};
|
|
213
|
+
}
|