@flighthq/swf 0.3.0-next.1781.5d072ef → 0.3.0-next.1848.58f06fb
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/package.json +22 -22
- package/src/swfEditText.test.ts +23 -1
- package/src/swfImageResources.test.ts +174 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/swf",
|
|
3
|
-
"version": "0.3.0-next.
|
|
3
|
+
"version": "0.3.0-next.1848.58f06fb",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,27 +38,27 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/abc": "0.3.0-next.
|
|
42
|
-
"@flighthq/adjustments": "0.3.0-next.
|
|
43
|
-
"@flighthq/audio": "0.3.0-next.
|
|
44
|
-
"@flighthq/bitmap": "0.3.0-next.
|
|
45
|
-
"@flighthq/clip": "0.3.0-next.
|
|
46
|
-
"@flighthq/compression": "0.3.0-next.
|
|
47
|
-
"@flighthq/effects": "0.3.0-next.
|
|
48
|
-
"@flighthq/geometry": "0.3.0-next.
|
|
49
|
-
"@flighthq/image": "0.3.0-next.
|
|
50
|
-
"@flighthq/log": "0.3.0-next.
|
|
51
|
-
"@flighthq/math": "0.3.0-next.
|
|
52
|
-
"@flighthq/movieclip": "0.3.0-next.
|
|
53
|
-
"@flighthq/node": "0.3.0-next.
|
|
54
|
-
"@flighthq/path": "0.3.0-next.
|
|
55
|
-
"@flighthq/scene2d": "0.3.0-next.
|
|
56
|
-
"@flighthq/scene2d-resources": "0.3.0-next.
|
|
57
|
-
"@flighthq/shape": "0.3.0-next.
|
|
58
|
-
"@flighthq/text": "0.3.0-next.
|
|
59
|
-
"@flighthq/text-markup": "0.3.0-next.
|
|
60
|
-
"@flighthq/texture": "0.3.0-next.
|
|
61
|
-
"@flighthq/types": "0.3.0-next.
|
|
41
|
+
"@flighthq/abc": "0.3.0-next.1848.58f06fb",
|
|
42
|
+
"@flighthq/adjustments": "0.3.0-next.1848.58f06fb",
|
|
43
|
+
"@flighthq/audio": "0.3.0-next.1848.58f06fb",
|
|
44
|
+
"@flighthq/bitmap": "0.3.0-next.1848.58f06fb",
|
|
45
|
+
"@flighthq/clip": "0.3.0-next.1848.58f06fb",
|
|
46
|
+
"@flighthq/compression": "0.3.0-next.1848.58f06fb",
|
|
47
|
+
"@flighthq/effects": "0.3.0-next.1848.58f06fb",
|
|
48
|
+
"@flighthq/geometry": "0.3.0-next.1848.58f06fb",
|
|
49
|
+
"@flighthq/image": "0.3.0-next.1848.58f06fb",
|
|
50
|
+
"@flighthq/log": "0.3.0-next.1848.58f06fb",
|
|
51
|
+
"@flighthq/math": "0.3.0-next.1848.58f06fb",
|
|
52
|
+
"@flighthq/movieclip": "0.3.0-next.1848.58f06fb",
|
|
53
|
+
"@flighthq/node": "0.3.0-next.1848.58f06fb",
|
|
54
|
+
"@flighthq/path": "0.3.0-next.1848.58f06fb",
|
|
55
|
+
"@flighthq/scene2d": "0.3.0-next.1848.58f06fb",
|
|
56
|
+
"@flighthq/scene2d-resources": "0.3.0-next.1848.58f06fb",
|
|
57
|
+
"@flighthq/shape": "0.3.0-next.1848.58f06fb",
|
|
58
|
+
"@flighthq/text": "0.3.0-next.1848.58f06fb",
|
|
59
|
+
"@flighthq/text-markup": "0.3.0-next.1848.58f06fb",
|
|
60
|
+
"@flighthq/texture": "0.3.0-next.1848.58f06fb",
|
|
61
|
+
"@flighthq/types": "0.3.0-next.1848.58f06fb"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@flighthq/font": "*",
|
package/src/swfEditText.test.ts
CHANGED
|
@@ -44,6 +44,23 @@ describe('readSwfEditTextFactory', () => {
|
|
|
44
44
|
expect(field.data.selectable).toBe(false);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it('parses the markup of a field flagged as html into text and format ranges', () => {
|
|
48
|
+
const field = build({ html: true, text: 'Hit <b>hard</b> now' })(() => '');
|
|
49
|
+
|
|
50
|
+
// Without the parse the field would display its own tags, which is the whole point of the flag.
|
|
51
|
+
expect(field.data.text).toBe('Hit hard now');
|
|
52
|
+
expect(field.data.textFormatRanges).toEqual([{ end: 8, format: { bold: true }, start: 4 }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('keeps markup verbatim in a field that is not flagged as html', () => {
|
|
56
|
+
const field = build({ text: 'Hit <b>hard</b> now' })(() => '');
|
|
57
|
+
|
|
58
|
+
// The flag is what declares the string to be markup; an unflagged field states its characters
|
|
59
|
+
// literally, so parsing it anyway would eat text the author wrote.
|
|
60
|
+
expect(field.data.text).toBe('Hit <b>hard</b> now');
|
|
61
|
+
expect(field.data.textFormatRanges).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
47
64
|
it('returns null for a field that runs out mid-record', () => {
|
|
48
65
|
const bytes = bytesFor({ text: 'x' });
|
|
49
66
|
|
|
@@ -55,6 +72,7 @@ interface FieldOptions {
|
|
|
55
72
|
border?: boolean;
|
|
56
73
|
fontHeight?: number;
|
|
57
74
|
fontId?: number;
|
|
75
|
+
html?: boolean;
|
|
58
76
|
indent?: number;
|
|
59
77
|
leading?: number;
|
|
60
78
|
leftMargin?: number;
|
|
@@ -85,7 +103,11 @@ function bytesFor(options: Readonly<FieldOptions>): Uint8Array {
|
|
|
85
103
|
};
|
|
86
104
|
|
|
87
105
|
const flags = 0x80 | 0x04 | 0x01 | (options.wordWrap === true ? 0x40 : 0) | (options.multiline === true ? 0x20 : 0);
|
|
88
|
-
const layoutFlags =
|
|
106
|
+
const layoutFlags =
|
|
107
|
+
0x20 |
|
|
108
|
+
(options.border === true ? 0x08 : 0) |
|
|
109
|
+
(options.noSelect === true ? 0x10 : 0) |
|
|
110
|
+
(options.html === true ? 0x02 : 0);
|
|
89
111
|
u8(flags);
|
|
90
112
|
u8(layoutFlags);
|
|
91
113
|
u16(options.fontId ?? 1);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { getNodeChildren } from '@flighthq/node/contract';
|
|
3
|
+
import { loadScene2DImageResources } from '@flighthq/scene2d-resources/contract';
|
|
4
|
+
import { getTextureSource } from '@flighthq/texture/contract';
|
|
5
|
+
import type { Image, Shape, Texture2D, TextureSource } from '@flighthq/types/contract';
|
|
6
|
+
import {
|
|
7
|
+
ImageResourceReferenceKind,
|
|
8
|
+
ImageTextureSourceKind,
|
|
9
|
+
ResourceResolutionState,
|
|
10
|
+
ShapeKind,
|
|
11
|
+
} from '@flighthq/types/contract';
|
|
12
|
+
|
|
13
|
+
import { createScene2DFromSwf } from './swfDocument';
|
|
14
|
+
import { ShapeWriter } from './swfShapeTestHelper';
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// jsdom has the browser image surface but does not decode image bytes. Keep the production Blob ->
|
|
18
|
+
// HTMLImageElement path intact and model the dimensions a real decode of this 1x1 PNG supplies.
|
|
19
|
+
HTMLImageElement.prototype.decode = vi.fn(function (this: HTMLImageElement) {
|
|
20
|
+
this.width = 1;
|
|
21
|
+
this.height = 1;
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
delete (HTMLImageElement.prototype as Partial<HTMLImageElement>).decode;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('SWF image resources', () => {
|
|
32
|
+
it('decodes an embedded PNG and binds its pixels to the bitmap-fill texture', async () => {
|
|
33
|
+
const imageBytes = decodeBase64(
|
|
34
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
35
|
+
);
|
|
36
|
+
const document = createScene2DFromSwf(createBitmapFillSwf(imageBytes))!;
|
|
37
|
+
const shape = getNodeChildren(document.root)[0] as Shape;
|
|
38
|
+
const texture = shape.data.commands[2] as Texture2D;
|
|
39
|
+
const reference = document.imageResources[0];
|
|
40
|
+
|
|
41
|
+
expect(shape.kind).toBe(ShapeKind);
|
|
42
|
+
expect(shape.data.commands[0]).toBe('beginTextureFill');
|
|
43
|
+
expect(reference.kind).toBe(ImageResourceReferenceKind.Embedded);
|
|
44
|
+
if (reference.kind !== ImageResourceReferenceKind.Embedded) throw new Error('Expected embedded image resource');
|
|
45
|
+
expect(reference.mimeType).toBe('image/png');
|
|
46
|
+
expect(reference.bytes).toEqual(imageBytes);
|
|
47
|
+
expect(reference.textures).toEqual([texture]);
|
|
48
|
+
expect(getTextureSource(texture)).toBeNull();
|
|
49
|
+
|
|
50
|
+
const resources = await loadScene2DImageResources(document);
|
|
51
|
+
const image = getTextureSource(texture);
|
|
52
|
+
|
|
53
|
+
expect(resources.resolved).toEqual([reference]);
|
|
54
|
+
expect(resources.unresolved).toEqual([]);
|
|
55
|
+
expect(reference.state).toBe(ResourceResolutionState.Resolved);
|
|
56
|
+
expect(image?.kind).toBe(ImageTextureSourceKind);
|
|
57
|
+
if (!isImageTextureSource(image)) throw new Error('Expected decoded image source');
|
|
58
|
+
expect(image.width).toBe(1);
|
|
59
|
+
expect(image.height).toBe(1);
|
|
60
|
+
expect(image.source).toBeInstanceOf(HTMLImageElement);
|
|
61
|
+
expect(HTMLImageElement.prototype.decode).toHaveBeenCalledOnce();
|
|
62
|
+
expect(getTextureSource(reference.textures![0])).toBe(image);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function isImageTextureSource(source: Readonly<TextureSource> | null): source is Image {
|
|
67
|
+
return source?.kind === ImageTextureSourceKind;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createBitmapFillSwf(imageBytes: Uint8Array): Uint8Array {
|
|
71
|
+
const art = new ShapeWriter();
|
|
72
|
+
art.writeFillStyleCount(1);
|
|
73
|
+
art.writeBitmapFillStyle(0x41, IMAGE_CHARACTER_ID, 20);
|
|
74
|
+
art.writeLineStyleCount(0);
|
|
75
|
+
art.writeStyleBits(1, 0);
|
|
76
|
+
art.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
77
|
+
art.writeStraightEdge(20, 0);
|
|
78
|
+
art.writeStraightEdge(0, 20);
|
|
79
|
+
art.writeStraightEdge(-20, 0);
|
|
80
|
+
art.writeStraightEdge(0, -20);
|
|
81
|
+
art.writeEndShape();
|
|
82
|
+
|
|
83
|
+
return createSwf([
|
|
84
|
+
createTag(TAG_DEFINE_BITS_JPEG_2, joinBytes(uint16(IMAGE_CHARACTER_ID), imageBytes)),
|
|
85
|
+
createTag(TAG_DEFINE_SHAPE_3, joinBytes(uint16(SHAPE_CHARACTER_ID), createRectangle(0, 20, 0, 20), art.toBytes())),
|
|
86
|
+
createTag(
|
|
87
|
+
TAG_PLACE_OBJECT_2,
|
|
88
|
+
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(SHAPE_CHARACTER_ID)),
|
|
89
|
+
),
|
|
90
|
+
createTag(TAG_SHOW_FRAME),
|
|
91
|
+
createTag(TAG_END),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createRectangle(xMin: number, xMax: number, yMin: number, yMax: number): Uint8Array {
|
|
96
|
+
const values = [xMin, xMax, yMin, yMax];
|
|
97
|
+
const bits = signedBitCount(values);
|
|
98
|
+
const writer = new BitWriter();
|
|
99
|
+
writer.writeUnsigned(bits, 5);
|
|
100
|
+
for (const value of values) writer.writeSigned(value, bits);
|
|
101
|
+
return writer.toBytes();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createSwf(tags: ReadonlyArray<Uint8Array>): Uint8Array {
|
|
105
|
+
const body = joinBytes(createRectangle(0, 20, 0, 20), uint16(24 * 256), uint16(1), ...tags);
|
|
106
|
+
return joinBytes(new Uint8Array([0x46, 0x57, 0x53, 9]), uint32(SWF_PREFIX_LENGTH + body.length), body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createTag(code: number, body: Uint8Array = new Uint8Array()): Uint8Array {
|
|
110
|
+
const shortLength = body.length < 0x3f ? body.length : 0x3f;
|
|
111
|
+
const header = uint16((code << 6) | shortLength);
|
|
112
|
+
return shortLength === 0x3f ? joinBytes(header, uint32(body.length), body) : joinBytes(header, body);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function joinBytes(...parts: ReadonlyArray<Uint8Array>): Uint8Array {
|
|
116
|
+
const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));
|
|
117
|
+
let offset = 0;
|
|
118
|
+
for (const part of parts) {
|
|
119
|
+
result.set(part, offset);
|
|
120
|
+
offset += part.length;
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodeBase64(value: string): Uint8Array {
|
|
126
|
+
return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function signedBitCount(values: ReadonlyArray<number>): number {
|
|
130
|
+
for (let bits = 1; bits < 32; bits++) {
|
|
131
|
+
const minimum = -(2 ** (bits - 1));
|
|
132
|
+
const maximum = 2 ** (bits - 1) - 1;
|
|
133
|
+
if (values.every((value) => value >= minimum && value <= maximum)) return bits;
|
|
134
|
+
}
|
|
135
|
+
return 32;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function uint16(value: number): Uint8Array {
|
|
139
|
+
return new Uint8Array([value & 0xff, (value >> 8) & 0xff]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function uint32(value: number): Uint8Array {
|
|
143
|
+
return new Uint8Array([value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class BitWriter {
|
|
147
|
+
private readonly bits: number[] = [];
|
|
148
|
+
|
|
149
|
+
toBytes(): Uint8Array {
|
|
150
|
+
const bytes = new Uint8Array(Math.ceil(this.bits.length / 8));
|
|
151
|
+
for (let index = 0; index < this.bits.length; index++) {
|
|
152
|
+
bytes[Math.floor(index / 8)] |= this.bits[index] << (7 - (index % 8));
|
|
153
|
+
}
|
|
154
|
+
return bytes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeSigned(value: number, count: number): void {
|
|
158
|
+
this.writeUnsigned(value < 0 ? value + 2 ** count : value, count);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
writeUnsigned(value: number, count: number): void {
|
|
162
|
+
for (let index = count - 1; index >= 0; index--) this.bits.push(Math.floor(value / 2 ** index) & 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const IMAGE_CHARACTER_ID = 9;
|
|
167
|
+
const PLACE_HAS_CHARACTER = 0x02;
|
|
168
|
+
const SHAPE_CHARACTER_ID = 7;
|
|
169
|
+
const SWF_PREFIX_LENGTH = 8;
|
|
170
|
+
const TAG_DEFINE_BITS_JPEG_2 = 21;
|
|
171
|
+
const TAG_DEFINE_SHAPE_3 = 32;
|
|
172
|
+
const TAG_END = 0;
|
|
173
|
+
const TAG_PLACE_OBJECT_2 = 26;
|
|
174
|
+
const TAG_SHOW_FRAME = 1;
|