apexify.js 4.9.28 → 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 +727 -456
- 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 +1416 -420
- 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/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +181 -2
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- 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/enhancedTextRenderer.d.ts +33 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.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 +171 -10
- 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 +1416 -420
- 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/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +181 -2
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- 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/enhancedTextRenderer.d.ts +33 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +237 -32
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.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 +171 -10
- 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 +1309 -267
- 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/Image/imageProperties.ts +207 -2
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +274 -36
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
- package/lib/Canvas/utils/types.ts +173 -10
- 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
|
+
}
|
|
183
275
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
283
|
+
|
|
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);
|
|
@@ -556,7 +797,8 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
556
797
|
width: strokeWidth = 2,
|
|
557
798
|
position = 0,
|
|
558
799
|
blur = 0,
|
|
559
|
-
opacity = 1
|
|
800
|
+
opacity = 1,
|
|
801
|
+
style = 'solid'
|
|
560
802
|
} = stroke;
|
|
561
803
|
|
|
562
804
|
ctx.save();
|
|
@@ -573,9 +815,18 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
573
815
|
|
|
574
816
|
ctx.lineWidth = strokeWidth;
|
|
575
817
|
|
|
818
|
+
// Apply stroke style
|
|
819
|
+
this.#applyShapeStrokeStyle(ctx, style, strokeWidth);
|
|
820
|
+
|
|
576
821
|
// Create stroke path
|
|
577
822
|
createShapePath(ctx, shapeType, x, y, width, height, shapeProps);
|
|
578
|
-
|
|
823
|
+
|
|
824
|
+
// Handle complex stroke styles
|
|
825
|
+
if (style === 'groove' || style === 'ridge' || style === 'double') {
|
|
826
|
+
this.#applyComplexShapeStroke(ctx, style, strokeWidth, color, gradient);
|
|
827
|
+
} else {
|
|
828
|
+
ctx.stroke();
|
|
829
|
+
}
|
|
579
830
|
|
|
580
831
|
ctx.filter = "none";
|
|
581
832
|
ctx.globalAlpha = 1;
|
|
@@ -637,18 +888,34 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
637
888
|
* ], canvasBuffer);
|
|
638
889
|
* ```
|
|
639
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
|
+
|
|
640
906
|
async createText(textArray: TextProperties | TextProperties[], canvasBuffer: CanvasResults | Buffer): Promise<Buffer> {
|
|
641
907
|
try {
|
|
908
|
+
// Validate inputs
|
|
909
|
+
if (!canvasBuffer) {
|
|
910
|
+
throw new Error("createText: canvasBuffer is required.");
|
|
911
|
+
}
|
|
912
|
+
this.#validateTextArray(textArray);
|
|
913
|
+
|
|
642
914
|
// Ensure textArray is an array
|
|
643
915
|
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
644
916
|
|
|
645
|
-
// Validate each text object
|
|
646
|
-
for (const textProps of textList) {
|
|
647
|
-
this.#validateTextProperties(textProps);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
917
|
// Load existing canvas buffer
|
|
651
|
-
let existingImage:
|
|
918
|
+
let existingImage: Image;
|
|
652
919
|
|
|
653
920
|
if (Buffer.isBuffer(canvasBuffer)) {
|
|
654
921
|
existingImage = await loadImage(canvasBuffer);
|
|
@@ -680,21 +947,44 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
680
947
|
|
|
681
948
|
return canvas.toBuffer("image/png");
|
|
682
949
|
} catch (error) {
|
|
683
|
-
|
|
684
|
-
throw new Error(
|
|
950
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
951
|
+
throw new Error(`createText failed: ${errorMessage}`);
|
|
685
952
|
}
|
|
686
953
|
}
|
|
687
954
|
|
|
688
955
|
|
|
689
956
|
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
}
|
|
692
976
|
|
|
693
|
-
|
|
694
|
-
|
|
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.");
|
|
695
982
|
}
|
|
983
|
+
this.#validateCustomOptions(options);
|
|
984
|
+
|
|
985
|
+
const opts = Array.isArray(options) ? options : [options];
|
|
696
986
|
|
|
697
|
-
let existingImage:
|
|
987
|
+
let existingImage: Image;
|
|
698
988
|
|
|
699
989
|
if (Buffer.isBuffer(buffer)) {
|
|
700
990
|
existingImage = await loadImage(buffer);
|
|
@@ -713,17 +1003,49 @@ async createCanvas(canvas: CanvasConfig): Promise<CanvasResults> {
|
|
|
713
1003
|
|
|
714
1004
|
ctx.drawImage(existingImage, 0, 0);
|
|
715
1005
|
|
|
716
|
-
customLines(ctx,
|
|
1006
|
+
await customLines(ctx, opts);
|
|
717
1007
|
|
|
718
1008
|
return canvas.toBuffer("image/png");
|
|
719
1009
|
} catch (error) {
|
|
720
|
-
|
|
721
|
-
|
|
1010
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1011
|
+
throw new Error(`createCustom failed: ${errorMessage}`);
|
|
722
1012
|
}
|
|
723
|
-
}
|
|
1013
|
+
}
|
|
724
1014
|
|
|
725
|
-
|
|
726
|
-
|
|
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) {
|
|
727
1049
|
const canvas = createCanvas(targetWidth, targetHeight);
|
|
728
1050
|
const ctx = canvas.getContext("2d");
|
|
729
1051
|
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
@@ -747,38 +1069,10 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
747
1069
|
getBuffer: function (): Buffer {
|
|
748
1070
|
return Buffer.concat(chunks);
|
|
749
1071
|
}
|
|
750
|
-
} as
|
|
1072
|
+
} as PassThrough & { getBuffer: () => Buffer };
|
|
751
1073
|
}
|
|
752
1074
|
|
|
753
|
-
|
|
754
|
-
if (options.outputFormat === "file" && !options.outputFile) {
|
|
755
|
-
throw new Error("Output file path is required when using file output format.");
|
|
756
|
-
}
|
|
757
|
-
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
758
|
-
throw new Error("Repeat must be a non-negative number or undefined.");
|
|
759
|
-
}
|
|
760
|
-
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
761
|
-
throw new Error("Quality must be a number between 1 and 20 or undefined.");
|
|
762
|
-
}
|
|
763
|
-
if (options.watermark && typeof options.watermark.enable !== "boolean") {
|
|
764
|
-
throw new Error("Watermark must be a boolean or undefined.");
|
|
765
|
-
}
|
|
766
|
-
if (options.textOverlay) {
|
|
767
|
-
const textOptions = options.textOverlay;
|
|
768
|
-
if (!textOptions.text || typeof textOptions.text !== "string") {
|
|
769
|
-
throw new Error("Text overlay text is required and must be a string.");
|
|
770
|
-
}
|
|
771
|
-
if (textOptions.fontSize !== undefined && (!Number.isInteger(textOptions.fontSize) || textOptions.fontSize <= 0)) {
|
|
772
|
-
throw new Error("Text overlay fontSize must be a positive integer or undefined.");
|
|
773
|
-
}
|
|
774
|
-
if (textOptions.fontColor !== undefined && typeof textOptions.fontColor !== "string") {
|
|
775
|
-
throw new Error("Text overlay fontColor must be a string or undefined.");
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
try {
|
|
781
|
-
validateOptions(options);
|
|
1075
|
+
// Validation is done in #validateGIFOptions
|
|
782
1076
|
|
|
783
1077
|
const canvasWidth = options.width || 1200;
|
|
784
1078
|
const canvasHeight = options.height || 1200;
|
|
@@ -793,7 +1087,8 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
793
1087
|
encoder.setQuality(options.quality || 10);
|
|
794
1088
|
|
|
795
1089
|
const canvas = createCanvas(canvasWidth, canvasHeight);
|
|
796
|
-
const ctx
|
|
1090
|
+
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
1091
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
797
1092
|
|
|
798
1093
|
for (const frame of gifFrames) {
|
|
799
1094
|
const image = await loadImage(frame.background);
|
|
@@ -814,18 +1109,19 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
814
1109
|
}
|
|
815
1110
|
|
|
816
1111
|
encoder.setDelay(frame.duration);
|
|
817
|
-
encoder.addFrame(ctx);
|
|
1112
|
+
encoder.addFrame(ctx as unknown as CanvasRenderingContext2D);
|
|
818
1113
|
}
|
|
819
1114
|
|
|
820
1115
|
encoder.finish();
|
|
821
1116
|
outputStream.end();
|
|
822
1117
|
|
|
823
1118
|
if (options.outputFormat === "file") {
|
|
824
|
-
await new Promise((resolve) => outputStream.on("finish", resolve));
|
|
1119
|
+
await new Promise<void>((resolve) => outputStream.on("finish", () => resolve()));
|
|
825
1120
|
} else if (options.outputFormat === "base64") {
|
|
826
1121
|
if ('getBuffer' in outputStream) {
|
|
827
1122
|
return outputStream.getBuffer().toString("base64");
|
|
828
1123
|
}
|
|
1124
|
+
throw new Error("createGIF: Unable to get buffer for base64 output.");
|
|
829
1125
|
} else if (options.outputFormat === "attachment") {
|
|
830
1126
|
const gifStream = encoder.createReadStream();
|
|
831
1127
|
return [{ attachment: gifStream, name: "gif.js" }];
|
|
@@ -833,69 +1129,225 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
833
1129
|
if ('getBuffer' in outputStream) {
|
|
834
1130
|
return outputStream.getBuffer();
|
|
835
1131
|
}
|
|
1132
|
+
throw new Error("createGIF: Unable to get buffer for buffer output.");
|
|
836
1133
|
} else {
|
|
837
1134
|
throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
|
|
838
1135
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
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
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
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
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
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
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
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
|
+
}
|
|
842
1216
|
}
|
|
843
|
-
}
|
|
844
1217
|
|
|
845
|
-
async
|
|
846
|
-
|
|
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
|
+
}
|
|
847
1226
|
}
|
|
848
1227
|
|
|
849
|
-
|
|
850
|
-
|
|
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
|
+
}
|
|
851
1241
|
}
|
|
852
1242
|
|
|
853
|
-
async
|
|
854
|
-
|
|
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
|
+
}
|
|
855
1251
|
}
|
|
856
1252
|
|
|
857
|
-
async
|
|
858
|
-
|
|
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
|
+
}
|
|
859
1263
|
}
|
|
860
1264
|
|
|
861
|
-
async
|
|
862
|
-
|
|
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
|
+
}
|
|
863
1283
|
}
|
|
864
1284
|
|
|
865
|
-
async
|
|
866
|
-
|
|
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
|
+
}
|
|
867
1298
|
}
|
|
868
1299
|
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
}
|
|
871
1332
|
}
|
|
872
1333
|
|
|
873
1334
|
async blend(
|
|
874
|
-
layers: {
|
|
1335
|
+
layers: Array<{
|
|
875
1336
|
image: string | Buffer;
|
|
876
|
-
blendMode:
|
|
877
|
-
'destination-over' | 'destination-in' | 'destination-out' |
|
|
878
|
-
'destination-atop' | 'lighter' | 'copy' | 'xor' |
|
|
879
|
-
'multiply' | 'screen' | 'overlay' | 'darken' |
|
|
880
|
-
'lighten' | 'color-dodge' | 'color-burn' |
|
|
881
|
-
'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
|
|
882
|
-
'hue' | 'saturation' | 'color' | 'luminosity';
|
|
1337
|
+
blendMode: GlobalCompositeOperation;
|
|
883
1338
|
position?: { x: number; y: number };
|
|
884
1339
|
opacity?: number;
|
|
885
|
-
}
|
|
1340
|
+
}>,
|
|
886
1341
|
baseImageBuffer: Buffer,
|
|
887
|
-
defaultBlendMode:
|
|
888
|
-
'destination-over' | 'destination-in' | 'destination-out' |
|
|
889
|
-
'destination-atop' | 'lighter' | 'copy' | 'xor' |
|
|
890
|
-
'multiply' | 'screen' | 'overlay' | 'darken' |
|
|
891
|
-
'lighten' | 'color-dodge' | 'color-burn' |
|
|
892
|
-
'hard-light' | 'soft-light' | 'difference' | 'exclusion' |
|
|
893
|
-
'hue' | 'saturation' | 'color' | 'luminosity' = 'source-over'
|
|
1342
|
+
defaultBlendMode: GlobalCompositeOperation = 'source-over'
|
|
894
1343
|
): Promise<Buffer> {
|
|
895
1344
|
try {
|
|
1345
|
+
this.#validateBlendInputs(layers, baseImageBuffer);
|
|
1346
|
+
|
|
896
1347
|
const baseImage = await loadImage(baseImageBuffer);
|
|
897
1348
|
const canvas = createCanvas(baseImage.width, baseImage.height);
|
|
898
|
-
const ctx = canvas.getContext('2d');
|
|
1349
|
+
const ctx = canvas.getContext('2d') as SKRSContext2D;
|
|
1350
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
899
1351
|
|
|
900
1352
|
ctx.globalCompositeOperation = defaultBlendMode;
|
|
901
1353
|
ctx.drawImage(baseImage, 0, 0);
|
|
@@ -913,29 +1365,52 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
913
1365
|
|
|
914
1366
|
return canvas.toBuffer('image/png');
|
|
915
1367
|
} catch (error) {
|
|
916
|
-
|
|
917
|
-
throw new Error(
|
|
1368
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1369
|
+
throw new Error(`blend failed: ${errorMessage}`);
|
|
918
1370
|
}
|
|
919
1371
|
}
|
|
920
1372
|
|
|
921
1373
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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.");
|
|
926
1383
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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.");
|
|
1389
|
+
}
|
|
1390
|
+
if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
|
|
1391
|
+
throw new Error("createChart: type.chartNumber must be a positive number.");
|
|
930
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
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async createChart(data: unknown, type: { chartType: string; chartNumber: number }): Promise<Buffer> {
|
|
1400
|
+
try {
|
|
1401
|
+
this.#validateChartInputs(data, type);
|
|
931
1402
|
|
|
932
|
-
|
|
1403
|
+
const { chartType, chartNumber } = type;
|
|
933
1404
|
|
|
934
1405
|
switch (chartType.toLowerCase()) {
|
|
935
1406
|
case 'bar':
|
|
936
1407
|
switch (chartNumber) {
|
|
937
1408
|
case 1:
|
|
938
|
-
|
|
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;
|
|
939
1414
|
case 2:
|
|
940
1415
|
throw new Error('Type 2 is still under development.');
|
|
941
1416
|
default:
|
|
@@ -944,7 +1419,9 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
944
1419
|
case 'line':
|
|
945
1420
|
switch (chartNumber) {
|
|
946
1421
|
case 1:
|
|
947
|
-
|
|
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 });
|
|
948
1425
|
case 2:
|
|
949
1426
|
throw new Error('Type 2 is still under development.');
|
|
950
1427
|
default:
|
|
@@ -953,7 +1430,7 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
953
1430
|
case 'pie':
|
|
954
1431
|
switch (chartNumber) {
|
|
955
1432
|
case 1:
|
|
956
|
-
return await pieChart(data);
|
|
1433
|
+
return await pieChart(data as PieChartData);
|
|
957
1434
|
case 2:
|
|
958
1435
|
throw new Error('Type 2 is still under development.');
|
|
959
1436
|
default:
|
|
@@ -961,37 +1438,162 @@ async createGIF(gifFrames: { background: string; duration: number }[], options:
|
|
|
961
1438
|
}
|
|
962
1439
|
default:
|
|
963
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}`);
|
|
964
1445
|
}
|
|
965
1446
|
}
|
|
966
1447
|
|
|
967
1448
|
|
|
968
|
-
|
|
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> {
|
|
969
1470
|
try {
|
|
970
|
-
|
|
971
|
-
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);
|
|
972
1472
|
|
|
973
1473
|
if (options.crop === 'outer') {
|
|
974
|
-
|
|
975
|
-
} else if (options.crop === 'inner') {
|
|
976
|
-
return await cropInner(options);
|
|
1474
|
+
return await cropOuter(options);
|
|
977
1475
|
} else {
|
|
978
|
-
|
|
1476
|
+
return await cropInner(options);
|
|
979
1477
|
}
|
|
980
1478
|
} catch (error) {
|
|
981
|
-
|
|
982
|
-
throw
|
|
1479
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1480
|
+
throw new Error(`cropImage failed: ${errorMessage}`);
|
|
983
1481
|
}
|
|
984
1482
|
}
|
|
985
1483
|
|
|
986
|
-
|
|
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`);
|
|
987
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
|
+
}
|
|
988
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
|
+
}
|
|
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
|
+
}
|
|
989
1589
|
}
|
|
990
|
-
|
|
991
1590
|
|
|
992
|
-
async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions): Promise<
|
|
993
|
-
|
|
994
|
-
|
|
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');
|
|
995
1597
|
|
|
996
1598
|
if (!fs.existsSync(frameDir)) {
|
|
997
1599
|
fs.mkdirSync(frameDir);
|
|
@@ -1054,10 +1656,14 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
|
|
|
1054
1656
|
});
|
|
1055
1657
|
}
|
|
1056
1658
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
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
|
+
}
|
|
1061
1667
|
|
|
1062
1668
|
|
|
1063
1669
|
|
|
@@ -1067,13 +1673,45 @@ async extractFrames(videoSource: string | Buffer, options: ExtractFramesOptions)
|
|
|
1067
1673
|
|
|
1068
1674
|
|
|
1069
1675
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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);
|
|
1077
1715
|
|
|
1078
1716
|
const canvas = createCanvas(img.width, img.height);
|
|
1079
1717
|
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
@@ -1105,21 +1743,58 @@ async masking(
|
|
|
1105
1743
|
|
|
1106
1744
|
if (options.invert) alphaValue = 255 - alphaValue;
|
|
1107
1745
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1746
|
+
imgData.data[i + 3] = alphaValue;
|
|
1747
|
+
}
|
|
1110
1748
|
|
|
1111
|
-
|
|
1749
|
+
ctx.putImageData(imgData, 0, 0);
|
|
1112
1750
|
|
|
1113
|
-
|
|
1114
|
-
}
|
|
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
|
+
}
|
|
1115
1757
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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");
|
|
1123
1798
|
|
|
1124
1799
|
ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
1125
1800
|
|
|
@@ -1152,38 +1827,78 @@ async gradientBlend(
|
|
|
1152
1827
|
ctx.drawImage(mask, 0, 0, img.width, img.height);
|
|
1153
1828
|
}
|
|
1154
1829
|
|
|
1155
|
-
|
|
1830
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1831
|
+
|
|
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
|
+
}
|
|
1156
1838
|
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
+
}
|
|
1159
1871
|
|
|
1160
|
-
async animate(
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1872
|
+
async animate(
|
|
1873
|
+
frames: Frame[],
|
|
1874
|
+
defaultDuration: number,
|
|
1875
|
+
defaultWidth: number = 800,
|
|
1876
|
+
defaultHeight: number = 600,
|
|
1877
|
+
options?: {
|
|
1166
1878
|
gif?: boolean;
|
|
1167
1879
|
gifPath?: string;
|
|
1168
1880
|
onStart?: () => void;
|
|
1169
1881
|
onFrame?: (index: number) => void;
|
|
1170
1882
|
onEnd?: () => void;
|
|
1171
|
-
|
|
1172
|
-
): Promise<Buffer[] | undefined> {
|
|
1173
|
-
|
|
1174
|
-
|
|
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;
|
|
1175
1890
|
|
|
1176
|
-
|
|
1891
|
+
if (options?.onStart) options.onStart();
|
|
1177
1892
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1893
|
+
let encoder: GIFEncoder | null = null;
|
|
1894
|
+
let gifStream: fs.WriteStream | null = null;
|
|
1180
1895
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
throw new Error("
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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);
|
|
1187
1902
|
encoder.createReadStream().pipe(gifStream);
|
|
1188
1903
|
encoder.start();
|
|
1189
1904
|
encoder.setRepeat(0);
|
|
@@ -1288,39 +2003,366 @@ async animate(
|
|
|
1288
2003
|
encoder.finish();
|
|
1289
2004
|
}
|
|
1290
2005
|
|
|
1291
|
-
|
|
2006
|
+
if (options?.onEnd) options.onEnd();
|
|
1292
2007
|
|
|
1293
|
-
|
|
1294
|
-
}
|
|
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
|
+
}
|
|
1295
2014
|
|
|
1296
2015
|
|
|
1297
2016
|
|
|
1298
|
-
|
|
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
|
+
}
|
|
2062
|
+
|
|
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
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
2122
|
+
|
|
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
|
+
}
|
|
1299
2140
|
const hexPattern = /^#[0-9a-fA-F]{6}$/;
|
|
1300
2141
|
if (!hexPattern.test(hexColor)) {
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
return true
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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}`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* Applies stroke style to shape context
|
|
2191
|
+
* @private
|
|
2192
|
+
* @param ctx - Canvas 2D context
|
|
2193
|
+
* @param style - Stroke style type
|
|
2194
|
+
* @param width - Stroke width for calculating dash patterns
|
|
2195
|
+
*/
|
|
2196
|
+
#applyShapeStrokeStyle(
|
|
2197
|
+
ctx: SKRSContext2D,
|
|
2198
|
+
style: 'solid' | 'dashed' | 'dotted' | 'groove' | 'ridge' | 'double',
|
|
2199
|
+
width: number
|
|
2200
|
+
): void {
|
|
2201
|
+
switch (style) {
|
|
2202
|
+
case 'solid':
|
|
2203
|
+
ctx.setLineDash([]);
|
|
2204
|
+
ctx.lineCap = 'butt';
|
|
2205
|
+
ctx.lineJoin = 'miter';
|
|
2206
|
+
break;
|
|
2207
|
+
|
|
2208
|
+
case 'dashed':
|
|
2209
|
+
ctx.setLineDash([width * 3, width * 2]);
|
|
2210
|
+
ctx.lineCap = 'butt';
|
|
2211
|
+
ctx.lineJoin = 'miter';
|
|
2212
|
+
break;
|
|
2213
|
+
|
|
2214
|
+
case 'dotted':
|
|
2215
|
+
ctx.setLineDash([width, width]);
|
|
2216
|
+
ctx.lineCap = 'round';
|
|
2217
|
+
ctx.lineJoin = 'round';
|
|
2218
|
+
break;
|
|
2219
|
+
|
|
2220
|
+
case 'groove':
|
|
2221
|
+
case 'ridge':
|
|
2222
|
+
case 'double':
|
|
2223
|
+
ctx.setLineDash([]);
|
|
2224
|
+
ctx.lineCap = 'butt';
|
|
2225
|
+
ctx.lineJoin = 'miter';
|
|
2226
|
+
break;
|
|
2227
|
+
|
|
1322
2228
|
default:
|
|
1323
|
-
|
|
2229
|
+
ctx.setLineDash([]);
|
|
2230
|
+
ctx.lineCap = 'butt';
|
|
2231
|
+
ctx.lineJoin = 'miter';
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Applies complex shape stroke styles that require multiple passes
|
|
2238
|
+
* @private
|
|
2239
|
+
* @param ctx - Canvas 2D context
|
|
2240
|
+
* @param style - Complex stroke style type
|
|
2241
|
+
* @param width - Stroke width
|
|
2242
|
+
* @param color - Base stroke color
|
|
2243
|
+
* @param gradient - Optional gradient
|
|
2244
|
+
*/
|
|
2245
|
+
#applyComplexShapeStroke(
|
|
2246
|
+
ctx: SKRSContext2D,
|
|
2247
|
+
style: 'groove' | 'ridge' | 'double',
|
|
2248
|
+
width: number,
|
|
2249
|
+
color: string,
|
|
2250
|
+
gradient: any
|
|
2251
|
+
): void {
|
|
2252
|
+
const halfWidth = width / 2;
|
|
2253
|
+
|
|
2254
|
+
switch (style) {
|
|
2255
|
+
case 'groove':
|
|
2256
|
+
// Groove: dark outer, light inner
|
|
2257
|
+
ctx.lineWidth = halfWidth;
|
|
2258
|
+
|
|
2259
|
+
// Outer dark stroke
|
|
2260
|
+
if (gradient) {
|
|
2261
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2262
|
+
ctx.strokeStyle = gstroke as any;
|
|
2263
|
+
} else {
|
|
2264
|
+
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
2265
|
+
}
|
|
2266
|
+
ctx.stroke();
|
|
2267
|
+
|
|
2268
|
+
// Inner light stroke
|
|
2269
|
+
ctx.lineWidth = halfWidth;
|
|
2270
|
+
if (gradient) {
|
|
2271
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2272
|
+
ctx.strokeStyle = gstroke as any;
|
|
2273
|
+
} else {
|
|
2274
|
+
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
2275
|
+
}
|
|
2276
|
+
ctx.stroke();
|
|
2277
|
+
break;
|
|
2278
|
+
|
|
2279
|
+
case 'ridge':
|
|
2280
|
+
// Ridge: light outer, dark inner
|
|
2281
|
+
ctx.lineWidth = halfWidth;
|
|
2282
|
+
|
|
2283
|
+
// Outer light stroke
|
|
2284
|
+
if (gradient) {
|
|
2285
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2286
|
+
ctx.strokeStyle = gstroke as any;
|
|
2287
|
+
} else {
|
|
2288
|
+
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
2289
|
+
}
|
|
2290
|
+
ctx.stroke();
|
|
2291
|
+
|
|
2292
|
+
// Inner dark stroke
|
|
2293
|
+
ctx.lineWidth = halfWidth;
|
|
2294
|
+
if (gradient) {
|
|
2295
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2296
|
+
ctx.strokeStyle = gstroke as any;
|
|
2297
|
+
} else {
|
|
2298
|
+
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
2299
|
+
}
|
|
2300
|
+
ctx.stroke();
|
|
2301
|
+
break;
|
|
2302
|
+
|
|
2303
|
+
case 'double':
|
|
2304
|
+
// Double: two parallel strokes
|
|
2305
|
+
ctx.lineWidth = halfWidth;
|
|
2306
|
+
|
|
2307
|
+
// First stroke (outer)
|
|
2308
|
+
if (gradient) {
|
|
2309
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2310
|
+
ctx.strokeStyle = gstroke as any;
|
|
2311
|
+
} else {
|
|
2312
|
+
ctx.strokeStyle = color;
|
|
2313
|
+
}
|
|
2314
|
+
ctx.stroke();
|
|
2315
|
+
|
|
2316
|
+
// Second stroke (inner)
|
|
2317
|
+
ctx.lineWidth = halfWidth;
|
|
2318
|
+
if (gradient) {
|
|
2319
|
+
const gstroke = createGradientFill(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
2320
|
+
ctx.strokeStyle = gstroke as any;
|
|
2321
|
+
} else {
|
|
2322
|
+
ctx.strokeStyle = color;
|
|
2323
|
+
}
|
|
2324
|
+
ctx.stroke();
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Darkens a color by a factor
|
|
2331
|
+
* @private
|
|
2332
|
+
* @param color - Color string
|
|
2333
|
+
* @param factor - Darkening factor (0-1)
|
|
2334
|
+
* @returns Darkened color string
|
|
2335
|
+
*/
|
|
2336
|
+
#darkenColor(color: string, factor: number): string {
|
|
2337
|
+
// Simple darkening for hex colors
|
|
2338
|
+
if (color.startsWith('#')) {
|
|
2339
|
+
const hex = color.slice(1);
|
|
2340
|
+
const num = parseInt(hex, 16);
|
|
2341
|
+
const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
|
|
2342
|
+
const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
|
|
2343
|
+
const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
|
|
2344
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
2345
|
+
}
|
|
2346
|
+
return color; // Return original for non-hex colors
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* Lightens a color by a factor
|
|
2351
|
+
* @private
|
|
2352
|
+
* @param color - Color string
|
|
2353
|
+
* @param factor - Lightening factor (0-1)
|
|
2354
|
+
* @returns Lightened color string
|
|
2355
|
+
*/
|
|
2356
|
+
#lightenColor(color: string, factor: number): string {
|
|
2357
|
+
// Simple lightening for hex colors
|
|
2358
|
+
if (color.startsWith('#')) {
|
|
2359
|
+
const hex = color.slice(1);
|
|
2360
|
+
const num = parseInt(hex, 16);
|
|
2361
|
+
const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
|
|
2362
|
+
const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
|
|
2363
|
+
const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
|
|
2364
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
1324
2365
|
}
|
|
2366
|
+
return color; // Return original for non-hex colors
|
|
1325
2367
|
}
|
|
1326
2368
|
}
|