facebetter 1.0.3 → 1.0.4

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.
@@ -0,0 +1,1123 @@
1
+ /**
2
+ * Facebetter Error Classes
3
+ * Platform-agnostic error handling
4
+ */
5
+
6
+ /**
7
+ * Facebetter error class
8
+ */
9
+ class FacebetterError extends Error {
10
+ constructor(message, code = -1) {
11
+ super(message);
12
+ this.name = 'FacebetterError';
13
+ this.code = code;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Checks if a result code indicates success
19
+ * @param {number} result - Result code from WASM function
20
+ * @throws {FacebetterError} If result indicates failure
21
+ */
22
+ function checkResult(result, errorMessage = 'Operation failed') {
23
+ if (result !== 0) {
24
+ throw new FacebetterError(`${errorMessage} (error code: ${result})`, result);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Engine Configuration
30
+ * Platform-agnostic configuration class
31
+ */
32
+
33
+
34
+ /**
35
+ * Engine configuration class
36
+ * Similar to EngineConfig struct in C++/Java/OC
37
+ */
38
+ class EngineConfig {
39
+ /**
40
+ * Creates a new EngineConfig instance
41
+ * @param {Object} config - Configuration object
42
+ * @param {string} [config.appId] - Application ID (required if licenseJson is not provided)
43
+ * @param {string} [config.appKey] - Application key (required if licenseJson is not provided)
44
+ * @param {string} [config.licenseJson] - License JSON string (optional, if provided, appId and appKey are not required)
45
+ */
46
+ constructor(config = {}) {
47
+ this.appId = config.appId || null;
48
+ this.appKey = config.appKey || null;
49
+ this.licenseJson = config.licenseJson || null;
50
+ this.resourcePath = '/facebetter/resource.bundle';
51
+ }
52
+
53
+ /**
54
+ * Validates the configuration
55
+ * @returns {boolean} True if valid
56
+ */
57
+ isValid() {
58
+ // 如果提供了 licenseJson,则使用授权数据验证
59
+ if (this.licenseJson) {
60
+ return typeof this.licenseJson === 'string' && this.licenseJson.trim() !== '';
61
+ }
62
+ // 否则需要 appId 和 appKey
63
+ return this.appId && typeof this.appId === 'string' && this.appId.trim() !== '' &&
64
+ this.appKey && typeof this.appKey === 'string' && this.appKey.trim() !== '';
65
+ }
66
+
67
+ /**
68
+ * Returns a string representation of the config
69
+ * @returns {string} String representation
70
+ */
71
+ toString() {
72
+ return `EngineConfig{appId='${this.appId ? this.appId.substring(0, Math.min(this.appId.length, 8)) + '...' : 'null'}', appKey='${this.appKey ? '***' : 'null'}', licenseJson=${this.licenseJson ? 'provided' : 'null'}}`;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Facebetter Constants and Enums
78
+ * Platform-agnostic constants
79
+ */
80
+
81
+ /**
82
+ * Beauty type enumeration
83
+ */
84
+ const BeautyType$1 = {
85
+ Basic: 0, // 基础美颜
86
+ Reshape: 1, // 面部重塑
87
+ Makeup: 2, // 美妆效果
88
+ Segmentation: 3 // 人像分割
89
+ };
90
+
91
+ /**
92
+ * Basic beauty parameter enumeration
93
+ */
94
+ const BasicParam$1 = {
95
+ Smoothing: 0, // 磨皮
96
+ Sharpening: 1, // 锐化
97
+ Whitening: 2, // 美白
98
+ Rosiness: 3 // 红润
99
+ };
100
+
101
+ /**
102
+ * Face reshape parameter enumeration
103
+ */
104
+ const ReshapeParam$1 = {
105
+ FaceThin: 0, // 瘦脸
106
+ FaceVShape: 1, // V脸
107
+ FaceNarrow: 2, // 窄脸
108
+ FaceShort: 3, // 短脸
109
+ Cheekbone: 4, // 颧骨
110
+ Jawbone: 5, // 下颌骨
111
+ Chin: 6, // 下巴
112
+ NoseSlim: 7, // 瘦鼻梁
113
+ EyeSize: 8, // 大眼
114
+ EyeDistance: 9 // 眼距
115
+ };
116
+
117
+ /**
118
+ * Makeup parameter enumeration
119
+ */
120
+ const MakeupParam$1 = {
121
+ Lipstick: 0, // 口红
122
+ Blush: 1 // 腮红
123
+ };
124
+
125
+ /**
126
+ * Background mode enumeration
127
+ */
128
+ const BackgroundMode$1 = {
129
+ None: 0, // 无背景处理
130
+ Blur: 1, // 模糊背景
131
+ Image: 2 // 背景图片替换
132
+ };
133
+
134
+ /**
135
+ * Process mode enumeration
136
+ */
137
+ const ProcessMode$1 = {
138
+ Image: 0, // 图片模式
139
+ Video: 1 // 视频模式
140
+ };
141
+
142
+ var constants = /*#__PURE__*/Object.freeze({
143
+ __proto__: null,
144
+ BackgroundMode: BackgroundMode$1,
145
+ BasicParam: BasicParam$1,
146
+ BeautyType: BeautyType$1,
147
+ MakeupParam: MakeupParam$1,
148
+ ProcessMode: ProcessMode$1,
149
+ ReshapeParam: ReshapeParam$1
150
+ });
151
+
152
+ /**
153
+ * Beauty Effect Engine Core
154
+ * Platform-agnostic engine implementation with dependency injection
155
+ */
156
+
157
+
158
+ /**
159
+ * Beauty effect engine class
160
+ * Provides high-level API for face beauty effects processing
161
+ * Uses dependency injection for platform-specific functionality
162
+ */
163
+ class BeautyEffectEngine {
164
+ /**
165
+ * Creates a new BeautyEffectEngine instance
166
+ * @param {EngineConfig|Object} config - EngineConfig object or configuration object
167
+ * @param {Object} platformAPI - Platform-specific API (injected)
168
+ * @param {Function} platformAPI.loadWasmModule - WASM module loader
169
+ * @param {Function} platformAPI.getWasmModule - Get WASM module instance
170
+ * @param {Function} platformAPI.getWasmBuffer - Get WASM memory buffer
171
+ * @param {Function} platformAPI.toImageData - Convert to ImageData
172
+ * @param {Function} platformAPI.verifyAppKeyOnline - Verify app key online
173
+ * @param {Function} [platformAPI.ensureGPUPixelCanvas] - Ensure GPU canvas exists (browser only)
174
+ * @param {Function} [platformAPI.cleanupGPUPixelCanvas] - Cleanup GPU canvas (browser only)
175
+ */
176
+ constructor(config, platformAPI) {
177
+ if (!config) {
178
+ throw new FacebetterError('Config is required. Expected EngineConfig or configuration object');
179
+ }
180
+
181
+ if (!platformAPI) {
182
+ throw new FacebetterError('Platform API is required');
183
+ }
184
+
185
+ // 如果传入的是 EngineConfig 实例,直接使用;否则创建新的 EngineConfig
186
+ let engineConfig;
187
+ if (config instanceof EngineConfig) {
188
+ engineConfig = config;
189
+ } else if (config && typeof config === 'object') {
190
+ engineConfig = new EngineConfig(config);
191
+ } else {
192
+ throw new FacebetterError('Invalid configuration. Expected EngineConfig or configuration object');
193
+ }
194
+
195
+ if (!engineConfig.isValid()) {
196
+ throw new FacebetterError('Invalid config: ' + engineConfig.toString());
197
+ }
198
+
199
+ this.config = engineConfig;
200
+ this.appId = engineConfig.appId;
201
+ this.appKey = engineConfig.appKey;
202
+ this.licenseJson = engineConfig.licenseJson;
203
+ this.resourcePath = '/facebetter/resource.bundle';
204
+ this.enginePtr = null;
205
+ this.initialized = false;
206
+ this.srcBufferPtr = null;
207
+ this.dstBufferPtr = null;
208
+ this.bufferSize = 0;
209
+
210
+ // Store platform API
211
+ this._platformAPI = platformAPI;
212
+ this._loadWasmModule = platformAPI.loadWasmModule;
213
+ this._getWasmModule = platformAPI.getWasmModule;
214
+ this._getWasmBuffer = platformAPI.getWasmBuffer;
215
+ this._toImageData = platformAPI.toImageData;
216
+ this._verifyAppKeyOnline = platformAPI.verifyAppKeyOnline;
217
+ this._ensureGPUPixelCanvas = platformAPI.ensureGPUPixelCanvas;
218
+ this._cleanupGPUPixelCanvas = platformAPI.cleanupGPUPixelCanvas;
219
+
220
+ // Browser-specific state
221
+ this._gpupixelCanvas = null;
222
+ this._createdGPUPixelCanvas = false;
223
+ this._offscreenCanvas = null;
224
+ this._offscreenCtx = null;
225
+
226
+ // Automatically create gpupixel_canvas if it doesn't exist (browser only)
227
+ if (this._ensureGPUPixelCanvas) {
228
+ this._ensureGPUPixelCanvas();
229
+ }
230
+
231
+ // Start loading WASM module immediately in constructor
232
+ this._wasmLoadPromise = this._loadWasmModule();
233
+ }
234
+
235
+ /**
236
+ * Initializes the engine
237
+ * @returns {Promise<void>} Promise that resolves when initialization is complete
238
+ */
239
+ async init() {
240
+ if (this.initialized) {
241
+ return;
242
+ }
243
+
244
+ // Wait for WASM module to be loaded (started in constructor)
245
+ await this._wasmLoadPromise;
246
+ const Module = this._getWasmModule();
247
+
248
+ let licenseJsonToUse = this.licenseJson;
249
+
250
+ // 如果用户提供了 appId 和 appKey(但没有提供 licenseJson),则在 JS 层发送 HTTP 请求
251
+ if (this.appId && this.appKey && !this.licenseJson && this._verifyAppKeyOnline) {
252
+ try {
253
+ // 在 JS 层调用 online_auth API 获取服务器响应
254
+ const authResponse = await this._verifyAppKeyOnline(this.appId, this.appKey);
255
+
256
+ if (!authResponse) {
257
+ throw new FacebetterError('Failed to get server response from online_auth API');
258
+ }
259
+
260
+ // 将服务器响应转换为 JSON 字符串,作为 licenseJson 传递给 WASM 层
261
+ licenseJsonToUse = JSON.stringify(authResponse);
262
+ console.log('Online auth response received, using as licenseJson');
263
+ } catch (error) {
264
+ throw new FacebetterError(`Failed to verify app key online: ${error.message}`);
265
+ }
266
+ }
267
+
268
+ // 如果用户直接提供了 licenseJson,则直接使用
269
+ // 如果通过 appId/appKey 获取到了响应,则使用响应作为 licenseJson
270
+ // WASM 层只需要验证 licenseJson,不需要发送 HTTP 请求
271
+ const enginePtr = Module.ccall(
272
+ 'CreateBeautyEffectEngine',
273
+ 'number',
274
+ ['string', 'string'],
275
+ [
276
+ this.resourcePath,
277
+ licenseJsonToUse || '' // 使用 licenseJson 验证
278
+ ]
279
+ );
280
+
281
+ if (!enginePtr) {
282
+ throw new FacebetterError('Failed to create BeautyEffect engine');
283
+ }
284
+
285
+ this.enginePtr = enginePtr;
286
+ this.initialized = true;
287
+ }
288
+
289
+ /**
290
+ * Destroys the engine and releases resources
291
+ */
292
+ destroy() {
293
+ if (!this.initialized || !this.enginePtr) {
294
+ return;
295
+ }
296
+
297
+ const Module = this._getWasmModule();
298
+
299
+ if (this.srcBufferPtr) {
300
+ Module._free(this.srcBufferPtr);
301
+ this.srcBufferPtr = null;
302
+ }
303
+
304
+ if (this.dstBufferPtr) {
305
+ Module._free(this.dstBufferPtr);
306
+ this.dstBufferPtr = null;
307
+ }
308
+
309
+ Module.ccall('DestroyBeautyEffectEngine', null, ['number'], [this.enginePtr]);
310
+ this.enginePtr = null;
311
+ this.initialized = false;
312
+ this.bufferSize = 0;
313
+
314
+ // Clean up offscreen canvas
315
+ if (this._offscreenCanvas) {
316
+ this._offscreenCanvas = null;
317
+ this._offscreenCtx = null;
318
+ }
319
+
320
+ // Clean up gpupixel_canvas if we created it
321
+ if (this._cleanupGPUPixelCanvas) {
322
+ this._cleanupGPUPixelCanvas();
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Sets log configuration
328
+ * Can be called before init() since SetLogConfig is a global function
329
+ * @param {Object} config - Log configuration
330
+ * @param {boolean} config.consoleEnabled - Enable console logging
331
+ * @param {boolean} config.fileEnabled - Enable file logging
332
+ * @param {number} config.level - Log level
333
+ * @param {string} config.fileName - Log file name
334
+ * @returns {Promise<void>} Promise that resolves when log config is set
335
+ */
336
+ async setLogConfig(config) {
337
+ // Wait for WASM module to be loaded (started in constructor)
338
+ await this._wasmLoadPromise;
339
+ const Module = this._getWasmModule();
340
+
341
+ const result = Module.ccall(
342
+ 'SetLogConfig',
343
+ 'number',
344
+ ['bool', 'bool', 'number', 'string'],
345
+ [
346
+ config.consoleEnabled || false,
347
+ config.fileEnabled || false,
348
+ config.level || 0,
349
+ config.fileName || ''
350
+ ]
351
+ );
352
+
353
+ checkResult(result, 'Failed to set log config');
354
+ }
355
+
356
+ /**
357
+ * Enables or disables a beauty type
358
+ * @param {number} beautyType - Beauty type (use BeautyType enum)
359
+ * @param {boolean} enabled - Enable or disable
360
+ */
361
+ setBeautyTypeEnabled(beautyType, enabled) {
362
+ this._ensureInitialized();
363
+ const Module = this._getWasmModule();
364
+
365
+ const result = Module.ccall(
366
+ 'SetBeautyTypeEnabled',
367
+ 'number',
368
+ ['number', 'number', 'number'],
369
+ [this.enginePtr, beautyType, enabled ? 1 : 0]
370
+ );
371
+
372
+ checkResult(result, `Failed to ${enabled ? 'enable' : 'disable'} beauty type`);
373
+ }
374
+
375
+ /**
376
+ * Checks if a beauty type is enabled
377
+ * @param {number} beautyType - Beauty type (use BeautyType enum)
378
+ * @returns {boolean} True if enabled
379
+ */
380
+ isBeautyTypeEnabled(beautyType) {
381
+ this._ensureInitialized();
382
+ const Module = this._getWasmModule();
383
+
384
+ const result = Module.ccall(
385
+ 'IsBeautyTypeEnabled',
386
+ 'number',
387
+ ['number', 'number'],
388
+ [this.enginePtr, beautyType]
389
+ );
390
+
391
+ if (result === -1) {
392
+ throw new FacebetterError('Failed to check beauty type status');
393
+ }
394
+
395
+ return result === 1;
396
+ }
397
+
398
+ /**
399
+ * Disables all beauty types
400
+ */
401
+ disableAllBeautyTypes() {
402
+ this._ensureInitialized();
403
+ const Module = this._getWasmModule();
404
+
405
+ const result = Module.ccall(
406
+ 'DisableAllBeautyTypes',
407
+ 'number',
408
+ ['number'],
409
+ [this.enginePtr]
410
+ );
411
+
412
+ checkResult(result, 'Failed to disable all beauty types');
413
+ }
414
+
415
+ /**
416
+ * Sets a basic beauty parameter
417
+ * @param {number} param - Parameter (use BasicParam enum)
418
+ * @param {number} value - Parameter value (0.0 - 1.0)
419
+ */
420
+ setBasicParam(param, value) {
421
+ this._ensureInitialized();
422
+ this._setBeautyParam('SetBeautyParamBasic', param, value);
423
+ }
424
+
425
+ /**
426
+ * Sets a reshape parameter
427
+ * @param {number} param - Parameter (use ReshapeParam enum)
428
+ * @param {number} value - Parameter value (0.0 - 1.0)
429
+ */
430
+ setReshapeParam(param, value) {
431
+ this._ensureInitialized();
432
+ this._setBeautyParam('SetBeautyParamReshape', param, value);
433
+ }
434
+
435
+ /**
436
+ * Sets a makeup parameter
437
+ * @param {number} param - Parameter (use MakeupParam enum)
438
+ * @param {number} value - Parameter value (0.0 - 1.0)
439
+ */
440
+ setMakeupParam(param, value) {
441
+ this._ensureInitialized();
442
+ this._setBeautyParam('SetBeautyParamMakeup', param, value);
443
+ }
444
+
445
+ /**
446
+ * Internal method to set beauty parameters
447
+ * @private
448
+ */
449
+ _setBeautyParam(functionName, param, value) {
450
+ if (value < 0 || value > 1) {
451
+ throw new FacebetterError('Parameter value must be between 0.0 and 1.0');
452
+ }
453
+
454
+ const Module = this._getWasmModule();
455
+ const result = Module.ccall(
456
+ functionName,
457
+ 'number',
458
+ ['number', 'number', 'number'],
459
+ [this.enginePtr, param, value]
460
+ );
461
+
462
+ checkResult(result, `Failed to set beauty parameter`);
463
+ }
464
+
465
+ /**
466
+ * Sets virtual background mode
467
+ * @param {number} mode - Background mode (use BackgroundMode enum)
468
+ */
469
+ setVirtualBackgroundMode(mode) {
470
+ this._ensureInitialized();
471
+ const Module = this._getWasmModule();
472
+
473
+ const result = Module.ccall(
474
+ 'SetVirtualBackgroundMode',
475
+ 'number',
476
+ ['number', 'number'],
477
+ [this.enginePtr, mode]
478
+ );
479
+
480
+ checkResult(result, 'Failed to set virtual background mode');
481
+ }
482
+
483
+ /**
484
+ * Sets virtual background image
485
+ * @param {ImageData|HTMLImageElement|HTMLCanvasElement} image - Background image
486
+ */
487
+ setVirtualBackgroundImage(image) {
488
+ this._ensureInitialized();
489
+ const imageData = this._toImageData(image);
490
+ const Module = this._getWasmModule();
491
+
492
+ const bufferSize = imageData.width * imageData.height * 4;
493
+ const imageBufferPtr = Module._malloc(bufferSize);
494
+
495
+ try {
496
+ const buffer = this._getWasmBuffer();
497
+ const view = new Uint8Array(buffer, imageBufferPtr, bufferSize);
498
+ view.set(imageData.data);
499
+
500
+ const result = Module.ccall(
501
+ 'SetVirtualBackgroundImage',
502
+ 'number',
503
+ ['number', 'number', 'number', 'number', 'number'],
504
+ [
505
+ this.enginePtr,
506
+ imageBufferPtr,
507
+ imageData.width,
508
+ imageData.height,
509
+ imageData.width * 4
510
+ ]
511
+ );
512
+
513
+ checkResult(result, 'Failed to set virtual background image');
514
+ } finally {
515
+ Module._free(imageBufferPtr);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Ensures buffers are allocated for the given dimensions
521
+ * @private
522
+ */
523
+ _ensureBuffers(width, height) {
524
+ const requiredSize = width * height * 4;
525
+
526
+ if (this.bufferSize < requiredSize) {
527
+ const Module = this._getWasmModule();
528
+
529
+ if (this.srcBufferPtr) {
530
+ Module._free(this.srcBufferPtr);
531
+ }
532
+ if (this.dstBufferPtr) {
533
+ Module._free(this.dstBufferPtr);
534
+ }
535
+
536
+ this.srcBufferPtr = Module._malloc(requiredSize);
537
+ this.dstBufferPtr = Module._malloc(requiredSize);
538
+ this.bufferSize = requiredSize;
539
+
540
+ if (!this.srcBufferPtr || !this.dstBufferPtr) {
541
+ throw new FacebetterError('Failed to allocate memory buffers');
542
+ }
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Processes an image
548
+ * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|Uint8ClampedArray} input - Input image
549
+ * @param {number} width - Image width (required if input is Uint8ClampedArray)
550
+ * @param {number} height - Image height (required if input is Uint8ClampedArray)
551
+ * @param {number} processMode - Process mode (use ProcessMode enum, default: ProcessMode.Image)
552
+ * @returns {ImageData} Processed image data
553
+ */
554
+ processImage(input, width, height, processMode = ProcessMode$1.Image) {
555
+ this._ensureInitialized();
556
+ const imageData = this._platformAPI.toImageData(input, width, height);
557
+ const Module = this._getWasmModule();
558
+
559
+ this._ensureBuffers(imageData.width, imageData.height);
560
+
561
+ const bufferSize = imageData.width * imageData.height * 4;
562
+
563
+ try {
564
+ const buffer = this._getWasmBuffer();
565
+
566
+ const srcView = new Uint8Array(buffer, this.srcBufferPtr, bufferSize);
567
+ srcView.set(imageData.data);
568
+
569
+ const result = Module.ccall(
570
+ 'ProcessImageRGBA',
571
+ 'number',
572
+ ['number', 'number', 'number', 'number', 'number', 'number', 'number'],
573
+ [
574
+ this.enginePtr,
575
+ this.srcBufferPtr,
576
+ imageData.width,
577
+ imageData.height,
578
+ imageData.width * 4,
579
+ this.dstBufferPtr,
580
+ processMode
581
+ ]
582
+ );
583
+
584
+ checkResult(result, 'Failed to process image');
585
+
586
+ const currentBuffer = this._getWasmBuffer();
587
+ const dstView = new Uint8Array(currentBuffer, this.dstBufferPtr, bufferSize);
588
+ const processedData = new Uint8ClampedArray(dstView);
589
+
590
+ return new ImageData(processedData, imageData.width, imageData.height);
591
+ } catch (error) {
592
+ if (error instanceof FacebetterError) {
593
+ throw error;
594
+ }
595
+ throw new FacebetterError(`Image processing error: ${error.message}`);
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Ensures the engine is initialized
601
+ * @private
602
+ * @throws {FacebetterError} If engine is not initialized
603
+ */
604
+ _ensureInitialized() {
605
+ if (!this.initialized || !this.enginePtr) {
606
+ throw new FacebetterError('Engine not initialized. Call init() first.');
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Ensures gpupixel_canvas exists in the DOM (browser only)
612
+ * This canvas is required by GPUPixel for WebGL context creation
613
+ * @private
614
+ */
615
+ _ensureGPUPixelCanvas() {
616
+ if (typeof document === 'undefined') {
617
+ return;
618
+ }
619
+
620
+ let canvas = document.getElementById('gpupixel_canvas');
621
+ if (!canvas) {
622
+ canvas = document.createElement('canvas');
623
+ canvas.id = 'gpupixel_canvas';
624
+ canvas.style.display = 'none';
625
+ canvas.width = 1;
626
+ canvas.height = 1;
627
+ document.body.appendChild(canvas);
628
+ this._createdGPUPixelCanvas = true;
629
+ }
630
+ this._gpupixelCanvas = canvas;
631
+ }
632
+
633
+ /**
634
+ * Cleans up gpupixel_canvas if it was created by this engine instance (browser only)
635
+ * @private
636
+ */
637
+ _cleanupGPUPixelCanvas() {
638
+ if (this._createdGPUPixelCanvas && this._gpupixelCanvas) {
639
+ this._gpupixelCanvas.remove();
640
+ this._gpupixelCanvas = null;
641
+ this._createdGPUPixelCanvas = false;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Ensures offscreen canvas exists and matches the required dimensions (browser only)
647
+ * This canvas is reused for video processing to avoid creating temporary canvases
648
+ * @private
649
+ * @param {number} width - Required canvas width
650
+ * @param {number} height - Required canvas height
651
+ */
652
+ _ensureOffscreenCanvas(width, height) {
653
+ if (typeof document === 'undefined') {
654
+ return null;
655
+ }
656
+
657
+ if (!this._offscreenCanvas ||
658
+ this._offscreenCanvas.width !== width ||
659
+ this._offscreenCanvas.height !== height) {
660
+ this._offscreenCanvas = document.createElement('canvas');
661
+ this._offscreenCanvas.width = width;
662
+ this._offscreenCanvas.height = height;
663
+ this._offscreenCtx = this._offscreenCanvas.getContext('2d', { willReadFrequently: true });
664
+ }
665
+ return this._offscreenCanvas;
666
+ }
667
+
668
+ }
669
+
670
+ /**
671
+ * Browser-specific Image Utilities
672
+ * Uses DOM APIs (canvas, ImageData, etc.)
673
+ */
674
+
675
+
676
+ /**
677
+ * Converts various image sources to ImageData (browser version)
678
+ * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|Uint8ClampedArray} source - Image source
679
+ * @param {number} width - Image width (required if source is Uint8ClampedArray)
680
+ * @param {number} height - Image height (required if source is Uint8ClampedArray)
681
+ * @returns {ImageData} ImageData object
682
+ */
683
+ function toImageData(source, width, height) {
684
+ if (source instanceof ImageData) {
685
+ return source;
686
+ }
687
+
688
+ if (source instanceof HTMLCanvasElement || source instanceof HTMLVideoElement) {
689
+ const canvas = source instanceof HTMLCanvasElement ? source : document.createElement('canvas');
690
+ if (source instanceof HTMLVideoElement) {
691
+ canvas.width = source.videoWidth;
692
+ canvas.height = source.videoHeight;
693
+ const ctx = canvas.getContext('2d');
694
+ ctx.drawImage(source, 0, 0);
695
+ }
696
+ const ctx = canvas.getContext('2d');
697
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
698
+ }
699
+
700
+ if (source instanceof HTMLImageElement) {
701
+ const canvas = document.createElement('canvas');
702
+ canvas.width = source.naturalWidth || source.width;
703
+ canvas.height = source.naturalHeight || source.height;
704
+ const ctx = canvas.getContext('2d');
705
+ ctx.drawImage(source, 0, 0);
706
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
707
+ }
708
+
709
+ if (source instanceof Uint8ClampedArray) {
710
+ if (width === undefined || height === undefined) {
711
+ throw new FacebetterError('Width and height are required when source is Uint8ClampedArray');
712
+ }
713
+ return new ImageData(source, width, height);
714
+ }
715
+
716
+ throw new FacebetterError('Unsupported image source type');
717
+ }
718
+
719
+ /**
720
+ * Gets User-Agent string for web platform
721
+ * @returns {string} User-Agent string
722
+ */
723
+ function getUserAgentString() {
724
+ return 'FB/1.0 (web; wasm32)';
725
+ }
726
+
727
+ /**
728
+ * Generates HMAC-SHA256 signature (browser version)
729
+ * @param {string} key - HMAC key (app_key)
730
+ * @param {string} data - Data to sign (timestamp string)
731
+ * @returns {Promise<string>} Promise that resolves with hex-encoded signature
732
+ */
733
+ async function generateHMACSignature(key, data) {
734
+ const encoder = new TextEncoder();
735
+ const keyData = encoder.encode(key);
736
+ const messageData = encoder.encode(data);
737
+
738
+ const cryptoKey = await crypto.subtle.importKey(
739
+ 'raw',
740
+ keyData,
741
+ { name: 'HMAC', hash: 'SHA-256' },
742
+ false,
743
+ ['sign']
744
+ );
745
+
746
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
747
+
748
+ // Convert to hex string
749
+ const hashArray = Array.from(new Uint8Array(signature));
750
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
751
+ return hashHex;
752
+ }
753
+
754
+ /**
755
+ * Calls online_auth API to verify app_key (browser version)
756
+ * @param {string} appId - Application ID
757
+ * @param {string} appKey - Application key
758
+ * @returns {Promise<Object>} Promise that resolves with response data
759
+ */
760
+ async function verifyAppKeyOnline(appId, appKey) {
761
+ try {
762
+ const timestamp = Math.floor(Date.now() / 1000);
763
+ const timestampStr = timestamp.toString();
764
+ const hmacSignature = await generateHMACSignature(appKey, timestampStr);
765
+
766
+ const requestBody = {
767
+ p_app_id: appId,
768
+ p_hmac_signature: hmacSignature,
769
+ p_timestamp: timestamp,
770
+ p_user_agent: getUserAgentString()
771
+ };
772
+
773
+ const response = await fetch('https://facebetter.pixpark.net/rest/v1/rpc/online_auth', {
774
+ method: 'POST',
775
+ headers: {
776
+ 'Content-Type': 'application/json',
777
+ 'User-Agent': getUserAgentString()
778
+ },
779
+ body: JSON.stringify(requestBody)
780
+ });
781
+
782
+ const responseData = await response.json();
783
+ return responseData;
784
+ } catch (error) {
785
+ console.error('Online auth request failed:', error);
786
+ return null;
787
+ }
788
+ }
789
+
790
+ /**
791
+ * ESM Entry Point
792
+ * For Vue/React/Vite/Webpack (ES Module)
793
+ * Uses import.meta.url for automatic path resolution
794
+ */
795
+
796
+
797
+ // ESM-specific WASM loader
798
+ let wasmModuleInstance = null;
799
+ let wasmModulePromise = null;
800
+
801
+ /**
802
+ * Gets the base path of the current module
803
+ * Uses import.meta.url (ES Module equivalent of __dirname)
804
+ * @returns {string} Base path of the current module (with trailing slash)
805
+ */
806
+ function getModuleBasePath() {
807
+ try {
808
+ const url = new URL('.', import.meta.url);
809
+ return url.href;
810
+ } catch (e) {
811
+ // Fallback
812
+ return './';
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Resolves a relative path to absolute URL (ESM version)
818
+ * Handles various path formats
819
+ * @param {string} relativePath - Relative path
820
+ * @returns {string} Absolute URL
821
+ */
822
+ function resolvePath(relativePath) {
823
+ // If relativePath is already absolute, return as-is
824
+ if (relativePath.startsWith('http://') || relativePath.startsWith('https://') || relativePath.startsWith('//')) {
825
+ return relativePath;
826
+ }
827
+
828
+ try {
829
+ return new URL(relativePath, import.meta.url).href;
830
+ } catch (e) {
831
+ // Fallback
832
+ const base = getModuleBasePath();
833
+ const cleanPath = relativePath.replace(/^\.\//, '');
834
+ return base + cleanPath;
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Loads the WebAssembly module (ESM version)
840
+ * @param {Object} [options] - Options
841
+ * @param {string} [options.wasmUrl] - Custom WASM URL (auto-detected if not provided)
842
+ * @param {Function} [options.locateFile] - Custom locateFile function
843
+ * @returns {Promise<Object>} Promise that resolves with the WASM module
844
+ */
845
+ async function loadWasmModule(options = {}) {
846
+ if (wasmModuleInstance) {
847
+ return wasmModuleInstance;
848
+ }
849
+
850
+ if (wasmModulePromise) {
851
+ return wasmModulePromise;
852
+ }
853
+
854
+ wasmModulePromise = (async () => {
855
+ // Try to import the WASM module factory
856
+ // facebetter_wasm.js is an Emscripten-generated UMD/IIFE file
857
+ let FaceBetterModuleFactory;
858
+
859
+ try {
860
+ // Get the WASM module URL
861
+ let wasmModuleUrl;
862
+ if (options.wasmUrl) {
863
+ wasmModuleUrl = options.wasmUrl;
864
+ } else {
865
+ // Priority 1: Use package exports path (works for npm packages)
866
+ // Vite will correctly resolve 'facebetter/wasm.js' to node_modules/facebetter/dist/facebetter_wasm.js
867
+ // This works even if Vite pre-bundles the main module, because package exports are resolved at runtime
868
+ // IMPORTANT: This MUST work for npm-installed packages, otherwise relative path will fail
869
+ // when Vite pre-bundles the module to .vite/deps/
870
+ if (typeof window !== 'undefined') {
871
+ try {
872
+ // Method 1: Use import.meta.resolve (Vite 5+ and modern bundlers)
873
+ // This resolves based on package.json exports, not import.meta.url
874
+ if (typeof import.meta.resolve === 'function') {
875
+ wasmModuleUrl = import.meta.resolve('facebetter/wasm.js');
876
+ } else {
877
+ // Method 2: Use dynamic import with ?url suffix (Vite-specific)
878
+ // Vite will resolve package exports even for pre-bundled modules
879
+ const pkgPath = 'facebetter/wasm.js';
880
+ // Use Function constructor to avoid Rollup static analysis
881
+ const dynamicImport = new Function('path', 'return import(path)');
882
+ const wasmModule = await dynamicImport(pkgPath + '?url');
883
+ if (wasmModule && (wasmModule.default || wasmModule)) {
884
+ wasmModuleUrl = wasmModule.default || wasmModule;
885
+ }
886
+ }
887
+ } catch (e) {
888
+ // Package exports resolution failed
889
+ // This should not happen for npm-installed packages
890
+ console.error('Failed to resolve facebetter/wasm.js via package exports:', e);
891
+ console.error('This usually means package.json exports are misconfigured or Vite cannot resolve the path');
892
+ }
893
+ }
894
+
895
+ // Priority 2: Fallback to relative path (for local development with file: protocol)
896
+ // WARNING: This will fail for npm-installed packages if Vite pre-bundles the module
897
+ // because import.meta.url will point to .vite/deps/ instead of node_modules/facebetter/dist/
898
+ // That's why we MUST use package exports path above
899
+ if (!wasmModuleUrl) {
900
+ wasmModuleUrl = resolvePath('./facebetter_wasm.js');
901
+ }
902
+ }
903
+
904
+ // For browser environment, load via script tag since it's UMD/IIFE
905
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
906
+ // Check if Module is already initialized
907
+ if (window.Module && window.Module.ready) {
908
+ // Module already loaded and initialized
909
+ FaceBetterModuleFactory = (opts) => {
910
+ // Merge options with existing Module
911
+ Object.assign(window.Module, opts);
912
+ return Promise.resolve(window.Module);
913
+ };
914
+ } else {
915
+ // Initialize Module object before loading script
916
+ if (typeof window.Module === 'undefined') {
917
+ window.Module = {};
918
+ }
919
+
920
+ // Configure locateFile before script loads
921
+ const basePath = getModuleBasePath();
922
+ const originalLocateFile = window.Module.locateFile || function(path, prefix) {
923
+ return prefix + path;
924
+ };
925
+
926
+ window.Module.locateFile = function(path, prefix) {
927
+ // Use custom locateFile if provided
928
+ if (options.locateFile) {
929
+ return options.locateFile(path, prefix);
930
+ }
931
+
932
+ // Auto-resolve .wasm and .data files
933
+ if (path.endsWith('.wasm') || path.endsWith('.data')) {
934
+ try {
935
+ if (typeof import.meta.resolve === 'function') {
936
+ if (path.endsWith('.wasm')) {
937
+ return import.meta.resolve('facebetter/wasm');
938
+ } else if (path.endsWith('.data')) {
939
+ const wasmUrl = import.meta.resolve('facebetter/wasm');
940
+ return wasmUrl.replace(/\.wasm$/, '.data');
941
+ }
942
+ }
943
+ } catch (e) {
944
+ // Fallback
945
+ }
946
+ return resolvePath(path);
947
+ }
948
+
949
+ return originalLocateFile(path, prefix);
950
+ };
951
+
952
+ // Load script and wait for initialization
953
+ await new Promise((resolve, reject) => {
954
+ // Set up onRuntimeInitialized callback
955
+ const originalInit = window.Module.onRuntimeInitialized;
956
+ window.Module.onRuntimeInitialized = () => {
957
+ if (originalInit) originalInit();
958
+ window.Module.ready = true;
959
+ resolve();
960
+ };
961
+
962
+ // Check if script already exists
963
+ const existingScript = Array.from(document.scripts).find(
964
+ s => s.src && s.src.includes('facebetter_wasm.js')
965
+ );
966
+
967
+ if (existingScript && window.Module.ready) {
968
+ // Script already loaded and initialized
969
+ resolve();
970
+ return;
971
+ }
972
+
973
+ // Load script dynamically
974
+ const script = document.createElement('script');
975
+ script.type = 'text/javascript';
976
+ script.src = wasmModuleUrl;
977
+ script.async = true;
978
+
979
+ script.onload = () => {
980
+ // Script loaded, wait for onRuntimeInitialized to be called
981
+ // If Module.ready is already true, resolve immediately
982
+ if (window.Module && window.Module.ready) {
983
+ resolve();
984
+ }
985
+ // Otherwise wait for onRuntimeInitialized callback
986
+ };
987
+
988
+ script.onerror = () => {
989
+ reject(new Error(`Failed to load WASM script from ${wasmModuleUrl}`));
990
+ };
991
+
992
+ document.head.appendChild(script);
993
+ });
994
+
995
+ // Create factory function that uses the initialized Module
996
+ FaceBetterModuleFactory = (opts) => {
997
+ // Merge options with Module
998
+ if (opts) {
999
+ Object.assign(window.Module, opts);
1000
+ }
1001
+ return Promise.resolve(window.Module);
1002
+ };
1003
+ }
1004
+ } else {
1005
+ // Node.js environment: try dynamic import
1006
+ const module = await import(wasmModuleUrl);
1007
+ FaceBetterModuleFactory = module.default || module;
1008
+ }
1009
+ } catch (e) {
1010
+ throw new FacebetterError(`Failed to load WASM module: ${e.message}`);
1011
+ }
1012
+
1013
+ // Initialize the module (options already configured in Module.locateFile)
1014
+ wasmModuleInstance = await FaceBetterModuleFactory(options);
1015
+
1016
+ if (!wasmModuleInstance) {
1017
+ throw new FacebetterError('Failed to initialize WASM module');
1018
+ }
1019
+
1020
+ wasmModuleInstance.ready = true;
1021
+ return wasmModuleInstance;
1022
+ })();
1023
+
1024
+ return wasmModulePromise;
1025
+ }
1026
+
1027
+ /**
1028
+ * Gets the current WASM module instance
1029
+ * @returns {Object} WASM module instance
1030
+ * @throws {Error} If module is not loaded
1031
+ */
1032
+ function getWasmModule() {
1033
+ if (!wasmModuleInstance) {
1034
+ throw new Error('WASM module not loaded. Call loadWasmModule() first or use BeautyEffectEngine.init()');
1035
+ }
1036
+ return wasmModuleInstance;
1037
+ }
1038
+
1039
+ /**
1040
+ * Gets the current WASM memory buffer
1041
+ * @returns {ArrayBuffer} Current WASM memory buffer
1042
+ */
1043
+ function getWasmBuffer() {
1044
+ const Module = getWasmModule();
1045
+
1046
+ if (Module.buffer) {
1047
+ return Module.buffer;
1048
+ } else if (Module.HEAPU8 && Module.HEAPU8.buffer) {
1049
+ return Module.HEAPU8.buffer;
1050
+ } else if (Module.asm && Module.asm.memory) {
1051
+ return Module.asm.memory.buffer;
1052
+ }
1053
+
1054
+ throw new Error('Unable to access WASM memory buffer');
1055
+ }
1056
+
1057
+ // Browser-specific GPU canvas management
1058
+ function ensureGPUPixelCanvas() {
1059
+ if (typeof document === 'undefined') {
1060
+ return;
1061
+ }
1062
+
1063
+ let canvas = document.getElementById('gpupixel_canvas');
1064
+ if (!canvas) {
1065
+ canvas = document.createElement('canvas');
1066
+ canvas.id = 'gpupixel_canvas';
1067
+ canvas.style.display = 'none';
1068
+ canvas.width = 1;
1069
+ canvas.height = 1;
1070
+ if (document.body) {
1071
+ document.body.appendChild(canvas);
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ function cleanupGPUPixelCanvas() {
1077
+ // Cleanup is handled by engine instance
1078
+ }
1079
+
1080
+ // Create platform API
1081
+ const platformAPI = {
1082
+ loadWasmModule: () => loadWasmModule(),
1083
+ getWasmModule,
1084
+ getWasmBuffer,
1085
+ toImageData: toImageData,
1086
+ verifyAppKeyOnline: verifyAppKeyOnline,
1087
+ ensureGPUPixelCanvas,
1088
+ cleanupGPUPixelCanvas
1089
+ };
1090
+
1091
+ // Export factory function
1092
+ function createBeautyEffectEngine(config) {
1093
+ return new BeautyEffectEngine(config, platformAPI);
1094
+ }
1095
+
1096
+ // Create a wrapped BeautyEffectEngine class that automatically injects platformAPI
1097
+ // This allows users to use: new BeautyEffectEngine(config) without passing platformAPI
1098
+ class ESMBeautyEffectEngine extends BeautyEffectEngine {
1099
+ constructor(config) {
1100
+ // Automatically inject platformAPI for ESM environment
1101
+ super(config, platformAPI);
1102
+ }
1103
+ }
1104
+
1105
+ // Export constants with proper names
1106
+ const BeautyType = BeautyType$1;
1107
+ const BasicParam = BasicParam$1;
1108
+ const ReshapeParam = ReshapeParam$1;
1109
+ const MakeupParam = MakeupParam$1;
1110
+ const BackgroundMode = BackgroundMode$1;
1111
+ const ProcessMode = ProcessMode$1;
1112
+
1113
+ // Default export
1114
+ var index = {
1115
+ BeautyEffectEngine: ESMBeautyEffectEngine,
1116
+ EngineConfig,
1117
+ FacebetterError,
1118
+ ...constants,
1119
+ loadWasmModule
1120
+ };
1121
+
1122
+ export { BackgroundMode, BasicParam, ESMBeautyEffectEngine as BeautyEffectEngine, BeautyType, EngineConfig, FacebetterError, MakeupParam, ProcessMode, ReshapeParam, createBeautyEffectEngine, index as default, getWasmBuffer, getWasmModule, loadWasmModule };
1123
+ //# sourceMappingURL=facebetter.esm.js.map