facebetter 1.0.3 → 1.0.6

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