apexify.js 4.9.30 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +56 -1
  2. package/dist/cjs/Canvas/ApexPainter.d.ts +96 -145
  3. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  4. package/dist/cjs/Canvas/ApexPainter.js +1247 -418
  5. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  6. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +7 -2
  7. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +1 -1
  8. package/dist/cjs/Canvas/utils/Charts/charts.js +3 -1
  9. package/dist/cjs/Canvas/utils/Charts/charts.js.map +1 -1
  10. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  11. package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  12. package/dist/cjs/Canvas/utils/Custom/advancedLines.js +263 -0
  13. package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -0
  14. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts +2 -1
  15. package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  16. package/dist/cjs/Canvas/utils/Custom/customLines.js +73 -6
  17. package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
  18. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts +17 -0
  19. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  20. package/dist/cjs/Canvas/utils/General/batchOperations.js +88 -0
  21. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -0
  22. package/dist/cjs/Canvas/utils/General/general functions.d.ts +25 -3
  23. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  24. package/dist/cjs/Canvas/utils/General/general functions.js +37 -9
  25. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  26. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts +19 -0
  27. package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  28. package/dist/cjs/Canvas/utils/General/imageCompression.js +262 -0
  29. package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -0
  30. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts +20 -0
  31. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  32. package/dist/cjs/Canvas/utils/General/imageStitching.js +227 -0
  33. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -0
  34. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts +37 -0
  35. package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  36. package/dist/cjs/Canvas/utils/Image/imageEffects.js +128 -0
  37. package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -0
  38. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts +67 -0
  39. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  40. package/dist/cjs/Canvas/utils/Image/imageMasking.js +276 -0
  41. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -0
  42. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  43. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  44. package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  45. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  46. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  47. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +233 -0
  48. package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  49. package/dist/cjs/Canvas/utils/types.d.ts +121 -0
  50. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  51. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  52. package/dist/cjs/Canvas/utils/utils.d.ts +9 -2
  53. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  54. package/dist/cjs/Canvas/utils/utils.js +32 -1
  55. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  56. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  57. package/dist/esm/Canvas/ApexPainter.d.ts +96 -145
  58. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  59. package/dist/esm/Canvas/ApexPainter.js +1247 -418
  60. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  61. package/dist/esm/Canvas/utils/Charts/charts.d.ts +7 -2
  62. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +1 -1
  63. package/dist/esm/Canvas/utils/Charts/charts.js +3 -1
  64. package/dist/esm/Canvas/utils/Charts/charts.js.map +1 -1
  65. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts +75 -0
  66. package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -0
  67. package/dist/esm/Canvas/utils/Custom/advancedLines.js +263 -0
  68. package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -0
  69. package/dist/esm/Canvas/utils/Custom/customLines.d.ts +2 -1
  70. package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
  71. package/dist/esm/Canvas/utils/Custom/customLines.js +73 -6
  72. package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
  73. package/dist/esm/Canvas/utils/General/batchOperations.d.ts +17 -0
  74. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -0
  75. package/dist/esm/Canvas/utils/General/batchOperations.js +88 -0
  76. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -0
  77. package/dist/esm/Canvas/utils/General/general functions.d.ts +25 -3
  78. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  79. package/dist/esm/Canvas/utils/General/general functions.js +37 -9
  80. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  81. package/dist/esm/Canvas/utils/General/imageCompression.d.ts +19 -0
  82. package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -0
  83. package/dist/esm/Canvas/utils/General/imageCompression.js +262 -0
  84. package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -0
  85. package/dist/esm/Canvas/utils/General/imageStitching.d.ts +20 -0
  86. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -0
  87. package/dist/esm/Canvas/utils/General/imageStitching.js +227 -0
  88. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -0
  89. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts +37 -0
  90. package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -0
  91. package/dist/esm/Canvas/utils/Image/imageEffects.js +128 -0
  92. package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -0
  93. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts +67 -0
  94. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -0
  95. package/dist/esm/Canvas/utils/Image/imageMasking.js +276 -0
  96. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -0
  97. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
  98. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +16 -8
  99. package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
  100. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts +17 -0
  101. package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -0
  102. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +233 -0
  103. package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -0
  104. package/dist/esm/Canvas/utils/types.d.ts +121 -0
  105. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  106. package/dist/esm/Canvas/utils/types.js.map +1 -1
  107. package/dist/esm/Canvas/utils/utils.d.ts +9 -2
  108. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  109. package/dist/esm/Canvas/utils/utils.js +32 -1
  110. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  111. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  112. package/lib/Canvas/ApexPainter.ts +1118 -266
  113. package/lib/Canvas/utils/Charts/charts.ts +16 -7
  114. package/lib/Canvas/utils/Custom/advancedLines.ts +335 -0
  115. package/lib/Canvas/utils/Custom/customLines.ts +84 -9
  116. package/lib/Canvas/utils/General/batchOperations.ts +103 -0
  117. package/lib/Canvas/utils/General/general functions.ts +85 -41
  118. package/lib/Canvas/utils/General/imageCompression.ts +316 -0
  119. package/lib/Canvas/utils/General/imageStitching.ts +252 -0
  120. package/lib/Canvas/utils/Image/imageEffects.ts +175 -0
  121. package/lib/Canvas/utils/Image/imageMasking.ts +335 -0
  122. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +455 -444
  123. package/lib/Canvas/utils/Texts/textPathRenderer.ts +320 -0
  124. package/lib/Canvas/utils/types.ts +121 -0
  125. package/lib/Canvas/utils/utils.ts +49 -2
  126. package/package.json +69 -34
@@ -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?: any, opacity: number = 1) {
103
+ export async function applyColorFilters(imagePath: string, gradientOptions?: string | GradientConfig, opacity: number = 1): Promise<Buffer> {
104
104
  try {
105
- let image: any;
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: any, opacity: number) {
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 }: any) => {
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 }: any) => {
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
- export async function imgEffects(imagePath: string, filters: any[]) {
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
- pixelate(ctx, filter.size, filter.x, filter.y, filter.w, filter.h);
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: any) {
248
- console.error("Error processing image:", error.message);
249
- throw new Error("Failed to process image");
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: any;
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: any;
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 any;
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: any, width: number, height: number, horizontal = false, vertical = false) {
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: any, canvas: any, degrees: number) {
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: any, value: number) {
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: any, value: number) {
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: any) {
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: any) {
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: any) {
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: any, radius: number) {
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: any, levels: number) {
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: any, size: number, startX = 0, startY = 0, width = ctx.canvas.width, height = ctx.canvas.height) {
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
+