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
|
@@ -5,16 +5,26 @@ import { PassThrough} from "stream";
|
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import fs, { PathLike } from "fs";
|
|
7
7
|
import path from "path";
|
|
8
|
-
import { OutputFormat, CanvasConfig,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { OutputFormat, CanvasConfig, TextProperties, ImageProperties, GIFOptions, GIFResults, CustomOptions, cropOptions,
|
|
9
|
+
drawBackgroundGradient, drawBackgroundColor, customBackground, customLines,
|
|
10
|
+
converter, resizingImg, applyColorFilters, imgEffects,verticalBarChart, pieChart,
|
|
11
|
+
lineChart, cropInner, cropOuter, bgRemoval, detectColors, removeColor, dataURL,
|
|
12
|
+
base64, arrayBuffer, blob, url, GradientConfig, Frame,
|
|
13
|
+
ExtractFramesOptions, buildPath, ResizeOptions, MaskOptions, BlendOptions,
|
|
13
14
|
applyCanvasZoom, applyNoise,
|
|
14
15
|
applyStroke, applyRotation, applyShadow, drawBoxBackground, fitInto, loadImageCached,
|
|
15
|
-
drawShape, isShapeSource, ShapeType, createShapePath, createGradientFill,
|
|
16
|
+
drawShape, isShapeSource, ShapeType, createShapePath, createGradientFill, applySimpleProfessionalFilters,
|
|
17
|
+
ImageFilter, barChart_1, PieChartData, LineChartConfig,
|
|
18
|
+
// New features
|
|
19
|
+
applyImageMask, applyClipPath, applyPerspectiveDistortion, applyBulgeDistortion, applyMeshWarp,
|
|
20
|
+
applyVignette, applyLensFlare, applyChromaticAberration, applyFilmGrain,
|
|
21
|
+
renderTextOnPath,
|
|
22
|
+
drawArrow, drawMarker, createSmoothPath, createCatmullRomPath, applyLinePattern, applyLineTexture, getPointOnLinePath,
|
|
23
|
+
batchOperations, chainOperations,
|
|
24
|
+
stitchImages as stitchImagesUtil, createCollage,
|
|
25
|
+
compressImage, extractPalette as extractPaletteUtil,
|
|
26
|
+
BatchOperation, ChainOperation, StitchOptions, CollageLayout, CompressionOptions, PaletteOptions
|
|
16
27
|
} from "./utils/utils";
|
|
17
|
-
import { } from "./utils/Image/imageProperties";
|
|
18
28
|
import { EnhancedTextRenderer } from "./utils/Texts/enhancedTextRenderer";
|
|
19
29
|
import { EnhancedPatternRenderer } from "./utils/Patterns/enhancedPatternRenderer";
|
|
20
30
|
|
|
@@ -60,7 +70,12 @@ export class ApexPainter {
|
|
|
60
70
|
* @param textProps - Text properties
|
|
61
71
|
*/
|
|
62
72
|
async #renderEnhancedText(ctx: SKRSContext2D, textProps: TextProperties): Promise<void> {
|
|
63
|
-
|
|
73
|
+
// Check if text should be rendered on a path
|
|
74
|
+
if (textProps.path && textProps.textOnPath) {
|
|
75
|
+
renderTextOnPath(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
|
|
76
|
+
} else {
|
|
77
|
+
await EnhancedTextRenderer.renderText(ctx, textProps);
|
|
78
|
+
}
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
/**
|
|
@@ -94,20 +109,66 @@ export class ApexPainter {
|
|
|
94
109
|
* const buffer = result.buffer;
|
|
95
110
|
* ```
|
|
96
111
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
canvas
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Validates canvas configuration.
|
|
114
|
+
* @private
|
|
115
|
+
* @param canvas - Canvas configuration to validate
|
|
116
|
+
*/
|
|
117
|
+
#validateCanvasConfig(canvas: CanvasConfig): void {
|
|
118
|
+
if (!canvas) {
|
|
119
|
+
throw new Error("createCanvas: canvas configuration is required.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
|
|
123
|
+
throw new Error("createCanvas: width must be a positive number.");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
|
|
127
|
+
throw new Error("createCanvas: height must be a positive number.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
|
|
131
|
+
throw new Error("createCanvas: opacity must be a number between 0 and 1.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
|
|
135
|
+
throw new Error("createCanvas: zoom.scale must be a positive number.");
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
|
|
139
|
+
async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
140
|
+
try {
|
|
141
|
+
// Validate canvas configuration
|
|
142
|
+
this.#validateCanvasConfig(canvas);
|
|
143
|
+
|
|
144
|
+
// Handle inherit sizing
|
|
145
|
+
if (canvas.customBg?.inherit) {
|
|
146
|
+
let p = canvas.customBg.source;
|
|
147
|
+
if (!/^https?:\/\//i.test(p)) p = path.join(process.cwd(), p);
|
|
148
|
+
try {
|
|
149
|
+
const img = await loadImage(p);
|
|
150
|
+
canvas.width = img.width;
|
|
151
|
+
canvas.height = img.height;
|
|
152
|
+
} catch (e: unknown) {
|
|
153
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
154
|
+
throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle video background inherit sizing
|
|
159
|
+
if (canvas.videoBg) {
|
|
160
|
+
try {
|
|
161
|
+
const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0);
|
|
162
|
+
if (frameBuffer) {
|
|
163
|
+
const img = await loadImage(frameBuffer);
|
|
164
|
+
if (!canvas.width) canvas.width = img.width;
|
|
165
|
+
if (!canvas.height) canvas.height = img.height;
|
|
166
|
+
}
|
|
167
|
+
} catch (e: unknown) {
|
|
168
|
+
console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
111
172
|
// 2) Use final width/height after inherit
|
|
112
173
|
const width = canvas.width ?? 500;
|
|
113
174
|
const height = canvas.height ?? 500;
|
|
@@ -118,7 +179,7 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
118
179
|
borderRadius = 0,
|
|
119
180
|
borderPosition = 'all',
|
|
120
181
|
opacity = 1,
|
|
121
|
-
colorBg, customBg, gradientBg,
|
|
182
|
+
colorBg, customBg, gradientBg, videoBg,
|
|
122
183
|
patternBg, noiseBg, blendMode,
|
|
123
184
|
zoom, stroke, shadow,
|
|
124
185
|
blur
|
|
@@ -135,54 +196,97 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
135
196
|
throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
|
|
136
197
|
}
|
|
137
198
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
199
|
+
const cv = createCanvas(width, height);
|
|
200
|
+
const ctx = cv.getContext('2d') as SKRSContext2D;
|
|
201
|
+
if (!ctx) throw new Error('Unable to get 2D context');
|
|
141
202
|
|
|
142
203
|
|
|
143
|
-
|
|
204
|
+
ctx.globalAlpha = opacity;
|
|
144
205
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
// ---- BACKGROUND (clipped) ----
|
|
207
|
+
ctx.save();
|
|
208
|
+
applyRotation(ctx, rotation, x, y, width, height);
|
|
148
209
|
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
211
|
+
ctx.clip();
|
|
151
212
|
|
|
152
|
-
|
|
213
|
+
applyCanvasZoom(ctx, width, height, zoom);
|
|
153
214
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
215
|
+
ctx.translate(x, y);
|
|
216
|
+
if (typeof blendMode === 'string') {
|
|
217
|
+
ctx.globalCompositeOperation = blendMode as GlobalCompositeOperation;
|
|
218
|
+
}
|
|
158
219
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
220
|
+
// Draw video background if specified
|
|
221
|
+
if (videoBg) {
|
|
222
|
+
try {
|
|
223
|
+
const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0);
|
|
224
|
+
if (frameBuffer) {
|
|
225
|
+
const videoImg = await loadImage(frameBuffer);
|
|
226
|
+
ctx.globalAlpha = videoBg.opacity ?? 1;
|
|
227
|
+
ctx.drawImage(videoImg, 0, 0, width, height);
|
|
228
|
+
ctx.globalAlpha = opacity;
|
|
229
|
+
}
|
|
230
|
+
} catch (e: unknown) {
|
|
231
|
+
console.warn('createCanvas: Failed to load video background frame');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
162
234
|
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
// Draw custom background with filters and opacity support
|
|
236
|
+
if (customBg) {
|
|
237
|
+
await customBackground(ctx, { ...canvas, blur });
|
|
238
|
+
// Apply filters to background if specified
|
|
239
|
+
if (customBg.filters && customBg.filters.length > 0) {
|
|
240
|
+
const tempCanvas = createCanvas(width, height);
|
|
241
|
+
const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
|
|
242
|
+
if (tempCtx) {
|
|
243
|
+
tempCtx.drawImage(cv, 0, 0);
|
|
244
|
+
await applySimpleProfessionalFilters(tempCtx, customBg.filters, width, height);
|
|
245
|
+
ctx.clearRect(0, 0, width, height);
|
|
246
|
+
ctx.globalAlpha = customBg.opacity ?? 1;
|
|
247
|
+
ctx.drawImage(tempCanvas, 0, 0);
|
|
248
|
+
ctx.globalAlpha = opacity;
|
|
249
|
+
}
|
|
250
|
+
} else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
|
|
251
|
+
ctx.globalAlpha = customBg.opacity;
|
|
252
|
+
await customBackground(ctx, { ...canvas, blur });
|
|
253
|
+
ctx.globalAlpha = opacity;
|
|
254
|
+
} else {
|
|
255
|
+
await customBackground(ctx, { ...canvas, blur });
|
|
256
|
+
}
|
|
257
|
+
} else if (gradientBg) {
|
|
258
|
+
await drawBackgroundGradient(ctx, { ...canvas, blur });
|
|
259
|
+
} else {
|
|
260
|
+
await drawBackgroundColor(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
|
|
261
|
+
}
|
|
165
262
|
|
|
166
|
-
|
|
263
|
+
if (patternBg) await EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
|
|
264
|
+
if (noiseBg) applyNoise(ctx, width, height, noiseBg.intensity ?? 0.05);
|
|
167
265
|
|
|
168
|
-
|
|
169
|
-
if (shadow) {
|
|
170
|
-
ctx.save();
|
|
171
|
-
buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
172
|
-
applyShadow(ctx, shadow, x, y, width, height);
|
|
173
|
-
ctx.restore();
|
|
174
|
-
}
|
|
266
|
+
ctx.restore();
|
|
175
267
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
268
|
+
// Apply shadow effect
|
|
269
|
+
if (shadow) {
|
|
270
|
+
ctx.save();
|
|
271
|
+
buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
272
|
+
applyShadow(ctx, shadow, x, y, width, height);
|
|
273
|
+
ctx.restore();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Apply stroke effect
|
|
277
|
+
if (stroke) {
|
|
278
|
+
ctx.save();
|
|
279
|
+
buildPath(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
280
|
+
applyStroke(ctx, stroke, x, y, width, height);
|
|
281
|
+
ctx.restore();
|
|
282
|
+
}
|
|
183
283
|
|
|
184
|
-
|
|
185
|
-
}
|
|
284
|
+
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
287
|
+
throw new Error(`createCanvas failed: ${errorMessage}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
186
290
|
|
|
187
291
|
|
|
188
292
|
|
|
@@ -228,31 +332,57 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
228
332
|
* ], canvasBuffer);
|
|
229
333
|
* ```
|
|
230
334
|
*/
|
|
335
|
+
/**
|
|
336
|
+
* Validates image/shape properties array.
|
|
337
|
+
* @private
|
|
338
|
+
* @param images - Image properties to validate
|
|
339
|
+
*/
|
|
340
|
+
#validateImageArray(images: ImageProperties | ImageProperties[]): void {
|
|
341
|
+
const list = Array.isArray(images) ? images : [images];
|
|
342
|
+
if (list.length === 0) {
|
|
343
|
+
throw new Error("createImage: At least one image/shape is required.");
|
|
344
|
+
}
|
|
345
|
+
for (const ip of list) {
|
|
346
|
+
this.#validateImageProperties(ip);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
231
350
|
async createImage(
|
|
232
351
|
images: ImageProperties | ImageProperties[],
|
|
233
352
|
canvasBuffer: CanvasResults | Buffer
|
|
234
353
|
): Promise<Buffer> {
|
|
235
|
-
|
|
354
|
+
try {
|
|
355
|
+
// Validate inputs
|
|
356
|
+
if (!canvasBuffer) {
|
|
357
|
+
throw new Error("createImage: canvasBuffer is required.");
|
|
358
|
+
}
|
|
359
|
+
this.#validateImageArray(images);
|
|
360
|
+
|
|
361
|
+
const list = Array.isArray(images) ? images : [images];
|
|
236
362
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
363
|
+
// Load base canvas buffer
|
|
364
|
+
const base: Image = Buffer.isBuffer(canvasBuffer)
|
|
365
|
+
? await loadImage(canvasBuffer)
|
|
366
|
+
: await loadImage((canvasBuffer as CanvasResults).buffer);
|
|
241
367
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
368
|
+
const cv = createCanvas(base.width, base.height);
|
|
369
|
+
const ctx = cv.getContext("2d") as SKRSContext2D;
|
|
370
|
+
if (!ctx) throw new Error("Unable to get 2D rendering context");
|
|
245
371
|
|
|
246
|
-
|
|
247
|
-
|
|
372
|
+
// Paint bg
|
|
373
|
+
ctx.drawImage(base, 0, 0);
|
|
248
374
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
375
|
+
// Draw each image/shape on canvas
|
|
376
|
+
for (const ip of list) {
|
|
377
|
+
await this.#drawImageBitmap(ctx, ip);
|
|
378
|
+
}
|
|
253
379
|
|
|
254
|
-
|
|
255
|
-
|
|
380
|
+
// Return updated buffer
|
|
381
|
+
return cv.toBuffer("image/png");
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
384
|
+
throw new Error(`createImage failed: ${errorMessage}`);
|
|
385
|
+
}
|
|
256
386
|
}
|
|
257
387
|
|
|
258
388
|
/**
|
|
@@ -277,7 +407,14 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
277
407
|
stroke,
|
|
278
408
|
boxBackground,
|
|
279
409
|
shape,
|
|
280
|
-
filters
|
|
410
|
+
filters,
|
|
411
|
+
filterIntensity = 1,
|
|
412
|
+
filterOrder = 'post',
|
|
413
|
+
mask,
|
|
414
|
+
clipPath,
|
|
415
|
+
distortion,
|
|
416
|
+
meshWarp,
|
|
417
|
+
effects
|
|
281
418
|
} = ip;
|
|
282
419
|
|
|
283
420
|
this.#validateImageProperties(ip);
|
|
@@ -318,10 +455,14 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
318
455
|
// 2) Optional box background (under bitmap, inside clip) — color or gradient
|
|
319
456
|
drawBoxBackground(ctx, box, boxBackground, borderRadius, borderPosition);
|
|
320
457
|
|
|
321
|
-
// 3) Clip to image border radius, then draw the bitmap with blur/opacity and fit/align
|
|
458
|
+
// 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
|
|
322
459
|
ctx.save();
|
|
323
|
-
|
|
324
|
-
|
|
460
|
+
if (clipPath && clipPath.length >= 3) {
|
|
461
|
+
applyClipPath(ctx, clipPath);
|
|
462
|
+
} else {
|
|
463
|
+
buildPath(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
|
|
464
|
+
ctx.clip();
|
|
465
|
+
}
|
|
325
466
|
|
|
326
467
|
const { dx, dy, dw, dh, sx, sy, sw, sh } =
|
|
327
468
|
fitInto(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
|
|
@@ -330,17 +471,117 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
330
471
|
ctx.globalAlpha = opacity ?? 1;
|
|
331
472
|
if ((blur ?? 0) > 0) ctx.filter = `blur(${blur}px)`;
|
|
332
473
|
|
|
333
|
-
// Apply professional image filters BEFORE drawing
|
|
334
|
-
if (filters && filters.length > 0) {
|
|
335
|
-
|
|
474
|
+
// Apply professional image filters BEFORE drawing if filterOrder is 'pre'
|
|
475
|
+
if (filters && filters.length > 0 && filterOrder === 'pre') {
|
|
476
|
+
const adjustedFilters = filters.map(f => ({
|
|
477
|
+
...f,
|
|
478
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
479
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
480
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
481
|
+
}));
|
|
482
|
+
await applySimpleProfessionalFilters(ctx, adjustedFilters, dw, dh);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Apply distortion if specified (before drawing)
|
|
486
|
+
if (distortion) {
|
|
487
|
+
if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
|
|
488
|
+
applyPerspectiveDistortion(ctx, img, distortion.points, dx, dy, dw, dh);
|
|
489
|
+
ctx.filter = "none";
|
|
490
|
+
ctx.globalAlpha = prevAlpha;
|
|
491
|
+
ctx.restore();
|
|
492
|
+
ctx.restore();
|
|
493
|
+
return;
|
|
494
|
+
} else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
|
|
495
|
+
const centerX = dx + dw / 2;
|
|
496
|
+
const centerY = dy + dh / 2;
|
|
497
|
+
const radius = Math.min(dw, dh) / 2;
|
|
498
|
+
const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
|
|
499
|
+
applyBulgeDistortion(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
|
|
500
|
+
ctx.filter = "none";
|
|
501
|
+
ctx.globalAlpha = prevAlpha;
|
|
502
|
+
ctx.restore();
|
|
503
|
+
ctx.restore();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Apply mesh warp if specified
|
|
509
|
+
if (meshWarp && meshWarp.controlPoints) {
|
|
510
|
+
applyMeshWarp(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
|
|
511
|
+
ctx.filter = "none";
|
|
512
|
+
ctx.globalAlpha = prevAlpha;
|
|
513
|
+
ctx.restore();
|
|
514
|
+
ctx.restore();
|
|
515
|
+
return;
|
|
336
516
|
}
|
|
337
517
|
|
|
338
|
-
|
|
518
|
+
// Draw image with or without masking
|
|
519
|
+
if (mask) {
|
|
520
|
+
await applyImageMask(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
|
|
521
|
+
} else {
|
|
522
|
+
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
523
|
+
}
|
|
339
524
|
|
|
340
525
|
ctx.filter = "none";
|
|
341
526
|
ctx.globalAlpha = prevAlpha;
|
|
342
527
|
ctx.restore();
|
|
343
528
|
|
|
529
|
+
// Apply professional image filters AFTER drawing if filterOrder is 'post'
|
|
530
|
+
if (filters && filters.length > 0 && filterOrder === 'post') {
|
|
531
|
+
ctx.save();
|
|
532
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
533
|
+
const tempCanvas = createCanvas(box.w, box.h);
|
|
534
|
+
const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
|
|
535
|
+
if (tempCtx) {
|
|
536
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
537
|
+
const adjustedFilters = filters.map(f => ({
|
|
538
|
+
...f,
|
|
539
|
+
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
540
|
+
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
541
|
+
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
542
|
+
}));
|
|
543
|
+
await applySimpleProfessionalFilters(tempCtx, adjustedFilters, box.w, box.h);
|
|
544
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
545
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
546
|
+
}
|
|
547
|
+
ctx.restore();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Apply effects stack
|
|
551
|
+
if (effects) {
|
|
552
|
+
ctx.save();
|
|
553
|
+
const effectsCtx = ctx;
|
|
554
|
+
if (effects.vignette) {
|
|
555
|
+
applyVignette(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
|
|
556
|
+
}
|
|
557
|
+
if (effects.lensFlare) {
|
|
558
|
+
applyLensFlare(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
|
|
559
|
+
}
|
|
560
|
+
if (effects.chromaticAberration) {
|
|
561
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
562
|
+
const tempCanvas = createCanvas(box.w, box.h);
|
|
563
|
+
const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
|
|
564
|
+
if (tempCtx) {
|
|
565
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
566
|
+
applyChromaticAberration(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
|
|
567
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
568
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (effects.filmGrain) {
|
|
572
|
+
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
573
|
+
const tempCanvas = createCanvas(box.w, box.h);
|
|
574
|
+
const tempCtx = tempCanvas.getContext('2d') as SKRSContext2D;
|
|
575
|
+
if (tempCtx) {
|
|
576
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
577
|
+
applyFilmGrain(tempCtx, effects.filmGrain.intensity, box.w, box.h);
|
|
578
|
+
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
579
|
+
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
ctx.restore();
|
|
583
|
+
}
|
|
584
|
+
|
|
344
585
|
|
|
345
586
|
// 4) Stroke (independent) — supports gradient or color
|
|
346
587
|
applyStroke(ctx, box, stroke);
|
|
@@ -647,18 +888,34 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
647
888
|
* ], canvasBuffer);
|
|
648
889
|
* ```
|
|
649
890
|
*/
|
|
891
|
+
/**
|
|
892
|
+
* Validates text properties array.
|
|
893
|
+
* @private
|
|
894
|
+
* @param textArray - Text properties to validate
|
|
895
|
+
*/
|
|
896
|
+
#validateTextArray(textArray: TextProperties | TextProperties[]): void {
|
|
897
|
+
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
898
|
+
if (textList.length === 0) {
|
|
899
|
+
throw new Error("createText: At least one text object is required.");
|
|
900
|
+
}
|
|
901
|
+
for (const textProps of textList) {
|
|
902
|
+
this.#validateTextProperties(textProps);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
650
906
|
async createText(textArray: TextProperties | TextProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
|
|
651
907
|
try {
|
|
908
|
+
// Validate inputs
|
|
909
|
+
if (!canvasBuffer) {
|
|
910
|
+
throw new Error("createText: canvasBuffer is required.");
|
|
911
|
+
}
|
|
912
|
+
this.#validateTextArray(textArray);
|
|
913
|
+
|
|
652
914
|
// Ensure textArray is an array
|
|
653
915
|
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
654
916
|
|
|
655
|
-
// Validate each text object
|
|
656
|
-
for (const textProps of textList) {
|
|
657
|
-
this.#validateTextProperties(textProps);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
917
|
// Load existing canvas buffer
|
|
661
|
-
let existingImage:
|
|
918
|
+
let existingImage: Image;
|
|
662
919
|
|
|
663
920
|
if (Buffer.isBuffer(canvasBuffer)) {
|
|
664
921
|
existingImage = await loadImage(canvasBuffer);
|
|
@@ -690,21 +947,44 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
690
947
|
|
|
691
948
|
return canvas.toBuffer("image/png");
|
|
692
949
|
} catch (error) {
|
|
693
|
-
|
|
694
|
-
throw new Error(
|
|
950
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
951
|
+
throw new Error(`createText failed: ${errorMessage}`);
|
|
695
952
|
}
|
|
696
953
|
}
|
|
697
954
|
|
|
698
955
|
|
|
699
956
|
|
|
700
|
-
|
|
701
|
-
|
|
957
|
+
/**
|
|
958
|
+
* Validates custom line options.
|
|
959
|
+
* @private
|
|
960
|
+
* @param options - Custom options to validate
|
|
961
|
+
*/
|
|
962
|
+
#validateCustomOptions(options: CustomOptions | CustomOptions[]): void {
|
|
963
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
964
|
+
if (opts.length === 0) {
|
|
965
|
+
throw new Error("createCustom: At least one custom option is required.");
|
|
966
|
+
}
|
|
967
|
+
for (const opt of opts) {
|
|
968
|
+
if (!opt.startCoordinates || typeof opt.startCoordinates.x !== 'number' || typeof opt.startCoordinates.y !== 'number') {
|
|
969
|
+
throw new Error("createCustom: startCoordinates with valid x and y are required.");
|
|
970
|
+
}
|
|
971
|
+
if (!opt.endCoordinates || typeof opt.endCoordinates.x !== 'number' || typeof opt.endCoordinates.y !== 'number') {
|
|
972
|
+
throw new Error("createCustom: endCoordinates with valid x and y are required.");
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
702
976
|
|
|
703
|
-
|
|
704
|
-
|
|
977
|
+
async createCustom(options: CustomOptions | CustomOptions[], buffer: CanvasResults | Buffer): Promise<Buffer> {
|
|
978
|
+
try {
|
|
979
|
+
// Validate inputs
|
|
980
|
+
if (!buffer) {
|
|
981
|
+
throw new Error("createCustom: buffer is required.");
|
|
705
982
|
}
|
|
983
|
+
this.#validateCustomOptions(options);
|
|
706
984
|
|
|
707
|
-
|
|
985
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
986
|
+
|
|
987
|
+
let existingImage: Image;
|
|
708
988
|
|
|
709
989
|
if (Buffer.isBuffer(buffer)) {
|
|
710
990
|
existingImage = await loadImage(buffer);
|
|
@@ -723,17 +1003,49 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
723
1003
|
|
|
724
1004
|
ctx.drawImage(existingImage, 0, 0);
|
|
725
1005
|
|
|
726
|
-
customLines(ctx,
|
|
1006
|
+
await customLines(ctx, opts);
|
|
727
1007
|
|
|
728
1008
|
return canvas.toBuffer("image/png");
|
|
729
1009
|
} catch (error) {
|
|
730
|
-
|
|
731
|
-
|
|
1010
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1011
|
+
throw new Error(`createCustom failed: ${errorMessage}`);
|
|
732
1012
|
}
|
|
733
|
-
}
|
|
1013
|
+
}
|
|
734
1014
|
|
|
735
|
-
|
|
736
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* Validates GIF options and frames.
|
|
1017
|
+
* @private
|
|
1018
|
+
* @param gifFrames - GIF frames to validate
|
|
1019
|
+
* @param options - GIF options to validate
|
|
1020
|
+
*/
|
|
1021
|
+
#validateGIFOptions(gifFrames: { background: string; duration: number }[], options: GIFOptions): void {
|
|
1022
|
+
if (!gifFrames || gifFrames.length === 0) {
|
|
1023
|
+
throw new Error("createGIF: At least one frame is required.");
|
|
1024
|
+
}
|
|
1025
|
+
for (const frame of gifFrames) {
|
|
1026
|
+
if (!frame.background) {
|
|
1027
|
+
throw new Error("createGIF: Each frame must have a background property.");
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof frame.duration !== 'number' || frame.duration < 0) {
|
|
1030
|
+
throw new Error("createGIF: Each frame duration must be a non-negative number.");
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (options.outputFormat === "file" && !options.outputFile) {
|
|
1034
|
+
throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
|
|
1035
|
+
}
|
|
1036
|
+
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
1037
|
+
throw new Error("createGIF: repeat must be a non-negative number or undefined.");
|
|
1038
|
+
}
|
|
1039
|
+
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
1040
|
+
throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async createGIF(gifFrames: { background: string; duration: number }[], options: GIFOptions): Promise<GIFResults | Buffer | string | Array<{ attachment: NodeJS.ReadableStream | any; name: string }> | undefined> {
|
|
1045
|
+
try {
|
|
1046
|
+
this.#validateGIFOptions(gifFrames, options);
|
|
1047
|
+
|
|
1048
|
+
async function resizeImage(image: Image, targetWidth: number, targetHeight: number) {
|
|
737
1049
|
const canvas = createCanvas(targetWidth, targetHeight);
|
|
738
1050
|
const ctx = canvas.getContext("2d");
|
|
739
1051
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
@@ -757,38 +1069,10 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
757
1069
|
getBuffer: function (): Buffer {
|
|
758
1070
|
return Buffer.concat(chunks);
|
|
759
1071
|
}
|
|
760
|
-
} as
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function validateOptions(options: GIFOptions) {
|
|
764
|
-
if (options.outputFormat === "file" && !options.outputFile) {
|
|
765
|
-
throw new Error("Output file path is required when using file output format.");
|
|
766
|
-
}
|
|
767
|
-
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
768
|
-
throw new Error("Repeat must be a non-negative number or undefined.");
|
|
769
|
-
}
|
|
770
|
-
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
771
|
-
throw new Error("Quality must be a number between 1 and 20 or undefined.");
|
|
772
|
-
}
|
|
773
|
-
if (options.watermark && typeof options.watermark.enable !== "boolean") {
|
|
774
|
-
throw new Error("Watermark must be a boolean or undefined.");
|
|
775
|
-
}
|
|
776
|
-
if (options.textOverlay) {
|
|
777
|
-
const textOptions = options.textOverlay;
|
|
778
|
-
if (!textOptions.text || typeof textOptions.text !== "string") {
|
|
779
|
-
throw new Error("Text overlay text is required and must be a string.");
|
|
780
|
-
}
|
|
781
|
-
if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
|
|
782
|
-
throw new Error("Text overlay fontSize must be a positive integer or undefined.");
|
|
783
|
-
}
|
|
784
|
-
if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
|
|
785
|
-
throw new Error("Text overlay fontColor must be a string or undefined.");
|
|
786
|
-
}
|
|
787
|
-
}
|
|
1072
|
+
} as PassThrough & { getBuffer: () => Buffer };
|
|
788
1073
|
}
|
|
789
1074
|
|
|
790
|
-
|
|
791
|
-
validateOptions(options);
|
|
1075
|
+
// Validation is done in #validateGIFOptions
|
|
792
1076
|
|
|
793
1077
|
const canvasWidth = options.width || 1200;
|
|
794
1078
|
const canvasHeight = options.height || 1200;
|
|
@@ -803,7 +1087,8 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
803
1087
|
encoder.setQuality(options.quality || 10);
|
|
804
1088
|
|
|
805
1089
|
const canvas = createCanvas(canvasWidth, canvasHeight);
|
|
806
|
-
const ctx
|
|
1090
|
+
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
1091
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
807
1092
|
|
|
808
1093
|
for (const frame of gifFrames) {
|
|
809
1094
|
const image = await loadImage(frame.background);
|
|
@@ -824,18 +1109,19 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
824
1109
|
}
|
|
825
1110
|
|
|
826
1111
|
encoder.setDelay(frame.duration);
|
|
827
|
-
encoder.addFrame(ctx);
|
|
1112
|
+
encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
|
|
828
1113
|
}
|
|
829
1114
|
|
|
830
1115
|
encoder.finish();
|
|
831
1116
|
outputStream.end();
|
|
832
1117
|
|
|
833
1118
|
if (options.outputFormat === "file") {
|
|
834
|
-
await new Promise((resolve) => outputStream.on("finish", resolve));
|
|
1119
|
+
await new Promise<void>((resolve) => outputStream.on("finish", () => resolve()));
|
|
835
1120
|
} else if (options.outputFormat === "base64") {
|
|
836
1121
|
if ('getBuffer' in outputStream) {
|
|
837
1122
|
return outputStream.getBuffer().toString("base64");
|
|
838
1123
|
}
|
|
1124
|
+
throw new Error("createGIF: Unable to get buffer for base64 output.");
|
|
839
1125
|
} else if (options.outputFormat === "attachment") {
|
|
840
1126
|
const gifStream = encoder.createReadStream();
|
|
841
1127
|
return [{ attachment: gifStream, name: "gif.js" }];
|
|
@@ -843,69 +1129,225 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
843
1129
|
if ('getBuffer' in outputStream) {
|
|
844
1130
|
return outputStream.getBuffer();
|
|
845
1131
|
}
|
|
1132
|
+
throw new Error("createGIF: Unable to get buffer for buffer output.");
|
|
846
1133
|
} else {
|
|
847
1134
|
throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
|
|
848
1135
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
throw
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1138
|
+
throw new Error(`createGIF failed: ${errorMessage}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Validates resize options.
|
|
1144
|
+
* @private
|
|
1145
|
+
* @param options - Resize options to validate
|
|
1146
|
+
*/
|
|
1147
|
+
#validateResizeOptions(options: ResizeOptions): void {
|
|
1148
|
+
if (!options || !options.imagePath) {
|
|
1149
|
+
throw new Error("resize: imagePath is required.");
|
|
1150
|
+
}
|
|
1151
|
+
if (options.size) {
|
|
1152
|
+
if (options.size.width !== undefined && (typeof options.size.width !== 'number' || options.size.width <= 0)) {
|
|
1153
|
+
throw new Error("resize: size.width must be a positive number.");
|
|
1154
|
+
}
|
|
1155
|
+
if (options.size.height !== undefined && (typeof options.size.height !== 'number' || options.size.height <= 0)) {
|
|
1156
|
+
throw new Error("resize: size.height must be a positive number.");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (options.quality !== undefined && (typeof options.quality !== 'number' || options.quality < 0 || options.quality > 100)) {
|
|
1160
|
+
throw new Error("resize: quality must be a number between 0 and 100.");
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async resize(resizeOptions: ResizeOptions): Promise<Buffer> {
|
|
1165
|
+
try {
|
|
1166
|
+
this.#validateResizeOptions(resizeOptions);
|
|
1167
|
+
return await resizingImg(resizeOptions);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1170
|
+
throw new Error(`resize failed: ${errorMessage}`);
|
|
1171
|
+
}
|
|
852
1172
|
}
|
|
853
|
-
}
|
|
854
1173
|
|
|
855
|
-
|
|
856
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* Validates image converter inputs.
|
|
1176
|
+
* @private
|
|
1177
|
+
* @param source - Image source to validate
|
|
1178
|
+
* @param newExtension - Extension to validate
|
|
1179
|
+
*/
|
|
1180
|
+
#validateConverterInputs(source: string, newExtension: string): void {
|
|
1181
|
+
if (!source) {
|
|
1182
|
+
throw new Error("imgConverter: source is required.");
|
|
1183
|
+
}
|
|
1184
|
+
if (!newExtension) {
|
|
1185
|
+
throw new Error("imgConverter: newExtension is required.");
|
|
1186
|
+
}
|
|
1187
|
+
const validExtensions = ['jpeg', 'png', 'webp', 'tiff', 'gif', 'avif', 'heif', 'raw', 'pdf', 'svg'];
|
|
1188
|
+
if (!validExtensions.includes(newExtension.toLowerCase())) {
|
|
1189
|
+
throw new Error(`imgConverter: Invalid extension. Supported: ${validExtensions.join(', ')}`);
|
|
1190
|
+
}
|
|
857
1191
|
}
|
|
858
1192
|
|
|
859
|
-
async imgConverter(source: string, newExtension: string) {
|
|
860
|
-
|
|
1193
|
+
async imgConverter(source: string, newExtension: string): Promise<Buffer> {
|
|
1194
|
+
try {
|
|
1195
|
+
this.#validateConverterInputs(source, newExtension);
|
|
1196
|
+
return await converter(source, newExtension);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1199
|
+
throw new Error(`imgConverter failed: ${errorMessage}`);
|
|
1200
|
+
}
|
|
861
1201
|
}
|
|
862
1202
|
|
|
863
|
-
|
|
864
|
-
|
|
1203
|
+
/**
|
|
1204
|
+
* Validates effects inputs.
|
|
1205
|
+
* @private
|
|
1206
|
+
* @param source - Image source to validate
|
|
1207
|
+
* @param filters - Filters array to validate
|
|
1208
|
+
*/
|
|
1209
|
+
#validateEffectsInputs(source: string, filters: ImageFilter[]): void {
|
|
1210
|
+
if (!source) {
|
|
1211
|
+
throw new Error("effects: source is required.");
|
|
1212
|
+
}
|
|
1213
|
+
if (!filters || !Array.isArray(filters) || filters.length === 0) {
|
|
1214
|
+
throw new Error("effects: filters array with at least one filter is required.");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async effects(source: string, filters: ImageFilter[]): Promise<Buffer> {
|
|
1219
|
+
try {
|
|
1220
|
+
this.#validateEffectsInputs(source, filters);
|
|
1221
|
+
return await imgEffects(source, filters);
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1224
|
+
throw new Error(`effects failed: ${errorMessage}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Validates color filter inputs.
|
|
1230
|
+
* @private
|
|
1231
|
+
* @param source - Image source to validate
|
|
1232
|
+
* @param opacity - Opacity to validate
|
|
1233
|
+
*/
|
|
1234
|
+
#validateColorFilterInputs(source: string, opacity?: number): void {
|
|
1235
|
+
if (!source) {
|
|
1236
|
+
throw new Error("colorsFilter: source is required.");
|
|
1237
|
+
}
|
|
1238
|
+
if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 1)) {
|
|
1239
|
+
throw new Error("colorsFilter: opacity must be a number between 0 and 1.");
|
|
1240
|
+
}
|
|
865
1241
|
}
|
|
866
1242
|
|
|
867
|
-
async colorsFilter(source: string, filterColor:
|
|
868
|
-
|
|
1243
|
+
async colorsFilter(source: string, filterColor: string | GradientConfig, opacity?: number): Promise<Buffer> {
|
|
1244
|
+
try {
|
|
1245
|
+
this.#validateColorFilterInputs(source, opacity);
|
|
1246
|
+
return await applyColorFilters(source, filterColor, opacity);
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1249
|
+
throw new Error(`colorsFilter failed: ${errorMessage}`);
|
|
1250
|
+
}
|
|
869
1251
|
}
|
|
870
1252
|
|
|
871
|
-
async colorAnalysis(source: string) {
|
|
872
|
-
|
|
1253
|
+
async colorAnalysis(source: string): Promise<{ color: string; frequency: string }[]> {
|
|
1254
|
+
try {
|
|
1255
|
+
if (!source) {
|
|
1256
|
+
throw new Error("colorAnalysis: source is required.");
|
|
1257
|
+
}
|
|
1258
|
+
return await detectColors(source);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1261
|
+
throw new Error(`colorAnalysis failed: ${errorMessage}`);
|
|
1262
|
+
}
|
|
873
1263
|
}
|
|
874
1264
|
|
|
875
|
-
async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }) {
|
|
876
|
-
|
|
1265
|
+
async colorsRemover(source: string, colorToRemove: { red: number, green: number, blue: number }): Promise<Buffer | undefined> {
|
|
1266
|
+
try {
|
|
1267
|
+
if (!source) {
|
|
1268
|
+
throw new Error("colorsRemover: source is required.");
|
|
1269
|
+
}
|
|
1270
|
+
if (!colorToRemove || typeof colorToRemove.red !== 'number' || typeof colorToRemove.green !== 'number' || typeof colorToRemove.blue !== 'number') {
|
|
1271
|
+
throw new Error("colorsRemover: colorToRemove must be an object with red, green, and blue properties (0-255).");
|
|
1272
|
+
}
|
|
1273
|
+
if (colorToRemove.red < 0 || colorToRemove.red > 255 ||
|
|
1274
|
+
colorToRemove.green < 0 || colorToRemove.green > 255 ||
|
|
1275
|
+
colorToRemove.blue < 0 || colorToRemove.blue > 255) {
|
|
1276
|
+
throw new Error("colorsRemover: colorToRemove RGB values must be between 0 and 255.");
|
|
1277
|
+
}
|
|
1278
|
+
return await removeColor(source, colorToRemove);
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1281
|
+
throw new Error(`colorsRemover failed: ${errorMessage}`);
|
|
1282
|
+
}
|
|
877
1283
|
}
|
|
878
1284
|
|
|
879
|
-
async removeBackground(imageURL: string, apiKey: string) {
|
|
880
|
-
|
|
1285
|
+
async removeBackground(imageURL: string, apiKey: string): Promise<Buffer | undefined> {
|
|
1286
|
+
try {
|
|
1287
|
+
if (!imageURL) {
|
|
1288
|
+
throw new Error("removeBackground: imageURL is required.");
|
|
1289
|
+
}
|
|
1290
|
+
if (!apiKey) {
|
|
1291
|
+
throw new Error("removeBackground: apiKey is required.");
|
|
1292
|
+
}
|
|
1293
|
+
return await bgRemoval(imageURL, apiKey);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1296
|
+
throw new Error(`removeBackground failed: ${errorMessage}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Validates blend inputs.
|
|
1302
|
+
* @private
|
|
1303
|
+
* @param layers - Layers to validate
|
|
1304
|
+
* @param baseImageBuffer - Base image buffer to validate
|
|
1305
|
+
*/
|
|
1306
|
+
#validateBlendInputs(
|
|
1307
|
+
layers: Array<{
|
|
1308
|
+
image: string | Buffer;
|
|
1309
|
+
blendMode: GlobalCompositeOperation;
|
|
1310
|
+
position?: { x: number; y: number };
|
|
1311
|
+
opacity?: number;
|
|
1312
|
+
}>,
|
|
1313
|
+
baseImageBuffer: Buffer
|
|
1314
|
+
): void {
|
|
1315
|
+
if (!baseImageBuffer || !Buffer.isBuffer(baseImageBuffer)) {
|
|
1316
|
+
throw new Error("blend: baseImageBuffer must be a valid Buffer.");
|
|
1317
|
+
}
|
|
1318
|
+
if (!layers || !Array.isArray(layers) || layers.length === 0) {
|
|
1319
|
+
throw new Error("blend: layers array with at least one layer is required.");
|
|
1320
|
+
}
|
|
1321
|
+
for (const layer of layers) {
|
|
1322
|
+
if (!layer.image) {
|
|
1323
|
+
throw new Error("blend: Each layer must have an image property.");
|
|
1324
|
+
}
|
|
1325
|
+
if (!layer.blendMode) {
|
|
1326
|
+
throw new Error("blend: Each layer must have a blendMode property.");
|
|
1327
|
+
}
|
|
1328
|
+
if (layer.opacity !== undefined && (typeof layer.opacity !== 'number' || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1329
|
+
throw new Error("blend: Layer opacity must be a number between 0 and 1.");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
881
1332
|
}
|
|
882
1333
|
|
|
883
1334
|
async blend(
|
|
884
|
-
layers: {
|
|
1335
|
+
layers: Array<{
|
|
885
1336
|
image: string | Buffer;
|
|
886
|
-
blendMode:
|
|
887
|
-
'destination-over' | 'destination-in' | 'destination-out' |
|
|
888
|
-
'destination-atop' | 'lighter' | 'copy' | 'xor' |
|
|
889
|
-
'multiply' | 'screen' | 'overlay' | 'darken' |
|
|
890
|
-
'lighten' | 'color-dodge' | 'color-burn' |
|
|
891
|
-
'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
|
|
892
|
-
'hue' | 'saturation' | 'color' | 'luminosity';
|
|
1337
|
+
blendMode: GlobalCompositeOperation;
|
|
893
1338
|
position?: { x: number; y: number };
|
|
894
1339
|
opacity?: number;
|
|
895
|
-
}
|
|
1340
|
+
}>,
|
|
896
1341
|
baseImageBuffer: Buffer,
|
|
897
|
-
defaultBlendMode:
|
|
898
|
-
'destination-over' | 'destination-in' | 'destination-out' |
|
|
899
|
-
'destination-atop' | 'lighter' | 'copy' | 'xor' |
|
|
900
|
-
'multiply' | 'screen' | 'overlay' | 'darken' |
|
|
901
|
-
'lighten' | 'color-dodge' | 'color-burn' |
|
|
902
|
-
'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
|
|
903
|
-
'hue' | 'saturation' | 'color' | 'luminosity' = 'source-over'
|
|
1342
|
+
defaultBlendMode: GlobalCompositeOperation = 'source-over'
|
|
904
1343
|
): Promise<Buffer> {
|
|
905
1344
|
try {
|
|
1345
|
+
this.#validateBlendInputs(layers, baseImageBuffer);
|
|
1346
|
+
|
|
906
1347
|
const baseImage = await loadImage(baseImageBuffer);
|
|
907
1348
|
const canvas = createCanvas(baseImage.width, baseImage.height);
|
|
908
|
-
const ctx = canvas.getContext('2d');
|
|
1349
|
+
const ctx = canvas.getContext('2d') as SKRSContext2D;
|
|
1350
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
909
1351
|
|
|
910
1352
|
ctx.globalCompositeOperation = defaultBlendMode;
|
|
911
1353
|
ctx.drawImage(baseImage, 0, 0);
|
|
@@ -923,29 +1365,52 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
923
1365
|
|
|
924
1366
|
return canvas.toBuffer('image/png');
|
|
925
1367
|
} catch (error) {
|
|
926
|
-
|
|
927
|
-
throw new Error(
|
|
1368
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1369
|
+
throw new Error(`blend failed: ${errorMessage}`);
|
|
928
1370
|
}
|
|
929
1371
|
}
|
|
930
1372
|
|
|
931
1373
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* Validates chart inputs.
|
|
1376
|
+
* @private
|
|
1377
|
+
* @param data - Chart data to validate
|
|
1378
|
+
* @param type - Chart type configuration to validate
|
|
1379
|
+
*/
|
|
1380
|
+
#validateChartInputs(data: unknown, type: { chartType: string; chartNumber: number }): void {
|
|
1381
|
+
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
1382
|
+
throw new Error("createChart: data object with datasets is required.");
|
|
936
1383
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1384
|
+
if (!type || typeof type !== 'object') {
|
|
1385
|
+
throw new Error("createChart: type configuration object is required.");
|
|
1386
|
+
}
|
|
1387
|
+
if (!type.chartType || typeof type.chartType !== 'string') {
|
|
1388
|
+
throw new Error("createChart: type.chartType must be a string.");
|
|
940
1389
|
}
|
|
1390
|
+
if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
|
|
1391
|
+
throw new Error("createChart: type.chartNumber must be a positive number.");
|
|
1392
|
+
}
|
|
1393
|
+
const validChartTypes = ['bar', 'line', 'pie'];
|
|
1394
|
+
if (!validChartTypes.includes(type.chartType.toLowerCase())) {
|
|
1395
|
+
throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
941
1398
|
|
|
942
|
-
|
|
1399
|
+
async createChart(data: unknown, type: { chartType: string; chartNumber: number }): Promise<Buffer> {
|
|
1400
|
+
try {
|
|
1401
|
+
this.#validateChartInputs(data, type);
|
|
1402
|
+
|
|
1403
|
+
const { chartType, chartNumber } = type;
|
|
943
1404
|
|
|
944
1405
|
switch (chartType.toLowerCase()) {
|
|
945
1406
|
case 'bar':
|
|
946
1407
|
switch (chartNumber) {
|
|
947
1408
|
case 1:
|
|
948
|
-
|
|
1409
|
+
const barResult = await verticalBarChart(data as barChart_1);
|
|
1410
|
+
if (!barResult) {
|
|
1411
|
+
throw new Error("createChart: Failed to generate bar chart.");
|
|
1412
|
+
}
|
|
1413
|
+
return barResult;
|
|
949
1414
|
case 2:
|
|
950
1415
|
throw new Error('Type 2 is still under development.');
|
|
951
1416
|
default:
|
|
@@ -954,7 +1419,9 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
954
1419
|
case 'line':
|
|
955
1420
|
switch (chartNumber) {
|
|
956
1421
|
case 1:
|
|
957
|
-
|
|
1422
|
+
// LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
|
|
1423
|
+
// Type assertion needed because there are two different DataPoint interfaces
|
|
1424
|
+
return await lineChart(data as unknown as { data: Array<Array<{ label: string; y: number }>>; lineConfig: LineChartConfig });
|
|
958
1425
|
case 2:
|
|
959
1426
|
throw new Error('Type 2 is still under development.');
|
|
960
1427
|
default:
|
|
@@ -963,7 +1430,7 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
963
1430
|
case 'pie':
|
|
964
1431
|
switch (chartNumber) {
|
|
965
1432
|
case 1:
|
|
966
|
-
return await pieChart(data);
|
|
1433
|
+
return await pieChart(data as PieChartData);
|
|
967
1434
|
case 2:
|
|
968
1435
|
throw new Error('Type 2 is still under development.');
|
|
969
1436
|
default:
|
|
@@ -971,37 +1438,162 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
971
1438
|
}
|
|
972
1439
|
default:
|
|
973
1440
|
throw new Error(`Unsupported chart type "${chartType}".`);
|
|
1441
|
+
}
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1444
|
+
throw new Error(`createChart failed: ${errorMessage}`);
|
|
974
1445
|
}
|
|
975
1446
|
}
|
|
976
1447
|
|
|
977
1448
|
|
|
978
|
-
|
|
1449
|
+
/**
|
|
1450
|
+
* Validates crop options.
|
|
1451
|
+
* @private
|
|
1452
|
+
* @param options - Crop options to validate
|
|
1453
|
+
*/
|
|
1454
|
+
#validateCropOptions(options: cropOptions): void {
|
|
1455
|
+
if (!options) {
|
|
1456
|
+
throw new Error("cropImage: options object is required.");
|
|
1457
|
+
}
|
|
1458
|
+
if (!options.imageSource) {
|
|
1459
|
+
throw new Error("cropImage: imageSource is required.");
|
|
1460
|
+
}
|
|
1461
|
+
if (!options.coordinates || !Array.isArray(options.coordinates) || options.coordinates.length < 3) {
|
|
1462
|
+
throw new Error("cropImage: coordinates array with at least 3 points is required.");
|
|
1463
|
+
}
|
|
1464
|
+
if (options.crop !== 'inner' && options.crop !== 'outer') {
|
|
1465
|
+
throw new Error("cropImage: crop must be either 'inner' or 'outer'.");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async cropImage(options: cropOptions): Promise<Buffer> {
|
|
979
1470
|
try {
|
|
980
|
-
|
|
981
|
-
if (!options.coordinates || options.coordinates.length < 3) throw new Error('The "coordinates" option is needed. Please provide coordinates to crop the image.');
|
|
1471
|
+
this.#validateCropOptions(options);
|
|
982
1472
|
|
|
983
1473
|
if (options.crop === 'outer') {
|
|
984
|
-
|
|
985
|
-
} else if (options.crop === 'inner') {
|
|
986
|
-
return await cropInner(options);
|
|
1474
|
+
return await cropOuter(options);
|
|
987
1475
|
} else {
|
|
988
|
-
|
|
1476
|
+
return await cropInner(options);
|
|
989
1477
|
}
|
|
990
1478
|
} catch (error) {
|
|
991
|
-
|
|
992
|
-
throw
|
|
1479
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1480
|
+
throw new Error(`cropImage failed: ${errorMessage}`);
|
|
993
1481
|
}
|
|
994
1482
|
}
|
|
995
1483
|
|
|
996
|
-
|
|
1484
|
+
/**
|
|
1485
|
+
* Extracts a single frame from a video
|
|
1486
|
+
* @private
|
|
1487
|
+
* @param videoSource - Video source (path, URL, or Buffer)
|
|
1488
|
+
* @param frameNumber - Frame number to extract (default: 0)
|
|
1489
|
+
* @returns Buffer containing the frame image
|
|
1490
|
+
*/
|
|
1491
|
+
async #extractVideoFrame(videoSource: string | Buffer, frameNumber: number = 0): Promise<Buffer | null> {
|
|
1492
|
+
try {
|
|
1493
|
+
const frameDir = path.join(process.cwd(), '.temp-frames');
|
|
1494
|
+
if (!fs.existsSync(frameDir)) {
|
|
1495
|
+
fs.mkdirSync(frameDir, { recursive: true });
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const tempVideoPath = path.join(frameDir, `temp-video-${Date.now()}.mp4`);
|
|
1499
|
+
const frameOutputPath = path.join(frameDir, `frame-${Date.now()}.jpg`);
|
|
997
1500
|
|
|
1501
|
+
// Handle video source
|
|
1502
|
+
if (Buffer.isBuffer(videoSource)) {
|
|
1503
|
+
fs.writeFileSync(tempVideoPath, videoSource);
|
|
1504
|
+
} else if (typeof videoSource === 'string' && videoSource.startsWith('http')) {
|
|
1505
|
+
const response = await axios({
|
|
1506
|
+
method: 'get',
|
|
1507
|
+
url: videoSource,
|
|
1508
|
+
responseType: 'arraybuffer'
|
|
1509
|
+
});
|
|
1510
|
+
fs.writeFileSync(tempVideoPath, Buffer.from(response.data));
|
|
1511
|
+
} else {
|
|
1512
|
+
// Local file path
|
|
1513
|
+
if (!fs.existsSync(videoSource)) {
|
|
1514
|
+
throw new Error(`Video file not found: ${videoSource}`);
|
|
1515
|
+
}
|
|
1516
|
+
// Use the existing path
|
|
1517
|
+
return await new Promise<Buffer | null>((resolve, reject) => {
|
|
1518
|
+
ffmpeg(videoSource)
|
|
1519
|
+
.seekInput(frameNumber / 1000) // Convert frame to seconds (approximate)
|
|
1520
|
+
.frames(1)
|
|
1521
|
+
.output(frameOutputPath)
|
|
1522
|
+
.on('end', () => {
|
|
1523
|
+
try {
|
|
1524
|
+
const buffer = fs.readFileSync(frameOutputPath);
|
|
1525
|
+
// Cleanup
|
|
1526
|
+
if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
|
|
1527
|
+
resolve(buffer);
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
resolve(null);
|
|
1530
|
+
}
|
|
1531
|
+
})
|
|
1532
|
+
.on('error', (err: any) => {
|
|
1533
|
+
reject(err);
|
|
1534
|
+
})
|
|
1535
|
+
.run();
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Extract frame from temp video
|
|
1540
|
+
return await new Promise<Buffer | null>((resolve, reject) => {
|
|
1541
|
+
ffmpeg(tempVideoPath)
|
|
1542
|
+
.seekInput(frameNumber / 1000)
|
|
1543
|
+
.frames(1)
|
|
1544
|
+
.output(frameOutputPath)
|
|
1545
|
+
.on('end', () => {
|
|
1546
|
+
try {
|
|
1547
|
+
const buffer = fs.readFileSync(frameOutputPath);
|
|
1548
|
+
// Cleanup
|
|
1549
|
+
if (fs.existsSync(tempVideoPath)) fs.unlinkSync(tempVideoPath);
|
|
1550
|
+
if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
|
|
1551
|
+
resolve(buffer);
|
|
1552
|
+
} catch (e) {
|
|
1553
|
+
resolve(null);
|
|
1554
|
+
}
|
|
1555
|
+
})
|
|
1556
|
+
.on('error', (err: any) => {
|
|
1557
|
+
// Cleanup on error
|
|
1558
|
+
if (fs.existsSync(tempVideoPath)) fs.unlinkSync(tempVideoPath);
|
|
1559
|
+
if (fs.existsSync(frameOutputPath)) fs.unlinkSync(frameOutputPath);
|
|
1560
|
+
reject(err);
|
|
1561
|
+
})
|
|
1562
|
+
.run();
|
|
1563
|
+
});
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
console.error('Error extracting video frame:', error);
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
998
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* Validates extract frames inputs.
|
|
1572
|
+
* @private
|
|
1573
|
+
* @param videoSource - Video source to validate
|
|
1574
|
+
* @param options - Extract frames options to validate
|
|
1575
|
+
*/
|
|
1576
|
+
#validateExtractFramesInputs(videoSource: string | Buffer, options: ExtractFramesOptions): void {
|
|
1577
|
+
if (!videoSource) {
|
|
1578
|
+
throw new Error("extractFrames: videoSource is required.");
|
|
1579
|
+
}
|
|
1580
|
+
if (!options || typeof options !== 'object') {
|
|
1581
|
+
throw new Error("extractFrames: options object is required.");
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof options.interval !== 'number' || options.interval <= 0) {
|
|
1584
|
+
throw new Error("extractFrames: options.interval must be a positive number (milliseconds).");
|
|
1585
|
+
}
|
|
1586
|
+
if (options.outputFormat && !['jpg', 'png'].includes(options.outputFormat)) {
|
|
1587
|
+
throw new Error("extractFrames: outputFormat must be 'jpg' or 'png'.");
|
|
1588
|
+
}
|
|
999
1589
|
}
|
|
1000
|
-
|
|
1001
1590
|
|
|
1002
|
-
async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<
|
|
1003
|
-
|
|
1004
|
-
|
|
1591
|
+
async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<Array<{ source: string; isRemote: boolean }>> {
|
|
1592
|
+
try {
|
|
1593
|
+
this.#validateExtractFramesInputs(videoSource, options);
|
|
1594
|
+
|
|
1595
|
+
const frames: Array<{ source: string; isRemote: boolean }> = [];
|
|
1596
|
+
const frameDir = path.join(__dirname, 'frames');
|
|
1005
1597
|
|
|
1006
1598
|
if (!fs.existsSync(frameDir)) {
|
|
1007
1599
|
fs.mkdirSync(frameDir);
|
|
@@ -1064,10 +1656,14 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
|
|
|
1064
1656
|
});
|
|
1065
1657
|
}
|
|
1066
1658
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1659
|
+
return new Promise((resolve, reject) => {
|
|
1660
|
+
processVideoExtraction(videoPath, frames, options, resolve, reject);
|
|
1661
|
+
});
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1664
|
+
throw new Error(`extractFrames failed: ${errorMessage}`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1071
1667
|
|
|
1072
1668
|
|
|
1073
1669
|
|
|
@@ -1077,13 +1673,45 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
|
|
|
1077
1673
|
|
|
1078
1674
|
|
|
1079
1675
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1676
|
+
/**
|
|
1677
|
+
* Validates masking inputs.
|
|
1678
|
+
* @private
|
|
1679
|
+
* @param source - Source image to validate
|
|
1680
|
+
* @param maskSource - Mask image to validate
|
|
1681
|
+
* @param options - Mask options to validate
|
|
1682
|
+
*/
|
|
1683
|
+
#validateMaskingInputs(
|
|
1684
|
+
source: string | Buffer | PathLike | Uint8Array,
|
|
1685
|
+
maskSource: string | Buffer | PathLike | Uint8Array,
|
|
1686
|
+
options: MaskOptions
|
|
1687
|
+
): void {
|
|
1688
|
+
if (!source) {
|
|
1689
|
+
throw new Error("masking: source is required.");
|
|
1690
|
+
}
|
|
1691
|
+
if (!maskSource) {
|
|
1692
|
+
throw new Error("masking: maskSource is required.");
|
|
1693
|
+
}
|
|
1694
|
+
if (options.type && !['alpha', 'grayscale', 'color'].includes(options.type)) {
|
|
1695
|
+
throw new Error("masking: type must be 'alpha', 'grayscale', or 'color'.");
|
|
1696
|
+
}
|
|
1697
|
+
if (options.type === 'color' && !options.colorKey) {
|
|
1698
|
+
throw new Error("masking: colorKey is required when type is 'color'.");
|
|
1699
|
+
}
|
|
1700
|
+
if (options.threshold !== undefined && (typeof options.threshold !== 'number' || options.threshold < 0 || options.threshold > 255)) {
|
|
1701
|
+
throw new Error("masking: threshold must be a number between 0 and 255.");
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async masking(
|
|
1706
|
+
source: string | Buffer | PathLike | Uint8Array,
|
|
1707
|
+
maskSource: string | Buffer | PathLike | Uint8Array,
|
|
1708
|
+
options: MaskOptions = { type: "alpha" }
|
|
1709
|
+
): Promise<Buffer> {
|
|
1710
|
+
try {
|
|
1711
|
+
this.#validateMaskingInputs(source, maskSource, options);
|
|
1712
|
+
|
|
1713
|
+
const img = await loadImage(source);
|
|
1714
|
+
const mask = await loadImage(maskSource);
|
|
1087
1715
|
|
|
1088
1716
|
const canvas = createCanvas(img.width, img.height);
|
|
1089
1717
|
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
@@ -1115,21 +1743,58 @@ async masking(
|
|
|
1115
1743
|
|
|
1116
1744
|
if (options.invert) alphaValue = 255 - alphaValue;
|
|
1117
1745
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1746
|
+
imgData.data[i + 3] = alphaValue;
|
|
1747
|
+
}
|
|
1120
1748
|
|
|
1121
|
-
|
|
1749
|
+
ctx.putImageData(imgData, 0, 0);
|
|
1122
1750
|
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1751
|
+
return canvas.toBuffer("image/png");
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1754
|
+
throw new Error(`masking failed: ${errorMessage}`);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1125
1757
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1758
|
+
/**
|
|
1759
|
+
* Validates gradient blend inputs.
|
|
1760
|
+
* @private
|
|
1761
|
+
* @param source - Source image to validate
|
|
1762
|
+
* @param options - Blend options to validate
|
|
1763
|
+
*/
|
|
1764
|
+
#validateGradientBlendInputs(source: string | Buffer | PathLike | Uint8Array, options: BlendOptions): void {
|
|
1765
|
+
if (!source) {
|
|
1766
|
+
throw new Error("gradientBlend: source is required.");
|
|
1767
|
+
}
|
|
1768
|
+
if (!options || typeof options !== 'object') {
|
|
1769
|
+
throw new Error("gradientBlend: options object is required.");
|
|
1770
|
+
}
|
|
1771
|
+
if (!options.colors || !Array.isArray(options.colors) || options.colors.length === 0) {
|
|
1772
|
+
throw new Error("gradientBlend: options.colors array with at least one color stop is required.");
|
|
1773
|
+
}
|
|
1774
|
+
if (options.type && !['linear', 'radial', 'conic'].includes(options.type)) {
|
|
1775
|
+
throw new Error("gradientBlend: type must be 'linear', 'radial', or 'conic'.");
|
|
1776
|
+
}
|
|
1777
|
+
for (const colorStop of options.colors) {
|
|
1778
|
+
if (typeof colorStop.stop !== 'number' || colorStop.stop < 0 || colorStop.stop > 1) {
|
|
1779
|
+
throw new Error("gradientBlend: Each color stop must have a stop value between 0 and 1.");
|
|
1780
|
+
}
|
|
1781
|
+
if (!colorStop.color || typeof colorStop.color !== 'string') {
|
|
1782
|
+
throw new Error("gradientBlend: Each color stop must have a valid color string.");
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async gradientBlend(
|
|
1788
|
+
source: string | Buffer | PathLike | Uint8Array,
|
|
1789
|
+
options: BlendOptions
|
|
1790
|
+
): Promise<Buffer> {
|
|
1791
|
+
try {
|
|
1792
|
+
this.#validateGradientBlendInputs(source, options);
|
|
1793
|
+
|
|
1794
|
+
const img = await loadImage(source);
|
|
1795
|
+
const canvas = createCanvas(img.width, img.height);
|
|
1796
|
+
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
1797
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
1133
1798
|
|
|
1134
1799
|
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1135
1800
|
|
|
@@ -1162,38 +1827,78 @@ async gradientBlend(
|
|
|
1162
1827
|
ctx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1163
1828
|
}
|
|
1164
1829
|
|
|
1165
|
-
|
|
1830
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1166
1831
|
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1832
|
+
return canvas.toBuffer("image/png");
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1835
|
+
throw new Error(`gradientBlend failed: ${errorMessage}`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1169
1838
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1839
|
+
/**
|
|
1840
|
+
* Validates animate inputs.
|
|
1841
|
+
* @private
|
|
1842
|
+
* @param frames - Animation frames to validate
|
|
1843
|
+
* @param defaultDuration - Default duration to validate
|
|
1844
|
+
* @param defaultWidth - Default width to validate
|
|
1845
|
+
* @param defaultHeight - Default height to validate
|
|
1846
|
+
* @param options - Animation options to validate
|
|
1847
|
+
*/
|
|
1848
|
+
#validateAnimateInputs(
|
|
1849
|
+
frames: Frame[],
|
|
1850
|
+
defaultDuration: number,
|
|
1851
|
+
defaultWidth: number,
|
|
1852
|
+
defaultHeight: number,
|
|
1853
|
+
options?: { gif?: boolean; gifPath?: string; onStart?: () => void; onFrame?: (index: number) => void; onEnd?: () => void }
|
|
1854
|
+
): void {
|
|
1855
|
+
if (!frames || !Array.isArray(frames) || frames.length === 0) {
|
|
1856
|
+
throw new Error("animate: frames array with at least one frame is required.");
|
|
1857
|
+
}
|
|
1858
|
+
if (typeof defaultDuration !== 'number' || defaultDuration < 0) {
|
|
1859
|
+
throw new Error("animate: defaultDuration must be a non-negative number.");
|
|
1860
|
+
}
|
|
1861
|
+
if (typeof defaultWidth !== 'number' || defaultWidth <= 0) {
|
|
1862
|
+
throw new Error("animate: defaultWidth must be a positive number.");
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof defaultHeight !== 'number' || defaultHeight <= 0) {
|
|
1865
|
+
throw new Error("animate: defaultHeight must be a positive number.");
|
|
1866
|
+
}
|
|
1867
|
+
if (options?.gif && !options.gifPath) {
|
|
1868
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async animate(
|
|
1873
|
+
frames: Frame[],
|
|
1874
|
+
defaultDuration: number,
|
|
1875
|
+
defaultWidth: number = 800,
|
|
1876
|
+
defaultHeight: number = 600,
|
|
1877
|
+
options?: {
|
|
1176
1878
|
gif?: boolean;
|
|
1177
1879
|
gifPath?: string;
|
|
1178
1880
|
onStart?: () => void;
|
|
1179
1881
|
onFrame?: (index: number) => void;
|
|
1180
1882
|
onEnd?: () => void;
|
|
1181
|
-
|
|
1182
|
-
): Promise<Buffer[] | undefined> {
|
|
1183
|
-
|
|
1184
|
-
|
|
1883
|
+
}
|
|
1884
|
+
): Promise<Buffer[] | undefined> {
|
|
1885
|
+
try {
|
|
1886
|
+
this.#validateAnimateInputs(frames, defaultDuration, defaultWidth, defaultHeight, options);
|
|
1887
|
+
|
|
1888
|
+
const buffers: Buffer[] = [];
|
|
1889
|
+
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
1185
1890
|
|
|
1186
|
-
|
|
1891
|
+
if (options?.onStart) options.onStart();
|
|
1187
1892
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1893
|
+
let encoder: GIFEncoder | null = null;
|
|
1894
|
+
let gifStream: fs.WriteStream | null = null;
|
|
1190
1895
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
throw new Error("
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1896
|
+
if (options?.gif) {
|
|
1897
|
+
if (!options.gifPath) {
|
|
1898
|
+
throw new Error("animate: gifPath is required when gif is enabled.");
|
|
1899
|
+
}
|
|
1900
|
+
encoder = new GIFEncoder(defaultWidth, defaultHeight);
|
|
1901
|
+
gifStream = fs.createWriteStream(options.gifPath);
|
|
1197
1902
|
encoder.createReadStream().pipe(gifStream);
|
|
1198
1903
|
encoder.start();
|
|
1199
1904
|
encoder.setRepeat(0);
|
|
@@ -1298,39 +2003,186 @@ async animate(
|
|
|
1298
2003
|
encoder.finish();
|
|
1299
2004
|
}
|
|
1300
2005
|
|
|
1301
|
-
|
|
2006
|
+
if (options?.onEnd) options.onEnd();
|
|
2007
|
+
|
|
2008
|
+
return options?.gif ? undefined : buffers;
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2011
|
+
throw new Error(`animate failed: ${errorMessage}`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* Processes multiple operations in parallel
|
|
2019
|
+
* @param operations - Array of operations to process
|
|
2020
|
+
* @returns Array of result buffers
|
|
2021
|
+
*/
|
|
2022
|
+
async batch(operations: BatchOperation[]): Promise<Buffer[]> {
|
|
2023
|
+
try {
|
|
2024
|
+
return await batchOperations(this, operations);
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2027
|
+
throw new Error(`batch failed: ${errorMessage}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Chains multiple operations sequentially
|
|
2033
|
+
* @param operations - Array of operations to chain
|
|
2034
|
+
* @returns Final result buffer
|
|
2035
|
+
*/
|
|
2036
|
+
async chain(operations: ChainOperation[]): Promise<Buffer> {
|
|
2037
|
+
try {
|
|
2038
|
+
return await chainOperations(this, operations);
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2041
|
+
throw new Error(`chain failed: ${errorMessage}`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Stitches multiple images together
|
|
2047
|
+
* @param images - Array of image sources
|
|
2048
|
+
* @param options - Stitching options
|
|
2049
|
+
* @returns Stitched image buffer
|
|
2050
|
+
*/
|
|
2051
|
+
async stitchImages(images: Array<string | Buffer>, options?: StitchOptions): Promise<Buffer> {
|
|
2052
|
+
try {
|
|
2053
|
+
if (!images || images.length === 0) {
|
|
2054
|
+
throw new Error("stitchImages: images array is required");
|
|
2055
|
+
}
|
|
2056
|
+
return await stitchImagesUtil(images, options);
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2059
|
+
throw new Error(`stitchImages failed: ${errorMessage}`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
1302
2062
|
|
|
1303
|
-
|
|
1304
|
-
|
|
2063
|
+
/**
|
|
2064
|
+
* Creates an image collage
|
|
2065
|
+
* @param images - Array of image sources with optional dimensions
|
|
2066
|
+
* @param layout - Collage layout configuration
|
|
2067
|
+
* @returns Collage image buffer
|
|
2068
|
+
*/
|
|
2069
|
+
async createCollage(
|
|
2070
|
+
images: Array<{ source: string | Buffer; width?: number; height?: number }>,
|
|
2071
|
+
layout: CollageLayout
|
|
2072
|
+
): Promise<Buffer> {
|
|
2073
|
+
try {
|
|
2074
|
+
if (!images || images.length === 0) {
|
|
2075
|
+
throw new Error("createCollage: images array is required");
|
|
2076
|
+
}
|
|
2077
|
+
if (!layout) {
|
|
2078
|
+
throw new Error("createCollage: layout configuration is required");
|
|
2079
|
+
}
|
|
2080
|
+
return await createCollage(images, layout);
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2083
|
+
throw new Error(`createCollage failed: ${errorMessage}`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1305
2086
|
|
|
2087
|
+
/**
|
|
2088
|
+
* Compresses an image with quality control
|
|
2089
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
2090
|
+
* @param options - Compression options
|
|
2091
|
+
* @returns Compressed image buffer
|
|
2092
|
+
*/
|
|
2093
|
+
async compress(image: string | Buffer, options?: CompressionOptions): Promise<Buffer> {
|
|
2094
|
+
try {
|
|
2095
|
+
if (!image) {
|
|
2096
|
+
throw new Error("compress: image is required");
|
|
2097
|
+
}
|
|
2098
|
+
return await compressImage(image, options);
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2101
|
+
throw new Error(`compress failed: ${errorMessage}`);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
1306
2104
|
|
|
2105
|
+
/**
|
|
2106
|
+
* Extracts color palette from an image
|
|
2107
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
2108
|
+
* @param options - Palette extraction options
|
|
2109
|
+
* @returns Array of colors with percentages
|
|
2110
|
+
*/
|
|
2111
|
+
async extractPalette(image: string | Buffer, options?: PaletteOptions): Promise<Array<{ color: string; percentage: number }>> {
|
|
2112
|
+
try {
|
|
2113
|
+
if (!image) {
|
|
2114
|
+
throw new Error("extractPalette: image is required");
|
|
2115
|
+
}
|
|
2116
|
+
return await extractPaletteUtil(image, options);
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2119
|
+
throw new Error(`extractPalette failed: ${errorMessage}`);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
1307
2122
|
|
|
1308
|
-
|
|
2123
|
+
/**
|
|
2124
|
+
* Validates a hexadecimal color string.
|
|
2125
|
+
* @param hexColor - Hexadecimal color string to validate (format: #RRGGBB)
|
|
2126
|
+
* @returns True if the color is valid
|
|
2127
|
+
* @throws Error if the color format is invalid
|
|
2128
|
+
*
|
|
2129
|
+
* @example
|
|
2130
|
+
* ```typescript
|
|
2131
|
+
* painter.validHex('#ff0000'); // true
|
|
2132
|
+
* painter.validHex('#FF00FF'); // true
|
|
2133
|
+
* painter.validHex('invalid'); // throws Error
|
|
2134
|
+
* ```
|
|
2135
|
+
*/
|
|
2136
|
+
public validHex(hexColor: string): boolean {
|
|
2137
|
+
if (typeof hexColor !== 'string') {
|
|
2138
|
+
throw new Error("validHex: hexColor must be a string.");
|
|
2139
|
+
}
|
|
1309
2140
|
const hexPattern = /^#[0-9a-fA-F]{6}$/;
|
|
1310
2141
|
if (!hexPattern.test(hexColor)) {
|
|
1311
|
-
|
|
1312
|
-
}
|
|
1313
|
-
return true
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2142
|
+
throw new Error("validHex: Invalid hexadecimal color format. It should be in the format '#RRGGBB'.");
|
|
2143
|
+
}
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Converts results to the configured output format.
|
|
2149
|
+
* @param results - Buffer or result to convert
|
|
2150
|
+
* @returns Converted result in the configured format
|
|
2151
|
+
* @throws Error if format is unsupported or conversion fails
|
|
2152
|
+
*
|
|
2153
|
+
* @example
|
|
2154
|
+
* ```typescript
|
|
2155
|
+
* const painter = new ApexPainter({ type: 'base64' });
|
|
2156
|
+
* const result = await painter.createCanvas({ width: 100, height: 100 });
|
|
2157
|
+
* const base64String = await painter.outPut(result.buffer); // Returns base64 string
|
|
2158
|
+
* ```
|
|
2159
|
+
*/
|
|
2160
|
+
public async outPut(results: Buffer): Promise<Buffer | string | Blob | ArrayBuffer> {
|
|
2161
|
+
try {
|
|
2162
|
+
if (!Buffer.isBuffer(results)) {
|
|
2163
|
+
throw new Error("outPut: results must be a Buffer.");
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const formatType: string = this.format?.type || 'buffer';
|
|
2167
|
+
switch (formatType) {
|
|
2168
|
+
case 'buffer':
|
|
2169
|
+
return results;
|
|
2170
|
+
case 'url':
|
|
2171
|
+
return await url(results);
|
|
2172
|
+
case 'dataURL':
|
|
2173
|
+
return dataURL(results);
|
|
2174
|
+
case 'blob':
|
|
2175
|
+
return blob(results);
|
|
2176
|
+
case 'base64':
|
|
2177
|
+
return base64(results);
|
|
2178
|
+
case 'arraybuffer':
|
|
2179
|
+
return arrayBuffer(results);
|
|
2180
|
+
default:
|
|
2181
|
+
throw new Error(`outPut: Unsupported format '${formatType}'. Supported: buffer, url, dataURL, blob, base64, arraybuffer`);
|
|
2182
|
+
}
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2185
|
+
throw new Error(`outPut failed: ${errorMessage}`);
|
|
1334
2186
|
}
|
|
1335
2187
|
}
|
|
1336
2188
|
|