apexify.js 4.9.30 → 5.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 +56 -1
- package/dist/cjs/Canvas/ApexPainter.d.ts +96 -145
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +1247 -418
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts +7 -2
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js +3 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts +75 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js +263 -0
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -0
- package/dist/cjs/Canvas/utils/Custom/customLines.d.ts +2 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.js +73 -6
- package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts +17 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.js +88 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/general functions.d.ts +25 -3
- package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.js +37 -9
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageCompression.d.ts +19 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.js +262 -0
- package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts +20 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.js +227 -0
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts +37 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.js +128 -0
- package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts +67 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +276 -0
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -0
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +121 -0
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +32 -1
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +96 -145
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +1247 -418
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
- package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
- package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
- package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
- package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
- package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
- package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
- package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
- package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
- package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.js +37 -9
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
- package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
- package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
- package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +121 -0
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +9 -2
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +32 -1
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/Canvas/ApexPainter.ts +1118 -266
- package/lib/Canvas/utils/Charts/charts.ts +16 -7
- package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
- package/lib/Canvas/utils/Custom/customLines.ts +84 -9
- package/lib/Canvas/utils/General/batchOperations.ts +103 -0
- package/lib/Canvas/utils/General/general functions.ts +85 -41
- package/lib/Canvas/utils/General/imageCompression.ts +316 -0
- package/lib/Canvas/utils/General/imageStitching.ts +252 -0
- package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
- package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
- package/lib/Canvas/utils/types.ts +121 -0
- package/lib/Canvas/utils/utils.ts +49 -2
- package/package.json +69 -34
|
@@ -46,7 +46,13 @@ class ApexPainter {
|
|
|
46
46
|
* @param textProps - Text properties
|
|
47
47
|
*/
|
|
48
48
|
async #renderEnhancedText(ctx, textProps) {
|
|
49
|
-
|
|
49
|
+
// Check if text should be rendered on a path
|
|
50
|
+
if (textProps.path && textProps.textOnPath) {
|
|
51
|
+
(0, utils_1.renderTextOnPath)(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await enhancedTextRenderer_1.EnhancedTextRenderer.renderText(ctx, textProps);
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Creates a canvas with the given configuration.
|
|
@@ -79,75 +85,162 @@ class ApexPainter {
|
|
|
79
85
|
* const buffer = result.buffer;
|
|
80
86
|
* ```
|
|
81
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* Validates canvas configuration.
|
|
90
|
+
* @private
|
|
91
|
+
* @param canvas - Canvas configuration to validate
|
|
92
|
+
*/
|
|
93
|
+
#validateCanvasConfig(canvas) {
|
|
94
|
+
if (!canvas) {
|
|
95
|
+
throw new Error("createCanvas: canvas configuration is required.");
|
|
96
|
+
}
|
|
97
|
+
if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
|
|
98
|
+
throw new Error("createCanvas: width must be a positive number.");
|
|
99
|
+
}
|
|
100
|
+
if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
|
|
101
|
+
throw new Error("createCanvas: height must be a positive number.");
|
|
102
|
+
}
|
|
103
|
+
if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
|
|
104
|
+
throw new Error("createCanvas: opacity must be a number between 0 and 1.");
|
|
105
|
+
}
|
|
106
|
+
if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
|
|
107
|
+
throw new Error("createCanvas: zoom.scale must be a positive number.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
82
110
|
async createCanvas(canvas) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Apply shadow effect
|
|
137
|
-
if (shadow) {
|
|
111
|
+
try {
|
|
112
|
+
// Validate canvas configuration
|
|
113
|
+
this.#validateCanvasConfig(canvas);
|
|
114
|
+
// Handle inherit sizing
|
|
115
|
+
if (canvas.customBg?.inherit) {
|
|
116
|
+
let p = canvas.customBg.source;
|
|
117
|
+
if (!/^https?:\/\//i.test(p))
|
|
118
|
+
p = path_1.default.join(process.cwd(), p);
|
|
119
|
+
try {
|
|
120
|
+
const img = await (0, canvas_1.loadImage)(p);
|
|
121
|
+
canvas.width = img.width;
|
|
122
|
+
canvas.height = img.height;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
126
|
+
throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Handle video background inherit sizing
|
|
130
|
+
if (canvas.videoBg) {
|
|
131
|
+
try {
|
|
132
|
+
const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0);
|
|
133
|
+
if (frameBuffer) {
|
|
134
|
+
const img = await (0, canvas_1.loadImage)(frameBuffer);
|
|
135
|
+
if (!canvas.width)
|
|
136
|
+
canvas.width = img.width;
|
|
137
|
+
if (!canvas.height)
|
|
138
|
+
canvas.height = img.height;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 2) Use final width/height after inherit
|
|
146
|
+
const width = canvas.width ?? 500;
|
|
147
|
+
const height = canvas.height ?? 500;
|
|
148
|
+
const { x = 0, y = 0, rotation = 0, borderRadius = 0, borderPosition = 'all', opacity = 1, colorBg, customBg, gradientBg, videoBg, patternBg, noiseBg, blendMode, zoom, stroke, shadow, blur } = canvas;
|
|
149
|
+
// Validate background configuration
|
|
150
|
+
const bgSources = [
|
|
151
|
+
canvas.colorBg ? 'colorBg' : null,
|
|
152
|
+
canvas.gradientBg ? 'gradientBg' : null,
|
|
153
|
+
canvas.customBg ? 'customBg' : null
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
if (bgSources.length > 1) {
|
|
156
|
+
throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
const cv = (0, canvas_1.createCanvas)(width, height);
|
|
159
|
+
const ctx = cv.getContext('2d');
|
|
160
|
+
if (!ctx)
|
|
161
|
+
throw new Error('Unable to get 2D context');
|
|
162
|
+
ctx.globalAlpha = opacity;
|
|
163
|
+
// ---- BACKGROUND (clipped) ----
|
|
138
164
|
ctx.save();
|
|
165
|
+
(0, utils_1.applyRotation)(ctx, rotation, x, y, width, height);
|
|
139
166
|
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
140
|
-
|
|
167
|
+
ctx.clip();
|
|
168
|
+
(0, utils_1.applyCanvasZoom)(ctx, width, height, zoom);
|
|
169
|
+
ctx.translate(x, y);
|
|
170
|
+
if (typeof blendMode === 'string') {
|
|
171
|
+
ctx.globalCompositeOperation = blendMode;
|
|
172
|
+
}
|
|
173
|
+
// Draw video background if specified
|
|
174
|
+
if (videoBg) {
|
|
175
|
+
try {
|
|
176
|
+
const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0);
|
|
177
|
+
if (frameBuffer) {
|
|
178
|
+
const videoImg = await (0, canvas_1.loadImage)(frameBuffer);
|
|
179
|
+
ctx.globalAlpha = videoBg.opacity ?? 1;
|
|
180
|
+
ctx.drawImage(videoImg, 0, 0, width, height);
|
|
181
|
+
ctx.globalAlpha = opacity;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
console.warn('createCanvas: Failed to load video background frame');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Draw custom background with filters and opacity support
|
|
189
|
+
if (customBg) {
|
|
190
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
191
|
+
// Apply filters to background if specified
|
|
192
|
+
if (customBg.filters && customBg.filters.length > 0) {
|
|
193
|
+
const tempCanvas = (0, canvas_1.createCanvas)(width, height);
|
|
194
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
195
|
+
if (tempCtx) {
|
|
196
|
+
tempCtx.drawImage(cv, 0, 0);
|
|
197
|
+
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, customBg.filters, width, height);
|
|
198
|
+
ctx.clearRect(0, 0, width, height);
|
|
199
|
+
ctx.globalAlpha = customBg.opacity ?? 1;
|
|
200
|
+
ctx.drawImage(tempCanvas, 0, 0);
|
|
201
|
+
ctx.globalAlpha = opacity;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
|
|
205
|
+
ctx.globalAlpha = customBg.opacity;
|
|
206
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
207
|
+
ctx.globalAlpha = opacity;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (gradientBg) {
|
|
214
|
+
await (0, utils_1.drawBackgroundGradient)(ctx, { ...canvas, blur });
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
await (0, utils_1.drawBackgroundColor)(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
|
|
218
|
+
}
|
|
219
|
+
if (patternBg)
|
|
220
|
+
await enhancedPatternRenderer_1.EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
|
|
221
|
+
if (noiseBg)
|
|
222
|
+
(0, utils_1.applyNoise)(ctx, width, height, noiseBg.intensity ?? 0.05);
|
|
141
223
|
ctx.restore();
|
|
224
|
+
// Apply shadow effect
|
|
225
|
+
if (shadow) {
|
|
226
|
+
ctx.save();
|
|
227
|
+
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
228
|
+
(0, utils_1.applyShadow)(ctx, shadow, x, y, width, height);
|
|
229
|
+
ctx.restore();
|
|
230
|
+
}
|
|
231
|
+
// Apply stroke effect
|
|
232
|
+
if (stroke) {
|
|
233
|
+
ctx.save();
|
|
234
|
+
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
235
|
+
(0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
|
|
236
|
+
ctx.restore();
|
|
237
|
+
}
|
|
238
|
+
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
142
239
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
147
|
-
(0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
|
|
148
|
-
ctx.restore();
|
|
240
|
+
catch (error) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
242
|
+
throw new Error(`createCanvas failed: ${errorMessage}`);
|
|
149
243
|
}
|
|
150
|
-
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
151
244
|
}
|
|
152
245
|
/**
|
|
153
246
|
* Draws one or more images (or shapes) on an existing canvas buffer.
|
|
@@ -191,24 +284,49 @@ class ApexPainter {
|
|
|
191
284
|
* ], canvasBuffer);
|
|
192
285
|
* ```
|
|
193
286
|
*/
|
|
194
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Validates image/shape properties array.
|
|
289
|
+
* @private
|
|
290
|
+
* @param images - Image properties to validate
|
|
291
|
+
*/
|
|
292
|
+
#validateImageArray(images) {
|
|
195
293
|
const list = Array.isArray(images) ? images : [images];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
200
|
-
const cv = (0, canvas_1.createCanvas)(base.width, base.height);
|
|
201
|
-
const ctx = cv.getContext("2d");
|
|
202
|
-
if (!ctx)
|
|
203
|
-
throw new Error("Unable to get 2D rendering context");
|
|
204
|
-
// Paint bg
|
|
205
|
-
ctx.drawImage(base, 0, 0);
|
|
206
|
-
// Draw each image/shape on canvas
|
|
294
|
+
if (list.length === 0) {
|
|
295
|
+
throw new Error("createImage: At least one image/shape is required.");
|
|
296
|
+
}
|
|
207
297
|
for (const ip of list) {
|
|
208
|
-
|
|
298
|
+
this.#validateImageProperties(ip);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async createImage(images, canvasBuffer) {
|
|
302
|
+
try {
|
|
303
|
+
// Validate inputs
|
|
304
|
+
if (!canvasBuffer) {
|
|
305
|
+
throw new Error("createImage: canvasBuffer is required.");
|
|
306
|
+
}
|
|
307
|
+
this.#validateImageArray(images);
|
|
308
|
+
const list = Array.isArray(images) ? images : [images];
|
|
309
|
+
// Load base canvas buffer
|
|
310
|
+
const base = Buffer.isBuffer(canvasBuffer)
|
|
311
|
+
? await (0, canvas_1.loadImage)(canvasBuffer)
|
|
312
|
+
: await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
313
|
+
const cv = (0, canvas_1.createCanvas)(base.width, base.height);
|
|
314
|
+
const ctx = cv.getContext("2d");
|
|
315
|
+
if (!ctx)
|
|
316
|
+
throw new Error("Unable to get 2D rendering context");
|
|
317
|
+
// Paint bg
|
|
318
|
+
ctx.drawImage(base, 0, 0);
|
|
319
|
+
// Draw each image/shape on canvas
|
|
320
|
+
for (const ip of list) {
|
|
321
|
+
await this.#drawImageBitmap(ctx, ip);
|
|
322
|
+
}
|
|
323
|
+
// Return updated buffer
|
|
324
|
+
return cv.toBuffer("image/png");
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
328
|
+
throw new Error(`createImage failed: ${errorMessage}`);
|
|
209
329
|
}
|
|
210
|
-
// Return updated buffer
|
|
211
|
-
return cv.toBuffer("image/png");
|
|
212
330
|
}
|
|
213
331
|
/**
|
|
214
332
|
* Draws a single bitmap or shape with independent shadow & stroke.
|
|
@@ -217,7 +335,7 @@ class ApexPainter {
|
|
|
217
335
|
* @param ip - Image properties
|
|
218
336
|
*/
|
|
219
337
|
async #drawImageBitmap(ctx, ip) {
|
|
220
|
-
const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters } = ip;
|
|
338
|
+
const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters, filterIntensity = 1, filterOrder = 'post', mask, clipPath, distortion, meshWarp, effects } = ip;
|
|
221
339
|
this.#validateImageProperties(ip);
|
|
222
340
|
// Check if source is a shape
|
|
223
341
|
if ((0, utils_1.isShapeSource)(source)) {
|
|
@@ -248,23 +366,126 @@ class ApexPainter {
|
|
|
248
366
|
(0, utils_1.applyShadow)(ctx, box, shadow);
|
|
249
367
|
// 2) Optional box background (under bitmap, inside clip) — color or gradient
|
|
250
368
|
(0, utils_1.drawBoxBackground)(ctx, box, boxBackground, borderRadius, borderPosition);
|
|
251
|
-
// 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
|
|
369
|
+
// 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
|
|
252
370
|
ctx.save();
|
|
253
|
-
|
|
254
|
-
|
|
371
|
+
if (clipPath && clipPath.length >= 3) {
|
|
372
|
+
(0, utils_1.applyClipPath)(ctx, clipPath);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
(0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
|
|
376
|
+
ctx.clip();
|
|
377
|
+
}
|
|
255
378
|
const { dx, dy, dw, dh, sx, sy, sw, sh } = (0, utils_1.fitInto)(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
|
|
256
379
|
const prevAlpha = ctx.globalAlpha;
|
|
257
380
|
ctx.globalAlpha = opacity ?? 1;
|
|
258
381
|
if ((blur ?? 0) > 0)
|
|
259
382
|
ctx.filter = `blur(${blur}px)`;
|
|
260
|
-
// Apply professional image filters BEFORE drawing
|
|
261
|
-
if (filters && filters.length > 0) {
|
|
262
|
-
|
|
383
|
+
// Apply professional image filters BEFORE drawing if filterOrder is 'pre'
|
|
384
|
+
if (filters && filters.length > 0 && filterOrder === 'pre') {
|
|
385
|
+
const adjustedFilters = filters.map(f => ({
|
|
386
|
+
...f,
|
|
387
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
388
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
389
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
390
|
+
}));
|
|
391
|
+
await (0, utils_1.applySimpleProfessionalFilters)(ctx, adjustedFilters, dw, dh);
|
|
392
|
+
}
|
|
393
|
+
// Apply distortion if specified (before drawing)
|
|
394
|
+
if (distortion) {
|
|
395
|
+
if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
|
|
396
|
+
(0, utils_1.applyPerspectiveDistortion)(ctx, img, distortion.points, dx, dy, dw, dh);
|
|
397
|
+
ctx.filter = "none";
|
|
398
|
+
ctx.globalAlpha = prevAlpha;
|
|
399
|
+
ctx.restore();
|
|
400
|
+
ctx.restore();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
|
|
404
|
+
const centerX = dx + dw / 2;
|
|
405
|
+
const centerY = dy + dh / 2;
|
|
406
|
+
const radius = Math.min(dw, dh) / 2;
|
|
407
|
+
const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
|
|
408
|
+
(0, utils_1.applyBulgeDistortion)(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
|
|
409
|
+
ctx.filter = "none";
|
|
410
|
+
ctx.globalAlpha = prevAlpha;
|
|
411
|
+
ctx.restore();
|
|
412
|
+
ctx.restore();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Apply mesh warp if specified
|
|
417
|
+
if (meshWarp && meshWarp.controlPoints) {
|
|
418
|
+
(0, utils_1.applyMeshWarp)(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
|
|
419
|
+
ctx.filter = "none";
|
|
420
|
+
ctx.globalAlpha = prevAlpha;
|
|
421
|
+
ctx.restore();
|
|
422
|
+
ctx.restore();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Draw image with or without masking
|
|
426
|
+
if (mask) {
|
|
427
|
+
await (0, utils_1.applyImageMask)(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
263
431
|
}
|
|
264
|
-
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
265
432
|
ctx.filter = "none";
|
|
266
433
|
ctx.globalAlpha = prevAlpha;
|
|
267
434
|
ctx.restore();
|
|
435
|
+
// Apply professional image filters AFTER drawing if filterOrder is 'post'
|
|
436
|
+
if (filters && filters.length > 0 && filterOrder === 'post') {
|
|
437
|
+
ctx.save();
|
|
438
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
439
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
440
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
441
|
+
if (tempCtx) {
|
|
442
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
443
|
+
const adjustedFilters = filters.map(f => ({
|
|
444
|
+
...f,
|
|
445
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
446
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
447
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
448
|
+
}));
|
|
449
|
+
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, adjustedFilters, box.w, box.h);
|
|
450
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
451
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
452
|
+
}
|
|
453
|
+
ctx.restore();
|
|
454
|
+
}
|
|
455
|
+
// Apply effects stack
|
|
456
|
+
if (effects) {
|
|
457
|
+
ctx.save();
|
|
458
|
+
const effectsCtx = ctx;
|
|
459
|
+
if (effects.vignette) {
|
|
460
|
+
(0, utils_1.applyVignette)(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
|
|
461
|
+
}
|
|
462
|
+
if (effects.lensFlare) {
|
|
463
|
+
(0, utils_1.applyLensFlare)(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
|
|
464
|
+
}
|
|
465
|
+
if (effects.chromaticAberration) {
|
|
466
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
467
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
468
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
469
|
+
if (tempCtx) {
|
|
470
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
471
|
+
(0, utils_1.applyChromaticAberration)(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
|
|
472
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
473
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (effects.filmGrain) {
|
|
477
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
478
|
+
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
479
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
480
|
+
if (tempCtx) {
|
|
481
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
482
|
+
(0, utils_1.applyFilmGrain)(tempCtx, effects.filmGrain.intensity, box.w, box.h);
|
|
483
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
484
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
ctx.restore();
|
|
488
|
+
}
|
|
268
489
|
// 4) Stroke (independent) — supports gradient or color
|
|
269
490
|
(0, utils_1.applyStroke)(ctx, box, stroke);
|
|
270
491
|
ctx.restore();
|
|
@@ -488,14 +709,29 @@ class ApexPainter {
|
|
|
488
709
|
* ], canvasBuffer);
|
|
489
710
|
* ```
|
|
490
711
|
*/
|
|
712
|
+
/**
|
|
713
|
+
* Validates text properties array.
|
|
714
|
+
* @private
|
|
715
|
+
* @param textArray - Text properties to validate
|
|
716
|
+
*/
|
|
717
|
+
#validateTextArray(textArray) {
|
|
718
|
+
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
719
|
+
if (textList.length === 0) {
|
|
720
|
+
throw new Error("createText: At least one text object is required.");
|
|
721
|
+
}
|
|
722
|
+
for (const textProps of textList) {
|
|
723
|
+
this.#validateTextProperties(textProps);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
491
726
|
async createText(textArray, canvasBuffer) {
|
|
492
727
|
try {
|
|
728
|
+
// Validate inputs
|
|
729
|
+
if (!canvasBuffer) {
|
|
730
|
+
throw new Error("createText: canvasBuffer is required.");
|
|
731
|
+
}
|
|
732
|
+
this.#validateTextArray(textArray);
|
|
493
733
|
// Ensure textArray is an array
|
|
494
734
|
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
495
|
-
// Validate each text object
|
|
496
|
-
for (const textProps of textList) {
|
|
497
|
-
this.#validateTextProperties(textProps);
|
|
498
|
-
}
|
|
499
735
|
// Load existing canvas buffer
|
|
500
736
|
let existingImage;
|
|
501
737
|
if (Buffer.isBuffer(canvasBuffer)) {
|
|
@@ -525,15 +761,37 @@ class ApexPainter {
|
|
|
525
761
|
return canvas.toBuffer("image/png");
|
|
526
762
|
}
|
|
527
763
|
catch (error) {
|
|
528
|
-
|
|
529
|
-
throw new Error(
|
|
764
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
765
|
+
throw new Error(`createText failed: ${errorMessage}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Validates custom line options.
|
|
770
|
+
* @private
|
|
771
|
+
* @param options - Custom options to validate
|
|
772
|
+
*/
|
|
773
|
+
#validateCustomOptions(options) {
|
|
774
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
775
|
+
if (opts.length === 0) {
|
|
776
|
+
throw new Error("createCustom: At least one custom option is required.");
|
|
777
|
+
}
|
|
778
|
+
for (const opt of opts) {
|
|
779
|
+
if (!opt.startCoordinates || typeof opt.startCoordinates.x !== 'number' || typeof opt.startCoordinates.y !== 'number') {
|
|
780
|
+
throw new Error("createCustom: startCoordinates with valid x and y are required.");
|
|
781
|
+
}
|
|
782
|
+
if (!opt.endCoordinates || typeof opt.endCoordinates.x !== 'number' || typeof opt.endCoordinates.y !== 'number') {
|
|
783
|
+
throw new Error("createCustom: endCoordinates with valid x and y are required.");
|
|
784
|
+
}
|
|
530
785
|
}
|
|
531
786
|
}
|
|
532
787
|
async createCustom(options, buffer) {
|
|
533
788
|
try {
|
|
534
|
-
|
|
535
|
-
|
|
789
|
+
// Validate inputs
|
|
790
|
+
if (!buffer) {
|
|
791
|
+
throw new Error("createCustom: buffer is required.");
|
|
536
792
|
}
|
|
793
|
+
this.#validateCustomOptions(options);
|
|
794
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
537
795
|
let existingImage;
|
|
538
796
|
if (Buffer.isBuffer(buffer)) {
|
|
539
797
|
existingImage = await (0, canvas_1.loadImage)(buffer);
|
|
@@ -550,65 +808,68 @@ class ApexPainter {
|
|
|
550
808
|
const canvas = (0, canvas_1.createCanvas)(existingImage.width, existingImage.height);
|
|
551
809
|
const ctx = canvas.getContext("2d");
|
|
552
810
|
ctx.drawImage(existingImage, 0, 0);
|
|
553
|
-
(0, utils_1.customLines)(ctx,
|
|
811
|
+
await (0, utils_1.customLines)(ctx, opts);
|
|
554
812
|
return canvas.toBuffer("image/png");
|
|
555
813
|
}
|
|
556
814
|
catch (error) {
|
|
557
|
-
|
|
558
|
-
throw new Error(
|
|
815
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
816
|
+
throw new Error(`createCustom failed: ${errorMessage}`);
|
|
559
817
|
}
|
|
560
818
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
function createBufferStream() {
|
|
572
|
-
const bufferStream = new stream_1.PassThrough();
|
|
573
|
-
const chunks = [];
|
|
574
|
-
bufferStream.on('data', (chunk) => {
|
|
575
|
-
chunks.push(chunk);
|
|
576
|
-
});
|
|
577
|
-
return {
|
|
578
|
-
...bufferStream,
|
|
579
|
-
getBuffer: function () {
|
|
580
|
-
return Buffer.concat(chunks);
|
|
581
|
-
}
|
|
582
|
-
};
|
|
819
|
+
/**
|
|
820
|
+
* Validates GIF options and frames.
|
|
821
|
+
* @private
|
|
822
|
+
* @param gifFrames - GIF frames to validate
|
|
823
|
+
* @param options - GIF options to validate
|
|
824
|
+
*/
|
|
825
|
+
#validateGIFOptions(gifFrames, options) {
|
|
826
|
+
if (!gifFrames || gifFrames.length === 0) {
|
|
827
|
+
throw new Error("createGIF: At least one frame is required.");
|
|
583
828
|
}
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
throw new Error("
|
|
829
|
+
for (const frame of gifFrames) {
|
|
830
|
+
if (!frame.background) {
|
|
831
|
+
throw new Error("createGIF: Each frame must have a background property.");
|
|
587
832
|
}
|
|
588
|
-
if (
|
|
589
|
-
throw new Error("
|
|
833
|
+
if (typeof frame.duration !== 'number' || frame.duration < 0) {
|
|
834
|
+
throw new Error("createGIF: Each frame duration must be a non-negative number.");
|
|
590
835
|
}
|
|
591
|
-
|
|
592
|
-
|
|
836
|
+
}
|
|
837
|
+
if (options.outputFormat === "file" && !options.outputFile) {
|
|
838
|
+
throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
|
|
839
|
+
}
|
|
840
|
+
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
841
|
+
throw new Error("createGIF: repeat must be a non-negative number or undefined.");
|
|
842
|
+
}
|
|
843
|
+
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
844
|
+
throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async createGIF(gifFrames, options) {
|
|
848
|
+
try {
|
|
849
|
+
this.#validateGIFOptions(gifFrames, options);
|
|
850
|
+
async function resizeImage(image, targetWidth, targetHeight) {
|
|
851
|
+
const canvas = (0, canvas_1.createCanvas)(targetWidth, targetHeight);
|
|
852
|
+
const ctx = canvas.getContext("2d");
|
|
853
|
+
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
854
|
+
return canvas;
|
|
593
855
|
}
|
|
594
|
-
|
|
595
|
-
|
|
856
|
+
function createOutputStream(outputFile) {
|
|
857
|
+
return fs_1.default.createWriteStream(outputFile);
|
|
596
858
|
}
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
859
|
+
function createBufferStream() {
|
|
860
|
+
const bufferStream = new stream_1.PassThrough();
|
|
861
|
+
const chunks = [];
|
|
862
|
+
bufferStream.on('data', (chunk) => {
|
|
863
|
+
chunks.push(chunk);
|
|
864
|
+
});
|
|
865
|
+
return {
|
|
866
|
+
...bufferStream,
|
|
867
|
+
getBuffer: function () {
|
|
868
|
+
return Buffer.concat(chunks);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
608
871
|
}
|
|
609
|
-
|
|
610
|
-
try {
|
|
611
|
-
validateOptions(options);
|
|
872
|
+
// Validation is done in #validateGIFOptions
|
|
612
873
|
const canvasWidth = options.width || 1200;
|
|
613
874
|
const canvasHeight = options.height || 1200;
|
|
614
875
|
const encoder = new gifencoder_1.default(canvasWidth, canvasHeight);
|
|
@@ -619,6 +880,8 @@ class ApexPainter {
|
|
|
619
880
|
encoder.setQuality(options.quality || 10);
|
|
620
881
|
const canvas = (0, canvas_1.createCanvas)(canvasWidth, canvasHeight);
|
|
621
882
|
const ctx = canvas.getContext("2d");
|
|
883
|
+
if (!ctx)
|
|
884
|
+
throw new Error("Unable to get 2D context");
|
|
622
885
|
for (const frame of gifFrames) {
|
|
623
886
|
const image = await (0, canvas_1.loadImage)(frame.background);
|
|
624
887
|
const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
|
|
@@ -639,12 +902,13 @@ class ApexPainter {
|
|
|
639
902
|
encoder.finish();
|
|
640
903
|
outputStream.end();
|
|
641
904
|
if (options.outputFormat === "file") {
|
|
642
|
-
await new Promise((resolve) => outputStream.on("finish", resolve));
|
|
905
|
+
await new Promise((resolve) => outputStream.on("finish", () => resolve()));
|
|
643
906
|
}
|
|
644
907
|
else if (options.outputFormat === "base64") {
|
|
645
908
|
if ('getBuffer' in outputStream) {
|
|
646
909
|
return outputStream.getBuffer().toString("base64");
|
|
647
910
|
}
|
|
911
|
+
throw new Error("createGIF: Unable to get buffer for base64 output.");
|
|
648
912
|
}
|
|
649
913
|
else if (options.outputFormat === "attachment") {
|
|
650
914
|
const gifStream = encoder.createReadStream();
|
|
@@ -654,42 +918,204 @@ class ApexPainter {
|
|
|
654
918
|
if ('getBuffer' in outputStream) {
|
|
655
919
|
return outputStream.getBuffer();
|
|
656
920
|
}
|
|
921
|
+
throw new Error("createGIF: Unable to get buffer for buffer output.");
|
|
657
922
|
}
|
|
658
923
|
else {
|
|
659
924
|
throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
|
|
660
925
|
}
|
|
661
926
|
}
|
|
662
|
-
catch (
|
|
663
|
-
|
|
664
|
-
throw
|
|
927
|
+
catch (error) {
|
|
928
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
929
|
+
throw new Error(`createGIF failed: ${errorMessage}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Validates resize options.
|
|
934
|
+
* @private
|
|
935
|
+
* @param options - Resize options to validate
|
|
936
|
+
*/
|
|
937
|
+
#validateResizeOptions(options) {
|
|
938
|
+
if (!options || !options.imagePath) {
|
|
939
|
+
throw new Error("resize: imagePath is required.");
|
|
940
|
+
}
|
|
941
|
+
if (options.size) {
|
|
942
|
+
if (options.size.width !== undefined && (typeof options.size.width !== 'number' || options.size.width <= 0)) {
|
|
943
|
+
throw new Error("resize: size.width must be a positive number.");
|
|
944
|
+
}
|
|
945
|
+
if (options.size.height !== undefined && (typeof options.size.height !== 'number' || options.size.height <= 0)) {
|
|
946
|
+
throw new Error("resize: size.height must be a positive number.");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (options.quality !== undefined && (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 100)) {
|
|
950
|
+
throw new Error("resize: quality must be a number between 0 and 100.");
|
|
665
951
|
}
|
|
666
952
|
}
|
|
667
953
|
async resize(resizeOptions) {
|
|
668
|
-
|
|
954
|
+
try {
|
|
955
|
+
this.#validateResizeOptions(resizeOptions);
|
|
956
|
+
return await (0, utils_1.resizingImg)(resizeOptions);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
960
|
+
throw new Error(`resize failed: ${errorMessage}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Validates image converter inputs.
|
|
965
|
+
* @private
|
|
966
|
+
* @param source - Image source to validate
|
|
967
|
+
* @param newExtension - Extension to validate
|
|
968
|
+
*/
|
|
969
|
+
#validateConverterInputs(source, newExtension) {
|
|
970
|
+
if (!source) {
|
|
971
|
+
throw new Error("imgConverter: source is required.");
|
|
972
|
+
}
|
|
973
|
+
if (!newExtension) {
|
|
974
|
+
throw new Error("imgConverter: newExtension is required.");
|
|
975
|
+
}
|
|
976
|
+
const validExtensions = ['jpeg', 'png', 'webp', 'tiff', 'gif', 'avif', 'heif', 'raw', 'pdf', 'svg'];
|
|
977
|
+
if (!validExtensions.includes(newExtension.toLowerCase())) {
|
|
978
|
+
throw new Error(`imgConverter: Invalid extension. Supported: ${validExtensions.join(', ')}`);
|
|
979
|
+
}
|
|
669
980
|
}
|
|
670
981
|
async imgConverter(source, newExtension) {
|
|
671
|
-
|
|
982
|
+
try {
|
|
983
|
+
this.#validateConverterInputs(source, newExtension);
|
|
984
|
+
return await (0, utils_1.converter)(source, newExtension);
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
988
|
+
throw new Error(`imgConverter failed: ${errorMessage}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Validates effects inputs.
|
|
993
|
+
* @private
|
|
994
|
+
* @param source - Image source to validate
|
|
995
|
+
* @param filters - Filters array to validate
|
|
996
|
+
*/
|
|
997
|
+
#validateEffectsInputs(source, filters) {
|
|
998
|
+
if (!source) {
|
|
999
|
+
throw new Error("effects: source is required.");
|
|
1000
|
+
}
|
|
1001
|
+
if (!filters || !Array.isArray(filters) || filters.length === 0) {
|
|
1002
|
+
throw new Error("effects: filters array with at least one filter is required.");
|
|
1003
|
+
}
|
|
672
1004
|
}
|
|
673
1005
|
async effects(source, filters) {
|
|
674
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
this.#validateEffectsInputs(source, filters);
|
|
1008
|
+
return await (0, utils_1.imgEffects)(source, filters);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1012
|
+
throw new Error(`effects failed: ${errorMessage}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Validates color filter inputs.
|
|
1017
|
+
* @private
|
|
1018
|
+
* @param source - Image source to validate
|
|
1019
|
+
* @param opacity - Opacity to validate
|
|
1020
|
+
*/
|
|
1021
|
+
#validateColorFilterInputs(source, opacity) {
|
|
1022
|
+
if (!source) {
|
|
1023
|
+
throw new Error("colorsFilter: source is required.");
|
|
1024
|
+
}
|
|
1025
|
+
if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 1)) {
|
|
1026
|
+
throw new Error("colorsFilter: opacity must be a number between 0 and 1.");
|
|
1027
|
+
}
|
|
675
1028
|
}
|
|
676
1029
|
async colorsFilter(source, filterColor, opacity) {
|
|
677
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
this.#validateColorFilterInputs(source, opacity);
|
|
1032
|
+
return await (0, utils_1.applyColorFilters)(source, filterColor, opacity);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1036
|
+
throw new Error(`colorsFilter failed: ${errorMessage}`);
|
|
1037
|
+
}
|
|
678
1038
|
}
|
|
679
1039
|
async colorAnalysis(source) {
|
|
680
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
if (!source) {
|
|
1042
|
+
throw new Error("colorAnalysis: source is required.");
|
|
1043
|
+
}
|
|
1044
|
+
return await (0, utils_1.detectColors)(source);
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1048
|
+
throw new Error(`colorAnalysis failed: ${errorMessage}`);
|
|
1049
|
+
}
|
|
681
1050
|
}
|
|
682
1051
|
async colorsRemover(source, colorToRemove) {
|
|
683
|
-
|
|
1052
|
+
try {
|
|
1053
|
+
if (!source) {
|
|
1054
|
+
throw new Error("colorsRemover: source is required.");
|
|
1055
|
+
}
|
|
1056
|
+
if (!colorToRemove || typeof colorToRemove.red !== 'number' || typeof colorToRemove.green !== 'number' || typeof colorToRemove.blue !== 'number') {
|
|
1057
|
+
throw new Error("colorsRemover: colorToRemove must be an object with red, green, and blue properties (0-255).");
|
|
1058
|
+
}
|
|
1059
|
+
if (colorToRemove.red < 0 || colorToRemove.red > 255 ||
|
|
1060
|
+
colorToRemove.green < 0 || colorToRemove.green > 255 ||
|
|
1061
|
+
colorToRemove.blue < 0 || colorToRemove.blue > 255) {
|
|
1062
|
+
throw new Error("colorsRemover: colorToRemove RGB values must be between 0 and 255.");
|
|
1063
|
+
}
|
|
1064
|
+
return await (0, utils_1.removeColor)(source, colorToRemove);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1068
|
+
throw new Error(`colorsRemover failed: ${errorMessage}`);
|
|
1069
|
+
}
|
|
684
1070
|
}
|
|
685
1071
|
async removeBackground(imageURL, apiKey) {
|
|
686
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
if (!imageURL) {
|
|
1074
|
+
throw new Error("removeBackground: imageURL is required.");
|
|
1075
|
+
}
|
|
1076
|
+
if (!apiKey) {
|
|
1077
|
+
throw new Error("removeBackground: apiKey is required.");
|
|
1078
|
+
}
|
|
1079
|
+
return await (0, utils_1.bgRemoval)(imageURL, apiKey);
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1083
|
+
throw new Error(`removeBackground failed: ${errorMessage}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Validates blend inputs.
|
|
1088
|
+
* @private
|
|
1089
|
+
* @param layers - Layers to validate
|
|
1090
|
+
* @param baseImageBuffer - Base image buffer to validate
|
|
1091
|
+
*/
|
|
1092
|
+
#validateBlendInputs(layers, baseImageBuffer) {
|
|
1093
|
+
if (!baseImageBuffer || !Buffer.isBuffer(baseImageBuffer)) {
|
|
1094
|
+
throw new Error("blend: baseImageBuffer must be a valid Buffer.");
|
|
1095
|
+
}
|
|
1096
|
+
if (!layers || !Array.isArray(layers) || layers.length === 0) {
|
|
1097
|
+
throw new Error("blend: layers array with at least one layer is required.");
|
|
1098
|
+
}
|
|
1099
|
+
for (const layer of layers) {
|
|
1100
|
+
if (!layer.image) {
|
|
1101
|
+
throw new Error("blend: Each layer must have an image property.");
|
|
1102
|
+
}
|
|
1103
|
+
if (!layer.blendMode) {
|
|
1104
|
+
throw new Error("blend: Each layer must have a blendMode property.");
|
|
1105
|
+
}
|
|
1106
|
+
if (layer.opacity !== undefined && (typeof layer.opacity !== 'number' || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1107
|
+
throw new Error("blend: Layer opacity must be a number between 0 and 1.");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
687
1110
|
}
|
|
688
1111
|
async blend(layers, baseImageBuffer, defaultBlendMode = 'source-over') {
|
|
689
1112
|
try {
|
|
1113
|
+
this.#validateBlendInputs(layers, baseImageBuffer);
|
|
690
1114
|
const baseImage = await (0, canvas_1.loadImage)(baseImageBuffer);
|
|
691
1115
|
const canvas = (0, canvas_1.createCanvas)(baseImage.width, baseImage.height);
|
|
692
1116
|
const ctx = canvas.getContext('2d');
|
|
1117
|
+
if (!ctx)
|
|
1118
|
+
throw new Error("Unable to get 2D context");
|
|
693
1119
|
ctx.globalCompositeOperation = defaultBlendMode;
|
|
694
1120
|
ctx.drawImage(baseImage, 0, 0);
|
|
695
1121
|
for (const layer of layers) {
|
|
@@ -703,321 +1129,724 @@ class ApexPainter {
|
|
|
703
1129
|
return canvas.toBuffer('image/png');
|
|
704
1130
|
}
|
|
705
1131
|
catch (error) {
|
|
706
|
-
|
|
707
|
-
throw new Error(
|
|
1132
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1133
|
+
throw new Error(`blend failed: ${errorMessage}`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Validates chart inputs.
|
|
1138
|
+
* @private
|
|
1139
|
+
* @param data - Chart data to validate
|
|
1140
|
+
* @param type - Chart type configuration to validate
|
|
1141
|
+
*/
|
|
1142
|
+
#validateChartInputs(data, type) {
|
|
1143
|
+
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
1144
|
+
throw new Error("createChart: data object with datasets is required.");
|
|
1145
|
+
}
|
|
1146
|
+
if (!type || typeof type !== 'object') {
|
|
1147
|
+
throw new Error("createChart: type configuration object is required.");
|
|
1148
|
+
}
|
|
1149
|
+
if (!type.chartType || typeof type.chartType !== 'string') {
|
|
1150
|
+
throw new Error("createChart: type.chartType must be a string.");
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
|
|
1153
|
+
throw new Error("createChart: type.chartNumber must be a positive number.");
|
|
1154
|
+
}
|
|
1155
|
+
const validChartTypes = ['bar', 'line', 'pie'];
|
|
1156
|
+
if (!validChartTypes.includes(type.chartType.toLowerCase())) {
|
|
1157
|
+
throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
|
|
708
1158
|
}
|
|
709
1159
|
}
|
|
710
1160
|
async createChart(data, type) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1161
|
+
try {
|
|
1162
|
+
this.#validateChartInputs(data, type);
|
|
1163
|
+
const { chartType, chartNumber } = type;
|
|
1164
|
+
switch (chartType.toLowerCase()) {
|
|
1165
|
+
case 'bar':
|
|
1166
|
+
switch (chartNumber) {
|
|
1167
|
+
case 1:
|
|
1168
|
+
const barResult = await (0, utils_1.verticalBarChart)(data);
|
|
1169
|
+
if (!barResult) {
|
|
1170
|
+
throw new Error("createChart: Failed to generate bar chart.");
|
|
1171
|
+
}
|
|
1172
|
+
return barResult;
|
|
1173
|
+
case 2:
|
|
1174
|
+
throw new Error('Type 2 is still under development.');
|
|
1175
|
+
default:
|
|
1176
|
+
throw new Error('Invalid chart number for chart type "bar".');
|
|
1177
|
+
}
|
|
1178
|
+
case 'line':
|
|
1179
|
+
switch (chartNumber) {
|
|
1180
|
+
case 1:
|
|
1181
|
+
// LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
|
|
1182
|
+
// Type assertion needed because there are two different DataPoint interfaces
|
|
1183
|
+
return await (0, utils_1.lineChart)(data);
|
|
1184
|
+
case 2:
|
|
1185
|
+
throw new Error('Type 2 is still under development.');
|
|
1186
|
+
default:
|
|
1187
|
+
throw new Error('Invalid chart number for chart type "line".');
|
|
1188
|
+
}
|
|
1189
|
+
case 'pie':
|
|
1190
|
+
switch (chartNumber) {
|
|
1191
|
+
case 1:
|
|
1192
|
+
return await (0, utils_1.pieChart)(data);
|
|
1193
|
+
case 2:
|
|
1194
|
+
throw new Error('Type 2 is still under development.');
|
|
1195
|
+
default:
|
|
1196
|
+
throw new Error('Invalid chart number for chart type "pie".');
|
|
1197
|
+
}
|
|
1198
|
+
default:
|
|
1199
|
+
throw new Error(`Unsupported chart type "${chartType}".`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1204
|
+
throw new Error(`createChart failed: ${errorMessage}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Validates crop options.
|
|
1209
|
+
* @private
|
|
1210
|
+
* @param options - Crop options to validate
|
|
1211
|
+
*/
|
|
1212
|
+
#validateCropOptions(options) {
|
|
1213
|
+
if (!options) {
|
|
1214
|
+
throw new Error("cropImage: options object is required.");
|
|
1215
|
+
}
|
|
1216
|
+
if (!options.imageSource) {
|
|
1217
|
+
throw new Error("cropImage: imageSource is required.");
|
|
1218
|
+
}
|
|
1219
|
+
if (!options.coordinates || !Array.isArray(options.coordinates) || options.coordinates.length < 3) {
|
|
1220
|
+
throw new Error("cropImage: coordinates array with at least 3 points is required.");
|
|
1221
|
+
}
|
|
1222
|
+
if (options.crop !== 'inner' && options.crop !== 'outer') {
|
|
1223
|
+
throw new Error("cropImage: crop must be either 'inner' or 'outer'.");
|
|
748
1224
|
}
|
|
749
1225
|
}
|
|
750
1226
|
async cropImage(options) {
|
|
751
1227
|
try {
|
|
752
|
-
|
|
753
|
-
throw new Error('The "imageSource" option is needed. Please provide the path to the image to crop.');
|
|
754
|
-
if (!options.coordinates || options.coordinates.length < 3)
|
|
755
|
-
throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
|
|
1228
|
+
this.#validateCropOptions(options);
|
|
756
1229
|
if (options.crop === 'outer') {
|
|
757
1230
|
return await (0, utils_1.cropOuter)(options);
|
|
758
1231
|
}
|
|
759
|
-
else
|
|
1232
|
+
else {
|
|
760
1233
|
return await (0, utils_1.cropInner)(options);
|
|
761
1234
|
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1238
|
+
throw new Error(`cropImage failed: ${errorMessage}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Extracts a single frame from a video
|
|
1243
|
+
* @private
|
|
1244
|
+
* @param videoSource - Video source (path, URL, or Buffer)
|
|
1245
|
+
* @param frameNumber - Frame number to extract (default: 0)
|
|
1246
|
+
* @returns Buffer containing the frame image
|
|
1247
|
+
*/
|
|
1248
|
+
async #extractVideoFrame(videoSource, frameNumber = 0) {
|
|
1249
|
+
try {
|
|
1250
|
+
const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
|
|
1251
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
1252
|
+
fs_1.default.mkdirSync(frameDir, { recursive: true });
|
|
1253
|
+
}
|
|
1254
|
+
const tempVideoPath = path_1.default.join(frameDir, `temp-video-${Date.now()}.mp4`);
|
|
1255
|
+
const frameOutputPath = path_1.default.join(frameDir, `frame-${Date.now()}.jpg`);
|
|
1256
|
+
// Handle video source
|
|
1257
|
+
if (Buffer.isBuffer(videoSource)) {
|
|
1258
|
+
fs_1.default.writeFileSync(tempVideoPath, videoSource);
|
|
1259
|
+
}
|
|
1260
|
+
else if (typeof videoSource === 'string' && videoSource.startsWith('http')) {
|
|
1261
|
+
const response = await (0, axios_1.default)({
|
|
1262
|
+
method: 'get',
|
|
1263
|
+
url: videoSource,
|
|
1264
|
+
responseType: 'arraybuffer'
|
|
1265
|
+
});
|
|
1266
|
+
fs_1.default.writeFileSync(tempVideoPath, Buffer.from(response.data));
|
|
1267
|
+
}
|
|
762
1268
|
else {
|
|
763
|
-
|
|
1269
|
+
// Local file path
|
|
1270
|
+
if (!fs_1.default.existsSync(videoSource)) {
|
|
1271
|
+
throw new Error(`Video file not found: ${videoSource}`);
|
|
1272
|
+
}
|
|
1273
|
+
// Use the existing path
|
|
1274
|
+
return await new Promise((resolve, reject) => {
|
|
1275
|
+
(0, fluent_ffmpeg_1.default)(videoSource)
|
|
1276
|
+
.seekInput(frameNumber / 1000) // Convert frame to seconds (approximate)
|
|
1277
|
+
.frames(1)
|
|
1278
|
+
.output(frameOutputPath)
|
|
1279
|
+
.on('end', () => {
|
|
1280
|
+
try {
|
|
1281
|
+
const buffer = fs_1.default.readFileSync(frameOutputPath);
|
|
1282
|
+
// Cleanup
|
|
1283
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1284
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1285
|
+
resolve(buffer);
|
|
1286
|
+
}
|
|
1287
|
+
catch (e) {
|
|
1288
|
+
resolve(null);
|
|
1289
|
+
}
|
|
1290
|
+
})
|
|
1291
|
+
.on('error', (err) => {
|
|
1292
|
+
reject(err);
|
|
1293
|
+
})
|
|
1294
|
+
.run();
|
|
1295
|
+
});
|
|
764
1296
|
}
|
|
1297
|
+
// Extract frame from temp video
|
|
1298
|
+
return await new Promise((resolve, reject) => {
|
|
1299
|
+
(0, fluent_ffmpeg_1.default)(tempVideoPath)
|
|
1300
|
+
.seekInput(frameNumber / 1000)
|
|
1301
|
+
.frames(1)
|
|
1302
|
+
.output(frameOutputPath)
|
|
1303
|
+
.on('end', () => {
|
|
1304
|
+
try {
|
|
1305
|
+
const buffer = fs_1.default.readFileSync(frameOutputPath);
|
|
1306
|
+
// Cleanup
|
|
1307
|
+
if (fs_1.default.existsSync(tempVideoPath))
|
|
1308
|
+
fs_1.default.unlinkSync(tempVideoPath);
|
|
1309
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1310
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1311
|
+
resolve(buffer);
|
|
1312
|
+
}
|
|
1313
|
+
catch (e) {
|
|
1314
|
+
resolve(null);
|
|
1315
|
+
}
|
|
1316
|
+
})
|
|
1317
|
+
.on('error', (err) => {
|
|
1318
|
+
// Cleanup on error
|
|
1319
|
+
if (fs_1.default.existsSync(tempVideoPath))
|
|
1320
|
+
fs_1.default.unlinkSync(tempVideoPath);
|
|
1321
|
+
if (fs_1.default.existsSync(frameOutputPath))
|
|
1322
|
+
fs_1.default.unlinkSync(frameOutputPath);
|
|
1323
|
+
reject(err);
|
|
1324
|
+
})
|
|
1325
|
+
.run();
|
|
1326
|
+
});
|
|
765
1327
|
}
|
|
766
1328
|
catch (error) {
|
|
767
|
-
console.error('
|
|
768
|
-
|
|
1329
|
+
console.error('Error extracting video frame:', error);
|
|
1330
|
+
return null;
|
|
769
1331
|
}
|
|
770
1332
|
}
|
|
771
|
-
|
|
1333
|
+
/**
|
|
1334
|
+
* Validates extract frames inputs.
|
|
1335
|
+
* @private
|
|
1336
|
+
* @param videoSource - Video source to validate
|
|
1337
|
+
* @param options - Extract frames options to validate
|
|
1338
|
+
*/
|
|
1339
|
+
#validateExtractFramesInputs(videoSource, options) {
|
|
1340
|
+
if (!videoSource) {
|
|
1341
|
+
throw new Error("extractFrames: videoSource is required.");
|
|
1342
|
+
}
|
|
1343
|
+
if (!options || typeof options !== 'object') {
|
|
1344
|
+
throw new Error("extractFrames: options object is required.");
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof options.interval !== 'number' || options.interval <= 0) {
|
|
1347
|
+
throw new Error("extractFrames: options.interval must be a positive number (milliseconds).");
|
|
1348
|
+
}
|
|
1349
|
+
if (options.outputFormat && !['jpg', 'png'].includes(options.outputFormat)) {
|
|
1350
|
+
throw new Error("extractFrames: outputFormat must be 'jpg' or 'png'.");
|
|
1351
|
+
}
|
|
772
1352
|
}
|
|
773
1353
|
async extractFrames(videoSource, options) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1354
|
+
try {
|
|
1355
|
+
this.#validateExtractFramesInputs(videoSource, options);
|
|
1356
|
+
const frames = [];
|
|
1357
|
+
const frameDir = path_1.default.join(__dirname, 'frames');
|
|
1358
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
1359
|
+
fs_1.default.mkdirSync(frameDir);
|
|
1360
|
+
}
|
|
1361
|
+
const videoPath = typeof videoSource === 'string' ? videoSource : path_1.default.join(frameDir, 'temp-video.mp4');
|
|
1362
|
+
if (Buffer.isBuffer(videoSource)) {
|
|
1363
|
+
fs_1.default.writeFileSync(videoPath, videoSource);
|
|
1364
|
+
}
|
|
1365
|
+
else if (videoSource.startsWith('http')) {
|
|
1366
|
+
await (0, axios_1.default)({
|
|
1367
|
+
method: 'get',
|
|
1368
|
+
url: videoSource,
|
|
1369
|
+
responseType: 'arraybuffer'
|
|
1370
|
+
})
|
|
1371
|
+
.then((response) => {
|
|
1372
|
+
fs_1.default.writeFileSync(videoPath, response.data);
|
|
1373
|
+
})
|
|
1374
|
+
.catch(err => {
|
|
1375
|
+
throw new Error(`Error downloading video: ${err.message}`);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
else if (!fs_1.default.existsSync(videoPath)) {
|
|
1379
|
+
throw new Error("Video file not found at specified path.");
|
|
1380
|
+
}
|
|
1381
|
+
function processVideoExtraction(videoPath, frames, options, resolve, reject) {
|
|
1382
|
+
const outputFormat = options.outputFormat || 'jpg';
|
|
1383
|
+
const outputFileTemplate = `frame-%03d.${outputFormat}`;
|
|
1384
|
+
(0, fluent_ffmpeg_1.default)(videoPath)
|
|
1385
|
+
.on('end', () => {
|
|
1386
|
+
console.log('Frames extracted successfully.');
|
|
1387
|
+
resolve(frames);
|
|
1388
|
+
})
|
|
1389
|
+
.on('error', (err) => {
|
|
1390
|
+
console.error('Error extracting frames:', err.message);
|
|
1391
|
+
reject(err);
|
|
1392
|
+
})
|
|
1393
|
+
.outputOptions([`-vf fps=1/${options.interval / 1000}`, `-q:v 2`])
|
|
1394
|
+
.saveToFile(path_1.default.join(frameDir, outputFileTemplate));
|
|
1395
|
+
fluent_ffmpeg_1.default.ffprobe(videoPath, (err, metadata) => {
|
|
1396
|
+
if (err) {
|
|
1397
|
+
return reject(err);
|
|
1398
|
+
}
|
|
1399
|
+
const duration = metadata?.format?.duration;
|
|
1400
|
+
if (typeof duration !== "number") {
|
|
1401
|
+
return reject(new Error("Video duration not found in metadata."));
|
|
1402
|
+
}
|
|
1403
|
+
const totalFrames = Math.floor(duration * 1000 / options.interval);
|
|
1404
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
1405
|
+
if (options.frameSelection && (i < (options.frameSelection.start || 0) || i > (options.frameSelection.end || totalFrames - 1))) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
frames.push({
|
|
1409
|
+
source: path_1.default.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
|
|
1410
|
+
isRemote: false
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
return new Promise((resolve, reject) => {
|
|
1416
|
+
processVideoExtraction(videoPath, frames, options, resolve, reject);
|
|
794
1417
|
});
|
|
795
1418
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1421
|
+
throw new Error(`extractFrames failed: ${errorMessage}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Validates masking inputs.
|
|
1426
|
+
* @private
|
|
1427
|
+
* @param source - Source image to validate
|
|
1428
|
+
* @param maskSource - Mask image to validate
|
|
1429
|
+
* @param options - Mask options to validate
|
|
1430
|
+
*/
|
|
1431
|
+
#validateMaskingInputs(source, maskSource, options) {
|
|
1432
|
+
if (!source) {
|
|
1433
|
+
throw new Error("masking: source is required.");
|
|
1434
|
+
}
|
|
1435
|
+
if (!maskSource) {
|
|
1436
|
+
throw new Error("masking: maskSource is required.");
|
|
1437
|
+
}
|
|
1438
|
+
if (options.type && !['alpha', 'grayscale', 'color'].includes(options.type)) {
|
|
1439
|
+
throw new Error("masking: type must be 'alpha', 'grayscale', or 'color'.");
|
|
1440
|
+
}
|
|
1441
|
+
if (options.type === 'color' && !options.colorKey) {
|
|
1442
|
+
throw new Error("masking: colorKey is required when type is 'color'.");
|
|
1443
|
+
}
|
|
1444
|
+
if (options.threshold !== undefined && (typeof options.threshold !== 'number' || options.threshold < 0 || options.threshold > 255)) {
|
|
1445
|
+
throw new Error("masking: threshold must be a number between 0 and 255.");
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async masking(source, maskSource, options = { type: "alpha" }) {
|
|
1449
|
+
try {
|
|
1450
|
+
this.#validateMaskingInputs(source, maskSource, options);
|
|
1451
|
+
const img = await (0, canvas_1.loadImage)(source);
|
|
1452
|
+
const mask = await (0, canvas_1.loadImage)(maskSource);
|
|
1453
|
+
const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1454
|
+
const ctx = canvas.getContext("2d");
|
|
1455
|
+
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1456
|
+
const maskCanvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1457
|
+
const maskCtx = maskCanvas.getContext("2d");
|
|
1458
|
+
maskCtx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1459
|
+
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
|
|
1460
|
+
const imgData = ctx.getImageData(0, 0, img.width, img.height);
|
|
1461
|
+
for (let i = 0; i < maskData.data.length; i += 4) {
|
|
1462
|
+
let alphaValue = 255;
|
|
1463
|
+
if (options.type === "grayscale") {
|
|
1464
|
+
const grayscale = maskData.data[i] * 0.3 + maskData.data[i + 1] * 0.59 + maskData.data[i + 2] * 0.11;
|
|
1465
|
+
alphaValue = grayscale >= (options.threshold ?? 128) ? 255 : 0;
|
|
816
1466
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return reject(new Error("Video duration not found in metadata."));
|
|
1467
|
+
else if (options.type === "alpha") {
|
|
1468
|
+
alphaValue = maskData.data[i + 3];
|
|
820
1469
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
frames.push({
|
|
827
|
-
source: path_1.default.join(frameDir, `frame-${String(i).padStart(3, '0')}.${outputFormat}`),
|
|
828
|
-
isRemote: false
|
|
829
|
-
});
|
|
1470
|
+
else if (options.type === "color" && options.colorKey) {
|
|
1471
|
+
const colorMatch = maskData.data[i] === parseInt(options.colorKey.slice(1, 3), 16) &&
|
|
1472
|
+
maskData.data[i + 1] === parseInt(options.colorKey.slice(3, 5), 16) &&
|
|
1473
|
+
maskData.data[i + 2] === parseInt(options.colorKey.slice(5, 7), 16);
|
|
1474
|
+
alphaValue = colorMatch ? 0 : 255;
|
|
830
1475
|
}
|
|
831
|
-
|
|
1476
|
+
if (options.invert)
|
|
1477
|
+
alphaValue = 255 - alphaValue;
|
|
1478
|
+
imgData.data[i + 3] = alphaValue;
|
|
1479
|
+
}
|
|
1480
|
+
ctx.putImageData(imgData, 0, 0);
|
|
1481
|
+
return canvas.toBuffer("image/png");
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1485
|
+
throw new Error(`masking failed: ${errorMessage}`);
|
|
832
1486
|
}
|
|
833
|
-
return new Promise((resolve, reject) => {
|
|
834
|
-
processVideoExtraction(videoPath, frames, options, resolve, reject);
|
|
835
|
-
});
|
|
836
1487
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
alphaValue = 255 - alphaValue;
|
|
865
|
-
imgData.data[i + 3] = alphaValue;
|
|
866
|
-
}
|
|
867
|
-
ctx.putImageData(imgData, 0, 0);
|
|
868
|
-
return canvas.toBuffer("image/png");
|
|
1488
|
+
/**
|
|
1489
|
+
* Validates gradient blend inputs.
|
|
1490
|
+
* @private
|
|
1491
|
+
* @param source - Source image to validate
|
|
1492
|
+
* @param options - Blend options to validate
|
|
1493
|
+
*/
|
|
1494
|
+
#validateGradientBlendInputs(source, options) {
|
|
1495
|
+
if (!source) {
|
|
1496
|
+
throw new Error("gradientBlend: source is required.");
|
|
1497
|
+
}
|
|
1498
|
+
if (!options || typeof options !== 'object') {
|
|
1499
|
+
throw new Error("gradientBlend: options object is required.");
|
|
1500
|
+
}
|
|
1501
|
+
if (!options.colors || !Array.isArray(options.colors) || options.colors.length === 0) {
|
|
1502
|
+
throw new Error("gradientBlend: options.colors array with at least one color stop is required.");
|
|
1503
|
+
}
|
|
1504
|
+
if (options.type && !['linear', 'radial', 'conic'].includes(options.type)) {
|
|
1505
|
+
throw new Error("gradientBlend: type must be 'linear', 'radial', or 'conic'.");
|
|
1506
|
+
}
|
|
1507
|
+
for (const colorStop of options.colors) {
|
|
1508
|
+
if (typeof colorStop.stop !== 'number' || colorStop.stop < 0 || colorStop.stop > 1) {
|
|
1509
|
+
throw new Error("gradientBlend: Each color stop must have a stop value between 0 and 1.");
|
|
1510
|
+
}
|
|
1511
|
+
if (!colorStop.color || typeof colorStop.color !== 'string') {
|
|
1512
|
+
throw new Error("gradientBlend: Each color stop must have a valid color string.");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
869
1515
|
}
|
|
870
1516
|
async gradientBlend(source, options) {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1517
|
+
try {
|
|
1518
|
+
this.#validateGradientBlendInputs(source, options);
|
|
1519
|
+
const img = await (0, canvas_1.loadImage)(source);
|
|
1520
|
+
const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
|
|
1521
|
+
const ctx = canvas.getContext("2d");
|
|
1522
|
+
if (!ctx)
|
|
1523
|
+
throw new Error("Unable to get 2D context");
|
|
1524
|
+
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1525
|
+
let gradient;
|
|
1526
|
+
if (options.type === "linear") {
|
|
1527
|
+
const angle = options.angle ?? 0;
|
|
1528
|
+
const radians = (angle * Math.PI) / 180;
|
|
1529
|
+
const x1 = img.width / 2 - (Math.cos(radians) * img.width) / 2;
|
|
1530
|
+
const y1 = img.height / 2 - (Math.sin(radians) * img.height) / 2;
|
|
1531
|
+
const x2 = img.width / 2 + (Math.cos(radians) * img.width) / 2;
|
|
1532
|
+
const y2 = img.height / 2 + (Math.sin(radians) * img.height) / 2;
|
|
1533
|
+
gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
1534
|
+
}
|
|
1535
|
+
else if (options.type === "radial") {
|
|
1536
|
+
gradient = ctx.createRadialGradient(img.width / 2, img.height / 2, 0, img.width / 2, img.height / 2, Math.max(img.width, img.height));
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
|
|
1540
|
+
}
|
|
1541
|
+
options.colors.forEach(({ stop, color }) => gradient.addColorStop(stop, color));
|
|
1542
|
+
ctx.fillStyle = gradient;
|
|
1543
|
+
ctx.globalCompositeOperation = options.blendMode ?? "multiply";
|
|
1544
|
+
ctx.fillRect(0, 0, img.width, img.height);
|
|
1545
|
+
if (options.maskSource) {
|
|
1546
|
+
const mask = await (0, canvas_1.loadImage)(options.maskSource);
|
|
1547
|
+
ctx.globalCompositeOperation = "destination-in";
|
|
1548
|
+
ctx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1549
|
+
}
|
|
1550
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1551
|
+
return canvas.toBuffer("image/png");
|
|
1552
|
+
}
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1555
|
+
throw new Error(`gradientBlend failed: ${errorMessage}`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Validates animate inputs.
|
|
1560
|
+
* @private
|
|
1561
|
+
* @param frames - Animation frames to validate
|
|
1562
|
+
* @param defaultDuration - Default duration to validate
|
|
1563
|
+
* @param defaultWidth - Default width to validate
|
|
1564
|
+
* @param defaultHeight - Default height to validate
|
|
1565
|
+
* @param options - Animation options to validate
|
|
1566
|
+
*/
|
|
1567
|
+
#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options) {
|
|
1568
|
+
if (!frames || !Array.isArray(frames) || frames.length === 0) {
|
|
1569
|
+
throw new Error("animate: frames array with at least one frame is required.");
|
|
1570
|
+
}
|
|
1571
|
+
if (typeof defaultDuration !== 'number' || defaultDuration < 0) {
|
|
1572
|
+
throw new Error("animate: defaultDuration must be a non-negative number.");
|
|
1573
|
+
}
|
|
1574
|
+
if (typeof defaultWidth !== 'number' || defaultWidth <= 0) {
|
|
1575
|
+
throw new Error("animate: defaultWidth must be a positive number.");
|
|
1576
|
+
}
|
|
1577
|
+
if (typeof defaultHeight !== 'number' || defaultHeight <= 0) {
|
|
1578
|
+
throw new Error("animate: defaultHeight must be a positive number.");
|
|
1579
|
+
}
|
|
1580
|
+
if (options?.gif && !options.gifPath) {
|
|
1581
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
887
1582
|
}
|
|
888
|
-
else {
|
|
889
|
-
gradient = ctx.createConicGradient(Math.PI, img.width / 2, img.height / 2);
|
|
890
|
-
}
|
|
891
|
-
options.colors.forEach(({ stop, color }) => gradient.addColorStop(stop, color));
|
|
892
|
-
ctx.fillStyle = gradient;
|
|
893
|
-
ctx.globalCompositeOperation = options.blendMode ?? "multiply";
|
|
894
|
-
ctx.fillRect(0, 0, img.width, img.height);
|
|
895
|
-
if (options.maskSource) {
|
|
896
|
-
const mask = await (0, canvas_1.loadImage)(options.maskSource);
|
|
897
|
-
ctx.globalCompositeOperation = "destination-in";
|
|
898
|
-
ctx.drawImage(mask, 0, 0, img.width, img.height);
|
|
899
|
-
}
|
|
900
|
-
ctx.globalCompositeOperation = "source-over";
|
|
901
|
-
return canvas.toBuffer("image/png");
|
|
902
1583
|
}
|
|
903
1584
|
async animate(frames, defaultDuration, defaultWidth = 800, defaultHeight = 600, options) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
gifStream = fs_1.default.createWriteStream(options.gifPath);
|
|
916
|
-
encoder.createReadStream().pipe(gifStream);
|
|
917
|
-
encoder.start();
|
|
918
|
-
encoder.setRepeat(0);
|
|
919
|
-
encoder.setQuality(10);
|
|
920
|
-
}
|
|
921
|
-
for (let i = 0; i < frames.length; i++) {
|
|
922
|
-
const frame = frames[i];
|
|
923
|
-
const width = frame.width || defaultWidth;
|
|
924
|
-
const height = frame.height || defaultHeight;
|
|
925
|
-
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
926
|
-
const ctx = canvas.getContext('2d');
|
|
927
|
-
if (!isNode) {
|
|
928
|
-
canvas.width = width;
|
|
929
|
-
canvas.height = height;
|
|
930
|
-
document.body.appendChild(canvas);
|
|
931
|
-
}
|
|
932
|
-
ctx.clearRect(0, 0, width, height);
|
|
933
|
-
if (frame.transformations) {
|
|
934
|
-
const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
|
|
935
|
-
ctx.save();
|
|
936
|
-
ctx.translate(translateX, translateY);
|
|
937
|
-
ctx.rotate((rotate * Math.PI) / 180);
|
|
938
|
-
ctx.scale(scaleX, scaleY);
|
|
939
|
-
}
|
|
940
|
-
let fillStyle = null;
|
|
941
|
-
if (frame.gradient) {
|
|
942
|
-
const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
|
|
943
|
-
let gradient = null;
|
|
944
|
-
if (type === 'linear') {
|
|
945
|
-
gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
|
|
946
|
-
}
|
|
947
|
-
else if (type === 'radial') {
|
|
948
|
-
gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
|
|
1585
|
+
try {
|
|
1586
|
+
this.#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options);
|
|
1587
|
+
const buffers = [];
|
|
1588
|
+
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
1589
|
+
if (options?.onStart)
|
|
1590
|
+
options.onStart();
|
|
1591
|
+
let encoder = null;
|
|
1592
|
+
let gifStream = null;
|
|
1593
|
+
if (options?.gif) {
|
|
1594
|
+
if (!options.gifPath) {
|
|
1595
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
949
1596
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1597
|
+
encoder = new gifencoder_1.default(defaultWidth, defaultHeight);
|
|
1598
|
+
gifStream = fs_1.default.createWriteStream(options.gifPath);
|
|
1599
|
+
encoder.createReadStream().pipe(gifStream);
|
|
1600
|
+
encoder.start();
|
|
1601
|
+
encoder.setRepeat(0);
|
|
1602
|
+
encoder.setQuality(10);
|
|
955
1603
|
}
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
const
|
|
959
|
-
|
|
1604
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1605
|
+
const frame = frames[i];
|
|
1606
|
+
const width = frame.width || defaultWidth;
|
|
1607
|
+
const height = frame.height || defaultHeight;
|
|
1608
|
+
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
1609
|
+
const ctx = canvas.getContext('2d');
|
|
1610
|
+
if (!isNode) {
|
|
1611
|
+
canvas.width = width;
|
|
1612
|
+
canvas.height = height;
|
|
1613
|
+
document.body.appendChild(canvas);
|
|
1614
|
+
}
|
|
1615
|
+
ctx.clearRect(0, 0, width, height);
|
|
1616
|
+
if (frame.transformations) {
|
|
1617
|
+
const { scaleX = 1, scaleY = 1, rotate = 0, translateX = 0, translateY = 0 } = frame.transformations;
|
|
1618
|
+
ctx.save();
|
|
1619
|
+
ctx.translate(translateX, translateY);
|
|
1620
|
+
ctx.rotate((rotate * Math.PI) / 180);
|
|
1621
|
+
ctx.scale(scaleX, scaleY);
|
|
1622
|
+
}
|
|
1623
|
+
let fillStyle = null;
|
|
1624
|
+
if (frame.gradient) {
|
|
1625
|
+
const { type, startX, startY, endX, endY, startRadius, endRadius, colors } = frame.gradient;
|
|
1626
|
+
let gradient = null;
|
|
1627
|
+
if (type === 'linear') {
|
|
1628
|
+
gradient = ctx.createLinearGradient(startX || 0, startY || 0, endX || width, endY || height);
|
|
1629
|
+
}
|
|
1630
|
+
else if (type === 'radial') {
|
|
1631
|
+
gradient = ctx.createRadialGradient(startX || width / 2, startY || height / 2, startRadius || 0, endX || width / 2, endY || height / 2, endRadius || Math.max(width, height));
|
|
1632
|
+
}
|
|
1633
|
+
colors.forEach((colorStop) => {
|
|
1634
|
+
if (gradient)
|
|
1635
|
+
gradient.addColorStop(colorStop.stop, colorStop.color);
|
|
1636
|
+
});
|
|
1637
|
+
fillStyle = gradient;
|
|
1638
|
+
}
|
|
1639
|
+
if (frame.pattern) {
|
|
1640
|
+
const patternImage = await (0, canvas_1.loadImage)(frame.pattern.source);
|
|
1641
|
+
const pattern = ctx.createPattern(patternImage, frame.pattern.repeat || 'repeat');
|
|
1642
|
+
fillStyle = pattern;
|
|
1643
|
+
}
|
|
1644
|
+
if (!fillStyle && frame.backgroundColor) {
|
|
1645
|
+
fillStyle = frame.backgroundColor;
|
|
1646
|
+
}
|
|
1647
|
+
if (fillStyle) {
|
|
1648
|
+
ctx.fillStyle = fillStyle;
|
|
1649
|
+
ctx.fillRect(0, 0, width, height);
|
|
1650
|
+
}
|
|
1651
|
+
if (frame.source) {
|
|
1652
|
+
const image = await (0, canvas_1.loadImage)(frame.source);
|
|
1653
|
+
ctx.globalCompositeOperation = frame.blendMode || 'source-over';
|
|
1654
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
1655
|
+
}
|
|
1656
|
+
if (frame.onDrawCustom) {
|
|
1657
|
+
frame.onDrawCustom(ctx, canvas);
|
|
1658
|
+
}
|
|
1659
|
+
if (frame.transformations) {
|
|
1660
|
+
ctx.restore();
|
|
1661
|
+
}
|
|
1662
|
+
const buffer = canvas.toBuffer('image/png');
|
|
1663
|
+
buffers.push(buffer);
|
|
1664
|
+
if (encoder) {
|
|
1665
|
+
const frameDuration = frame.duration || defaultDuration;
|
|
1666
|
+
encoder.setDelay(frameDuration);
|
|
1667
|
+
encoder.addFrame(ctx);
|
|
1668
|
+
}
|
|
1669
|
+
if (options?.onFrame)
|
|
1670
|
+
options.onFrame(i);
|
|
1671
|
+
await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
|
|
960
1672
|
}
|
|
961
|
-
if (
|
|
962
|
-
|
|
1673
|
+
if (encoder) {
|
|
1674
|
+
encoder.finish();
|
|
963
1675
|
}
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
1676
|
+
if (options?.onEnd)
|
|
1677
|
+
options.onEnd();
|
|
1678
|
+
return options?.gif ? undefined : buffers;
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1682
|
+
throw new Error(`animate failed: ${errorMessage}`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Processes multiple operations in parallel
|
|
1687
|
+
* @param operations - Array of operations to process
|
|
1688
|
+
* @returns Array of result buffers
|
|
1689
|
+
*/
|
|
1690
|
+
async batch(operations) {
|
|
1691
|
+
try {
|
|
1692
|
+
return await (0, utils_1.batchOperations)(this, operations);
|
|
1693
|
+
}
|
|
1694
|
+
catch (error) {
|
|
1695
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1696
|
+
throw new Error(`batch failed: ${errorMessage}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Chains multiple operations sequentially
|
|
1701
|
+
* @param operations - Array of operations to chain
|
|
1702
|
+
* @returns Final result buffer
|
|
1703
|
+
*/
|
|
1704
|
+
async chain(operations) {
|
|
1705
|
+
try {
|
|
1706
|
+
return await (0, utils_1.chainOperations)(this, operations);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1710
|
+
throw new Error(`chain failed: ${errorMessage}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Stitches multiple images together
|
|
1715
|
+
* @param images - Array of image sources
|
|
1716
|
+
* @param options - Stitching options
|
|
1717
|
+
* @returns Stitched image buffer
|
|
1718
|
+
*/
|
|
1719
|
+
async stitchImages(images, options) {
|
|
1720
|
+
try {
|
|
1721
|
+
if (!images || images.length === 0) {
|
|
1722
|
+
throw new Error("stitchImages: images array is required");
|
|
967
1723
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1724
|
+
return await (0, utils_1.stitchImages)(images, options);
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1728
|
+
throw new Error(`stitchImages failed: ${errorMessage}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Creates an image collage
|
|
1733
|
+
* @param images - Array of image sources with optional dimensions
|
|
1734
|
+
* @param layout - Collage layout configuration
|
|
1735
|
+
* @returns Collage image buffer
|
|
1736
|
+
*/
|
|
1737
|
+
async createCollage(images, layout) {
|
|
1738
|
+
try {
|
|
1739
|
+
if (!images || images.length === 0) {
|
|
1740
|
+
throw new Error("createCollage: images array is required");
|
|
972
1741
|
}
|
|
973
|
-
if (
|
|
974
|
-
|
|
1742
|
+
if (!layout) {
|
|
1743
|
+
throw new Error("createCollage: layout configuration is required");
|
|
975
1744
|
}
|
|
976
|
-
|
|
977
|
-
|
|
1745
|
+
return await (0, utils_1.createCollage)(images, layout);
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1749
|
+
throw new Error(`createCollage failed: ${errorMessage}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Compresses an image with quality control
|
|
1754
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
1755
|
+
* @param options - Compression options
|
|
1756
|
+
* @returns Compressed image buffer
|
|
1757
|
+
*/
|
|
1758
|
+
async compress(image, options) {
|
|
1759
|
+
try {
|
|
1760
|
+
if (!image) {
|
|
1761
|
+
throw new Error("compress: image is required");
|
|
978
1762
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1763
|
+
return await (0, utils_1.compressImage)(image, options);
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1767
|
+
throw new Error(`compress failed: ${errorMessage}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Extracts color palette from an image
|
|
1772
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
1773
|
+
* @param options - Palette extraction options
|
|
1774
|
+
* @returns Array of colors with percentages
|
|
1775
|
+
*/
|
|
1776
|
+
async extractPalette(image, options) {
|
|
1777
|
+
try {
|
|
1778
|
+
if (!image) {
|
|
1779
|
+
throw new Error("extractPalette: image is required");
|
|
985
1780
|
}
|
|
986
|
-
|
|
987
|
-
options.onFrame(i);
|
|
988
|
-
await new Promise(resolve => setTimeout(resolve, frame.duration || defaultDuration));
|
|
1781
|
+
return await (0, utils_1.extractPalette)(image, options);
|
|
989
1782
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1783
|
+
catch (error) {
|
|
1784
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1785
|
+
throw new Error(`extractPalette failed: ${errorMessage}`);
|
|
992
1786
|
}
|
|
993
|
-
if (options?.onEnd)
|
|
994
|
-
options.onEnd();
|
|
995
|
-
return options?.gif ? undefined : buffers;
|
|
996
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Validates a hexadecimal color string.
|
|
1790
|
+
* @param hexColor - Hexadecimal color string to validate (format: #RRGGBB)
|
|
1791
|
+
* @returns True if the color is valid
|
|
1792
|
+
* @throws Error if the color format is invalid
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* ```typescript
|
|
1796
|
+
* painter.validHex('#ff0000'); // true
|
|
1797
|
+
* painter.validHex('#FF00FF'); // true
|
|
1798
|
+
* painter.validHex('invalid'); // throws Error
|
|
1799
|
+
* ```
|
|
1800
|
+
*/
|
|
997
1801
|
validHex(hexColor) {
|
|
1802
|
+
if (typeof hexColor !== 'string') {
|
|
1803
|
+
throw new Error("validHex: hexColor must be a string.");
|
|
1804
|
+
}
|
|
998
1805
|
const hexPattern = /^#[0-9a-fA-F]{6}$/;
|
|
999
1806
|
if (!hexPattern.test(hexColor)) {
|
|
1000
|
-
throw new Error("Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
|
|
1807
|
+
throw new Error("validHex: Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
|
|
1001
1808
|
}
|
|
1002
1809
|
return true;
|
|
1003
1810
|
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Converts results to the configured output format.
|
|
1813
|
+
* @param results - Buffer or result to convert
|
|
1814
|
+
* @returns Converted result in the configured format
|
|
1815
|
+
* @throws Error if format is unsupported or conversion fails
|
|
1816
|
+
*
|
|
1817
|
+
* @example
|
|
1818
|
+
* ```typescript
|
|
1819
|
+
* const painter = new ApexPainter({ type: 'base64' });
|
|
1820
|
+
* const result = await painter.createCanvas({ width: 100, height: 100 });
|
|
1821
|
+
* const base64String = await painter.outPut(result.buffer); // Returns base64 string
|
|
1822
|
+
* ```
|
|
1823
|
+
*/
|
|
1004
1824
|
async outPut(results) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1825
|
+
try {
|
|
1826
|
+
if (!Buffer.isBuffer(results)) {
|
|
1827
|
+
throw new Error("outPut: results must be a Buffer.");
|
|
1828
|
+
}
|
|
1829
|
+
const formatType = this.format?.type || 'buffer';
|
|
1830
|
+
switch (formatType) {
|
|
1831
|
+
case 'buffer':
|
|
1832
|
+
return results;
|
|
1833
|
+
case 'url':
|
|
1834
|
+
return await (0, utils_1.url)(results);
|
|
1835
|
+
case 'dataURL':
|
|
1836
|
+
return (0, utils_1.dataURL)(results);
|
|
1837
|
+
case 'blob':
|
|
1838
|
+
return (0, utils_1.blob)(results);
|
|
1839
|
+
case 'base64':
|
|
1840
|
+
return (0, utils_1.base64)(results);
|
|
1841
|
+
case 'arraybuffer':
|
|
1842
|
+
return (0, utils_1.arrayBuffer)(results);
|
|
1843
|
+
default:
|
|
1844
|
+
throw new Error(`outPut: Unsupported format '${formatType}'. Supported: buffer, url, dataURL, blob, base64, arraybuffer`);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
catch (error) {
|
|
1848
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1849
|
+
throw new Error(`outPut failed: ${errorMessage}`);
|
|
1021
1850
|
}
|
|
1022
1851
|
}
|
|
1023
1852
|
/**
|