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