@openfairygui/functions 0.2.0-alpha.19 → 0.2.0-alpha.20
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 +7 -2
- package/dist/index.cjs +3 -920
- package/dist/index.d.cts +2 -46
- package/dist/index.d.ts +3 -47
- package/dist/index.js +2 -919
- package/dist/node.cjs +138 -10
- package/dist/node.d.cts +7 -2
- package/dist/node.d.ts +8 -3
- package/dist/node.js +138 -11
- package/dist/{publish-DsPK0SJ1.js → publish-BJ_eelME.js} +1973 -2070
- package/dist/{publish-D7268_u4.cjs → publish-xFWT9Slz.cjs} +1973 -2070
- package/dist/restore-BW2xacB3.cjs +936 -0
- package/dist/{codegen-B8ZM1F4j.d.cts → restore-BeWaJNjR.d.cts} +49 -3
- package/dist/restore-Clk62n0O.js +931 -0
- package/dist/{codegen-CfbDHuFt.d.ts → restore-Dh0-Nvms.d.ts} +50 -4
- package/dist/web.cjs +4 -2
- package/dist/web.d.ts +1 -1
- package/dist/web.js +4 -2
- package/package.json +5 -2
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +1 -247
- package/src/adapters/web/raster.ts +251 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +515 -0
- package/src/atlas/jta.ts +211 -0
- package/src/atlas/packing.ts +767 -0
- package/src/atlas.ts +23 -1639
- package/src/node.ts +4 -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 +21 -1130
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +164 -0
- package/src/restore.ts +14 -329
- /package/dist/{atlas-CDn6TirX.d.ts → atlas-C6tbl7nn.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { l as PublishFileSystem, r as AtlasRasterBackend, t as AtlasOptions } from "./atlas-
|
|
2
|
-
import { Component, Document, Package, ProjectSettings, PublishSettings, Transform } from "@openfairygui/core";
|
|
1
|
+
import { l as PublishFileSystem, r as AtlasRasterBackend, t as AtlasOptions } from "./atlas-C6tbl7nn.js";
|
|
2
|
+
import { Component, Document, FileSystem, Package, ProjectSettings, PublishSettings, Transform } from "@openfairygui/core";
|
|
3
3
|
|
|
4
|
-
//#region src/publish.d.ts
|
|
4
|
+
//#region src/publish/options.d.ts
|
|
5
5
|
interface PublishOptions {
|
|
6
6
|
/**
|
|
7
7
|
* Output directory override for published files (.fui + atlas PNGs).
|
|
@@ -79,6 +79,8 @@ interface ResolvedPublishOptions {
|
|
|
79
79
|
* while callers still provide environment-specific concerns such as fs/encoder/basePath.
|
|
80
80
|
*/
|
|
81
81
|
declare function resolvePublishOptions(doc: Document, overrides?: ResolvePublishOptionsOverrides): ResolvedPublishOptions;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/publish.d.ts
|
|
82
84
|
/**
|
|
83
85
|
* Publishes a FairyGUI project.
|
|
84
86
|
*
|
|
@@ -239,4 +241,48 @@ declare function buildCodegenClasses(doc: Document, pkg: Package, plan: Resolved
|
|
|
239
241
|
declare function encodeText(value: string): Uint8Array;
|
|
240
242
|
declare function decodeText(value: Uint8Array): string;
|
|
241
243
|
//#endregion
|
|
242
|
-
|
|
244
|
+
//#region src/restore.d.ts
|
|
245
|
+
interface RestoreImageCropInput {
|
|
246
|
+
sourcePath: string;
|
|
247
|
+
outputPath: string;
|
|
248
|
+
left: number;
|
|
249
|
+
top: number;
|
|
250
|
+
width: number;
|
|
251
|
+
height: number;
|
|
252
|
+
rotated: boolean;
|
|
253
|
+
offsetX: number;
|
|
254
|
+
offsetY: number;
|
|
255
|
+
expectedWidth: number;
|
|
256
|
+
expectedHeight: number;
|
|
257
|
+
}
|
|
258
|
+
type RestoreImageCropper = (input: RestoreImageCropInput) => Promise<void>;
|
|
259
|
+
type RestoreImageExtractInput = Omit<RestoreImageCropInput, 'outputPath'>;
|
|
260
|
+
type RestoreImageExtractor = (input: RestoreImageExtractInput) => Promise<Uint8Array>;
|
|
261
|
+
interface RestoreResult {
|
|
262
|
+
document: Document;
|
|
263
|
+
projectPath: string;
|
|
264
|
+
warnings: string[];
|
|
265
|
+
}
|
|
266
|
+
interface RestoreFileSystem extends Pick<FileSystem, 'readFile' | 'readFileRaw' | 'writeFile' | 'writeFileRaw' | 'mkdir' | 'exists' | 'join' | 'dirname'> {
|
|
267
|
+
readdir(path: string): Promise<string[]>;
|
|
268
|
+
isFile(path: string): Promise<boolean>;
|
|
269
|
+
resolvePath(path: string): string | Promise<string>;
|
|
270
|
+
rm(path: string, options?: {
|
|
271
|
+
recursive?: boolean;
|
|
272
|
+
force?: boolean;
|
|
273
|
+
}): Promise<void>;
|
|
274
|
+
rename(from: string, to: string): Promise<void>;
|
|
275
|
+
}
|
|
276
|
+
interface RestoreOptions {
|
|
277
|
+
inputDir: string;
|
|
278
|
+
output: string;
|
|
279
|
+
fs: RestoreFileSystem;
|
|
280
|
+
packages?: string[];
|
|
281
|
+
force?: boolean;
|
|
282
|
+
projectType?: number;
|
|
283
|
+
cropImage?: RestoreImageCropper;
|
|
284
|
+
extractImage?: RestoreImageExtractor;
|
|
285
|
+
}
|
|
286
|
+
declare function restore(options: RestoreOptions): Promise<RestoreResult>;
|
|
287
|
+
//#endregion
|
|
288
|
+
export { ExtrasMap as A, ResolvedPublishOptions as B, MaybePromise as C, CliAtlasSettings as D, PluginModule as E, RootProjectSettings as F, publish as I, PublishOptions as L, HasOptionalSrc as M, HasOptionalUrl as N, CliCodeGenerationSettings as O, PublishDependency as P, ResolvePublishOptionsOverrides as R, LoadedPlugin as S, PluginManifest as T, resolvePublishOptions as V, encodeText as _, RestoreImageExtractor as a, CodeWriter as b, restore as c, CodegenMember as d, CodegenReferencedComponent as f, decodeText as g, buildCodegenClasses as h, RestoreImageExtractInput as i, HasOptionalFont as j, CliPublishSettings as k, AUTO_GENERATED_CODE_MARK as l, ResolvedPackageCodegenPlan as m, RestoreImageCropInput as n, RestoreOptions as o, PublishCodeGenerationOptions as p, RestoreImageCropper as r, RestoreResult as s, RestoreFileSystem as t, CodegenClass as u, publishCodeGeneration as v, Plugin as w, ICodeWriterConfig as x, resolvePackageCodegenPlan as y, ResolvedPublishAtlasOptions as z };
|
package/dist/web.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_publish = require("./publish-
|
|
2
|
+
const require_publish = require("./publish-xFWT9Slz.cjs");
|
|
3
3
|
let _openfairygui_core = require("@openfairygui/core");
|
|
4
|
-
//#region src/adapters/web/
|
|
4
|
+
//#region src/adapters/web/raster.ts
|
|
5
5
|
function getBrowserContext(canvas) {
|
|
6
6
|
const context = canvas.getContext("2d");
|
|
7
7
|
if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
|
|
@@ -169,6 +169,8 @@ function createBrowserImageEncoder(sourceFileSystem, outputFileSystem) {
|
|
|
169
169
|
return new BrowserImagePipeline(typeof input === "string" ? sourceFileSystem.readFileRaw(input).then((bytes) => decodeRaster(bytes, imageMimeType(input))) : input instanceof Uint8Array ? decode(input) : Promise.resolve(createRaster(input.create.width, input.create.height, input.create.background)), decode, outputFileSystem.writeFileRaw);
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/adapters/web/publish.ts
|
|
172
174
|
function createTrackingFileSystem(fileSystem, files) {
|
|
173
175
|
return {
|
|
174
176
|
join: (...paths) => fileSystem.join(...paths),
|
package/dist/web.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-
|
|
1
|
+
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-C6tbl7nn.js";
|
|
2
2
|
import { Document } from "@openfairygui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/web/publish.d.ts
|
package/dist/web.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as publish } from "./publish-
|
|
1
|
+
import { t as publish } from "./publish-BJ_eelME.js";
|
|
2
2
|
import { ProjectType } from "@openfairygui/core";
|
|
3
|
-
//#region src/adapters/web/
|
|
3
|
+
//#region src/adapters/web/raster.ts
|
|
4
4
|
function getBrowserContext(canvas) {
|
|
5
5
|
const context = canvas.getContext("2d");
|
|
6
6
|
if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
|
|
@@ -168,6 +168,8 @@ function createBrowserImageEncoder(sourceFileSystem, outputFileSystem) {
|
|
|
168
168
|
return new BrowserImagePipeline(typeof input === "string" ? sourceFileSystem.readFileRaw(input).then((bytes) => decodeRaster(bytes, imageMimeType(input))) : input instanceof Uint8Array ? decode(input) : Promise.resolve(createRaster(input.create.width, input.create.height, input.create.background)), decode, outputFileSystem.writeFileRaw);
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/adapters/web/publish.ts
|
|
171
173
|
function createTrackingFileSystem(fileSystem, files) {
|
|
172
174
|
return {
|
|
173
175
|
join: (...paths) => fileSystem.join(...paths),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/functions",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.20",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — composable transform functions.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"jiti": "^2.6.1",
|
|
77
|
-
"@openfairygui/core": "0.2.0-alpha.
|
|
77
|
+
"@openfairygui/core": "0.2.0-alpha.20"
|
|
78
|
+
},
|
|
79
|
+
"optionalDependencies": {
|
|
80
|
+
"sharp": ">=0.33.0"
|
|
78
81
|
},
|
|
79
82
|
"devDependencies": {
|
|
80
83
|
"ava": "^7.0.0",
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RestoreFileSystem,
|
|
3
|
+
RestoreImageCropInput,
|
|
4
|
+
RestoreImageCropper,
|
|
5
|
+
RestoreImageExtractInput,
|
|
6
|
+
RestoreImageExtractor,
|
|
7
|
+
RestoreOptions,
|
|
8
|
+
RestoreResult,
|
|
9
|
+
} from '../../restore.js';
|
|
10
|
+
import { restore } from '../../restore.js';
|
|
11
|
+
|
|
12
|
+
const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
|
|
13
|
+
|
|
14
|
+
interface RestoreImageProcessors {
|
|
15
|
+
cropImage: RestoreImageCropper;
|
|
16
|
+
extractImage: RestoreImageExtractor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RestoreNodeOptions extends Omit<RestoreOptions, 'fs' | 'cropImage' | 'extractImage'> {}
|
|
20
|
+
|
|
21
|
+
async function createNodeRestoreFileSystem(): Promise<RestoreFileSystem> {
|
|
22
|
+
const [fs, path] = await Promise.all([
|
|
23
|
+
importNative<typeof import('node:fs/promises')>('node:fs/promises'),
|
|
24
|
+
importNative<typeof import('node:path')>('node:path'),
|
|
25
|
+
]);
|
|
26
|
+
return {
|
|
27
|
+
async readFile(filePath: string): Promise<string> {
|
|
28
|
+
return fs.readFile(filePath, 'utf-8');
|
|
29
|
+
},
|
|
30
|
+
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
31
|
+
const buffer = await fs.readFile(filePath);
|
|
32
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
33
|
+
},
|
|
34
|
+
async writeFile(filePath: string, content: string): Promise<void> {
|
|
35
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
36
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
37
|
+
},
|
|
38
|
+
async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
39
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
40
|
+
await fs.writeFile(filePath, data);
|
|
41
|
+
},
|
|
42
|
+
async mkdir(dirPath: string): Promise<void> {
|
|
43
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
44
|
+
},
|
|
45
|
+
async readdir(dirPath: string): Promise<string[]> {
|
|
46
|
+
return fs.readdir(dirPath);
|
|
47
|
+
},
|
|
48
|
+
async exists(filePath: string): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
await fs.access(filePath);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async isFile(filePath: string): Promise<boolean> {
|
|
57
|
+
try {
|
|
58
|
+
return (await fs.stat(filePath)).isFile();
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async resolvePath(filePath: string): Promise<string> {
|
|
64
|
+
try {
|
|
65
|
+
return await fs.realpath(filePath);
|
|
66
|
+
} catch {
|
|
67
|
+
return path.resolve(filePath);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async rm(targetPath: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
|
|
71
|
+
await fs.rm(targetPath, {
|
|
72
|
+
recursive: options?.recursive ?? false,
|
|
73
|
+
force: options?.force ?? false,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
async rename(from: string, to: string): Promise<void> {
|
|
77
|
+
await fs.rename(from, to);
|
|
78
|
+
},
|
|
79
|
+
join(...paths: string[]): string {
|
|
80
|
+
return path.join(...paths);
|
|
81
|
+
},
|
|
82
|
+
dirname(filePath: string): string {
|
|
83
|
+
return path.dirname(filePath);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function createRestoreImageProcessors(): Promise<RestoreImageProcessors> {
|
|
89
|
+
let sharp: any;
|
|
90
|
+
try {
|
|
91
|
+
const loaded = await importNative<any>('sharp');
|
|
92
|
+
sharp = loaded.default ?? loaded;
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error('restoreNode: Sharp is required to crop atlas images. Install sharp before restoring.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function extractImage(input: RestoreImageExtractInput): Promise<Uint8Array> {
|
|
98
|
+
const targetPath = (input as RestoreImageCropInput).outputPath ?? input.sourcePath;
|
|
99
|
+
let image = sharp(input.sourcePath).extract({
|
|
100
|
+
left: input.left,
|
|
101
|
+
top: input.top,
|
|
102
|
+
width: input.width,
|
|
103
|
+
height: input.height,
|
|
104
|
+
});
|
|
105
|
+
if (input.rotated) image = image.rotate(90);
|
|
106
|
+
const { data, info } = await image.png().toBuffer({ resolveWithObject: true });
|
|
107
|
+
const needsOriginalCanvas =
|
|
108
|
+
input.expectedWidth > 0 &&
|
|
109
|
+
input.expectedHeight > 0 &&
|
|
110
|
+
(input.offsetX !== 0 ||
|
|
111
|
+
input.offsetY !== 0 ||
|
|
112
|
+
info.width !== input.expectedWidth ||
|
|
113
|
+
info.height !== input.expectedHeight);
|
|
114
|
+
|
|
115
|
+
if (needsOriginalCanvas) {
|
|
116
|
+
if (
|
|
117
|
+
input.offsetX < 0 ||
|
|
118
|
+
input.offsetY < 0 ||
|
|
119
|
+
input.offsetX + info.width > input.expectedWidth ||
|
|
120
|
+
input.offsetY + info.height > input.expectedHeight
|
|
121
|
+
) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`restore: Cropped image does not fit original canvas for ${targetPath}: ` +
|
|
124
|
+
`crop ${info.width}x${info.height} at ${input.offsetX},${input.offsetY}, ` +
|
|
125
|
+
`canvas ${input.expectedWidth}x${input.expectedHeight}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const composed = await sharp({
|
|
129
|
+
create: {
|
|
130
|
+
width: input.expectedWidth,
|
|
131
|
+
height: input.expectedHeight,
|
|
132
|
+
channels: 4,
|
|
133
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
.composite([{ input: data, left: input.offsetX, top: input.offsetY }])
|
|
137
|
+
.png()
|
|
138
|
+
.toBuffer({ resolveWithObject: true });
|
|
139
|
+
if (
|
|
140
|
+
composed.info.width !== input.expectedWidth ||
|
|
141
|
+
composed.info.height !== input.expectedHeight
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`restore: Cropped image size mismatch for ${targetPath}: ` +
|
|
145
|
+
`expected ${input.expectedWidth}x${input.expectedHeight}, ` +
|
|
146
|
+
`got ${composed.info.width}x${composed.info.height}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return composed.data;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
input.expectedWidth > 0 &&
|
|
154
|
+
input.expectedHeight > 0 &&
|
|
155
|
+
(info.width !== input.expectedWidth || info.height !== input.expectedHeight)
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`restore: Cropped image size mismatch for ${targetPath}: ` +
|
|
159
|
+
`expected ${input.expectedWidth}x${input.expectedHeight}, got ${info.width}x${info.height}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const fs = await importNative<typeof import('node:fs/promises')>('node:fs/promises');
|
|
166
|
+
const path = await importNative<typeof import('node:path')>('node:path');
|
|
167
|
+
return {
|
|
168
|
+
extractImage,
|
|
169
|
+
cropImage: async (input: RestoreImageCropInput): Promise<void> => {
|
|
170
|
+
await fs.mkdir(path.dirname(input.outputPath), { recursive: true });
|
|
171
|
+
await fs.writeFile(input.outputPath, await extractImage(input));
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Restore trusted local published artifacts through the standard Node host adapter. */
|
|
177
|
+
export async function restoreNode(options: RestoreNodeOptions): Promise<RestoreResult> {
|
|
178
|
+
const [fs, imageProcessors] = await Promise.all([
|
|
179
|
+
createNodeRestoreFileSystem(),
|
|
180
|
+
createRestoreImageProcessors(),
|
|
181
|
+
]);
|
|
182
|
+
return restore({
|
|
183
|
+
...options,
|
|
184
|
+
fs,
|
|
185
|
+
...imageProcessors,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -2,14 +2,11 @@ import { type Document, type ILogger, ProjectType } from '@openfairygui/core';
|
|
|
2
2
|
import type { AtlasOptions } from '../../atlas.js';
|
|
3
3
|
import { publish } from '../../publish.js';
|
|
4
4
|
import type {
|
|
5
|
-
AtlasRasterBackend,
|
|
6
|
-
AtlasRasterInput,
|
|
7
|
-
AtlasRasterPipeline,
|
|
8
|
-
AtlasRasterResolvedBuffer,
|
|
9
5
|
PublishFileSystem,
|
|
10
6
|
PublishOutputFileSystem,
|
|
11
7
|
PublishSourceFileSystem,
|
|
12
8
|
} from '../../publish/contracts.js';
|
|
9
|
+
import { assertBrowserImageSupport, createBrowserImageEncoder } from './raster.js';
|
|
13
10
|
|
|
14
11
|
export type BrowserPublishProjectType = 'layabox';
|
|
15
12
|
|
|
@@ -58,249 +55,6 @@ export interface BrowserPublishResult {
|
|
|
58
55
|
diagnostics: BrowserPublishDiagnostic[];
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
type BrowserCanvas = OffscreenCanvas | HTMLCanvasElement;
|
|
62
|
-
|
|
63
|
-
interface BrowserContext {
|
|
64
|
-
clearRect(x: number, y: number, width: number, height: number): void;
|
|
65
|
-
drawImage(image: CanvasImageSource, dx: number, dy: number): void;
|
|
66
|
-
drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
|
|
67
|
-
drawImage(
|
|
68
|
-
image: CanvasImageSource,
|
|
69
|
-
sx: number,
|
|
70
|
-
sy: number,
|
|
71
|
-
sw: number,
|
|
72
|
-
sh: number,
|
|
73
|
-
dx: number,
|
|
74
|
-
dy: number,
|
|
75
|
-
dw: number,
|
|
76
|
-
dh: number,
|
|
77
|
-
): void;
|
|
78
|
-
fillRect(x: number, y: number, width: number, height: number): void;
|
|
79
|
-
getImageData(sx: number, sy: number, sw: number, sh: number): ImageData;
|
|
80
|
-
rotate(angle: number): void;
|
|
81
|
-
restore(): void;
|
|
82
|
-
save(): void;
|
|
83
|
-
translate(x: number, y: number): void;
|
|
84
|
-
fillStyle: string | CanvasGradient | CanvasPattern;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface BrowserRaster {
|
|
88
|
-
canvas: BrowserCanvas;
|
|
89
|
-
width: number;
|
|
90
|
-
height: number;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function getBrowserContext(canvas: BrowserCanvas): BrowserContext {
|
|
94
|
-
const context = canvas.getContext('2d');
|
|
95
|
-
if (!context) throw new Error('publishBrowser: a 2D canvas context is unavailable.');
|
|
96
|
-
return context as unknown as BrowserContext;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function createBrowserCanvas(width: number, height: number): BrowserCanvas {
|
|
100
|
-
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(width, height);
|
|
101
|
-
if (typeof globalThis.document === 'undefined') {
|
|
102
|
-
throw new Error('publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.');
|
|
103
|
-
}
|
|
104
|
-
const canvas = globalThis.document.createElement('canvas');
|
|
105
|
-
canvas.width = width;
|
|
106
|
-
canvas.height = height;
|
|
107
|
-
return canvas;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function assertBrowserImageSupport(): void {
|
|
111
|
-
if (typeof createImageBitmap !== 'function') {
|
|
112
|
-
throw new Error('publishBrowser: createImageBitmap is required for atlas PNG generation.');
|
|
113
|
-
}
|
|
114
|
-
if (typeof OffscreenCanvas === 'undefined' && typeof globalThis.document === 'undefined') {
|
|
115
|
-
throw new Error('publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function createRaster(
|
|
120
|
-
width: number,
|
|
121
|
-
height: number,
|
|
122
|
-
background?: { r: number; g: number; b: number; alpha: number },
|
|
123
|
-
): BrowserRaster {
|
|
124
|
-
const canvas = createBrowserCanvas(width, height);
|
|
125
|
-
const context = getBrowserContext(canvas);
|
|
126
|
-
context.clearRect(0, 0, width, height);
|
|
127
|
-
if (background && background.alpha > 0) {
|
|
128
|
-
context.fillStyle = `rgba(${background.r}, ${background.g}, ${background.b}, ${background.alpha})`;
|
|
129
|
-
context.fillRect(0, 0, width, height);
|
|
130
|
-
}
|
|
131
|
-
return { canvas, width, height };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function imageMimeType(path: string): string {
|
|
135
|
-
if (/\.svg$/iu.test(path)) return 'image/svg+xml';
|
|
136
|
-
if (/\.jpe?g$/iu.test(path)) return 'image/jpeg';
|
|
137
|
-
if (/\.webp$/iu.test(path)) return 'image/webp';
|
|
138
|
-
if (/\.gif$/iu.test(path)) return 'image/gif';
|
|
139
|
-
return 'image/png';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function imageMimeTypeFromBytes(bytes: Uint8Array): string {
|
|
143
|
-
if (bytes[0] === 0xff && bytes[1] === 0xd8) return 'image/jpeg';
|
|
144
|
-
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) return 'image/gif';
|
|
145
|
-
if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) return 'image/webp';
|
|
146
|
-
return 'image/png';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function canvasToPng(canvas: BrowserCanvas): Promise<Uint8Array> {
|
|
150
|
-
let blob: Blob;
|
|
151
|
-
if ('convertToBlob' in canvas && typeof canvas.convertToBlob === 'function') {
|
|
152
|
-
blob = await canvas.convertToBlob({ type: 'image/png' });
|
|
153
|
-
} else {
|
|
154
|
-
blob = await new Promise<Blob>((resolve, reject) => {
|
|
155
|
-
(canvas as HTMLCanvasElement).toBlob((value) => {
|
|
156
|
-
if (value) resolve(value);
|
|
157
|
-
else reject(new Error('publishBrowser: canvas PNG encoding failed.'));
|
|
158
|
-
}, 'image/png');
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
return new Uint8Array(await blob.arrayBuffer());
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function decodeRaster(bytes: Uint8Array, mimeType: string): Promise<BrowserRaster> {
|
|
165
|
-
if (typeof createImageBitmap !== 'function') {
|
|
166
|
-
throw new Error('publishBrowser: createImageBitmap is required for atlas PNG generation.');
|
|
167
|
-
}
|
|
168
|
-
const copy = bytes.slice();
|
|
169
|
-
const bitmap = await createImageBitmap(new Blob([copy.buffer as ArrayBuffer], { type: mimeType }));
|
|
170
|
-
try {
|
|
171
|
-
const raster = createRaster(bitmap.width, bitmap.height);
|
|
172
|
-
getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
|
|
173
|
-
return raster;
|
|
174
|
-
} finally {
|
|
175
|
-
bitmap.close();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
class BrowserImagePipeline implements AtlasRasterPipeline {
|
|
180
|
-
private rawOutput = false;
|
|
181
|
-
|
|
182
|
-
constructor(
|
|
183
|
-
private raster: Promise<BrowserRaster>,
|
|
184
|
-
private readonly decode: (bytes: Uint8Array) => Promise<BrowserRaster>,
|
|
185
|
-
private readonly write: (path: string, data: Uint8Array) => Promise<void>,
|
|
186
|
-
) {}
|
|
187
|
-
|
|
188
|
-
ensureAlpha(): this {
|
|
189
|
-
return this;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
resize(options: { width: number; height: number; fit?: 'fill' }): this {
|
|
193
|
-
this.raster = this.raster.then((source) => {
|
|
194
|
-
const target = createRaster(options.width, options.height);
|
|
195
|
-
getBrowserContext(target.canvas).drawImage(source.canvas, 0, 0, options.width, options.height);
|
|
196
|
-
return target;
|
|
197
|
-
});
|
|
198
|
-
return this;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
raw(): this {
|
|
202
|
-
this.rawOutput = true;
|
|
203
|
-
return this;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
extract(options: { left: number; top: number; width: number; height: number }): this {
|
|
207
|
-
this.raster = this.raster.then((source) => {
|
|
208
|
-
const target = createRaster(options.width, options.height);
|
|
209
|
-
getBrowserContext(target.canvas).drawImage(
|
|
210
|
-
source.canvas,
|
|
211
|
-
options.left,
|
|
212
|
-
options.top,
|
|
213
|
-
options.width,
|
|
214
|
-
options.height,
|
|
215
|
-
0,
|
|
216
|
-
0,
|
|
217
|
-
options.width,
|
|
218
|
-
options.height,
|
|
219
|
-
);
|
|
220
|
-
return target;
|
|
221
|
-
});
|
|
222
|
-
return this;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
png(): this {
|
|
226
|
-
this.rawOutput = false;
|
|
227
|
-
return this;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
rotate(angle: number): this {
|
|
231
|
-
this.raster = this.raster.then((source) => {
|
|
232
|
-
if (angle % 180 === 0) return source;
|
|
233
|
-
const target = createRaster(source.height, source.width);
|
|
234
|
-
const context = getBrowserContext(target.canvas);
|
|
235
|
-
context.save();
|
|
236
|
-
if (angle === 270 || angle === -90) {
|
|
237
|
-
context.translate(0, source.width);
|
|
238
|
-
context.rotate(-Math.PI / 2);
|
|
239
|
-
} else {
|
|
240
|
-
context.translate(source.height, 0);
|
|
241
|
-
context.rotate(Math.PI / 2);
|
|
242
|
-
}
|
|
243
|
-
context.drawImage(source.canvas, 0, 0);
|
|
244
|
-
context.restore();
|
|
245
|
-
return target;
|
|
246
|
-
});
|
|
247
|
-
return this;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
composite(inputs: Array<{ input: Uint8Array; left: number; top: number }>): this {
|
|
251
|
-
this.raster = this.raster.then(async (target) => {
|
|
252
|
-
const context = getBrowserContext(target.canvas);
|
|
253
|
-
for (const input of inputs) {
|
|
254
|
-
const source = await this.decode(input.input);
|
|
255
|
-
context.drawImage(source.canvas, input.left, input.top);
|
|
256
|
-
}
|
|
257
|
-
return target;
|
|
258
|
-
});
|
|
259
|
-
return this;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async metadata(): Promise<{ width: number; height: number; channels: number; hasAlpha: boolean }> {
|
|
263
|
-
const raster = await this.raster;
|
|
264
|
-
return { width: raster.width, height: raster.height, channels: 4, hasAlpha: true };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async toBuffer(options: { resolveWithObject: true }): Promise<AtlasRasterResolvedBuffer>;
|
|
268
|
-
async toBuffer(options?: { resolveWithObject?: false }): Promise<Uint8Array>;
|
|
269
|
-
async toBuffer(options?: { resolveWithObject?: boolean }): Promise<Uint8Array | AtlasRasterResolvedBuffer> {
|
|
270
|
-
const raster = await this.raster;
|
|
271
|
-
if (options?.resolveWithObject) {
|
|
272
|
-
const data = getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data;
|
|
273
|
-
return { data: new Uint8Array(data), info: { width: raster.width, height: raster.height, channels: 4 } };
|
|
274
|
-
}
|
|
275
|
-
if (this.rawOutput)
|
|
276
|
-
return new Uint8Array(
|
|
277
|
-
getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data,
|
|
278
|
-
);
|
|
279
|
-
return canvasToPng(raster.canvas);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async toFile(path: string): Promise<void> {
|
|
283
|
-
const raster = await this.raster;
|
|
284
|
-
await this.write(path, await canvasToPng(raster.canvas));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function createBrowserImageEncoder(
|
|
289
|
-
sourceFileSystem: BrowserPublishSourceFileSystem,
|
|
290
|
-
outputFileSystem: PublishFileSystem,
|
|
291
|
-
): AtlasRasterBackend {
|
|
292
|
-
const decode = (bytes: Uint8Array) => decodeRaster(bytes, imageMimeTypeFromBytes(bytes));
|
|
293
|
-
return (input: AtlasRasterInput): BrowserImagePipeline => {
|
|
294
|
-
const raster =
|
|
295
|
-
typeof input === 'string'
|
|
296
|
-
? sourceFileSystem.readFileRaw(input).then((bytes) => decodeRaster(bytes, imageMimeType(input)))
|
|
297
|
-
: input instanceof Uint8Array
|
|
298
|
-
? decode(input)
|
|
299
|
-
: Promise.resolve(createRaster(input.create.width, input.create.height, input.create.background));
|
|
300
|
-
return new BrowserImagePipeline(raster, decode, outputFileSystem.writeFileRaw);
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
58
|
function createTrackingFileSystem(
|
|
305
59
|
fileSystem: BrowserPublishOutputFileSystem,
|
|
306
60
|
files: Map<string, number>,
|