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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from 'path'
|
|
1
|
+
import path from 'path';
|
|
2
2
|
import sharp from 'sharp';
|
|
3
|
-
import { cropOptions, ResizeOptions } from '../types';
|
|
4
|
-
import { createCanvas, loadImage, SKRSContext2D } from '@napi-rs/canvas';
|
|
3
|
+
import { cropOptions, ResizeOptions, GradientConfig, gradient, ImageFilter } from '../types';
|
|
4
|
+
import { createCanvas, loadImage, SKRSContext2D, Image, Canvas } from '@napi-rs/canvas';
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import axios from "axios";
|
|
7
7
|
|
|
@@ -100,9 +100,9 @@ export async function converter(imagePath: string, newExtension: string) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export async function applyColorFilters(imagePath: string, gradientOptions?:
|
|
103
|
+
export async function applyColorFilters(imagePath: string, gradientOptions?: string | GradientConfig, opacity: number = 1): Promise<Buffer> {
|
|
104
104
|
try {
|
|
105
|
-
let image:
|
|
105
|
+
let image: sharp.Sharp;
|
|
106
106
|
|
|
107
107
|
if (imagePath.startsWith("http")) {
|
|
108
108
|
const pngBuffer = await converter(imagePath, "png");
|
|
@@ -117,8 +117,10 @@ export async function applyColorFilters(imagePath: string, gradientOptions?: any
|
|
|
117
117
|
|
|
118
118
|
if (typeof gradientOptions === 'string') {
|
|
119
119
|
gradientImage = createSolidColorImage(metadata.width, metadata.height, gradientOptions, opacity);
|
|
120
|
-
} else {
|
|
120
|
+
} else if (gradientOptions) {
|
|
121
121
|
gradientImage = createGradientImage(metadata.width, metadata.height, gradientOptions, opacity);
|
|
122
|
+
} else {
|
|
123
|
+
throw new Error("applyColorFilters: gradientOptions must be a string or GradientConfig object.");
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
const outputBuffer = await image
|
|
@@ -132,9 +134,13 @@ export async function applyColorFilters(imagePath: string, gradientOptions?: any
|
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
function createSolidColorImage(width: number, height: number, color: string, opacity: number) {
|
|
137
|
+
function createSolidColorImage(width: number | undefined, height: number | undefined, color: string, opacity: number): Buffer {
|
|
138
|
+
if (!width || !height) {
|
|
139
|
+
throw new Error("createSolidColorImage: width and height are required.");
|
|
140
|
+
}
|
|
136
141
|
const solidColorCanvas = createCanvas(width, height);
|
|
137
|
-
const ctx = solidColorCanvas.getContext('2d');
|
|
142
|
+
const ctx = solidColorCanvas.getContext('2d') as SKRSContext2D;
|
|
143
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
138
144
|
|
|
139
145
|
ctx.globalAlpha = opacity;
|
|
140
146
|
|
|
@@ -144,11 +150,15 @@ function createSolidColorImage(width: number, height: number, color: string, opa
|
|
|
144
150
|
return solidColorCanvas.toBuffer('image/png');
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
function createGradientImage(width: number, height: number, options:
|
|
153
|
+
function createGradientImage(width: number | undefined, height: number | undefined, options: GradientConfig, opacity: number): Buffer {
|
|
154
|
+
if (!width || !height) {
|
|
155
|
+
throw new Error("createGradientImage: width and height are required.");
|
|
156
|
+
}
|
|
148
157
|
const { type, colors } = options;
|
|
149
158
|
|
|
150
159
|
const gradientCanvas = createCanvas(width, height);
|
|
151
|
-
const ctx = gradientCanvas.getContext('2d');
|
|
160
|
+
const ctx = gradientCanvas.getContext('2d') as SKRSContext2D;
|
|
161
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
152
162
|
|
|
153
163
|
if (type === 'linear') {
|
|
154
164
|
const gradient = ctx.createLinearGradient(
|
|
@@ -158,7 +168,7 @@ function createGradientImage(width: number, height: number, options: any, opacit
|
|
|
158
168
|
options.endY || height
|
|
159
169
|
);
|
|
160
170
|
|
|
161
|
-
colors.forEach(({ stop, color }:
|
|
171
|
+
colors.forEach(({ stop, color }: { stop: number; color: string }) => {
|
|
162
172
|
gradient.addColorStop(stop, color);
|
|
163
173
|
});
|
|
164
174
|
|
|
@@ -173,7 +183,7 @@ function createGradientImage(width: number, height: number, options: any, opacit
|
|
|
173
183
|
options.endRadius || Math.max(width, height)
|
|
174
184
|
);
|
|
175
185
|
|
|
176
|
-
colors.forEach(({ stop, color }:
|
|
186
|
+
colors.forEach(({ stop, color }: { stop: number; color: string }) => {
|
|
177
187
|
gradient.addColorStop(stop, color);
|
|
178
188
|
});
|
|
179
189
|
|
|
@@ -189,9 +199,33 @@ function createGradientImage(width: number, height: number, options: any, opacit
|
|
|
189
199
|
|
|
190
200
|
|
|
191
201
|
|
|
192
|
-
|
|
202
|
+
// Legacy filter type for imgEffects (different from ImageFilter)
|
|
203
|
+
// This supports both legacy filter types and standard ImageFilter types
|
|
204
|
+
type LegacyImageFilter = {
|
|
205
|
+
type: 'flip' | 'rotate' | 'brightness' | 'contrast' | 'invert' | 'greyscale' | 'sepia' | 'blur' | 'posterize' | 'pixelate';
|
|
206
|
+
horizontal?: boolean;
|
|
207
|
+
vertical?: boolean;
|
|
208
|
+
deg?: number;
|
|
209
|
+
value?: number;
|
|
210
|
+
radius?: number;
|
|
211
|
+
levels?: number;
|
|
212
|
+
size?: number;
|
|
213
|
+
x?: number;
|
|
214
|
+
y?: number;
|
|
215
|
+
w?: number;
|
|
216
|
+
h?: number;
|
|
217
|
+
} | {
|
|
218
|
+
type: 'brightness' | 'contrast' | 'invert' | 'grayscale' | 'sepia' | 'posterize' | 'pixelate' | 'gaussianBlur';
|
|
219
|
+
value?: number;
|
|
220
|
+
intensity?: number;
|
|
221
|
+
radius?: number;
|
|
222
|
+
levels?: number;
|
|
223
|
+
size?: number;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export async function imgEffects(imagePath: string, filters: LegacyImageFilter[] | ImageFilter[]): Promise<Buffer> {
|
|
193
227
|
try {
|
|
194
|
-
let image;
|
|
228
|
+
let image: Image;
|
|
195
229
|
|
|
196
230
|
if (imagePath.startsWith("http")) {
|
|
197
231
|
const response = await axios.get(imagePath, { responseType: "arraybuffer" });
|
|
@@ -202,7 +236,8 @@ export async function imgEffects(imagePath: string, filters: any[]) {
|
|
|
202
236
|
}
|
|
203
237
|
|
|
204
238
|
const canvas = createCanvas(image.width, image.height);
|
|
205
|
-
const ctx = canvas.getContext("2d");
|
|
239
|
+
const ctx = canvas.getContext("2d") as SKRSContext2D;
|
|
240
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
206
241
|
|
|
207
242
|
ctx.drawImage(image, 0, 0);
|
|
208
243
|
|
|
@@ -212,13 +247,13 @@ export async function imgEffects(imagePath: string, filters: any[]) {
|
|
|
212
247
|
flipCanvas(ctx, image.width, image.height, filter.horizontal, filter.vertical);
|
|
213
248
|
break;
|
|
214
249
|
case "rotate":
|
|
215
|
-
rotateCanvas(ctx, canvas, filter.deg);
|
|
250
|
+
rotateCanvas(ctx, canvas, filter.deg ?? 0);
|
|
216
251
|
break;
|
|
217
252
|
case "brightness":
|
|
218
|
-
adjustBrightness(ctx, filter.value);
|
|
253
|
+
adjustBrightness(ctx, filter.value ?? 0);
|
|
219
254
|
break;
|
|
220
255
|
case "contrast":
|
|
221
|
-
adjustContrast(ctx, filter.value);
|
|
256
|
+
adjustContrast(ctx, filter.value ?? 0);
|
|
222
257
|
break;
|
|
223
258
|
case "invert":
|
|
224
259
|
invertColors(ctx);
|
|
@@ -230,13 +265,19 @@ export async function imgEffects(imagePath: string, filters: any[]) {
|
|
|
230
265
|
applySepia(ctx);
|
|
231
266
|
break;
|
|
232
267
|
case "blur":
|
|
233
|
-
applyBlur(ctx, filter.radius);
|
|
268
|
+
applyBlur(ctx, filter.radius ?? 0);
|
|
234
269
|
break;
|
|
235
270
|
case "posterize":
|
|
236
|
-
posterize(ctx, filter.levels);
|
|
271
|
+
posterize(ctx, filter.levels ?? 4);
|
|
237
272
|
break;
|
|
238
273
|
case "pixelate":
|
|
239
|
-
|
|
274
|
+
if ('x' in filter && 'y' in filter && 'w' in filter && 'h' in filter) {
|
|
275
|
+
// Legacy filter with x, y, w, h properties
|
|
276
|
+
pixelate(ctx, filter.size ?? 10, filter.x ?? 0, filter.y ?? 0, filter.w ?? image.width, filter.h ?? image.height);
|
|
277
|
+
} else {
|
|
278
|
+
// Standard ImageFilter - use default values
|
|
279
|
+
pixelate(ctx, filter.size ?? 10, 0, 0, image.width, image.height);
|
|
280
|
+
}
|
|
240
281
|
break;
|
|
241
282
|
default:
|
|
242
283
|
console.error(`Unsupported filter type: ${filter.type}`);
|
|
@@ -244,9 +285,9 @@ export async function imgEffects(imagePath: string, filters: any[]) {
|
|
|
244
285
|
}
|
|
245
286
|
|
|
246
287
|
return canvas.toBuffer("image/png");
|
|
247
|
-
} catch (error
|
|
248
|
-
|
|
249
|
-
throw new Error(
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
290
|
+
throw new Error(`imgEffects failed: ${errorMessage}`);
|
|
250
291
|
}
|
|
251
292
|
}
|
|
252
293
|
|
|
@@ -258,7 +299,7 @@ export async function imgEffects(imagePath: string, filters: any[]) {
|
|
|
258
299
|
export async function cropInner(options: cropOptions): Promise<Buffer> {
|
|
259
300
|
try {
|
|
260
301
|
// Load the image (from HTTP or local path)
|
|
261
|
-
let image;
|
|
302
|
+
let image: Image;
|
|
262
303
|
if (options.imageSource.startsWith("http")) {
|
|
263
304
|
image = await loadImage(options.imageSource);
|
|
264
305
|
} else {
|
|
@@ -327,7 +368,7 @@ export async function cropInner(options: cropOptions): Promise<Buffer> {
|
|
|
327
368
|
*/
|
|
328
369
|
export async function cropOuter(options: cropOptions): Promise<Buffer> {
|
|
329
370
|
try {
|
|
330
|
-
let image;
|
|
371
|
+
let image: Image;
|
|
331
372
|
if (options.imageSource.startsWith("http")) {
|
|
332
373
|
image = await loadImage(options.imageSource);
|
|
333
374
|
} else {
|
|
@@ -380,7 +421,7 @@ export async function cropOuter(options: cropOptions): Promise<Buffer> {
|
|
|
380
421
|
*/
|
|
381
422
|
export async function detectColors(imagePath: string): Promise<{ color: string; frequency: string }[]> {
|
|
382
423
|
try {
|
|
383
|
-
let image:
|
|
424
|
+
let image: Image;
|
|
384
425
|
|
|
385
426
|
if (imagePath.startsWith('http')) {
|
|
386
427
|
const response = await fetch(imagePath);
|
|
@@ -393,7 +434,8 @@ export async function detectColors(imagePath: string): Promise<{ color: string;
|
|
|
393
434
|
}
|
|
394
435
|
|
|
395
436
|
const canvas = createCanvas(image.width, image.height);
|
|
396
|
-
const ctx = canvas.getContext('2d');
|
|
437
|
+
const ctx = canvas.getContext('2d') as SKRSContext2D;
|
|
438
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
397
439
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
|
398
440
|
|
|
399
441
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
@@ -427,7 +469,7 @@ export async function detectColors(imagePath: string): Promise<{ color: string;
|
|
|
427
469
|
|
|
428
470
|
export async function removeColor(inputImagePath: string, colorToRemove: { red: number; green: number; blue: number }): Promise<Buffer | undefined> {
|
|
429
471
|
try {
|
|
430
|
-
let image:
|
|
472
|
+
let image: Image;
|
|
431
473
|
if (inputImagePath.startsWith('http')) {
|
|
432
474
|
const response = await fetch(inputImagePath);
|
|
433
475
|
if (!response.ok) {
|
|
@@ -441,7 +483,8 @@ export async function removeColor(inputImagePath: string, colorToRemove: { red:
|
|
|
441
483
|
}
|
|
442
484
|
|
|
443
485
|
const canvas = createCanvas(image.width, image.height);
|
|
444
|
-
const ctx = canvas.getContext('2d') as
|
|
486
|
+
const ctx = canvas.getContext('2d') as SKRSContext2D;
|
|
487
|
+
if (!ctx) throw new Error("Unable to get 2D context");
|
|
445
488
|
|
|
446
489
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
|
447
490
|
|
|
@@ -498,7 +541,7 @@ export async function bgRemoval(imgURL: string, API_KEY: string): Promise<Buffer
|
|
|
498
541
|
}
|
|
499
542
|
|
|
500
543
|
|
|
501
|
-
function flipCanvas(ctx:
|
|
544
|
+
function flipCanvas(ctx: SKRSContext2D, width: number, height: number, horizontal = false, vertical = false): void {
|
|
502
545
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
503
546
|
const pixels = imageData.data;
|
|
504
547
|
|
|
@@ -521,10 +564,11 @@ function flipCanvas(ctx: any, width: number, height: number, horizontal = false,
|
|
|
521
564
|
ctx.putImageData(new ImageData(newData, width, height), 0, 0);
|
|
522
565
|
}
|
|
523
566
|
|
|
524
|
-
function rotateCanvas(ctx:
|
|
567
|
+
function rotateCanvas(ctx: SKRSContext2D, canvas: Canvas, degrees: number): void {
|
|
525
568
|
const radians = (degrees * Math.PI) / 180;
|
|
526
569
|
const newCanvas = createCanvas(canvas.width, canvas.height);
|
|
527
|
-
const newCtx = newCanvas.getContext("2d");
|
|
570
|
+
const newCtx = newCanvas.getContext("2d") as SKRSContext2D;
|
|
571
|
+
if (!newCtx) throw new Error("Unable to get 2D context");
|
|
528
572
|
|
|
529
573
|
newCtx.translate(canvas.width / 2, canvas.height / 2);
|
|
530
574
|
newCtx.rotate(radians);
|
|
@@ -533,7 +577,7 @@ function rotateCanvas(ctx: any, canvas: any, degrees: number) {
|
|
|
533
577
|
ctx.drawImage(newCanvas, 0, 0);
|
|
534
578
|
}
|
|
535
579
|
|
|
536
|
-
function adjustBrightness(ctx:
|
|
580
|
+
function adjustBrightness(ctx: SKRSContext2D, value: number): void {
|
|
537
581
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
538
582
|
const pixels = imageData.data;
|
|
539
583
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
@@ -544,7 +588,7 @@ function adjustBrightness(ctx: any, value: number) {
|
|
|
544
588
|
ctx.putImageData(imageData, 0, 0);
|
|
545
589
|
}
|
|
546
590
|
|
|
547
|
-
function adjustContrast(ctx:
|
|
591
|
+
function adjustContrast(ctx: SKRSContext2D, value: number): void {
|
|
548
592
|
const factor = (259 * (value + 255)) / (255 * (259 - value));
|
|
549
593
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
550
594
|
const pixels = imageData.data;
|
|
@@ -556,7 +600,7 @@ function adjustContrast(ctx: any, value: number) {
|
|
|
556
600
|
ctx.putImageData(imageData, 0, 0);
|
|
557
601
|
}
|
|
558
602
|
|
|
559
|
-
function invertColors(ctx:
|
|
603
|
+
function invertColors(ctx: SKRSContext2D): void {
|
|
560
604
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
561
605
|
const pixels = imageData.data;
|
|
562
606
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
@@ -567,7 +611,7 @@ function invertColors(ctx: any) {
|
|
|
567
611
|
ctx.putImageData(imageData, 0, 0);
|
|
568
612
|
}
|
|
569
613
|
|
|
570
|
-
function grayscale(ctx:
|
|
614
|
+
function grayscale(ctx: SKRSContext2D): void {
|
|
571
615
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
572
616
|
const pixels = imageData.data;
|
|
573
617
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
@@ -577,7 +621,7 @@ function grayscale(ctx: any) {
|
|
|
577
621
|
ctx.putImageData(imageData, 0, 0);
|
|
578
622
|
}
|
|
579
623
|
|
|
580
|
-
function applySepia(ctx:
|
|
624
|
+
function applySepia(ctx: SKRSContext2D): void {
|
|
581
625
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
582
626
|
const pixels = imageData.data;
|
|
583
627
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
@@ -593,7 +637,7 @@ function applySepia(ctx: any) {
|
|
|
593
637
|
}
|
|
594
638
|
|
|
595
639
|
|
|
596
|
-
function applyBlur(ctx:
|
|
640
|
+
function applyBlur(ctx: SKRSContext2D, radius: number): void {
|
|
597
641
|
if (radius <= 0) return;
|
|
598
642
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
599
643
|
const pixels = imageData.data;
|
|
@@ -625,7 +669,7 @@ function applyBlur(ctx: any, radius: number) {
|
|
|
625
669
|
ctx.putImageData(imageData, 0, 0);
|
|
626
670
|
}
|
|
627
671
|
|
|
628
|
-
function posterize(ctx:
|
|
672
|
+
function posterize(ctx: SKRSContext2D, levels: number): void {
|
|
629
673
|
if (levels < 2 || levels > 255) return;
|
|
630
674
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
631
675
|
const pixels = imageData.data;
|
|
@@ -640,7 +684,7 @@ function posterize(ctx: any, levels: number) {
|
|
|
640
684
|
ctx.putImageData(imageData, 0, 0);
|
|
641
685
|
}
|
|
642
686
|
|
|
643
|
-
function pixelate(ctx:
|
|
687
|
+
function pixelate(ctx: SKRSContext2D, size: number, startX = 0, startY = 0, width = ctx.canvas.width, height = ctx.canvas.height): void {
|
|
644
688
|
if (size < 1) return;
|
|
645
689
|
const imageData = ctx.getImageData(startX, startY, width, height);
|
|
646
690
|
const pixels = imageData.data;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CompressionOptions, PaletteOptions } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compresses an image with quality control
|
|
7
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
8
|
+
* @param options - Compression options
|
|
9
|
+
* @returns Compressed image buffer
|
|
10
|
+
*/
|
|
11
|
+
export async function compressImage(
|
|
12
|
+
image: string | Buffer,
|
|
13
|
+
options: CompressionOptions = {}
|
|
14
|
+
): Promise<Buffer> {
|
|
15
|
+
const {
|
|
16
|
+
quality = 90,
|
|
17
|
+
format = 'jpeg',
|
|
18
|
+
maxWidth,
|
|
19
|
+
maxHeight,
|
|
20
|
+
progressive = false
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
let sharpImage: sharp.Sharp;
|
|
24
|
+
|
|
25
|
+
if (Buffer.isBuffer(image)) {
|
|
26
|
+
sharpImage = sharp(image);
|
|
27
|
+
} else if (typeof image === 'string' && image.startsWith('http')) {
|
|
28
|
+
const response = await fetch(image);
|
|
29
|
+
const buffer = await response.arrayBuffer();
|
|
30
|
+
sharpImage = sharp(Buffer.from(buffer));
|
|
31
|
+
} else {
|
|
32
|
+
const imagePath = path.join(process.cwd(), image);
|
|
33
|
+
sharpImage = sharp(imagePath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resize if needed
|
|
37
|
+
if (maxWidth || maxHeight) {
|
|
38
|
+
sharpImage = sharpImage.resize(maxWidth, maxHeight, {
|
|
39
|
+
fit: 'inside',
|
|
40
|
+
withoutEnlargement: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Convert and compress
|
|
45
|
+
switch (format) {
|
|
46
|
+
case 'jpeg':
|
|
47
|
+
return await sharpImage
|
|
48
|
+
.jpeg({ quality, progressive })
|
|
49
|
+
.toBuffer();
|
|
50
|
+
|
|
51
|
+
case 'webp':
|
|
52
|
+
return await sharpImage
|
|
53
|
+
.webp({ quality })
|
|
54
|
+
.toBuffer();
|
|
55
|
+
|
|
56
|
+
case 'avif':
|
|
57
|
+
return await sharpImage
|
|
58
|
+
.avif({ quality })
|
|
59
|
+
.toBuffer();
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
return await sharpImage
|
|
63
|
+
.jpeg({ quality, progressive })
|
|
64
|
+
.toBuffer();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extracts color palette from an image
|
|
70
|
+
* @param image - Image source (path, URL, or Buffer)
|
|
71
|
+
* @param options - Palette extraction options
|
|
72
|
+
* @returns Array of colors with percentages
|
|
73
|
+
*/
|
|
74
|
+
export async function extractPalette(
|
|
75
|
+
image: string | Buffer,
|
|
76
|
+
options: PaletteOptions = {}
|
|
77
|
+
): Promise<Array<{ color: string; percentage: number }>> {
|
|
78
|
+
const {
|
|
79
|
+
count = 10,
|
|
80
|
+
method = 'kmeans',
|
|
81
|
+
format = 'hex'
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
let sharpImage: sharp.Sharp;
|
|
85
|
+
|
|
86
|
+
if (Buffer.isBuffer(image)) {
|
|
87
|
+
sharpImage = sharp(image);
|
|
88
|
+
} else if (typeof image === 'string' && image.startsWith('http')) {
|
|
89
|
+
const response = await fetch(image);
|
|
90
|
+
const buffer = await response.arrayBuffer();
|
|
91
|
+
sharpImage = sharp(Buffer.from(buffer));
|
|
92
|
+
} else {
|
|
93
|
+
const imagePath = path.join(process.cwd(), image);
|
|
94
|
+
sharpImage = sharp(imagePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resize for faster processing
|
|
98
|
+
const { data, info } = await sharpImage
|
|
99
|
+
.resize(200, 200, { fit: 'inside' })
|
|
100
|
+
.raw()
|
|
101
|
+
.toBuffer({ resolveWithObject: true });
|
|
102
|
+
|
|
103
|
+
const pixels: Array<{ r: number; g: number; b: number }> = [];
|
|
104
|
+
for (let i = 0; i < data.length; i += info.channels) {
|
|
105
|
+
pixels.push({
|
|
106
|
+
r: data[i],
|
|
107
|
+
g: data[i + 1],
|
|
108
|
+
b: data[i + 2]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract colors based on method
|
|
113
|
+
let colors: Array<{ r: number; g: number; b: number; count: number }> = [];
|
|
114
|
+
|
|
115
|
+
if (method === 'median-cut') {
|
|
116
|
+
colors = medianCut(pixels, count);
|
|
117
|
+
} else if (method === 'octree') {
|
|
118
|
+
colors = octreeQuantization(pixels, count);
|
|
119
|
+
} else {
|
|
120
|
+
// kmeans (default)
|
|
121
|
+
colors = kmeansClustering(pixels, count);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Convert to requested format and calculate percentages
|
|
125
|
+
const totalPixels = pixels.length;
|
|
126
|
+
const palette = colors.map(color => {
|
|
127
|
+
let colorString: string;
|
|
128
|
+
|
|
129
|
+
if (format === 'hex') {
|
|
130
|
+
colorString = `#${[color.r, color.g, color.b].map(c =>
|
|
131
|
+
c.toString(16).padStart(2, '0')
|
|
132
|
+
).join('')}`;
|
|
133
|
+
} else if (format === 'rgb') {
|
|
134
|
+
colorString = `rgb(${color.r}, ${color.g}, ${color.b})`;
|
|
135
|
+
} else {
|
|
136
|
+
// hsl
|
|
137
|
+
const hsl = rgbToHsl(color.r, color.g, color.b);
|
|
138
|
+
colorString = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
color: colorString,
|
|
143
|
+
percentage: (color.count / totalPixels) * 100
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Sort by percentage descending
|
|
148
|
+
return palette.sort((a, b) => b.percentage - a.percentage);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* K-means clustering for color extraction
|
|
153
|
+
*/
|
|
154
|
+
function kmeansClustering(
|
|
155
|
+
pixels: Array<{ r: number; g: number; b: number }>,
|
|
156
|
+
k: number
|
|
157
|
+
): Array<{ r: number; g: number; b: number; count: number }> {
|
|
158
|
+
// Initialize centroids randomly
|
|
159
|
+
const centroids: Array<{ r: number; g: number; b: number }> = [];
|
|
160
|
+
for (let i = 0; i < k; i++) {
|
|
161
|
+
const randomPixel = pixels[Math.floor(Math.random() * pixels.length)];
|
|
162
|
+
centroids.push({ r: randomPixel.r, g: randomPixel.g, b: randomPixel.b });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Iterate
|
|
166
|
+
for (let iter = 0; iter < 10; iter++) {
|
|
167
|
+
const clusters: Array<Array<{ r: number; g: number; b: number }>> =
|
|
168
|
+
new Array(k).fill(null).map(() => []);
|
|
169
|
+
|
|
170
|
+
// Assign pixels to nearest centroid
|
|
171
|
+
for (const pixel of pixels) {
|
|
172
|
+
let minDist = Infinity;
|
|
173
|
+
let nearestCluster = 0;
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < centroids.length; i++) {
|
|
176
|
+
const dist = Math.sqrt(
|
|
177
|
+
Math.pow(pixel.r - centroids[i].r, 2) +
|
|
178
|
+
Math.pow(pixel.g - centroids[i].g, 2) +
|
|
179
|
+
Math.pow(pixel.b - centroids[i].b, 2)
|
|
180
|
+
);
|
|
181
|
+
if (dist < minDist) {
|
|
182
|
+
minDist = dist;
|
|
183
|
+
nearestCluster = i;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
clusters[nearestCluster].push(pixel);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Update centroids
|
|
190
|
+
for (let i = 0; i < k; i++) {
|
|
191
|
+
if (clusters[i].length > 0) {
|
|
192
|
+
const avgR = clusters[i].reduce((sum, p) => sum + p.r, 0) / clusters[i].length;
|
|
193
|
+
const avgG = clusters[i].reduce((sum, p) => sum + p.g, 0) / clusters[i].length;
|
|
194
|
+
const avgB = clusters[i].reduce((sum, p) => sum + p.b, 0) / clusters[i].length;
|
|
195
|
+
centroids[i] = { r: Math.round(avgR), g: Math.round(avgG), b: Math.round(avgB) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Count pixels in each cluster
|
|
201
|
+
const counts: number[] = new Array(k).fill(0);
|
|
202
|
+
for (const pixel of pixels) {
|
|
203
|
+
let minDist = Infinity;
|
|
204
|
+
let nearestCluster = 0;
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < centroids.length; i++) {
|
|
207
|
+
const dist = Math.sqrt(
|
|
208
|
+
Math.pow(pixel.r - centroids[i].r, 2) +
|
|
209
|
+
Math.pow(pixel.g - centroids[i].g, 2) +
|
|
210
|
+
Math.pow(pixel.b - centroids[i].b, 2)
|
|
211
|
+
);
|
|
212
|
+
if (dist < minDist) {
|
|
213
|
+
minDist = dist;
|
|
214
|
+
nearestCluster = i;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
counts[nearestCluster]++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return centroids.map((centroid, i) => ({
|
|
221
|
+
...centroid,
|
|
222
|
+
count: counts[i]
|
|
223
|
+
})).filter(c => c.count > 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Median cut algorithm for color extraction
|
|
228
|
+
*/
|
|
229
|
+
function medianCut(
|
|
230
|
+
pixels: Array<{ r: number; g: number; b: number }>,
|
|
231
|
+
count: number
|
|
232
|
+
): Array<{ r: number; g: number; b: number; count: number }> {
|
|
233
|
+
// Simplified median cut - divide color space
|
|
234
|
+
const buckets: Array<Array<{ r: number; g: number; b: number }>> = [pixels];
|
|
235
|
+
|
|
236
|
+
while (buckets.length < count && buckets.length < 8) {
|
|
237
|
+
const largestBucket = buckets.reduce((max, bucket, i) =>
|
|
238
|
+
bucket.length > buckets[max].length ? i : max, 0
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const bucket = buckets[largestBucket];
|
|
242
|
+
if (bucket.length <= 1) break;
|
|
243
|
+
|
|
244
|
+
// Find color channel with largest range
|
|
245
|
+
const ranges = {
|
|
246
|
+
r: Math.max(...bucket.map(p => p.r)) - Math.min(...bucket.map(p => p.r)),
|
|
247
|
+
g: Math.max(...bucket.map(p => p.g)) - Math.min(...bucket.map(p => p.g)),
|
|
248
|
+
b: Math.max(...bucket.map(p => p.b)) - Math.min(...bucket.map(p => p.b))
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const channel = ranges.r > ranges.g && ranges.r > ranges.b ? 'r' :
|
|
252
|
+
ranges.g > ranges.b ? 'g' : 'b';
|
|
253
|
+
|
|
254
|
+
// Sort by channel and split at median
|
|
255
|
+
bucket.sort((a, b) => a[channel] - b[channel]);
|
|
256
|
+
const median = Math.floor(bucket.length / 2);
|
|
257
|
+
|
|
258
|
+
buckets.splice(largestBucket, 1, bucket.slice(0, median), bucket.slice(median));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Calculate average color for each bucket
|
|
262
|
+
return buckets.map(bucket => {
|
|
263
|
+
const avgR = Math.round(bucket.reduce((sum, p) => sum + p.r, 0) / bucket.length);
|
|
264
|
+
const avgG = Math.round(bucket.reduce((sum, p) => sum + p.g, 0) / bucket.length);
|
|
265
|
+
const avgB = Math.round(bucket.reduce((sum, p) => sum + p.b, 0) / bucket.length);
|
|
266
|
+
return {
|
|
267
|
+
r: avgR,
|
|
268
|
+
g: avgG,
|
|
269
|
+
b: avgB,
|
|
270
|
+
count: bucket.length
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Octree quantization (simplified)
|
|
277
|
+
*/
|
|
278
|
+
function octreeQuantization(
|
|
279
|
+
pixels: Array<{ r: number; g: number; b: number }>,
|
|
280
|
+
count: number
|
|
281
|
+
): Array<{ r: number; g: number; b: number; count: number }> {
|
|
282
|
+
// Simplified octree - use kmeans as fallback
|
|
283
|
+
return kmeansClustering(pixels, count);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Converts RGB to HSL
|
|
288
|
+
*/
|
|
289
|
+
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
|
290
|
+
r /= 255;
|
|
291
|
+
g /= 255;
|
|
292
|
+
b /= 255;
|
|
293
|
+
|
|
294
|
+
const max = Math.max(r, g, b);
|
|
295
|
+
const min = Math.min(r, g, b);
|
|
296
|
+
let h = 0, s = 0;
|
|
297
|
+
const l = (max + min) / 2;
|
|
298
|
+
|
|
299
|
+
if (max !== min) {
|
|
300
|
+
const d = max - min;
|
|
301
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
302
|
+
|
|
303
|
+
switch (max) {
|
|
304
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
305
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
306
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
h: Math.round(h * 360),
|
|
312
|
+
s: Math.round(s * 100),
|
|
313
|
+
l: Math.round(l * 100)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|