@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.
Files changed (38) hide show
  1. package/README.md +7 -2
  2. package/dist/index.cjs +3 -920
  3. package/dist/index.d.cts +2 -46
  4. package/dist/index.d.ts +3 -47
  5. package/dist/index.js +2 -919
  6. package/dist/node.cjs +138 -10
  7. package/dist/node.d.cts +7 -2
  8. package/dist/node.d.ts +8 -3
  9. package/dist/node.js +138 -11
  10. package/dist/{publish-DsPK0SJ1.js → publish-BJ_eelME.js} +1973 -2070
  11. package/dist/{publish-D7268_u4.cjs → publish-xFWT9Slz.cjs} +1973 -2070
  12. package/dist/restore-BW2xacB3.cjs +936 -0
  13. package/dist/{codegen-B8ZM1F4j.d.cts → restore-BeWaJNjR.d.cts} +49 -3
  14. package/dist/restore-Clk62n0O.js +931 -0
  15. package/dist/{codegen-CfbDHuFt.d.ts → restore-Dh0-Nvms.d.ts} +50 -4
  16. package/dist/web.cjs +4 -2
  17. package/dist/web.d.ts +1 -1
  18. package/dist/web.js +4 -2
  19. package/package.json +5 -2
  20. package/src/adapters/node/restore.ts +187 -0
  21. package/src/adapters/web/publish.ts +1 -247
  22. package/src/adapters/web/raster.ts +251 -0
  23. package/src/atlas/font.ts +95 -0
  24. package/src/atlas/inputs.ts +515 -0
  25. package/src/atlas/jta.ts +211 -0
  26. package/src/atlas/packing.ts +767 -0
  27. package/src/atlas.ts +23 -1639
  28. package/src/node.ts +4 -0
  29. package/src/publish/external-resources.ts +117 -0
  30. package/src/publish/options.ts +180 -0
  31. package/src/publish/package-context.ts +608 -0
  32. package/src/publish/resource-references.ts +210 -0
  33. package/src/publish.ts +21 -1130
  34. package/src/restore-internals/font.ts +100 -0
  35. package/src/restore-internals/movie-clip.ts +104 -0
  36. package/src/restore-internals/output-transaction.ts +164 -0
  37. package/src/restore.ts +14 -329
  38. /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-CDn6TirX.js";
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
- export { ResolvedPublishAtlasOptions as A, HasOptionalFont as C, RootProjectSettings as D, PublishDependency as E, publish as M, resolvePublishOptions as N, PublishOptions as O, ExtrasMap as S, HasOptionalUrl as T, PluginManifest as _, PublishCodeGenerationOptions as a, CliCodeGenerationSettings as b, decodeText as c, resolvePackageCodegenPlan as d, CodeWriter as f, Plugin as g, MaybePromise as h, CodegenReferencedComponent as i, ResolvedPublishOptions as j, ResolvePublishOptionsOverrides as k, encodeText as l, LoadedPlugin as m, CodegenClass as n, ResolvedPackageCodegenPlan as o, ICodeWriterConfig as p, CodegenMember as r, buildCodegenClasses as s, AUTO_GENERATED_CODE_MARK as t, publishCodeGeneration as u, PluginModule as v, HasOptionalSrc as w, CliPublishSettings as x, CliAtlasSettings as y };
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-D7268_u4.cjs");
2
+ const require_publish = require("./publish-xFWT9Slz.cjs");
3
3
  let _openfairygui_core = require("@openfairygui/core");
4
- //#region src/adapters/web/publish.ts
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-CDn6TirX.js";
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-DsPK0SJ1.js";
1
+ import { t as publish } from "./publish-BJ_eelME.js";
2
2
  import { ProjectType } from "@openfairygui/core";
3
- //#region src/adapters/web/publish.ts
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.19",
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.19"
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>,