@kotaksurat/photobooth-frame-generator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +164 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +164 -0
- package/dist/src/types.d.ts +21 -0
- package/dist/src/types.js +2 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +2 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Photobooth Frame Generator
|
|
2
|
+
|
|
3
|
+
A fast and efficient TypeScript engine to automate placing user photos into a template frame's transparent slots. It analyzes the alpha channel of the frame to dynamically find transparent "slots" and creatively paints the user's photos underneath those areas.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dynamic Slot Detection**: Automatically finds empty (transparent) slots in a template frame using Breadth-First Search (BFS).
|
|
8
|
+
- **Auto Cover/Crop**: Intelligently fits your photos into the detected slots, preserving the aspect ratio (similar to `object-fit: cover`).
|
|
9
|
+
- **Memory Management**: Includes a `reset()` method to free memory by revoking File object URLs and hinting garbage collection.
|
|
10
|
+
- **Browser Ready**: Uses the HTML5 Canvas API and `ImageData` for fast, client-side processing.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
The engine supports inputs as `File` objects or `Base64` data URLs.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { PhotoboothFrameGenerator } from 'photobooth-frame-generator';
|
|
24
|
+
|
|
25
|
+
// 1. Initialize the engine
|
|
26
|
+
const engine = new PhotoboothFrameGenerator({
|
|
27
|
+
outputFormat: 'image/jpeg',
|
|
28
|
+
quality: 0.95
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function processPhotos(frameFile: File, userPhotos: File[]) {
|
|
32
|
+
try {
|
|
33
|
+
// 2. Generate the result
|
|
34
|
+
const result = await engine.create(frameFile, userPhotos);
|
|
35
|
+
|
|
36
|
+
console.log(`Generated image with ${result.slotsFound} slots!`);
|
|
37
|
+
|
|
38
|
+
// Use result.dataUrl as the src for an HTML Image element
|
|
39
|
+
const img = new Image();
|
|
40
|
+
img.src = result.dataUrl;
|
|
41
|
+
document.body.appendChild(img);
|
|
42
|
+
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Processing failed:', error);
|
|
45
|
+
} finally {
|
|
46
|
+
// 3. Clean up memory to avoid blobs piling up
|
|
47
|
+
engine.reset();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
When instantiating `PhotoboothFrameGenerator`, you can pass an optional configuration object:
|
|
55
|
+
|
|
56
|
+
| Property | Type | Default | Description |
|
|
57
|
+
|----------|------|---------|-------------|
|
|
58
|
+
| `alphaThreshold` | `number` | `10` | The alpha channel threshold (0-255) below which a pixel is considered transparent. |
|
|
59
|
+
| `minSlotSize` | `number` | `50` | Minimum width/height in pixels to be considered a valid slot (avoids micro-artifacts being selected). |
|
|
60
|
+
| `outputFormat` | `string` | `'image/png'` | The mime type of the generated image (`image/png`, `image/jpeg`, `image/webp`). |
|
|
61
|
+
| `quality` | `number` | `0.92` | The image quality from `0.0` to `1.0` (applicable for jpeg/webp formats). |
|
|
62
|
+
| `fillEmptySlots` | `boolean` | `true` | If true, intelligently loops through provided photos to fill any remaining empty frame slots. |
|
|
63
|
+
| `slotExpansion` | `number` | `5` | Expansion in pixels added to each detected slot's edges to cover anti-aliased transparency gaps. |
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
1. **Load Assets:** Internally converts `File` objects to blob URLs to be drawn onto a virtual canvas.
|
|
67
|
+
2. **Scan Alpha:** Performs a BFS scan over the `ImageData` to locate contiguous transparent regions (alpha < `alphaThreshold`).
|
|
68
|
+
3. **Sort Slots:** Sorts detected regions topologically (top-to-bottom, left-to-right).
|
|
69
|
+
4. **Draw Composition:** Draws the user photos onto the base layer positioned in the slots, and places the original frame natively as the top layer.
|
|
70
|
+
|
|
71
|
+
## Packages & Dependencies Used
|
|
72
|
+
|
|
73
|
+
This engine is lightweight and utilizes standard browser APIs internally, but relies on a few core packages for development:
|
|
74
|
+
|
|
75
|
+
- **`typescript` & `@types/node`**: Enables strict static typings and smooth transpilation from `src/` to a publishable package.
|
|
76
|
+
- **`vitest`**: A modern and extremely fast test runner tailored for TypeScript and built natively around Vite syntax.
|
|
77
|
+
- **`jsdom`**: Used alongside Vitest to seamlessly mock essential HTML5 Browser environments (like `URL` & partial native APIs) during runtime testing.
|
|
78
|
+
|
|
79
|
+
## Running the Example
|
|
80
|
+
|
|
81
|
+
We have provided a demo implementation showcasing how the package works in action globally.
|
|
82
|
+
You can find it under the `example/` directory.
|
|
83
|
+
|
|
84
|
+
To serve and test the example in your browser, run:
|
|
85
|
+
```sh
|
|
86
|
+
npx vite dev example
|
|
87
|
+
```
|
|
88
|
+
1. Open the local link (e.g., `http://localhost:5173/`) in the browser.
|
|
89
|
+
2. Under "Select Frame", upload a `PNG` containing transparent rectangular slots matching your final format.
|
|
90
|
+
3. Under "Select Photos", choose as many images as you need corresponding to the holes on the given frame.
|
|
91
|
+
4. Hit **Generate Result** to fetch the composed canvas snapshot.
|
|
92
|
+
|
|
93
|
+
## Running Tests
|
|
94
|
+
|
|
95
|
+
We implement unit tests focusing on robust config parsers and safe memory resetting tools using `vitest`.
|
|
96
|
+
|
|
97
|
+
To execute the entire test suite, make sure you have installed standard modules with `npm install`, then run:
|
|
98
|
+
```sh
|
|
99
|
+
npm run test
|
|
100
|
+
```
|
|
101
|
+
This will discover scripts inside the `test/` directory using the `vitest.config.ts` rules, executing assertions efficiently on the JSDOM mock environment.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageSource, PhotoboothConfig, RenderResult } from './types';
|
|
2
|
+
export declare class PhotoboothFrameGenerator {
|
|
3
|
+
private config;
|
|
4
|
+
private objectUrls;
|
|
5
|
+
constructor(config?: PhotoboothConfig);
|
|
6
|
+
/**
|
|
7
|
+
* Fungsi utama untuk memproses frame dan foto.
|
|
8
|
+
* Mendukung input berupa Base64 string atau File object.
|
|
9
|
+
*/
|
|
10
|
+
create(frameSource: ImageSource, userPhotos: ImageSource[]): Promise<RenderResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Fungsi Reset untuk Manajemen Memori.
|
|
13
|
+
* WAJIB dipanggil setelah proses selesai atau saat komponen di-unmount.
|
|
14
|
+
*/
|
|
15
|
+
reset(): void;
|
|
16
|
+
private loadImage;
|
|
17
|
+
private findSlotsBFS;
|
|
18
|
+
private drawCover;
|
|
19
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PhotoboothFrameGenerator = void 0;
|
|
13
|
+
class PhotoboothFrameGenerator {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
var _a, _b, _c, _d, _e, _f;
|
|
16
|
+
this.objectUrls = []; // Track untuk manajemen memori
|
|
17
|
+
this.config = {
|
|
18
|
+
alphaThreshold: (_a = config === null || config === void 0 ? void 0 : config.alphaThreshold) !== null && _a !== void 0 ? _a : 10,
|
|
19
|
+
minSlotSize: (_b = config === null || config === void 0 ? void 0 : config.minSlotSize) !== null && _b !== void 0 ? _b : 50,
|
|
20
|
+
outputFormat: (_c = config === null || config === void 0 ? void 0 : config.outputFormat) !== null && _c !== void 0 ? _c : 'image/png',
|
|
21
|
+
quality: (_d = config === null || config === void 0 ? void 0 : config.quality) !== null && _d !== void 0 ? _d : 0.92,
|
|
22
|
+
fillEmptySlots: (_e = config === null || config === void 0 ? void 0 : config.fillEmptySlots) !== null && _e !== void 0 ? _e : true,
|
|
23
|
+
slotExpansion: (_f = config === null || config === void 0 ? void 0 : config.slotExpansion) !== null && _f !== void 0 ? _f : 5,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fungsi utama untuk memproses frame dan foto.
|
|
28
|
+
* Mendukung input berupa Base64 string atau File object.
|
|
29
|
+
*/
|
|
30
|
+
create(frameSource, userPhotos) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
try {
|
|
33
|
+
const frame = yield this.loadImage(frameSource);
|
|
34
|
+
const canvas = document.createElement('canvas');
|
|
35
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
36
|
+
if (!ctx)
|
|
37
|
+
throw new Error("Canvas context 2D not found");
|
|
38
|
+
canvas.width = frame.width;
|
|
39
|
+
canvas.height = frame.height;
|
|
40
|
+
// 1. Deteksi Slot
|
|
41
|
+
ctx.drawImage(frame, 0, 0);
|
|
42
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
43
|
+
const slots = this.findSlotsBFS(imageData);
|
|
44
|
+
// 2. Render Final
|
|
45
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
46
|
+
// Gambar foto user (Layer bawah)
|
|
47
|
+
for (let i = 0; i < slots.length; i++) {
|
|
48
|
+
let photoSource = userPhotos[i];
|
|
49
|
+
// Jika fillEmptySlots true dan foto yang disuplai lebih sedikit dari slot
|
|
50
|
+
if (!photoSource && this.config.fillEmptySlots && userPhotos.length > 0) {
|
|
51
|
+
photoSource = userPhotos[i % userPhotos.length];
|
|
52
|
+
}
|
|
53
|
+
if (photoSource) {
|
|
54
|
+
const photo = yield this.loadImage(photoSource);
|
|
55
|
+
this.drawCover(ctx, photo, slots[i]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Gambar frame (Layer atas)
|
|
59
|
+
ctx.drawImage(frame, 0, 0);
|
|
60
|
+
return {
|
|
61
|
+
dataUrl: canvas.toDataURL(this.config.outputFormat, this.config.quality),
|
|
62
|
+
slotsFound: slots.length,
|
|
63
|
+
width: canvas.width,
|
|
64
|
+
height: canvas.height
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`PhotoboothFrameGenerator Error: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fungsi Reset untuk Manajemen Memori.
|
|
74
|
+
* WAJIB dipanggil setelah proses selesai atau saat komponen di-unmount.
|
|
75
|
+
*/
|
|
76
|
+
reset() {
|
|
77
|
+
// Revoke semua Object URL yang dibuat dari File untuk mengosongkan RAM
|
|
78
|
+
this.objectUrls.forEach(url => URL.revokeObjectURL(url));
|
|
79
|
+
this.objectUrls = [];
|
|
80
|
+
// Memberi sinyal ke GC bahwa referensi bisa dihapus
|
|
81
|
+
console.log("PhotoboothFrameGenerator: Memory cleared.");
|
|
82
|
+
}
|
|
83
|
+
loadImage(source) {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const img = new Image();
|
|
87
|
+
let url;
|
|
88
|
+
if (source instanceof File) {
|
|
89
|
+
url = URL.createObjectURL(source);
|
|
90
|
+
this.objectUrls.push(url); // Simpan untuk di-reset nanti
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
url = source; // Base64 atau URL string
|
|
94
|
+
}
|
|
95
|
+
img.onload = () => resolve(img);
|
|
96
|
+
img.onerror = () => reject("Failed to load image source");
|
|
97
|
+
img.src = url;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
findSlotsBFS(imageData) {
|
|
102
|
+
const { width, height, data } = imageData;
|
|
103
|
+
const visited = new Uint8Array(width * height);
|
|
104
|
+
const slots = [];
|
|
105
|
+
for (let i = 0; i < width * height; i++) {
|
|
106
|
+
if (data[i * 4 + 3] < this.config.alphaThreshold && !visited[i]) {
|
|
107
|
+
const queue = [i];
|
|
108
|
+
visited[i] = 1;
|
|
109
|
+
let minX = i % width, maxX = minX, minY = Math.floor(i / width), maxY = minY;
|
|
110
|
+
let head = 0;
|
|
111
|
+
while (head < queue.length) {
|
|
112
|
+
const curr = queue[head++];
|
|
113
|
+
const cx = curr % width, cy = Math.floor(curr / width);
|
|
114
|
+
const neighbors = [[cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]];
|
|
115
|
+
for (const [nx, ny] of neighbors) {
|
|
116
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
117
|
+
const nIdx = ny * width + nx;
|
|
118
|
+
if (!visited[nIdx] && data[nIdx * 4 + 3] < this.config.alphaThreshold) {
|
|
119
|
+
visited[nIdx] = 1;
|
|
120
|
+
queue.push(nIdx);
|
|
121
|
+
if (nx < minX)
|
|
122
|
+
minX = nx;
|
|
123
|
+
if (nx > maxX)
|
|
124
|
+
maxX = nx;
|
|
125
|
+
if (ny < minY)
|
|
126
|
+
minY = ny;
|
|
127
|
+
if (ny > maxY)
|
|
128
|
+
maxY = ny;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (maxX - minX > this.config.minSlotSize) {
|
|
134
|
+
slots.push({ x: minX, y: minY, width: maxX - minX, height: maxY - minY });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return slots.sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
139
|
+
}
|
|
140
|
+
drawCover(ctx, img, slot) {
|
|
141
|
+
const expansion = this.config.slotExpansion;
|
|
142
|
+
const targetX = slot.x - expansion;
|
|
143
|
+
const targetY = slot.y - expansion;
|
|
144
|
+
const targetW = slot.width + (expansion * 2);
|
|
145
|
+
const targetH = slot.height + (expansion * 2);
|
|
146
|
+
const imgRatio = img.width / img.height;
|
|
147
|
+
const slotRatio = targetW / targetH;
|
|
148
|
+
let sw, sh, sx, sy;
|
|
149
|
+
if (imgRatio > slotRatio) {
|
|
150
|
+
sw = img.height * slotRatio;
|
|
151
|
+
sh = img.height;
|
|
152
|
+
sx = (img.width - sw) / 2;
|
|
153
|
+
sy = 0;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
sw = img.width;
|
|
157
|
+
sh = img.width / slotRatio;
|
|
158
|
+
sx = 0;
|
|
159
|
+
sy = (img.height - sh) / 2;
|
|
160
|
+
}
|
|
161
|
+
ctx.drawImage(img, sx, sy, sw, sh, targetX, targetY, targetW, targetH);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.PhotoboothFrameGenerator = PhotoboothFrameGenerator;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageSource, PhotoboothConfig, RenderResult } from './types';
|
|
2
|
+
export declare class PhotoboothFrameGenerator {
|
|
3
|
+
private config;
|
|
4
|
+
private objectUrls;
|
|
5
|
+
constructor(config?: PhotoboothConfig);
|
|
6
|
+
/**
|
|
7
|
+
* Fungsi utama untuk memproses frame dan foto.
|
|
8
|
+
* Mendukung input berupa Base64 string atau File object.
|
|
9
|
+
*/
|
|
10
|
+
create(frameSource: ImageSource, userPhotos: ImageSource[]): Promise<RenderResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Fungsi Reset untuk Manajemen Memori.
|
|
13
|
+
* WAJIB dipanggil setelah proses selesai atau saat komponen di-unmount.
|
|
14
|
+
*/
|
|
15
|
+
reset(): void;
|
|
16
|
+
private loadImage;
|
|
17
|
+
private findSlotsBFS;
|
|
18
|
+
private drawCover;
|
|
19
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PhotoboothFrameGenerator = void 0;
|
|
13
|
+
class PhotoboothFrameGenerator {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
var _a, _b, _c, _d, _e, _f;
|
|
16
|
+
this.objectUrls = []; // Track untuk manajemen memori
|
|
17
|
+
this.config = {
|
|
18
|
+
alphaThreshold: (_a = config === null || config === void 0 ? void 0 : config.alphaThreshold) !== null && _a !== void 0 ? _a : 10,
|
|
19
|
+
minSlotSize: (_b = config === null || config === void 0 ? void 0 : config.minSlotSize) !== null && _b !== void 0 ? _b : 50,
|
|
20
|
+
outputFormat: (_c = config === null || config === void 0 ? void 0 : config.outputFormat) !== null && _c !== void 0 ? _c : 'image/png',
|
|
21
|
+
quality: (_d = config === null || config === void 0 ? void 0 : config.quality) !== null && _d !== void 0 ? _d : 0.92,
|
|
22
|
+
fillEmptySlots: (_e = config === null || config === void 0 ? void 0 : config.fillEmptySlots) !== null && _e !== void 0 ? _e : true,
|
|
23
|
+
slotExpansion: (_f = config === null || config === void 0 ? void 0 : config.slotExpansion) !== null && _f !== void 0 ? _f : 5,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fungsi utama untuk memproses frame dan foto.
|
|
28
|
+
* Mendukung input berupa Base64 string atau File object.
|
|
29
|
+
*/
|
|
30
|
+
create(frameSource, userPhotos) {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
try {
|
|
33
|
+
const frame = yield this.loadImage(frameSource);
|
|
34
|
+
const canvas = document.createElement('canvas');
|
|
35
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
36
|
+
if (!ctx)
|
|
37
|
+
throw new Error("Canvas context 2D not found");
|
|
38
|
+
canvas.width = frame.width;
|
|
39
|
+
canvas.height = frame.height;
|
|
40
|
+
// 1. Deteksi Slot
|
|
41
|
+
ctx.drawImage(frame, 0, 0);
|
|
42
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
43
|
+
const slots = this.findSlotsBFS(imageData);
|
|
44
|
+
// 2. Render Final
|
|
45
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
46
|
+
// Gambar foto user (Layer bawah)
|
|
47
|
+
for (let i = 0; i < slots.length; i++) {
|
|
48
|
+
let photoSource = userPhotos[i];
|
|
49
|
+
// Jika fillEmptySlots true dan foto yang disuplai lebih sedikit dari slot
|
|
50
|
+
if (!photoSource && this.config.fillEmptySlots && userPhotos.length > 0) {
|
|
51
|
+
photoSource = userPhotos[i % userPhotos.length];
|
|
52
|
+
}
|
|
53
|
+
if (photoSource) {
|
|
54
|
+
const photo = yield this.loadImage(photoSource);
|
|
55
|
+
this.drawCover(ctx, photo, slots[i]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Gambar frame (Layer atas)
|
|
59
|
+
ctx.drawImage(frame, 0, 0);
|
|
60
|
+
return {
|
|
61
|
+
dataUrl: canvas.toDataURL(this.config.outputFormat, this.config.quality),
|
|
62
|
+
slotsFound: slots.length,
|
|
63
|
+
width: canvas.width,
|
|
64
|
+
height: canvas.height
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`PhotoboothFrameGenerator Error: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fungsi Reset untuk Manajemen Memori.
|
|
74
|
+
* WAJIB dipanggil setelah proses selesai atau saat komponen di-unmount.
|
|
75
|
+
*/
|
|
76
|
+
reset() {
|
|
77
|
+
// Revoke semua Object URL yang dibuat dari File untuk mengosongkan RAM
|
|
78
|
+
this.objectUrls.forEach(url => URL.revokeObjectURL(url));
|
|
79
|
+
this.objectUrls = [];
|
|
80
|
+
// Memberi sinyal ke GC bahwa referensi bisa dihapus
|
|
81
|
+
console.log("PhotoboothFrameGenerator: Memory cleared.");
|
|
82
|
+
}
|
|
83
|
+
loadImage(source) {
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const img = new Image();
|
|
87
|
+
let url;
|
|
88
|
+
if (source instanceof File) {
|
|
89
|
+
url = URL.createObjectURL(source);
|
|
90
|
+
this.objectUrls.push(url); // Simpan untuk di-reset nanti
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
url = source; // Base64 atau URL string
|
|
94
|
+
}
|
|
95
|
+
img.onload = () => resolve(img);
|
|
96
|
+
img.onerror = () => reject("Failed to load image source");
|
|
97
|
+
img.src = url;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
findSlotsBFS(imageData) {
|
|
102
|
+
const { width, height, data } = imageData;
|
|
103
|
+
const visited = new Uint8Array(width * height);
|
|
104
|
+
const slots = [];
|
|
105
|
+
for (let i = 0; i < width * height; i++) {
|
|
106
|
+
if (data[i * 4 + 3] < this.config.alphaThreshold && !visited[i]) {
|
|
107
|
+
const queue = [i];
|
|
108
|
+
visited[i] = 1;
|
|
109
|
+
let minX = i % width, maxX = minX, minY = Math.floor(i / width), maxY = minY;
|
|
110
|
+
let head = 0;
|
|
111
|
+
while (head < queue.length) {
|
|
112
|
+
const curr = queue[head++];
|
|
113
|
+
const cx = curr % width, cy = Math.floor(curr / width);
|
|
114
|
+
const neighbors = [[cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]];
|
|
115
|
+
for (const [nx, ny] of neighbors) {
|
|
116
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
117
|
+
const nIdx = ny * width + nx;
|
|
118
|
+
if (!visited[nIdx] && data[nIdx * 4 + 3] < this.config.alphaThreshold) {
|
|
119
|
+
visited[nIdx] = 1;
|
|
120
|
+
queue.push(nIdx);
|
|
121
|
+
if (nx < minX)
|
|
122
|
+
minX = nx;
|
|
123
|
+
if (nx > maxX)
|
|
124
|
+
maxX = nx;
|
|
125
|
+
if (ny < minY)
|
|
126
|
+
minY = ny;
|
|
127
|
+
if (ny > maxY)
|
|
128
|
+
maxY = ny;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (maxX - minX > this.config.minSlotSize) {
|
|
134
|
+
slots.push({ x: minX, y: minY, width: maxX - minX, height: maxY - minY });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return slots.sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
139
|
+
}
|
|
140
|
+
drawCover(ctx, img, slot) {
|
|
141
|
+
const expansion = this.config.slotExpansion;
|
|
142
|
+
const targetX = slot.x - expansion;
|
|
143
|
+
const targetY = slot.y - expansion;
|
|
144
|
+
const targetW = slot.width + (expansion * 2);
|
|
145
|
+
const targetH = slot.height + (expansion * 2);
|
|
146
|
+
const imgRatio = img.width / img.height;
|
|
147
|
+
const slotRatio = targetW / targetH;
|
|
148
|
+
let sw, sh, sx, sy;
|
|
149
|
+
if (imgRatio > slotRatio) {
|
|
150
|
+
sw = img.height * slotRatio;
|
|
151
|
+
sh = img.height;
|
|
152
|
+
sx = (img.width - sw) / 2;
|
|
153
|
+
sy = 0;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
sw = img.width;
|
|
157
|
+
sh = img.width / slotRatio;
|
|
158
|
+
sx = 0;
|
|
159
|
+
sy = (img.height - sh) / 2;
|
|
160
|
+
}
|
|
161
|
+
ctx.drawImage(img, sx, sy, sw, sh, targetX, targetY, targetW, targetH);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.PhotoboothFrameGenerator = PhotoboothFrameGenerator;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ImageSource = string | File;
|
|
2
|
+
export interface Slot {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PhotoboothConfig {
|
|
9
|
+
alphaThreshold?: number;
|
|
10
|
+
minSlotSize?: number;
|
|
11
|
+
outputFormat?: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
12
|
+
quality?: number;
|
|
13
|
+
fillEmptySlots?: boolean;
|
|
14
|
+
slotExpansion?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface RenderResult {
|
|
17
|
+
dataUrl: string;
|
|
18
|
+
slotsFound: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ImageSource = string | File;
|
|
2
|
+
export interface Slot {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PhotoboothConfig {
|
|
9
|
+
alphaThreshold?: number;
|
|
10
|
+
minSlotSize?: number;
|
|
11
|
+
outputFormat?: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
12
|
+
quality?: number;
|
|
13
|
+
fillEmptySlots?: boolean;
|
|
14
|
+
slotExpansion?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface RenderResult {
|
|
17
|
+
dataUrl: string;
|
|
18
|
+
slotsFound: number;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kotaksurat/photobooth-frame-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/ahdja/photobooth-frame-generator"
|
|
7
|
+
},
|
|
8
|
+
"description": "A fast and efficient engine to automate placing user photos into a frame's transparent slots.",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"photobooth",
|
|
27
|
+
"canvas",
|
|
28
|
+
"image-processing",
|
|
29
|
+
"typescript"
|
|
30
|
+
],
|
|
31
|
+
"author": "Arief Hidayat Djauhar",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"type": "commonjs",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.5.2",
|
|
36
|
+
"jsdom": "^29.0.1",
|
|
37
|
+
"tslib": "^2.8.1",
|
|
38
|
+
"typescript": "^6.0.2",
|
|
39
|
+
"vitest": "^4.1.2"
|
|
40
|
+
}
|
|
41
|
+
}
|