@openfairygui/functions 0.2.0-alpha.3 → 0.2.0-alpha.31
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/README.md +50 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +16 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +3 -3594
- package/dist/node.cjs +256 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +254 -0
- package/dist/publish-BJk8UzRP.js +3246 -0
- package/dist/publish-DCP0AYx2.cjs +3335 -0
- package/dist/restore-BUaCK8ui.cjs +911 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-C98ugT2H.js +906 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +44 -1
- package/dist/uam-transaction.d.cts +16 -1
- package/dist/uam-transaction.d.ts +16 -1
- package/dist/uam-transaction.js +44 -1
- package/dist/web.cjs +274 -0
- package/dist/web.d.cts +41 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.js +273 -0
- package/package.json +28 -4
- package/src/adapters/node/plugins.ts +82 -0
- package/src/adapters/node/publish.ts +130 -0
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +159 -0
- package/src/adapters/web/raster.ts +251 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +480 -0
- package/src/atlas/jta.ts +211 -0
- package/src/atlas/packing.ts +762 -0
- package/src/atlas.ts +118 -1221
- package/src/codegen.ts +108 -82
- package/src/index.ts +43 -3
- package/src/node.ts +8 -0
- package/src/path-utils.ts +40 -0
- package/src/plugins/types.ts +56 -0
- package/src/publish/contracts.ts +80 -0
- package/src/publish/external-resources.ts +117 -0
- package/src/publish/options.ts +180 -0
- package/src/publish/package-context.ts +608 -0
- package/src/publish/resource-references.ts +210 -0
- package/src/publish.ts +286 -977
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +124 -0
- package/src/restore.ts +112 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +68 -0
- package/src/utils.ts +28 -0
- package/src/web.ts +11 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Document,
|
|
3
|
+
FontResource,
|
|
4
|
+
ILogger,
|
|
5
|
+
ImageResource,
|
|
6
|
+
MovieClipResource,
|
|
7
|
+
Package,
|
|
8
|
+
} from '@openfairygui/core';
|
|
9
|
+
import type { AtlasOptions } from '../atlas.js';
|
|
10
|
+
import type {
|
|
11
|
+
AtlasRasterBackend,
|
|
12
|
+
AtlasRasterInput,
|
|
13
|
+
AtlasRasterResolvedBuffer,
|
|
14
|
+
} from '../publish/contracts.js';
|
|
15
|
+
import {
|
|
16
|
+
isFontResource,
|
|
17
|
+
isImageResource,
|
|
18
|
+
isMovieClipResource,
|
|
19
|
+
resolveImageFileName,
|
|
20
|
+
resolveImagePath,
|
|
21
|
+
} from '../publish/package-context.js';
|
|
22
|
+
import type { ExtrasMap } from '../shared-types.js';
|
|
23
|
+
import { parseFnt } from './font.js';
|
|
24
|
+
import { extractJtaFrames } from './jta.js';
|
|
25
|
+
|
|
26
|
+
/** Trim info for a single image. */
|
|
27
|
+
interface TrimInfo {
|
|
28
|
+
/** Trimmed pixel data (PNG). */
|
|
29
|
+
buffer: Uint8Array;
|
|
30
|
+
/** Trimmed width. */
|
|
31
|
+
width: number;
|
|
32
|
+
/** Trimmed height. */
|
|
33
|
+
height: number;
|
|
34
|
+
/** Offset from original left edge. */
|
|
35
|
+
offsetX: number;
|
|
36
|
+
/** Offset from original top edge. */
|
|
37
|
+
offsetY: number;
|
|
38
|
+
/** Original width before trim. */
|
|
39
|
+
originalWidth: number;
|
|
40
|
+
/** Original height before trim. */
|
|
41
|
+
originalHeight: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type PackageResource = ReturnType<Package['listResources']>[number];
|
|
45
|
+
export type PackableResource = ImageResource | MovieClipResource | FontResource;
|
|
46
|
+
export type PackInputResource = ImageResource | MovieClipResource;
|
|
47
|
+
|
|
48
|
+
export function getPublishedItemId(resource: { getId(): string; getExtras(): ExtrasMap | undefined }): string {
|
|
49
|
+
return ((resource.getExtras() as ImageResourceExtras | undefined) ?? {})._publishedId ?? resource.getId();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ImageResourceExtras extends ExtrasMap {
|
|
53
|
+
_publishedId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface FontSpriteAlias {
|
|
57
|
+
fontId: string;
|
|
58
|
+
textureId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FontResourceExtras extends ExtrasMap {
|
|
62
|
+
_fontSpriteAlias?: FontSpriteAlias;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveFontFileName(fontName: string): string {
|
|
66
|
+
return /\.fnt$/i.test(fontName) ? fontName : `${fontName}.fnt`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Trim transparent edges from an image using the host raster backend.
|
|
71
|
+
* Returns the trimmed buffer, dimensions, and offsets.
|
|
72
|
+
* Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
|
|
73
|
+
*/
|
|
74
|
+
async function _trimImage(
|
|
75
|
+
encoder: AtlasRasterBackend,
|
|
76
|
+
input: AtlasRasterInput,
|
|
77
|
+
originalWidth: number,
|
|
78
|
+
originalHeight: number,
|
|
79
|
+
): Promise<TrimInfo> {
|
|
80
|
+
try {
|
|
81
|
+
const trimResult = await encoder(input).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
82
|
+
if (!isResolvedBuffer(trimResult)) {
|
|
83
|
+
throw new Error('atlas: encoder raw alpha trim did not return resolved metadata.');
|
|
84
|
+
}
|
|
85
|
+
const { data, info } = trimResult;
|
|
86
|
+
const width = info.width;
|
|
87
|
+
const height = info.height;
|
|
88
|
+
const channels = info.channels || 4;
|
|
89
|
+
let minX = width;
|
|
90
|
+
let minY = height;
|
|
91
|
+
let maxX = -1;
|
|
92
|
+
let maxY = -1;
|
|
93
|
+
|
|
94
|
+
for (let y = 0; y < height; y += 1) {
|
|
95
|
+
for (let x = 0; x < width; x += 1) {
|
|
96
|
+
const alphaIndex = (y * width + x) * channels + 3;
|
|
97
|
+
if ((data[alphaIndex] ?? 0) === 0) continue;
|
|
98
|
+
if (x < minX) minX = x;
|
|
99
|
+
if (y < minY) minY = y;
|
|
100
|
+
if (x > maxX) maxX = x;
|
|
101
|
+
if (y > maxY) maxY = y;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (maxX < minX || maxY < minY) {
|
|
106
|
+
return {
|
|
107
|
+
buffer: new Uint8Array(0),
|
|
108
|
+
width: 0,
|
|
109
|
+
height: 0,
|
|
110
|
+
offsetX: 0,
|
|
111
|
+
offsetY: 0,
|
|
112
|
+
originalWidth,
|
|
113
|
+
originalHeight,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const trimmedWidth = maxX - minX + 1;
|
|
118
|
+
const trimmedHeight = maxY - minY + 1;
|
|
119
|
+
const buffer = await encoder(input)
|
|
120
|
+
.extract({
|
|
121
|
+
left: minX,
|
|
122
|
+
top: minY,
|
|
123
|
+
width: trimmedWidth,
|
|
124
|
+
height: trimmedHeight,
|
|
125
|
+
})
|
|
126
|
+
.toBuffer();
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
buffer,
|
|
130
|
+
width: trimmedWidth,
|
|
131
|
+
height: trimmedHeight,
|
|
132
|
+
offsetX: minX,
|
|
133
|
+
offsetY: minY,
|
|
134
|
+
originalWidth,
|
|
135
|
+
originalHeight,
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
// Trim failed (e.g. JPEG without alpha, nothing to trim) — return original
|
|
139
|
+
const buf = await encoder(input).png().toBuffer();
|
|
140
|
+
return {
|
|
141
|
+
buffer: buf,
|
|
142
|
+
width: originalWidth,
|
|
143
|
+
height: originalHeight,
|
|
144
|
+
offsetX: 0,
|
|
145
|
+
offsetY: 0,
|
|
146
|
+
originalWidth,
|
|
147
|
+
originalHeight,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve an ImageResource to its actual file path on disk.
|
|
154
|
+
*/
|
|
155
|
+
export type InputItem = {
|
|
156
|
+
id: string;
|
|
157
|
+
width: number;
|
|
158
|
+
height: number;
|
|
159
|
+
originalWidth: number;
|
|
160
|
+
originalHeight: number;
|
|
161
|
+
offsetX: number;
|
|
162
|
+
offsetY: number;
|
|
163
|
+
resource: PackInputResource;
|
|
164
|
+
trimBuffer?: Uint8Array;
|
|
165
|
+
rasterizedBuffer?: Uint8Array;
|
|
166
|
+
sourceKind: 'image' | 'movieclip-frame';
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export interface StandaloneAtlasGroup {
|
|
170
|
+
resource: PackInputResource;
|
|
171
|
+
branchName: string;
|
|
172
|
+
branchOrdinal: number;
|
|
173
|
+
sizeMode: 'default' | 'npot' | 'multipleOf4';
|
|
174
|
+
inputs: InputItem[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface PagedAtlasGroup {
|
|
178
|
+
pageIndex: number;
|
|
179
|
+
branchName: string;
|
|
180
|
+
branchOrdinal: number;
|
|
181
|
+
inputs: InputItem[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Collect a single ImageResource into the inputs array. */
|
|
185
|
+
export async function collectImage(
|
|
186
|
+
resource: ImageResource,
|
|
187
|
+
pkg: Package,
|
|
188
|
+
inputs: InputItem[],
|
|
189
|
+
encoder: AtlasRasterBackend | undefined,
|
|
190
|
+
options: AtlasOptions,
|
|
191
|
+
doTrim: boolean,
|
|
192
|
+
logger: ILogger,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
let origW = resource.getWidth() ?? 0;
|
|
195
|
+
let origH = resource.getHeight() ?? 0;
|
|
196
|
+
const declaredWidth = origW;
|
|
197
|
+
const declaredHeight = origH;
|
|
198
|
+
let sourceHasAlpha = false;
|
|
199
|
+
let rasterizedBuffer: Uint8Array | undefined;
|
|
200
|
+
|
|
201
|
+
if (encoder && options.basePath) {
|
|
202
|
+
const filePath = resolveImagePath(resource, pkg, options.basePath);
|
|
203
|
+
try {
|
|
204
|
+
const metadata = await encoder(filePath).metadata();
|
|
205
|
+
if (origW === 0 || origH === 0) {
|
|
206
|
+
origW = metadata.width ?? 0;
|
|
207
|
+
origH = metadata.height ?? 0;
|
|
208
|
+
resource.setWidth(origW);
|
|
209
|
+
resource.setHeight(origH);
|
|
210
|
+
}
|
|
211
|
+
sourceHasAlpha = metadata.hasAlpha === true || metadata.channels === 4;
|
|
212
|
+
if (/\.svg$/i.test(resolveImageFileName(resource)) && declaredWidth > 0 && declaredHeight > 0) {
|
|
213
|
+
rasterizedBuffer = await encoder(filePath)
|
|
214
|
+
.resize({ width: declaredWidth, height: declaredHeight, fit: 'fill' })
|
|
215
|
+
.png()
|
|
216
|
+
.toBuffer();
|
|
217
|
+
sourceHasAlpha = true;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
if (options.strictOutput) {
|
|
221
|
+
throw new Error(`atlas: Could not read image "${filePath}".`);
|
|
222
|
+
}
|
|
223
|
+
if (origW === 0 || origH === 0) {
|
|
224
|
+
logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (origW <= 0 || origH <= 0) return;
|
|
231
|
+
|
|
232
|
+
let packW = origW,
|
|
233
|
+
packH = origH,
|
|
234
|
+
offX = 0,
|
|
235
|
+
offY = 0;
|
|
236
|
+
let trimBuf: Uint8Array | undefined;
|
|
237
|
+
|
|
238
|
+
if (doTrim && sourceHasAlpha && options.basePath && encoder) {
|
|
239
|
+
const filePath = resolveImagePath(resource, pkg, options.basePath);
|
|
240
|
+
try {
|
|
241
|
+
const trimResult = await _trimImage(encoder, rasterizedBuffer ?? filePath, origW, origH);
|
|
242
|
+
packW = trimResult.width;
|
|
243
|
+
packH = trimResult.height;
|
|
244
|
+
offX = trimResult.offsetX;
|
|
245
|
+
offY = trimResult.offsetY;
|
|
246
|
+
trimBuf = trimResult.buffer;
|
|
247
|
+
} catch {
|
|
248
|
+
logger.warn(`atlas: Could not trim "${filePath}", using original.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
inputs.push({
|
|
253
|
+
id: getPublishedItemId(resource),
|
|
254
|
+
width: packW,
|
|
255
|
+
height: packH,
|
|
256
|
+
originalWidth: origW,
|
|
257
|
+
originalHeight: origH,
|
|
258
|
+
offsetX: offX,
|
|
259
|
+
offsetY: offY,
|
|
260
|
+
resource,
|
|
261
|
+
trimBuffer: trimBuf,
|
|
262
|
+
rasterizedBuffer,
|
|
263
|
+
sourceKind: 'image',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Collect MovieClip frame textures from a .jta file into the inputs array. */
|
|
268
|
+
export async function collectMovieClipFrames(
|
|
269
|
+
doc: Document,
|
|
270
|
+
resource: MovieClipResource,
|
|
271
|
+
pkg: Package,
|
|
272
|
+
inputs: InputItem[],
|
|
273
|
+
encoder: AtlasRasterBackend | undefined,
|
|
274
|
+
options: AtlasOptions,
|
|
275
|
+
logger: ILogger,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
if (!options.basePath || !options.readFileRaw) {
|
|
278
|
+
if (options.strictOutput) {
|
|
279
|
+
throw new Error(`atlas: MovieClip "${resource.getId()}" requires basePath and readFileRaw for complete raster output.`);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!encoder && options.strictOutput) {
|
|
284
|
+
throw new Error(`atlas: MovieClip "${resource.getId()}" requires an encoder for complete raster output.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const mcId = resource.getId();
|
|
288
|
+
const mcName = resource.getName() + '.jta';
|
|
289
|
+
const mcPath = resource.getPath() ?? '/';
|
|
290
|
+
const filePath = `${options.basePath}/${pkg.getName()}${mcPath}${mcName}`;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const raw = await options.readFileRaw(filePath);
|
|
294
|
+
const jta = extractJtaFrames(raw);
|
|
295
|
+
if (jta.frames.length === 0) return;
|
|
296
|
+
|
|
297
|
+
const frameMetas = jta.meta?.frames ?? [];
|
|
298
|
+
for (const frame of resource.listFrames()) {
|
|
299
|
+
resource.removeFrame(frame);
|
|
300
|
+
}
|
|
301
|
+
resource
|
|
302
|
+
.setInterval(jta.meta?.interval ?? 100)
|
|
303
|
+
.setSwing(jta.meta?.swing ?? false)
|
|
304
|
+
.setRepeatDelay(jta.meta?.repeatDelay ?? 0);
|
|
305
|
+
|
|
306
|
+
if (frameMetas.length > 0) {
|
|
307
|
+
const firstFrameIndexByTextureIndex = new Map<number, number>();
|
|
308
|
+
for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
|
|
309
|
+
const meta = frameMetas[frameIndex];
|
|
310
|
+
const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
|
|
311
|
+
if (!firstFrameIndexByTextureIndex.has(textureIndex)) {
|
|
312
|
+
firstFrameIndexByTextureIndex.set(textureIndex, frameIndex);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const spriteIdByTextureIndex = new Map<number, string>();
|
|
317
|
+
for (let textureIndex = 0; textureIndex < jta.frames.length; textureIndex += 1) {
|
|
318
|
+
const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
|
|
319
|
+
if (exportFrameIndex === undefined) continue;
|
|
320
|
+
const itemId = `${mcId}_${exportFrameIndex}`;
|
|
321
|
+
const input = await createMovieClipFrameInput(
|
|
322
|
+
jta.frames[textureIndex],
|
|
323
|
+
itemId,
|
|
324
|
+
resource,
|
|
325
|
+
encoder,
|
|
326
|
+
options.strictOutput,
|
|
327
|
+
);
|
|
328
|
+
if (!input) continue;
|
|
329
|
+
inputs.push(input);
|
|
330
|
+
spriteIdByTextureIndex.set(textureIndex, itemId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
|
|
334
|
+
const meta = frameMetas[frameIndex];
|
|
335
|
+
const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
|
|
336
|
+
const frame = doc.createMovieFrame(`${mcId}_${frameIndex}`);
|
|
337
|
+
frame
|
|
338
|
+
.setRectX(meta.offsetX)
|
|
339
|
+
.setRectY(meta.offsetY)
|
|
340
|
+
.setRectWidth(meta.width)
|
|
341
|
+
.setRectHeight(meta.height)
|
|
342
|
+
.setAddDelay(meta.addDelay)
|
|
343
|
+
.setSpriteId(spriteIdByTextureIndex.get(textureIndex) ?? '');
|
|
344
|
+
resource.addFrame(frame);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
|
|
348
|
+
const itemId = `${mcId}_${frameIndex}`;
|
|
349
|
+
const input = await createMovieClipFrameInput(
|
|
350
|
+
jta.frames[frameIndex],
|
|
351
|
+
itemId,
|
|
352
|
+
resource,
|
|
353
|
+
encoder,
|
|
354
|
+
options.strictOutput,
|
|
355
|
+
);
|
|
356
|
+
if (!input) continue;
|
|
357
|
+
inputs.push(input);
|
|
358
|
+
const frame = doc.createMovieFrame(itemId);
|
|
359
|
+
frame
|
|
360
|
+
.setRectX(0)
|
|
361
|
+
.setRectY(0)
|
|
362
|
+
.setRectWidth(input.originalWidth)
|
|
363
|
+
.setRectHeight(input.originalHeight)
|
|
364
|
+
.setAddDelay(0)
|
|
365
|
+
.setSpriteId(itemId);
|
|
366
|
+
resource.addFrame(frame);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if ((jta.meta?.width ?? 0) > 0 && (jta.meta?.height ?? 0) > 0) {
|
|
371
|
+
resource.setWidth(jta.meta?.width ?? 0);
|
|
372
|
+
resource.setHeight(jta.meta?.height ?? 0);
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
const message = `atlas: Could not parse MovieClip "${filePath}".`;
|
|
376
|
+
if (options.strictOutput) throw new Error(message);
|
|
377
|
+
logger.warn(`${message} Skipping frames.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function createMovieClipFrameInput(
|
|
382
|
+
buffer: Uint8Array,
|
|
383
|
+
itemId: string,
|
|
384
|
+
resource: MovieClipResource,
|
|
385
|
+
encoder: AtlasRasterBackend | undefined,
|
|
386
|
+
strictOutput: boolean,
|
|
387
|
+
): Promise<InputItem | null> {
|
|
388
|
+
if (!encoder || buffer.length === 0) return null;
|
|
389
|
+
try {
|
|
390
|
+
const meta = await encoder(buffer).metadata();
|
|
391
|
+
const width = meta.width ?? 0;
|
|
392
|
+
const height = meta.height ?? 0;
|
|
393
|
+
if (width <= 0 || height <= 0) return null;
|
|
394
|
+
return {
|
|
395
|
+
id: itemId,
|
|
396
|
+
width,
|
|
397
|
+
height,
|
|
398
|
+
originalWidth: width,
|
|
399
|
+
originalHeight: height,
|
|
400
|
+
offsetX: 0,
|
|
401
|
+
offsetY: 0,
|
|
402
|
+
resource,
|
|
403
|
+
trimBuffer: buffer,
|
|
404
|
+
sourceKind: 'movieclip-frame',
|
|
405
|
+
};
|
|
406
|
+
} catch {
|
|
407
|
+
if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Collect a Bitmap Font's texture image, packed under the font's ID. */
|
|
413
|
+
export async function collectFontTexture(
|
|
414
|
+
doc: Document,
|
|
415
|
+
fontRes: FontResource,
|
|
416
|
+
pkg: Package,
|
|
417
|
+
options: AtlasOptions,
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
const textureId = fontRes.getTextureId?.() ?? '';
|
|
420
|
+
|
|
421
|
+
if (textureId) {
|
|
422
|
+
// Record font→texture mapping so we can add a duplicate sprite entry
|
|
423
|
+
// after atlas packing. The editor stores both the image ID (jb800) and
|
|
424
|
+
// the font ID (wa8u2r) as separate sprites at the same atlas position.
|
|
425
|
+
const fontId = fontRes.getId();
|
|
426
|
+
fontRes.setExtras({ ...fontRes.getExtras(), _fontSpriteAlias: { fontId, textureId } });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Parse .fnt file for glyph data (needed for binary encoding)
|
|
430
|
+
// This applies to ALL fonts, not just those with a textureId
|
|
431
|
+
if (options.readFileRaw && options.basePath) {
|
|
432
|
+
const fontName = resolveFontFileName(fontRes.getName());
|
|
433
|
+
const fontPath = fontRes.getPath() ?? '/';
|
|
434
|
+
const pkgName = pkg.getName();
|
|
435
|
+
const fntFile = `${options.basePath}/${pkgName}${fontPath}${fontName}`;
|
|
436
|
+
try {
|
|
437
|
+
const fntData = await options.readFileRaw(fntFile);
|
|
438
|
+
const fntText = new TextDecoder().decode(fntData);
|
|
439
|
+
const fntParsed = parseFnt(fntText);
|
|
440
|
+
for (const glyph of fontRes.listGlyphs()) {
|
|
441
|
+
fontRes.removeGlyph(glyph);
|
|
442
|
+
}
|
|
443
|
+
fontRes
|
|
444
|
+
.setTtf(fntParsed.hasFace)
|
|
445
|
+
.setTint(fntParsed.colored)
|
|
446
|
+
.setAutoScale(fntParsed.resizable)
|
|
447
|
+
.setHasChannel(fntParsed.hasChannel)
|
|
448
|
+
.setFontSize(fntParsed.fontSize)
|
|
449
|
+
.setXAdvance(fntParsed.xadvance)
|
|
450
|
+
.setLineHeight(fntParsed.lineHeight);
|
|
451
|
+
for (const item of fntParsed.glyphs) {
|
|
452
|
+
const glyph = doc.createFontGlyph(`${fontRes.getId()}_${item.charId}`);
|
|
453
|
+
glyph
|
|
454
|
+
.setCharId(item.charId)
|
|
455
|
+
.setChar(item.charId > 0 ? String.fromCodePoint(item.charId) : '')
|
|
456
|
+
.setImg(item.img ?? '')
|
|
457
|
+
.setX(item.x)
|
|
458
|
+
.setY(item.y)
|
|
459
|
+
.setXOffset(item.xoffset)
|
|
460
|
+
.setYOffset(item.yoffset)
|
|
461
|
+
.setWidth(item.width)
|
|
462
|
+
.setHeight(item.height)
|
|
463
|
+
.setAdvance(item.xadvance)
|
|
464
|
+
.setLineHeight(fntParsed.lineHeight)
|
|
465
|
+
.setChannel(item.channel);
|
|
466
|
+
fontRes.addGlyph(glyph);
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
/* .fnt not found */
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function isPackableResource(resource: PackageResource): resource is PackableResource {
|
|
475
|
+
return isImageResource(resource) || isMovieClipResource(resource) || isFontResource(resource);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function isResolvedBuffer(value: Uint8Array | AtlasRasterResolvedBuffer): value is AtlasRasterResolvedBuffer {
|
|
479
|
+
return typeof value === 'object' && value !== null && 'data' in value && 'info' in value;
|
|
480
|
+
}
|
package/src/atlas/jta.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
export interface JtaFrameMeta {
|
|
2
|
+
addDelay: number;
|
|
3
|
+
offsetX: number;
|
|
4
|
+
offsetY: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
textureIndex: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface JtaMeta {
|
|
11
|
+
interval: number;
|
|
12
|
+
repeatDelay: number;
|
|
13
|
+
swing: boolean;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
frames: JtaFrameMeta[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExtractedJtaData {
|
|
20
|
+
frames: Uint8Array[];
|
|
21
|
+
meta?: JtaMeta;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
25
|
+
|
|
26
|
+
export function extractJtaFrames(data: Uint8Array): ExtractedJtaData {
|
|
27
|
+
const frames: Uint8Array[] = [];
|
|
28
|
+
let offset = 0;
|
|
29
|
+
let firstPngOffset = -1;
|
|
30
|
+
|
|
31
|
+
while (offset < data.length) {
|
|
32
|
+
const signatureIndex = findPngSignature(data, offset);
|
|
33
|
+
if (signatureIndex === -1) break;
|
|
34
|
+
if (firstPngOffset === -1) firstPngOffset = signatureIndex;
|
|
35
|
+
const end = findPngEnd(data, signatureIndex);
|
|
36
|
+
if (end === -1) break;
|
|
37
|
+
frames.push(data.subarray(signatureIndex, end));
|
|
38
|
+
offset = end;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (firstPngOffset === -1 || frames.length === 0) {
|
|
42
|
+
return { frames: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
frames,
|
|
47
|
+
meta: parseJtaHeader(data, firstPngOffset, frames.length),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findPngSignature(data: Uint8Array, fromIndex: number): number {
|
|
52
|
+
for (let index = fromIndex; index <= data.length - PNG_SIGNATURE.length; index += 1) {
|
|
53
|
+
let matched = true;
|
|
54
|
+
for (let signatureIndex = 0; signatureIndex < PNG_SIGNATURE.length; signatureIndex += 1) {
|
|
55
|
+
if (data[index + signatureIndex] !== PNG_SIGNATURE[signatureIndex]) {
|
|
56
|
+
matched = false;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (matched) return index;
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findPngEnd(data: Uint8Array, start: number): number {
|
|
66
|
+
let position = start + PNG_SIGNATURE.length;
|
|
67
|
+
while (position + 8 <= data.length) {
|
|
68
|
+
const length = readUint32BE(data, position);
|
|
69
|
+
position += 8;
|
|
70
|
+
if (position + length + 4 > data.length) return -1;
|
|
71
|
+
const isEnd =
|
|
72
|
+
data[position - 4] === 0x49 &&
|
|
73
|
+
data[position - 3] === 0x45 &&
|
|
74
|
+
data[position - 2] === 0x4e &&
|
|
75
|
+
data[position - 1] === 0x44;
|
|
76
|
+
position += length + 4;
|
|
77
|
+
if (isEnd) return position;
|
|
78
|
+
}
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJtaHeader(data: Uint8Array, firstPngOffset: number, frameCount: number): JtaMeta | undefined {
|
|
83
|
+
if (data.length < 10) return undefined;
|
|
84
|
+
|
|
85
|
+
const state = { offset: 0 };
|
|
86
|
+
const end = Math.min(firstPngOffset, data.length);
|
|
87
|
+
const mark = readUtfBE(data, state, end);
|
|
88
|
+
if (!mark) return undefined;
|
|
89
|
+
|
|
90
|
+
const version = readInt32BEAt(data, state, end);
|
|
91
|
+
if (version == null) return undefined;
|
|
92
|
+
|
|
93
|
+
const fpsRaw = readInt8At(data, state, end);
|
|
94
|
+
if (fpsRaw == null) return undefined;
|
|
95
|
+
const fps = fpsRaw > 0 ? fpsRaw : 24;
|
|
96
|
+
|
|
97
|
+
if (state.offset + 3 > end) return undefined;
|
|
98
|
+
state.offset += 3;
|
|
99
|
+
|
|
100
|
+
if (version < 102) return undefined;
|
|
101
|
+
|
|
102
|
+
readUint16BEAt(data, state, end);
|
|
103
|
+
readUint16BEAt(data, state, end);
|
|
104
|
+
const width = readUint16BEAt(data, state, end);
|
|
105
|
+
const height = readUint16BEAt(data, state, end);
|
|
106
|
+
if (width == null || height == null) return undefined;
|
|
107
|
+
|
|
108
|
+
const speedRaw = readUint8At(data, state, end);
|
|
109
|
+
const repeatDelayRaw = readUint8At(data, state, end);
|
|
110
|
+
const swingRaw = readInt8At(data, state, end);
|
|
111
|
+
const frameTableCount = readInt16BEAt(data, state, end);
|
|
112
|
+
if (speedRaw == null || repeatDelayRaw == null || swingRaw == null || frameTableCount == null) return undefined;
|
|
113
|
+
|
|
114
|
+
const frames: JtaFrameMeta[] = [];
|
|
115
|
+
for (let index = 0; index < frameTableCount; index += 1) {
|
|
116
|
+
const delayRaw = readInt16BEAt(data, state, end);
|
|
117
|
+
const offsetX = readInt16BEAt(data, state, end);
|
|
118
|
+
const offsetY = readInt16BEAt(data, state, end);
|
|
119
|
+
const frameWidth = readInt16BEAt(data, state, end);
|
|
120
|
+
const frameHeight = readInt16BEAt(data, state, end);
|
|
121
|
+
const textureIndex = readInt16BEAt(data, state, end);
|
|
122
|
+
if (
|
|
123
|
+
delayRaw == null ||
|
|
124
|
+
offsetX == null ||
|
|
125
|
+
offsetY == null ||
|
|
126
|
+
frameWidth == null ||
|
|
127
|
+
frameHeight == null ||
|
|
128
|
+
textureIndex == null
|
|
129
|
+
) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
frames.push({
|
|
133
|
+
addDelay: Math.trunc((1000 / fps) * delayRaw),
|
|
134
|
+
offsetX,
|
|
135
|
+
offsetY,
|
|
136
|
+
width: frameWidth,
|
|
137
|
+
height: frameHeight,
|
|
138
|
+
textureIndex,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
interval: Math.trunc((1000 / fps) * (speedRaw || 1)),
|
|
144
|
+
repeatDelay: Math.trunc((1000 / fps) * repeatDelayRaw),
|
|
145
|
+
swing: swingRaw === 1,
|
|
146
|
+
width,
|
|
147
|
+
height,
|
|
148
|
+
frames: frames.length === 0 && frameCount > 0 ? [] : frames,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readUtfBE(data: Uint8Array, state: { offset: number }, end: number): string | null {
|
|
153
|
+
const length = readUint16BEAt(data, state, end);
|
|
154
|
+
if (length == null || state.offset + length > end) return null;
|
|
155
|
+
const value = new TextDecoder().decode(data.subarray(state.offset, state.offset + length));
|
|
156
|
+
state.offset += length;
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readUint8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
161
|
+
if (state.offset + 1 > end) return null;
|
|
162
|
+
const value = data[state.offset];
|
|
163
|
+
state.offset += 1;
|
|
164
|
+
return value ?? 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readInt8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
168
|
+
if (state.offset + 1 > end) return null;
|
|
169
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
170
|
+
const value = view.getInt8(state.offset);
|
|
171
|
+
state.offset += 1;
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readUint16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
176
|
+
if (state.offset + 2 > end) return null;
|
|
177
|
+
const value = readUint16BE(data, state.offset);
|
|
178
|
+
state.offset += 2;
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readInt16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
183
|
+
if (state.offset + 2 > end) return null;
|
|
184
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
185
|
+
const value = view.getInt16(state.offset, false);
|
|
186
|
+
state.offset += 2;
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readInt32BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
191
|
+
if (state.offset + 4 > end) return null;
|
|
192
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
193
|
+
const value = view.getInt32(state.offset, false);
|
|
194
|
+
state.offset += 4;
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readUint16BE(data: Uint8Array, offset: number): number {
|
|
199
|
+
if (offset + 1 >= data.length) return 0;
|
|
200
|
+
return (data[offset] << 8) | data[offset + 1];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readUint32BE(data: Uint8Array, offset: number): number {
|
|
204
|
+
if (offset + 3 >= data.length) return 0;
|
|
205
|
+
return (
|
|
206
|
+
data[offset] * 0x1000000 +
|
|
207
|
+
((data[offset + 1] ?? 0) << 16) +
|
|
208
|
+
((data[offset + 2] ?? 0) << 8) +
|
|
209
|
+
(data[offset + 3] ?? 0)
|
|
210
|
+
);
|
|
211
|
+
}
|