@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/dist/swfDocument.js
CHANGED
|
@@ -4,17 +4,18 @@ import { createClipRegionFromContours, createClipRegionFromPath } from '@flighth
|
|
|
4
4
|
import { getDecompressor } from '@flighthq/compression/contract';
|
|
5
5
|
import { createMatrix, inverseMatrix, matrixTransformPointXY, multiplyMatrix } from '@flighthq/geometry/contract';
|
|
6
6
|
import { createEmbeddedImageResourceReference } from '@flighthq/image/contract';
|
|
7
|
+
import { reportImportDiagnostic } from '@flighthq/importdiagnostics/contract';
|
|
7
8
|
import { addMovieClipFrameScript, createMovieClip, setMovieClipSource } from '@flighthq/movieclip/contract';
|
|
8
9
|
import { addNodeChild, addNodeOrderListEntry, applyNodeOrderList, clearNodeOrderList, createNodeOrderList, getNodeRuntime, invalidateNodeAppearance, invalidateNodeLocalBounds, removeNodeChild, setNodeColorAdjustments, setNodeLocalMatrix, } from '@flighthq/node/contract';
|
|
9
10
|
import { createScene2DDocument, createScene2DSlotReference, registerScene2DDocumentImporter, } from '@flighthq/scene2d-resources/contract';
|
|
10
11
|
import { createDisplayObject, createSprite, setNode2DClip } from '@flighthq/scene2d/contract';
|
|
11
12
|
import { copyShapeCommands, createScale9Shape, createShape, getShapeFillRegions, setMorphShapeProgress, } from '@flighthq/shape/contract';
|
|
12
|
-
import { createSampler, createTexture
|
|
13
|
-
import { AdvancedBlendMode, BlendMode, Compression, MorphShapeKind, TimelineAudioCueKind, TimelineStreamAudioCueKind, } from '@flighthq/types/contract';
|
|
14
|
-
import { createSwfLosslessBitmap } from './swfBitmap';
|
|
13
|
+
import { createSampler, createTexture } from '@flighthq/texture/contract';
|
|
14
|
+
import { AdvancedBlendMode, BlendMode, Compression, CompressionFraming, ImportDiagnosticSeverity, MorphShapeKind, TimelineAudioCueKind, TimelineStreamAudioCueKind, } from '@flighthq/types/contract';
|
|
15
15
|
import { readSwfEditTextFactory } from './swfEditText';
|
|
16
16
|
import { readSwfFilterList } from './swfFilter';
|
|
17
17
|
import { readSwfAbcFrameScripts, readSwfFrameActions } from './swfFrameAction';
|
|
18
|
+
import { SWF_LOSSLESS_ALPHA_MIME_TYPE, SWF_LOSSLESS_MIME_TYPE } from './swfImageDecoder';
|
|
18
19
|
import { createSwfMorphShape } from './swfMorphShape';
|
|
19
20
|
import { SwfReader } from './swfReader';
|
|
20
21
|
import { createSwfShape } from './swfShape';
|
|
@@ -22,23 +23,23 @@ import { createSwfTextShape, readSwfFontGlyphOutlineSource } from './swfText';
|
|
|
22
23
|
// Recovers every embedded DefineFont/2/3 as the generic, glyph-index-keyed outline seam. The map key
|
|
23
24
|
// is the SWF character id used by DefineText and DefineEditText. This is a separate parse entry from
|
|
24
25
|
// Scene2D construction so callers that only need embedded fonts do not have to retain a document.
|
|
25
|
-
export function createGlyphOutlineSourcesFromSwf(source) {
|
|
26
|
-
const file = readSwfFile(source);
|
|
26
|
+
export function createGlyphOutlineSourcesFromSwf(source, diagnostics) {
|
|
27
|
+
const file = readSwfFile(source, diagnostics);
|
|
27
28
|
return file === null ? null : new Map(file.parsed.fontOutlineSources);
|
|
28
29
|
}
|
|
29
30
|
// The document alone, for a caller that wants the graph and nothing else — the importer registry among
|
|
30
31
|
// them. A file whose placements carry an advanced blend or a filter list still imports fully here; what
|
|
31
32
|
// it loses is the report of them, which is what createScene2DImportFromSwf returns.
|
|
32
|
-
export function createScene2DFromSwf(source) {
|
|
33
|
-
return createScene2DImportFromSwf(source)?.document ?? null;
|
|
33
|
+
export function createScene2DFromSwf(source, diagnostics) {
|
|
34
|
+
return createScene2DImportFromSwf(source, diagnostics)?.document ?? null;
|
|
34
35
|
}
|
|
35
36
|
// The full import: the document, plus the placement appearance no node can carry. SWF puts a blend mode
|
|
36
37
|
// and a filter list on the same record as the matrix, and Flight expresses neither on a node — an
|
|
37
38
|
// advanced blend needs a BlendEffect and an effect is a descriptor a caller runs explicitly, since
|
|
38
39
|
// `displayObject.filters` is an anti-goal. Both therefore travel beside the document instead of being
|
|
39
40
|
// dropped at the seam or silently flattened onto a node.
|
|
40
|
-
export function createScene2DImportFromSwf(source) {
|
|
41
|
-
const file = readSwfFile(source);
|
|
41
|
+
export function createScene2DImportFromSwf(source, diagnostics) {
|
|
42
|
+
const file = readSwfFile(source, diagnostics);
|
|
42
43
|
if (file === null)
|
|
43
44
|
return null;
|
|
44
45
|
const { frameRate, parsed, stageBounds } = file;
|
|
@@ -46,17 +47,21 @@ export function createScene2DImportFromSwf(source) {
|
|
|
46
47
|
const instantiation = {
|
|
47
48
|
activeSymbols: new Set(),
|
|
48
49
|
appearances: [],
|
|
50
|
+
diagnostics,
|
|
49
51
|
frameRate: frameRate > 0 ? frameRate : null,
|
|
50
52
|
resolvingBounds: new Set(),
|
|
51
53
|
resolvedBounds: new Map(),
|
|
52
54
|
};
|
|
53
55
|
const root = createSwfTimelineNode(parsed.timeline, stageBounds, parsed, slots, instantiation, 0);
|
|
54
|
-
if (root === null)
|
|
56
|
+
if (root === null) {
|
|
57
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.timeline-instantiation-failed', 'createScene2DImportFromSwf');
|
|
55
58
|
return null;
|
|
56
|
-
|
|
59
|
+
}
|
|
60
|
+
const imageResources = createSwfImageResources(parsed);
|
|
57
61
|
return {
|
|
58
62
|
appearances: instantiation.appearances,
|
|
59
|
-
document: createScene2DDocument(root, slots, 'swf', parsed.backgroundColor,
|
|
63
|
+
document: createScene2DDocument(root, slots, 'swf', parsed.backgroundColor, imageResources.resources, createSwfAudioResources(parsed)),
|
|
64
|
+
jpegAlphaPayloads: createSwfJpegAlphaPayloads(parsed, imageResources.references),
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
// Instantiates a symbol the file exported by linkage name but never placed on a timeline. A library
|
|
@@ -70,8 +75,8 @@ export function createScene2DImportFromSwf(source) {
|
|
|
70
75
|
// Anything less would hand back artwork whose bitmaps could never be paired with their pixels, since
|
|
71
76
|
// each call parses afresh and its Textures are its own. `backgroundColor` stays null: the stage colour
|
|
72
77
|
// belongs to the stage, and a symbol instantiated into someone else's scene is not it.
|
|
73
|
-
export function createScene2DSymbolFromSwf(source, linkageName) {
|
|
74
|
-
const file = readSwfFile(source);
|
|
78
|
+
export function createScene2DSymbolFromSwf(source, linkageName, diagnostics) {
|
|
79
|
+
const file = readSwfFile(source, diagnostics);
|
|
75
80
|
if (file === null)
|
|
76
81
|
return null;
|
|
77
82
|
const { frameRate, parsed } = file;
|
|
@@ -80,12 +85,19 @@ export function createScene2DSymbolFromSwf(source, linkageName) {
|
|
|
80
85
|
if (name === linkageName)
|
|
81
86
|
characterId = id;
|
|
82
87
|
}
|
|
83
|
-
|
|
88
|
+
// A linkage name nothing exports is the caller naming a symbol this file does not have — worth telling
|
|
89
|
+
// them apart from a file that failed to parse, since both come back null.
|
|
90
|
+
if (characterId < 0) {
|
|
91
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.unknown-linkage-name', 'createScene2DSymbolFromSwf', {
|
|
92
|
+
linkageName,
|
|
93
|
+
});
|
|
84
94
|
return null;
|
|
95
|
+
}
|
|
85
96
|
const slots = [];
|
|
86
97
|
const instantiation = {
|
|
87
98
|
activeSymbols: new Set(),
|
|
88
99
|
appearances: [],
|
|
100
|
+
diagnostics,
|
|
89
101
|
frameRate: frameRate > 0 ? frameRate : null,
|
|
90
102
|
resolvingBounds: new Set(),
|
|
91
103
|
resolvedBounds: new Map(),
|
|
@@ -93,8 +105,7 @@ export function createScene2DSymbolFromSwf(source, linkageName) {
|
|
|
93
105
|
const root = createSwfSymbolNode(parsed, characterId, slots, instantiation);
|
|
94
106
|
if (root === null)
|
|
95
107
|
return null;
|
|
96
|
-
|
|
97
|
-
return createScene2DDocument(root, slots, 'swf', null, createSwfImageResources(parsed), createSwfAudioResources(parsed));
|
|
108
|
+
return createScene2DDocument(root, slots, 'swf', null, createSwfImageResources(parsed).resources, createSwfAudioResources(parsed));
|
|
98
109
|
}
|
|
99
110
|
// Every linkage name the file exported, whether or not the symbol was ever placed. Pair with
|
|
100
111
|
// `createScene2DSymbolFromSwf` to instantiate one.
|
|
@@ -105,58 +116,51 @@ export function readSwfExportedSymbolNames(source) {
|
|
|
105
116
|
export function registerSwfScene2DDocumentImporter(registry) {
|
|
106
117
|
registerScene2DDocumentImporter(registry, 'swf', matchesSwfDocument, (source) => createScene2DFromSwf(source));
|
|
107
118
|
}
|
|
108
|
-
function readSwfFile(source) {
|
|
109
|
-
const uncompressed = uncompressSwfSource(source);
|
|
119
|
+
function readSwfFile(source, diagnostics) {
|
|
120
|
+
const uncompressed = uncompressSwfSource(source, diagnostics);
|
|
110
121
|
if (uncompressed === null)
|
|
111
122
|
return null;
|
|
123
|
+
// Every rejection below loses the WHOLE document, and each has a distinct cause. Without a report per
|
|
124
|
+
// cause the caller receives one null from `createScene2DFromSwf` and cannot tell an unregistered
|
|
125
|
+
// decompressor — which IS reported, upstream — from a truncated header, which was not.
|
|
112
126
|
const header = new SwfReader(uncompressed, 0, uncompressed.length);
|
|
113
127
|
const signature = header.readUint8();
|
|
114
128
|
if (signature !== FWS_SIGNATURE || header.readUint8() !== W_SIGNATURE || header.readUint8() !== S_SIGNATURE) {
|
|
129
|
+
// Distinct from the container's `swf.invalid-signature`: that one reads the ORIGINAL bytes, this one
|
|
130
|
+
// reads the decompressed body, whose header the decompressor rewrote. NOTHING CAN REACH THIS TODAY —
|
|
131
|
+
// an FWS container is returned unchanged and every compressed path writes `FWS_SIGNATURE` into byte 0,
|
|
132
|
+
// while bytes 1 and 2 were validated before either. The report stays because unreachable-by-
|
|
133
|
+
// construction is a property of today's two container paths, not of the format: a third path, or a
|
|
134
|
+
// decompressor that stops rewriting the header, makes it reachable, and silence would return with it.
|
|
135
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.uncompressed-signature-invalid', 'readSwfFile');
|
|
115
136
|
return null;
|
|
116
137
|
}
|
|
117
138
|
const version = header.readUint8();
|
|
118
139
|
const fileLength = header.readUint32();
|
|
119
|
-
if (!header.valid || version === 0 || fileLength < MIN_SWF_LENGTH || fileLength > uncompressed.length)
|
|
140
|
+
if (!header.valid || version === 0 || fileLength < MIN_SWF_LENGTH || fileLength > uncompressed.length) {
|
|
141
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.header-fields-invalid', 'readSwfFile', {
|
|
142
|
+
declaredLength: fileLength,
|
|
143
|
+
available: uncompressed.length,
|
|
144
|
+
version,
|
|
145
|
+
});
|
|
120
146
|
return null;
|
|
147
|
+
}
|
|
121
148
|
const body = new SwfReader(uncompressed, SWF_PREFIX_LENGTH, fileLength);
|
|
122
149
|
const stageBounds = readSwfRectangle(body);
|
|
123
|
-
if (stageBounds === null)
|
|
150
|
+
if (stageBounds === null) {
|
|
151
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.stage-bounds-unreadable', 'readSwfFile');
|
|
124
152
|
return null;
|
|
153
|
+
}
|
|
125
154
|
// Header FrameRate is 8.8 fixed and governs every timeline in the file; the authored FrameCount that
|
|
126
155
|
// follows it is advisory, so the real root frame count comes from the ShowFrame tags themselves.
|
|
127
156
|
const frameRate = body.readUint16() / FIXED_8_8_ONE;
|
|
128
157
|
body.readUint16();
|
|
129
|
-
if (!body.valid)
|
|
158
|
+
if (!body.valid) {
|
|
159
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.header-truncated', 'readSwfFile');
|
|
130
160
|
return null;
|
|
131
|
-
const parsed = readSwfTags(body);
|
|
132
|
-
return parsed === null ? null : { frameRate, parsed, stageBounds };
|
|
133
|
-
}
|
|
134
|
-
// Fills in the pixels this package can unpack on its own. A lossless definition is not an image file — it
|
|
135
|
-
// is a raw raster plus zlib — so no generic decoder reaches it, but both halves needed to read it are
|
|
136
|
-
// already here: the shared decompressor the caller registered, and the layout knowledge in swfBitmap. Its
|
|
137
|
-
// pixels therefore resolve at import, and it earns no image resource because there is nothing left to
|
|
138
|
-
// load. The encoded formats leave as references and decode through @flighthq/image later.
|
|
139
|
-
//
|
|
140
|
-
// Only characters something actually samples are unpacked, so a library full of unused bitmaps costs
|
|
141
|
-
// nothing. That is the whole reason this walks the texture map rather than the payload map.
|
|
142
|
-
function fillSwfLosslessBitmapTextures(parsed) {
|
|
143
|
-
for (const [characterId, variants] of parsed.imageTextures) {
|
|
144
|
-
if (!isSwfLosslessImage(parsed.images.get(characterId)))
|
|
145
|
-
continue;
|
|
146
|
-
const image = parsed.images.get(characterId);
|
|
147
|
-
if (image === undefined)
|
|
148
|
-
continue;
|
|
149
|
-
const bitmap = createSwfLosslessBitmap(image.bytes, image.mimeType === SWF_LOSSLESS_ALPHA_MIME_TYPE);
|
|
150
|
-
if (bitmap === null)
|
|
151
|
-
continue;
|
|
152
|
-
for (const texture of variants.values())
|
|
153
|
-
setTextureSource(texture, bitmap);
|
|
154
161
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (image === undefined)
|
|
158
|
-
return false;
|
|
159
|
-
return image.mimeType === SWF_LOSSLESS_MIME_TYPE || image.mimeType === SWF_LOSSLESS_ALPHA_MIME_TYPE;
|
|
162
|
+
const parsed = readSwfTags(body, diagnostics);
|
|
163
|
+
return parsed === null ? null : { frameRate, parsed, stageBounds };
|
|
160
164
|
}
|
|
161
165
|
// Presents any container form as the uncompressed bytes the rest of the importer reads. `FWS` is already
|
|
162
166
|
// that and is returned as-is, with no copy. `CWS` and `ZWS` compress everything after the 8-byte header,
|
|
@@ -164,32 +168,61 @@ function isSwfLosslessImage(image) {
|
|
|
164
168
|
// to `FWS` — the declared length already counts uncompressed bytes, so it carries over untouched.
|
|
165
169
|
// Compression the caller has not registered a decompressor for is reported as the document's null
|
|
166
170
|
// sentinel, exactly like a malformed file: the bytes are unreadable either way.
|
|
167
|
-
function uncompressSwfSource(source) {
|
|
168
|
-
if (source.length < SWF_PREFIX_LENGTH || source[1] !== W_SIGNATURE || source[2] !== S_SIGNATURE)
|
|
171
|
+
function uncompressSwfSource(source, diagnostics) {
|
|
172
|
+
if (source.length < SWF_PREFIX_LENGTH || source[1] !== W_SIGNATURE || source[2] !== S_SIGNATURE) {
|
|
173
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.invalid-signature', 'uncompressSwfSource', {
|
|
174
|
+
length: source.length,
|
|
175
|
+
});
|
|
169
176
|
return null;
|
|
177
|
+
}
|
|
170
178
|
const signature = source[0];
|
|
171
179
|
if (signature === FWS_SIGNATURE)
|
|
172
180
|
return source;
|
|
173
181
|
const compression = signature === CWS_SIGNATURE ? Compression.Deflate : signature === ZWS_SIGNATURE ? Compression.Lzma : null;
|
|
174
|
-
if (compression === null)
|
|
182
|
+
if (compression === null) {
|
|
183
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.unknown-container', 'uncompressSwfSource', {
|
|
184
|
+
signature,
|
|
185
|
+
});
|
|
175
186
|
return null;
|
|
187
|
+
}
|
|
176
188
|
const decompress = getDecompressor(compression);
|
|
177
|
-
|
|
189
|
+
// Distinct from a malformed body on purpose. Both return the same null sentinel, but a caller that
|
|
190
|
+
// never registered a decompressor has a file it could read after one registration, while a corrupt
|
|
191
|
+
// stream is unreadable however the caller is configured — and only the crumb can tell them apart.
|
|
192
|
+
if (decompress === null) {
|
|
193
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.no-decompressor-registered', 'uncompressSwfSource', { compression });
|
|
178
194
|
return null;
|
|
195
|
+
}
|
|
179
196
|
const header = new SwfReader(source, 0, SWF_PREFIX_LENGTH);
|
|
180
197
|
header.readUint32();
|
|
181
198
|
const fileLength = header.readUint32();
|
|
182
|
-
if (fileLength < MIN_SWF_LENGTH)
|
|
199
|
+
if (fileLength < MIN_SWF_LENGTH) {
|
|
200
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.declared-length-too-small', 'uncompressSwfSource', {
|
|
201
|
+
fileLength,
|
|
202
|
+
});
|
|
183
203
|
return null;
|
|
204
|
+
}
|
|
184
205
|
// LZMA puts a compressed length and the 5 property bytes between the header and its stream; zlib starts
|
|
185
206
|
// its stream immediately. Either way the decompressor receives the stream itself.
|
|
186
207
|
const bodyLength = fileLength - SWF_PREFIX_LENGTH;
|
|
187
208
|
const streamStart = compression === Compression.Lzma ? SWF_LZMA_PREFIX_LENGTH : SWF_PREFIX_LENGTH;
|
|
188
|
-
if (streamStart > source.length)
|
|
209
|
+
if (streamStart > source.length) {
|
|
210
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.truncated-container', 'uncompressSwfSource', {
|
|
211
|
+
length: source.length,
|
|
212
|
+
streamStart,
|
|
213
|
+
});
|
|
189
214
|
return null;
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
}
|
|
216
|
+
const framing = compression === Compression.Deflate ? CompressionFraming.Rfc1950 : CompressionFraming.Raw;
|
|
217
|
+
const body = decompress(source.subarray(streamStart), bodyLength, framing);
|
|
218
|
+
if (body === null || body.length < bodyLength) {
|
|
219
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, 'swf.decompression-failed', 'uncompressSwfSource', {
|
|
220
|
+
compression,
|
|
221
|
+
expected: bodyLength,
|
|
222
|
+
received: body === null ? -1 : body.length,
|
|
223
|
+
});
|
|
192
224
|
return null;
|
|
225
|
+
}
|
|
193
226
|
const uncompressed = new Uint8Array(SWF_PREFIX_LENGTH + bodyLength);
|
|
194
227
|
uncompressed.set(source.subarray(0, SWF_PREFIX_LENGTH));
|
|
195
228
|
uncompressed[0] = FWS_SIGNATURE;
|
|
@@ -229,7 +262,7 @@ function populateSwfTimelineNode(clip, timeline, parsed, slots, state, depth) {
|
|
|
229
262
|
const clips = new Map();
|
|
230
263
|
for (const frame of timeline.frames) {
|
|
231
264
|
const ordered = [...frame.values()].sort(compareSwfPlacementDepth);
|
|
232
|
-
frames.push(buildSwfFrameEntries(ordered, parsed, clips));
|
|
265
|
+
frames.push(buildSwfFrameEntries(ordered, parsed, clips, state.diagnostics));
|
|
233
266
|
for (const placement of ordered) {
|
|
234
267
|
const key = createSwfInstanceKey(placement);
|
|
235
268
|
if (nodes.has(key))
|
|
@@ -268,7 +301,7 @@ function populateSwfTimelineNode(clip, timeline, parsed, slots, state, depth) {
|
|
|
268
301
|
const target = scale9 !== null
|
|
269
302
|
? scale9
|
|
270
303
|
: editText !== undefined
|
|
271
|
-
? createSwfEditTextTarget(editText, parsed, targetBounds)
|
|
304
|
+
? createSwfEditTextTarget(editText, parsed, targetBounds, state.diagnostics)
|
|
272
305
|
: image !== undefined
|
|
273
306
|
? createSwfTexturedSprite(acquireSwfImageTexture(parsed, placement.characterId, false, true), targetBounds)
|
|
274
307
|
: video !== undefined
|
|
@@ -291,7 +324,7 @@ function populateSwfTimelineNode(clip, timeline, parsed, slots, state, depth) {
|
|
|
291
324
|
}
|
|
292
325
|
}
|
|
293
326
|
}
|
|
294
|
-
collectSwfNodeAppearances(frames, nodes, state.appearances);
|
|
327
|
+
collectSwfNodeAppearances(frames, nodes, state.appearances, state.diagnostics);
|
|
295
328
|
setMovieClipSource(clip, createSwfTimelineSource(frames, nodes, timeline.labels, timeline.cues, state.frameRate));
|
|
296
329
|
// Frame scripts attach after the source, so the clip already knows how many frames it has when a
|
|
297
330
|
// recognized command addresses one.
|
|
@@ -307,15 +340,24 @@ function populateSwfTimelineNode(clip, timeline, parsed, slots, state, depth) {
|
|
|
307
340
|
//
|
|
308
341
|
// One entry per (instance, frame) that carries either, so a filter that changes across frames reads as
|
|
309
342
|
// the per-frame data it is. Frames are 1-based, matching gotoAndStopMovieClip.
|
|
310
|
-
function collectSwfNodeAppearances(frames, nodes, out) {
|
|
343
|
+
function collectSwfNodeAppearances(frames, nodes, out, diagnostics) {
|
|
311
344
|
for (let frame = 0; frame < frames.length; frame++) {
|
|
312
345
|
for (const entry of frames[frame]) {
|
|
313
346
|
const { advancedBlendMode, effects } = entry.placement;
|
|
314
347
|
if (advancedBlendMode === null && effects.length === 0)
|
|
315
348
|
continue;
|
|
316
349
|
const node = nodes.get(createSwfInstanceKey(entry.placement));
|
|
317
|
-
if (node === undefined)
|
|
350
|
+
if (node === undefined) {
|
|
351
|
+
// The appearance report is the only carrier for these two channels, so a placement whose node was
|
|
352
|
+
// never allocated loses them outright. Reaching here means the placement declared one or both,
|
|
353
|
+
// which is why the earlier `continue` is not a loss and this one is. A masking placement cannot
|
|
354
|
+
// reach here despite also earning no node: masks are excluded from frame entries upstream.
|
|
355
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Drop, 'swf.appearance-without-node', 'collectSwfNodeAppearances', {
|
|
356
|
+
capability: advancedBlendMode !== null ? 'swf.placement.blend-mode' : 'swf.placement.filter-list',
|
|
357
|
+
frame: frame + 1,
|
|
358
|
+
});
|
|
318
359
|
continue;
|
|
360
|
+
}
|
|
319
361
|
out.push({ advancedBlendMode, effects: [...effects], frame: frame + 1, node });
|
|
320
362
|
}
|
|
321
363
|
}
|
|
@@ -424,28 +466,51 @@ function createSwfTimelineSource(frames, nodes, labels, cues, frameRate) {
|
|
|
424
466
|
// placement with a clip depth covers every depth above its own through that clip depth — while Flight
|
|
425
467
|
// clips a node and its subtree. Applying one region to each covered sibling is equivalent to grouping
|
|
426
468
|
// them under a clipped container, and it leaves the attach/detach/reorder path untouched.
|
|
427
|
-
function buildSwfFrameEntries(ordered, parsed, clips) {
|
|
469
|
+
function buildSwfFrameEntries(ordered, parsed, clips, diagnostics) {
|
|
428
470
|
const entries = [];
|
|
429
471
|
for (const placement of ordered) {
|
|
430
472
|
if (placement.clipDepth > 0)
|
|
431
473
|
continue;
|
|
432
|
-
const mask = resolveSwfPlacementMask(ordered, placement);
|
|
433
|
-
|
|
474
|
+
const mask = resolveSwfPlacementMask(ordered, placement, diagnostics);
|
|
475
|
+
const clip = mask === null ? null : resolveSwfMaskClip(mask, placement, parsed, clips);
|
|
476
|
+
// A mask that resolves to no region leaves its covered instance UNCLIPPED, which is a visible
|
|
477
|
+
// difference and not a no-op: the mask character had no decoded geometry, so imposing nothing was
|
|
478
|
+
// the honest choice over imposing a wrong clip. Recover rather than Drop — the instance still draws.
|
|
479
|
+
if (mask !== null && clip === null) {
|
|
480
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Recover, 'swf.mask-without-geometry', 'buildSwfFrameEntries', {
|
|
481
|
+
capability: 'swf.placement.clip-depth',
|
|
482
|
+
depth: placement.depth,
|
|
483
|
+
maskCharacterId: mask.characterId,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
entries.push({ clip, placement });
|
|
434
487
|
}
|
|
435
488
|
return entries;
|
|
436
489
|
}
|
|
437
490
|
// The innermost mask covering a depth. Flight carries one clip per node, so where masks nest, the
|
|
438
491
|
// deepest one wins rather than intersecting them.
|
|
439
|
-
function resolveSwfPlacementMask(ordered, placement) {
|
|
492
|
+
function resolveSwfPlacementMask(ordered, placement, diagnostics) {
|
|
440
493
|
let mask = null;
|
|
494
|
+
let covering = 0;
|
|
441
495
|
for (const candidate of ordered) {
|
|
442
496
|
if (candidate.clipDepth <= 0 || candidate.depth >= placement.depth)
|
|
443
497
|
continue;
|
|
444
498
|
if (placement.depth > candidate.clipDepth)
|
|
445
499
|
continue;
|
|
500
|
+
covering++;
|
|
446
501
|
if (mask === null || candidate.depth > mask.depth)
|
|
447
502
|
mask = candidate;
|
|
448
503
|
}
|
|
504
|
+
// Two masks over one instance means the outer one is not applied at all, so the instance shows more
|
|
505
|
+
// than the file said it should. Skip rather than Recover: a node carries one clip, so this is a
|
|
506
|
+
// vocabulary gap in the clip subject rather than geometry this decoder failed to read.
|
|
507
|
+
if (covering > 1) {
|
|
508
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'swf.nested-mask-collapsed', 'resolveSwfPlacementMask', {
|
|
509
|
+
capability: 'swf.placement.clip-depth',
|
|
510
|
+
covering,
|
|
511
|
+
depth: placement.depth,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
449
514
|
return mask;
|
|
450
515
|
}
|
|
451
516
|
function resolveSwfMaskClip(mask, placement, parsed, clips) {
|
|
@@ -581,18 +646,40 @@ function createSwfAudioResources(parsed) {
|
|
|
581
646
|
return resources;
|
|
582
647
|
}
|
|
583
648
|
function createSwfImageResources(parsed) {
|
|
649
|
+
const references = new Map();
|
|
584
650
|
const resources = [];
|
|
585
651
|
for (const [characterId, variants] of parsed.imageTextures) {
|
|
586
652
|
const image = parsed.images.get(characterId);
|
|
587
|
-
|
|
588
|
-
// work that does not exist and would fail if anything tried to decode it as an image file.
|
|
589
|
-
if (image === undefined || isSwfLosslessImage(image))
|
|
653
|
+
if (image === undefined)
|
|
590
654
|
continue;
|
|
591
|
-
const
|
|
655
|
+
const alphaType = image.mimeType === SWF_LOSSLESS_ALPHA_MIME_TYPE
|
|
656
|
+
? 'premultiplied'
|
|
657
|
+
: image.mimeType === SWF_LOSSLESS_MIME_TYPE
|
|
658
|
+
? 'opaque'
|
|
659
|
+
: 'straight';
|
|
660
|
+
const reference = createEmbeddedImageResourceReference(image.bytes, image.mimeType, alphaType);
|
|
592
661
|
reference.textures = [...variants.values()];
|
|
662
|
+
references.set(characterId, reference);
|
|
593
663
|
resources.push(reference);
|
|
594
664
|
}
|
|
595
|
-
return resources;
|
|
665
|
+
return { references, resources };
|
|
666
|
+
}
|
|
667
|
+
// Completes the full-import sidecar after document references exist. An unplaced definition still earns
|
|
668
|
+
// a report record and its own zero-texture reference; a placed one reuses the document's exact object.
|
|
669
|
+
function createSwfJpegAlphaPayloads(parsed, references) {
|
|
670
|
+
const payloads = [];
|
|
671
|
+
for (const [characterId, source] of parsed.jpegAlphaPayloads) {
|
|
672
|
+
let reference = references.get(characterId);
|
|
673
|
+
if (reference === undefined) {
|
|
674
|
+
const image = parsed.images.get(characterId);
|
|
675
|
+
if (image === undefined)
|
|
676
|
+
continue;
|
|
677
|
+
reference = createEmbeddedImageResourceReference(image.bytes, image.mimeType);
|
|
678
|
+
references.set(characterId, reference);
|
|
679
|
+
}
|
|
680
|
+
payloads.push({ ...source, reference });
|
|
681
|
+
}
|
|
682
|
+
return payloads;
|
|
596
683
|
}
|
|
597
684
|
function createSwfTexturedSprite(texture, bounds) {
|
|
598
685
|
const target = createSprite();
|
|
@@ -624,8 +711,21 @@ function createSwfMovieClip(bounds) {
|
|
|
624
711
|
// A field becomes its own node per placement, because its text is per-instance state rather than shared
|
|
625
712
|
// artwork. The authored RECT still sizes it, so a field reports the box the tool drew even before any
|
|
626
713
|
// layout has run.
|
|
627
|
-
function createSwfEditTextTarget(create, parsed, bounds) {
|
|
628
|
-
|
|
714
|
+
function createSwfEditTextTarget(create, parsed, bounds, diagnostics) {
|
|
715
|
+
// The field survives with its size, box and colour and simply has no font family, which is the
|
|
716
|
+
// diminished case: it exists, it is smaller than authored, and nothing else says so. Reported once per
|
|
717
|
+
// unresolved id rather than per call, since the resolver runs for every run of text.
|
|
718
|
+
const unresolved = new Set();
|
|
719
|
+
const node = create((fontId) => {
|
|
720
|
+
const name = parsed.fontNames.get(fontId);
|
|
721
|
+
if (name !== undefined)
|
|
722
|
+
return name;
|
|
723
|
+
if (!unresolved.has(fontId)) {
|
|
724
|
+
unresolved.add(fontId);
|
|
725
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Drop, 'swf.edit-text-font-name-unresolved', 'createSwfEditTextTarget', { capability: 'swf.text.define-edit-text', fontId });
|
|
726
|
+
}
|
|
727
|
+
return '';
|
|
728
|
+
});
|
|
629
729
|
if (bounds !== null) {
|
|
630
730
|
node.data.authoredBounds = { ...bounds };
|
|
631
731
|
getNodeRuntime(node).computeLocalBoundsRectangle = computeSwfLocalBoundsRectangle;
|
|
@@ -726,7 +826,7 @@ function createSwfSymbolNode(parsed, characterId, slots, state) {
|
|
|
726
826
|
const bounds = resolveSwfCharacterBounds(parsed, characterId, state, 0);
|
|
727
827
|
const editText = parsed.editTexts.get(characterId);
|
|
728
828
|
if (editText !== undefined)
|
|
729
|
-
return createSwfEditTextTarget(editText, parsed, bounds);
|
|
829
|
+
return createSwfEditTextTarget(editText, parsed, bounds, state.diagnostics);
|
|
730
830
|
if (parsed.images.has(characterId)) {
|
|
731
831
|
return createSwfTexturedSprite(acquireSwfImageTexture(parsed, characterId, false, true), bounds);
|
|
732
832
|
}
|
|
@@ -759,16 +859,29 @@ function resolveSwfCharacterBounds(parsed, characterId, state, depth) {
|
|
|
759
859
|
// A symbol's authored extent covers everything it can show, so this unions every frame's placements
|
|
760
860
|
// rather than only the first frame's: the node's local bounds do not change as its playhead moves.
|
|
761
861
|
let bounds = null;
|
|
862
|
+
let missingChildren = 0;
|
|
762
863
|
for (const frame of sprite.frames) {
|
|
763
864
|
for (const placement of frame.values()) {
|
|
764
865
|
const childBounds = resolveSwfCharacterBounds(parsed, placement.characterId, state, depth + 1);
|
|
765
|
-
if (childBounds === null)
|
|
866
|
+
if (childBounds === null) {
|
|
867
|
+
// Two different things reach here and only one is a loss. A sprite that resolved to an empty
|
|
868
|
+
// union has no extent to contribute and is correct; a character with neither authored bounds nor
|
|
869
|
+
// a sprite body was never imported at all, and the union it should have widened is short.
|
|
870
|
+
if (!parsed.characterBounds.has(placement.characterId) && !parsed.sprites.has(placement.characterId)) {
|
|
871
|
+
missingChildren++;
|
|
872
|
+
}
|
|
766
873
|
continue;
|
|
874
|
+
}
|
|
767
875
|
const transformed = transformSwfRectangle(childBounds, placement.matrix);
|
|
768
876
|
bounds = bounds === null ? transformed : mergeSwfRectangles(bounds, transformed);
|
|
769
877
|
}
|
|
770
878
|
}
|
|
771
879
|
state.resolvingBounds.delete(characterId);
|
|
880
|
+
if (missingChildren > 0) {
|
|
881
|
+
// A count rather than a flag: the box survives and is simply smaller than the sprite's contents, and
|
|
882
|
+
// a count is the only thing that separates a full union from a short one.
|
|
883
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.sprite-bounds-short', 'resolveSwfCharacterBounds', { capability: 'swf.timeline.define-sprite', characterId, missingChildren });
|
|
884
|
+
}
|
|
772
885
|
state.resolvedBounds.set(characterId, bounds);
|
|
773
886
|
return bounds;
|
|
774
887
|
}
|
|
@@ -787,7 +900,7 @@ function transformSwfRectangle(bounds, matrix) {
|
|
|
787
900
|
const maxY = Math.max(y0, y1, y2, y3);
|
|
788
901
|
return { height: maxY - y, width: maxX - x, x, y };
|
|
789
902
|
}
|
|
790
|
-
function readPlaceObject(body, placements, hasExtendedFlags) {
|
|
903
|
+
function readPlaceObject(body, placements, hasExtendedFlags, diagnostics) {
|
|
791
904
|
const flags = body.readUint8();
|
|
792
905
|
const extendedFlags = hasExtendedFlags ? body.readUint8() : 0;
|
|
793
906
|
const depth = body.readUint16();
|
|
@@ -811,8 +924,15 @@ function readPlaceObject(body, placements, hasExtendedFlags) {
|
|
|
811
924
|
const hasFilterList = (extendedFlags & 0x01) !== 0;
|
|
812
925
|
const readEffects = [];
|
|
813
926
|
const readFilterAdjustments = [];
|
|
814
|
-
const filterListComplete = !hasFilterList || readSwfFilterList(body, readEffects, readFilterAdjustments);
|
|
815
|
-
const
|
|
927
|
+
const filterListComplete = !hasFilterList || readSwfFilterList(body, readEffects, readFilterAdjustments, diagnostics);
|
|
928
|
+
const declaresBlendMode = (extendedFlags & 0x02) !== 0;
|
|
929
|
+
const hasBlendMode = declaresBlendMode && filterListComplete;
|
|
930
|
+
if (declaresBlendMode && !filterListComplete) {
|
|
931
|
+
// The record declared a blend mode and the byte carrying it is unreachable behind a list that did
|
|
932
|
+
// not finish. The placement keeps the filters read so far, so nothing about the result says a
|
|
933
|
+
// declared channel went unread.
|
|
934
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Drop, 'swf.blend-mode-behind-unread-filters', 'readPlaceObject', { capability: 'swf.placement.blend-mode' });
|
|
935
|
+
}
|
|
816
936
|
const blendModeValue = hasBlendMode ? body.readUint8() : 0;
|
|
817
937
|
if (!body.valid || (isMove && existing === undefined) || (characterId === 0 && directLinkage === null))
|
|
818
938
|
return;
|
|
@@ -1006,10 +1126,11 @@ function readSwfRectangle(reader) {
|
|
|
1006
1126
|
y: yMin / TWIPS_PER_PIXEL,
|
|
1007
1127
|
};
|
|
1008
1128
|
}
|
|
1009
|
-
function readSwfTags(reader) {
|
|
1129
|
+
function readSwfTags(reader, diagnostics) {
|
|
1010
1130
|
const state = {
|
|
1011
1131
|
abcBlobs: [],
|
|
1012
1132
|
backgroundColor: null,
|
|
1133
|
+
diagnostics,
|
|
1013
1134
|
pendingInitActions: [],
|
|
1014
1135
|
editTexts: new Map(),
|
|
1015
1136
|
fontNames: new Map(),
|
|
@@ -1019,6 +1140,7 @@ function readSwfTags(reader) {
|
|
|
1019
1140
|
fontOutlineSources: new Map(),
|
|
1020
1141
|
images: new Map(),
|
|
1021
1142
|
imageTextures: new Map(),
|
|
1143
|
+
jpegAlphaPayloads: new Map(),
|
|
1022
1144
|
jpegTables: null,
|
|
1023
1145
|
pendingTexts: [],
|
|
1024
1146
|
linkages: new Map(),
|
|
@@ -1057,6 +1179,7 @@ function readSwfTags(reader) {
|
|
|
1057
1179
|
fontOutlineSources: state.fontOutlineSources,
|
|
1058
1180
|
images: state.images,
|
|
1059
1181
|
imageTextures: state.imageTextures,
|
|
1182
|
+
jpegAlphaPayloads: state.jpegAlphaPayloads,
|
|
1060
1183
|
linkages: state.linkages,
|
|
1061
1184
|
morphBounds: state.morphBounds,
|
|
1062
1185
|
morphShapes: state.morphShapes,
|
|
@@ -1098,9 +1221,11 @@ function appendSwfAbcFrameScripts(state, timeline) {
|
|
|
1098
1221
|
for (const [characterId, className] of state.linkages)
|
|
1099
1222
|
charactersByClass.set(className, characterId);
|
|
1100
1223
|
for (const blob of state.abcBlobs) {
|
|
1101
|
-
const byClass = readSwfAbcFrameScripts(blob);
|
|
1102
|
-
if (byClass === null)
|
|
1224
|
+
const byClass = readSwfAbcFrameScripts(blob.bytes, state.diagnostics);
|
|
1225
|
+
if (byClass === null) {
|
|
1226
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.abc-frame-scripts-unreadable', 'appendSwfAbcFrameScripts', { capability: blob.named ? 'swf.script.do-abc' : 'swf.script.do-abc-anonymous' });
|
|
1103
1227
|
continue;
|
|
1228
|
+
}
|
|
1104
1229
|
for (const [className, frames] of byClass) {
|
|
1105
1230
|
const characterId = charactersByClass.get(className);
|
|
1106
1231
|
if (characterId === undefined)
|
|
@@ -1116,8 +1241,17 @@ function appendSwfAbcFrameScripts(state, timeline) {
|
|
|
1116
1241
|
function appendSwfPendingTextShapes(reader, state) {
|
|
1117
1242
|
for (const pending of state.pendingTexts) {
|
|
1118
1243
|
const shape = createSwfTextShape(new SwfReader(reader.source, pending.start, pending.end), pending.version, state.fontOutlineSources);
|
|
1119
|
-
if (shape
|
|
1120
|
-
|
|
1244
|
+
if (shape === null) {
|
|
1245
|
+
// Composition is deferred because the records address glyphs by index into a font that may not
|
|
1246
|
+
// have been read yet, so the failure lands here rather than at the tag. Without a report the
|
|
1247
|
+
// character is simply absent and every placement of it resolves to nothing.
|
|
1248
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.text-shape-uncomposable', 'appendSwfPendingTextShapes', {
|
|
1249
|
+
capability: pending.version === 2 ? 'swf.text.define-text-2' : 'swf.text.define-text',
|
|
1250
|
+
characterId: pending.characterId,
|
|
1251
|
+
});
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
state.shapes.set(pending.characterId, shape);
|
|
1121
1255
|
}
|
|
1122
1256
|
}
|
|
1123
1257
|
function readSwfTimeline(reader, state) {
|
|
@@ -1148,27 +1282,43 @@ function readSwfTimeline(reader, state) {
|
|
|
1148
1282
|
frames.push(new Map(placements));
|
|
1149
1283
|
}
|
|
1150
1284
|
else if (code === TAG_DO_ABC || code === TAG_DO_ABC_ANONYMOUS) {
|
|
1151
|
-
state.abcBlobs.push(readSwfAbcPayload(body, code === TAG_DO_ABC));
|
|
1285
|
+
state.abcBlobs.push({ bytes: readSwfAbcPayload(body, code === TAG_DO_ABC), named: code === TAG_DO_ABC });
|
|
1152
1286
|
}
|
|
1153
1287
|
else if (code === TAG_DO_INIT_ACTION) {
|
|
1154
1288
|
// An init action runs once for the sprite it names, before that sprite's own first frame, so a
|
|
1155
1289
|
// recognized block belongs to frame 1 of that sprite rather than to the timeline reading the tag.
|
|
1156
1290
|
const spriteId = body.readUint16();
|
|
1157
1291
|
const script = readSwfFrameActions(new SwfReader(body.source, body.pos, body.end));
|
|
1158
|
-
if (script
|
|
1292
|
+
if (script === null) {
|
|
1293
|
+
// Same decline as DoAction below and the same Skip reasoning: the bytecode is outside this
|
|
1294
|
+
// importer's scope by charter rather than data it failed to read. It reports the sprite it names
|
|
1295
|
+
// rather than a frame, because an init action runs before that sprite's first frame.
|
|
1296
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Skip, 'swf.frame-script-declined', 'readSwfTimeline', { capability: 'swf.script.do-init-action', characterId: spriteId });
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1159
1299
|
state.pendingInitActions.push({ characterId: spriteId, script });
|
|
1300
|
+
}
|
|
1160
1301
|
}
|
|
1161
1302
|
else if (code === TAG_DO_ACTION) {
|
|
1162
1303
|
// A DoAction belongs to the frame being assembled — the one the next ShowFrame closes.
|
|
1163
1304
|
const script = readSwfFrameActions(new SwfReader(body.source, body.pos, body.end));
|
|
1164
1305
|
if (script !== null)
|
|
1165
1306
|
actions.set(frames.length + 1, script);
|
|
1307
|
+
// A block is recognized only when EVERY action in it is a playback command, because honouring the
|
|
1308
|
+
// legible half would misrepresent what the frame does. Skip rather than Drop: the bytecode is
|
|
1309
|
+
// outside this importer's scope by charter, not data it failed to read.
|
|
1310
|
+
else {
|
|
1311
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Skip, 'swf.frame-script-declined', 'readSwfTimeline', {
|
|
1312
|
+
capability: 'swf.script.do-action',
|
|
1313
|
+
frame: frames.length + 1,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1166
1316
|
}
|
|
1167
1317
|
else if (code === TAG_FRAME_LABEL) {
|
|
1168
1318
|
addSwfTimelineLabel(labels, frames.length + 1, body.readString());
|
|
1169
1319
|
}
|
|
1170
1320
|
else if (code === TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA) {
|
|
1171
|
-
readSwfSceneAndFrameLabelData(body, labels);
|
|
1321
|
+
readSwfSceneAndFrameLabelData(body, labels, state.diagnostics);
|
|
1172
1322
|
}
|
|
1173
1323
|
else if (code === TAG_DEFINE_SCALING_GRID) {
|
|
1174
1324
|
readSwfScalingGrid(body, state);
|
|
@@ -1177,7 +1327,7 @@ function readSwfTimeline(reader, state) {
|
|
|
1177
1327
|
readLegacyPlaceObject(body, placements);
|
|
1178
1328
|
}
|
|
1179
1329
|
else if (code === TAG_PLACE_OBJECT_2) {
|
|
1180
|
-
readPlaceObject(body, placements, false);
|
|
1330
|
+
readPlaceObject(body, placements, false, state.diagnostics);
|
|
1181
1331
|
}
|
|
1182
1332
|
else if (code === TAG_REMOVE_OBJECT) {
|
|
1183
1333
|
readLegacyRemoveObject(body, placements);
|
|
@@ -1238,7 +1388,7 @@ function readSwfTimeline(reader, state) {
|
|
|
1238
1388
|
state.sprites.set(spriteId, spriteTimeline);
|
|
1239
1389
|
}
|
|
1240
1390
|
else if (code === TAG_PLACE_OBJECT_3 || code === TAG_PLACE_OBJECT_4) {
|
|
1241
|
-
readPlaceObject(body, placements, true);
|
|
1391
|
+
readPlaceObject(body, placements, true, state.diagnostics);
|
|
1242
1392
|
}
|
|
1243
1393
|
else if (code === TAG_START_SOUND) {
|
|
1244
1394
|
readSwfStartSound(body, state, cues, frames.length + 1);
|
|
@@ -1263,6 +1413,15 @@ function readSwfTimeline(reader, state) {
|
|
|
1263
1413
|
if (body.valid && body.pos < body.end)
|
|
1264
1414
|
streamChunks.push(body.source.subarray(body.pos, body.end));
|
|
1265
1415
|
}
|
|
1416
|
+
else if (streamFormat >= 0) {
|
|
1417
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Skip, 'swf.stream-sound-format', 'readSwfTimeline', {
|
|
1418
|
+
capability: 'swf.axis.sound-format-non-mp3',
|
|
1419
|
+
format: streamFormat,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
reportSwfDeclinedTag(state.diagnostics, code);
|
|
1266
1425
|
}
|
|
1267
1426
|
if (!body.valid)
|
|
1268
1427
|
return null;
|
|
@@ -1273,15 +1432,46 @@ function readSwfTimeline(reader, state) {
|
|
|
1273
1432
|
if (frames.length === 0)
|
|
1274
1433
|
frames.push(placements);
|
|
1275
1434
|
appendSwfStreamSoundCue(state, cues, streamChunks, streamStartFrame);
|
|
1435
|
+
// A label or cue authored after the last ShowFrame names a frame the timeline never reaches, so it is
|
|
1436
|
+
// dropped rather than bound to a frame that does not exist. Found by auditing the claim rather than the
|
|
1437
|
+
// wire: `swf.scene-names` covered only the scene table, while THIS is the other way that tag's data is
|
|
1438
|
+
// lost, and silence about it was being reported as trustworthy.
|
|
1439
|
+
const reachableCues = cues.filter((cue) => cue.frame <= frames.length);
|
|
1440
|
+
const reachableLabels = labels.filter((label) => label.frame <= frames.length);
|
|
1441
|
+
if (reachableLabels.length !== labels.length) {
|
|
1442
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.label-past-last-frame', 'readSwfTimeline', {
|
|
1443
|
+
capability: 'swf.timeline.frame-label',
|
|
1444
|
+
dropped: labels.length - reachableLabels.length,
|
|
1445
|
+
frames: frames.length,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
if (reachableCues.length !== cues.length) {
|
|
1449
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.cue-past-last-frame', 'readSwfTimeline', {
|
|
1450
|
+
dropped: cues.length - reachableCues.length,
|
|
1451
|
+
frames: frames.length,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1276
1454
|
return {
|
|
1277
1455
|
actions,
|
|
1278
|
-
|
|
1279
|
-
cues: cues.filter((cue) => cue.frame <= frames.length),
|
|
1456
|
+
cues: reachableCues,
|
|
1280
1457
|
frames,
|
|
1281
|
-
|
|
1282
|
-
labels: labels.filter((label) => label.frame <= frames.length).sort(compareSwfTimelineLabelFrame),
|
|
1458
|
+
labels: reachableLabels.sort(compareSwfTimelineLabelFrame),
|
|
1283
1459
|
};
|
|
1284
1460
|
}
|
|
1461
|
+
// Reports the tags this importer deliberately reads past that cost a real capability, and stays silent on
|
|
1462
|
+
// the ones that cost nothing. The line matters: a document is not worse off for having skipped
|
|
1463
|
+
// `FileAttributes` or a font's hinting table, so reporting those would bury the entries that do mean
|
|
1464
|
+
// something under noise a caller has to filter. Every code below is a decision recorded in
|
|
1465
|
+
// `agents/packages/swf/tag-coverage.md`, not an unrecognized tag.
|
|
1466
|
+
function reportSwfDeclinedTag(diagnostics, code) {
|
|
1467
|
+
if (diagnostics === undefined)
|
|
1468
|
+
return;
|
|
1469
|
+
const kind = SWF_DECLINED_TAG_KINDS.get(code);
|
|
1470
|
+
if (kind === undefined)
|
|
1471
|
+
return;
|
|
1472
|
+
const capability = SWF_DECLINED_TAG_CAPABILITIES.get(code);
|
|
1473
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, kind, 'readSwfTimeline', capability === undefined ? { tag: code } : { capability, tag: code });
|
|
1474
|
+
}
|
|
1285
1475
|
// The stage colour, as an RGB record. SWF gives it no alpha, and a stage is opaque, so it packs to fully
|
|
1286
1476
|
// opaque RGBA. Last declaration wins, matching how a player applies the most recent one it has read.
|
|
1287
1477
|
// A button is a display character whose content is a small display list per interaction state. A document
|
|
@@ -1312,6 +1502,14 @@ function readSwfButtonDefinition(body, state, version) {
|
|
|
1312
1502
|
const colorTransform = version === 2 ? readSwfColorTransform(reader) : null;
|
|
1313
1503
|
if (!reader.valid)
|
|
1314
1504
|
return;
|
|
1505
|
+
if ((flags & BUTTON_STATE_UP) === 0 && characterId !== 0) {
|
|
1506
|
+
// A document is a still scene, so only the up state is held. The other states are a real capability
|
|
1507
|
+
// gap rather than data this decoder could not read, which is why they Skip rather than Drop.
|
|
1508
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Skip, 'swf.button-interaction-state', 'readSwfButtonDefinition', {
|
|
1509
|
+
characterId,
|
|
1510
|
+
flags,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1315
1513
|
if ((flags & BUTTON_STATE_UP) !== 0 && characterId !== 0) {
|
|
1316
1514
|
placements.set(depth, {
|
|
1317
1515
|
advancedBlendMode: null,
|
|
@@ -1348,9 +1546,28 @@ function readSwfFontDefinition(body, state, code) {
|
|
|
1348
1546
|
const version = code === TAG_DEFINE_FONT ? 1 : code === TAG_DEFINE_FONT_2 ? 2 : 3;
|
|
1349
1547
|
const reader = new SwfReader(body.source, body.pos, body.end);
|
|
1350
1548
|
const fontId = reader.source[body.pos] + reader.source[body.pos + 1] * 0x100;
|
|
1351
|
-
const source = readSwfFontGlyphOutlineSource(reader, version);
|
|
1352
|
-
|
|
1549
|
+
const source = readSwfFontGlyphOutlineSource(reader, version, state.diagnostics, fontId);
|
|
1550
|
+
// A glyph table this decoder cannot read at all costs the WHOLE font, not one glyph, so it is a
|
|
1551
|
+
// separate loss from `swf.font-glyph-outline` and must report separately: without this, a font that
|
|
1552
|
+
// vanished entirely and a font that imported cleanly both produce no crumb.
|
|
1553
|
+
if (source === null) {
|
|
1554
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.font-glyph-table', 'readSwfFontDefinition', {
|
|
1555
|
+
capability: version === 1 ? 'swf.font.define-font' : `swf.font.define-font-${version}`,
|
|
1556
|
+
characterId: fontId,
|
|
1557
|
+
});
|
|
1353
1558
|
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (fontId === 0)
|
|
1561
|
+
return;
|
|
1562
|
+
if (state.fontOutlineSources.has(fontId)) {
|
|
1563
|
+
// Every other definition kind rejects the document on a repeated character id; fonts do not, so the
|
|
1564
|
+
// second table silently replaces the first. The document imports, the font exists, and it is the
|
|
1565
|
+
// wrong font — no existence check and no count can see that, which is why it is reported here.
|
|
1566
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.font-character-id-reused', 'readSwfFontDefinition', {
|
|
1567
|
+
capability: version === 1 ? 'swf.font.define-font' : `swf.font.define-font-${version}`,
|
|
1568
|
+
characterId: fontId,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1354
1571
|
state.fontOutlineSources.set(fontId, source);
|
|
1355
1572
|
const fontName = readSwfFontName(body, version);
|
|
1356
1573
|
if (fontName !== '')
|
|
@@ -1438,12 +1655,21 @@ function readSwfScalingGrid(body, state) {
|
|
|
1438
1655
|
return;
|
|
1439
1656
|
state.scalingGrids.set(characterId, splitter);
|
|
1440
1657
|
}
|
|
1441
|
-
function readSwfSceneAndFrameLabelData(body, labels) {
|
|
1658
|
+
function readSwfSceneAndFrameLabelData(body, labels, diagnostics) {
|
|
1442
1659
|
const sceneCount = body.readEncodedUint32();
|
|
1443
1660
|
for (let i = 0; i < sceneCount && body.valid; i++) {
|
|
1444
1661
|
body.readEncodedUint32();
|
|
1445
1662
|
body.readString();
|
|
1446
1663
|
}
|
|
1664
|
+
// Scene names are read past to reach the label table behind them. Skip rather than Drop: Flight has
|
|
1665
|
+
// frame labels but no subject for a named frame range, so this is a capability gap rather than data
|
|
1666
|
+
// this decoder failed to read.
|
|
1667
|
+
if (sceneCount > 0) {
|
|
1668
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Skip, 'swf.scene-names', 'readSwfSceneAndFrameLabelData', {
|
|
1669
|
+
capability: 'swf.timeline.define-scene-and-frame-label-data',
|
|
1670
|
+
sceneCount,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1447
1673
|
const labelCount = body.readEncodedUint32();
|
|
1448
1674
|
for (let i = 0; i < labelCount && body.valid; i++) {
|
|
1449
1675
|
const frame = body.readEncodedUint32();
|
|
@@ -1494,8 +1720,12 @@ function readSwfBoundedDefinition(body, state, code) {
|
|
|
1494
1720
|
const reader = new SwfReader(body.source, body.pos, body.end);
|
|
1495
1721
|
const bounds = state.characterBounds.get(characterId);
|
|
1496
1722
|
const factory = readSwfEditTextFactory(reader, bounds?.width ?? 0, bounds?.height ?? 0);
|
|
1497
|
-
if (factory
|
|
1723
|
+
if (factory === null) {
|
|
1724
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.edit-text-unparseable', 'readSwfBoundedDefinition', { capability: 'swf.text.define-edit-text', characterId });
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1498
1727
|
state.editTexts.set(characterId, factory);
|
|
1728
|
+
}
|
|
1499
1729
|
}
|
|
1500
1730
|
if (code === TAG_DEFINE_TEXT || code === TAG_DEFINE_TEXT_2) {
|
|
1501
1731
|
const reader = new SwfReader(body.source, body.pos, body.end);
|
|
@@ -1525,9 +1755,18 @@ function readSwfShapeBody(body, state, characterId, version) {
|
|
|
1525
1755
|
return;
|
|
1526
1756
|
// The fill's character is not this shape's, hence the distinct name: a bitmap fill names whatever
|
|
1527
1757
|
// character carries its pixels, which may be defined later in the tag stream.
|
|
1528
|
-
const shape = createSwfShape(reader, version, (fillCharacterId, repeat, smoothed) => acquireSwfImageTexture(state, fillCharacterId, repeat, smoothed));
|
|
1529
|
-
if (shape
|
|
1530
|
-
|
|
1758
|
+
const shape = createSwfShape(reader, version, (fillCharacterId, repeat, smoothed) => acquireSwfImageTexture(state, fillCharacterId, repeat, smoothed), state.diagnostics);
|
|
1759
|
+
if (shape === null) {
|
|
1760
|
+
// Recover rather than Drop: the character survives as the bounded placeholder it was before any
|
|
1761
|
+
// geometry existed, so the document still places and sizes it — only the drawing is missing.
|
|
1762
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Recover, 'swf.shape-body-unreadable', 'readSwfShapeDefinition', {
|
|
1763
|
+
capability: version === 1 ? 'swf.shape.define-shape' : `swf.shape.define-shape-${version}`,
|
|
1764
|
+
characterId,
|
|
1765
|
+
version,
|
|
1766
|
+
});
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
state.shapes.set(characterId, shape);
|
|
1531
1770
|
}
|
|
1532
1771
|
// Decodes a morph definition's geometry and paint on a reader of its own, so a body this decoder cannot
|
|
1533
1772
|
// read costs only that body. The definition keeps its authored bounds and contributes no drawing, which
|
|
@@ -1539,9 +1778,17 @@ function readSwfMorphShapeBody(body, state, characterId, version) {
|
|
|
1539
1778
|
const source = body.source;
|
|
1540
1779
|
const start = body.pos;
|
|
1541
1780
|
const end = body.end;
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1781
|
+
// Only the parse-time validation call carries the sink. The stored closure runs again per placement,
|
|
1782
|
+
// potentially long after import returned, and a sink written to then would append to a collection
|
|
1783
|
+
// whose consumer has already read it.
|
|
1784
|
+
const decode = (diagnostics) => createSwfMorphShape(new SwfReader(source, start, end), version, (fillCharacterId, repeat, smoothed) => acquireSwfImageTexture(state, fillCharacterId, repeat, smoothed), diagnostics);
|
|
1785
|
+
if (decode(state.diagnostics) === null) {
|
|
1786
|
+
// The character is simply absent from the document afterwards, so without this the loss has no
|
|
1787
|
+
// signal at all: a placement of it resolves to nothing and the import still reports success.
|
|
1788
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.morph-shape-undecodable', 'readSwfMorphShapeBody', { capability: version === 2 ? 'swf.morph.define-morph-shape-2' : 'swf.morph.define-morph-shape', characterId });
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
state.morphShapes.set(characterId, decode);
|
|
1545
1792
|
}
|
|
1546
1793
|
function resolveSwfMorphShapeVersion(code) {
|
|
1547
1794
|
if (code === TAG_DEFINE_MORPH_SHAPE)
|
|
@@ -1569,8 +1816,13 @@ function readSwfLegacyImageDefinition(body, state) {
|
|
|
1569
1816
|
if (!body.valid || characterId === 0 || state.definedCharacters.has(characterId))
|
|
1570
1817
|
return;
|
|
1571
1818
|
const tables = state.jpegTables;
|
|
1572
|
-
if (tables === null)
|
|
1819
|
+
if (tables === null) {
|
|
1820
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.jpeg-tables-missing', 'readSwfLegacyImageDefinition', {
|
|
1821
|
+
capability: 'swf.bitmap.define-bits-jpeg-tables',
|
|
1822
|
+
characterId,
|
|
1823
|
+
});
|
|
1573
1824
|
return;
|
|
1825
|
+
}
|
|
1574
1826
|
const tablesEnd = tables.length >= 2 && tables[tables.length - 2] === 0xff && tables[tables.length - 1] === JPEG_END_OF_IMAGE
|
|
1575
1827
|
? tables.length - 2
|
|
1576
1828
|
: tables.length;
|
|
@@ -1579,8 +1831,13 @@ function readSwfLegacyImageDefinition(body, state) {
|
|
|
1579
1831
|
spliced.set(tables.subarray(0, tablesEnd));
|
|
1580
1832
|
spliced.set(body.source.subarray(imageStart, body.end), tablesEnd);
|
|
1581
1833
|
const image = readSwfEmbeddedImage(spliced, 0, spliced.length);
|
|
1582
|
-
if (image === null)
|
|
1834
|
+
if (image === null) {
|
|
1835
|
+
reportImportDiagnostic(state.diagnostics, ImportDiagnosticSeverity.Drop, 'swf.jpeg-tables-unsplittable', 'readSwfLegacyImageDefinition', {
|
|
1836
|
+
capability: 'swf.bitmap.define-bits-jpeg-tables',
|
|
1837
|
+
characterId,
|
|
1838
|
+
});
|
|
1583
1839
|
return;
|
|
1840
|
+
}
|
|
1584
1841
|
state.definedCharacters.add(characterId);
|
|
1585
1842
|
state.characterBounds.set(characterId, image.bounds);
|
|
1586
1843
|
state.images.set(characterId, { bytes: spliced, mimeType: image.mimeType });
|
|
@@ -1590,13 +1847,16 @@ function readSwfLegacyImageDefinition(body, state) {
|
|
|
1590
1847
|
// costs far more than the picture is worth.
|
|
1591
1848
|
function readSwfEmbeddedImageDefinition(body, state, code) {
|
|
1592
1849
|
const characterId = body.readUint16();
|
|
1850
|
+
let deblockingParameterRaw = null;
|
|
1593
1851
|
let imageStart = body.pos;
|
|
1594
1852
|
let imageEnd = body.end;
|
|
1853
|
+
let hasAlphaPayload = false;
|
|
1595
1854
|
if (code === TAG_DEFINE_BITS_JPEG_3 || code === TAG_DEFINE_BITS_JPEG_4) {
|
|
1855
|
+
hasAlphaPayload = true;
|
|
1596
1856
|
const alphaDataOffset = body.readUint32();
|
|
1597
1857
|
const alphaOffsetBase = body.pos;
|
|
1598
1858
|
if (code === TAG_DEFINE_BITS_JPEG_4)
|
|
1599
|
-
body.readUint16();
|
|
1859
|
+
deblockingParameterRaw = body.readUint16();
|
|
1600
1860
|
imageStart = body.pos;
|
|
1601
1861
|
imageEnd = alphaOffsetBase + alphaDataOffset;
|
|
1602
1862
|
}
|
|
@@ -1616,6 +1876,28 @@ function readSwfEmbeddedImageDefinition(body, state, code) {
|
|
|
1616
1876
|
bytes: stripSwfJpegStreamBoundary(body.source, imageStart, imageEnd, image.mimeType),
|
|
1617
1877
|
mimeType: image.mimeType,
|
|
1618
1878
|
});
|
|
1879
|
+
if (hasAlphaPayload) {
|
|
1880
|
+
const compressedAlphaBytes = body.source.subarray(imageEnd, body.end);
|
|
1881
|
+
state.jpegAlphaPayloads.set(characterId, {
|
|
1882
|
+
characterId,
|
|
1883
|
+
compressedAlphaBytes,
|
|
1884
|
+
deblockingParameterRaw,
|
|
1885
|
+
height: image.bounds.height,
|
|
1886
|
+
width: image.bounds.width,
|
|
1887
|
+
});
|
|
1888
|
+
// The full report retains this stream, but the document's colour reference still resolves without
|
|
1889
|
+
// applying it. Keep the existing fidelity diagnostic until the separately-gated composition stage.
|
|
1890
|
+
if (compressedAlphaBytes.length > 0) {
|
|
1891
|
+
reportImportDiagnostic(state.diagnostics,
|
|
1892
|
+
// Skip: the alpha stream is recognized and deliberately not applied until the composition stage
|
|
1893
|
+
// exists. A capability gap on a well-formed file, with no substitute written.
|
|
1894
|
+
ImportDiagnosticSeverity.Skip, 'swf.jpeg-alpha-stream', 'readSwfEmbeddedImageDefinition', {
|
|
1895
|
+
capability: code === TAG_DEFINE_BITS_JPEG_3 ? 'swf.bitmap.define-bits-jpeg-3' : 'swf.bitmap.define-bits-jpeg-4',
|
|
1896
|
+
characterId,
|
|
1897
|
+
discardedBytes: compressedAlphaBytes.length,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1619
1901
|
return;
|
|
1620
1902
|
}
|
|
1621
1903
|
// Identifies an embedded payload by its magic bytes and reads the dimensions out of its header, without
|
|
@@ -2030,8 +2312,6 @@ const S_SIGNATURE = 0x53;
|
|
|
2030
2312
|
const MORPH_RATIO_ONE = 0xffff;
|
|
2031
2313
|
const SWF_INSTANCE_KEY_SCALE = 0x10000;
|
|
2032
2314
|
const SWF_LZMA_PREFIX_LENGTH = 17;
|
|
2033
|
-
const SWF_LOSSLESS_ALPHA_MIME_TYPE = 'image/x-swf-lossless-alpha';
|
|
2034
|
-
const SWF_LOSSLESS_MIME_TYPE = 'image/x-swf-lossless';
|
|
2035
2315
|
const SWF_MIME_TYPE = 'application/x-shockwave-flash';
|
|
2036
2316
|
const SWF_PREFIX_LENGTH = 8;
|
|
2037
2317
|
const SOUND_ENVELOPE_LEVEL_ONE = 32768;
|
|
@@ -2065,6 +2345,22 @@ const TAG_START_SOUND_2 = 89;
|
|
|
2065
2345
|
const TAG_SOUND_STREAM_BLOCK = 19;
|
|
2066
2346
|
const TAG_SOUND_STREAM_HEAD = 18;
|
|
2067
2347
|
const TAG_SOUND_STREAM_HEAD_2 = 45;
|
|
2348
|
+
// Only tags whose absence loses something a caller could want. Metadata tags — `FileAttributes`,
|
|
2349
|
+
// `Metadata`, `ProductInfo`, `ScriptLimits`, `DebugID`, `EnableDebugger2`, `EnableTelemetry`, `Protect`,
|
|
2350
|
+
// `SetTabIndex`, `DefineButtonCxform`, and the font hinting/naming tables — are deliberately absent: they
|
|
2351
|
+
// carry no scene content, so skipping one is not a loss to report.
|
|
2352
|
+
// The declared capability a declined tag costs, where one exists. Deliberately partial: `DefineFont4`,
|
|
2353
|
+
// `DefineBinaryData`, `ImportAssets` and `DefineButtonSound` name no declared capability, and inventing
|
|
2354
|
+
// one so every crumb could carry a join key would put entries in the denominator that nothing measures.
|
|
2355
|
+
const SWF_DECLINED_TAG_CAPABILITIES = new Map([[61, 'swf.video.video-frame']]);
|
|
2356
|
+
const SWF_DECLINED_TAG_KINDS = new Map([
|
|
2357
|
+
[17, 'swf.define-button-sound'],
|
|
2358
|
+
[57, 'swf.import-assets'],
|
|
2359
|
+
[61, 'swf.video-frame-payload'],
|
|
2360
|
+
[71, 'swf.import-assets'],
|
|
2361
|
+
[87, 'swf.define-binary-data'],
|
|
2362
|
+
[91, 'swf.define-font-4'],
|
|
2363
|
+
]);
|
|
2068
2364
|
const TAG_DEFINE_SCALING_GRID = 78;
|
|
2069
2365
|
const TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA = 86;
|
|
2070
2366
|
const TAG_DEFINE_BITS_JPEG_2 = 21;
|