cross-image 0.2.1 → 0.2.3

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 (44) hide show
  1. package/README.md +160 -32
  2. package/esm/mod.d.ts +2 -1
  3. package/esm/mod.js +2 -1
  4. package/esm/src/formats/jpeg.d.ts +12 -1
  5. package/esm/src/formats/jpeg.js +633 -4
  6. package/esm/src/formats/png_base.d.ts +8 -0
  7. package/esm/src/formats/png_base.js +176 -3
  8. package/esm/src/formats/ppm.d.ts +50 -0
  9. package/esm/src/formats/ppm.js +242 -0
  10. package/esm/src/formats/tiff.d.ts +10 -1
  11. package/esm/src/formats/tiff.js +194 -44
  12. package/esm/src/formats/webp.d.ts +9 -2
  13. package/esm/src/formats/webp.js +211 -62
  14. package/esm/src/image.d.ts +81 -0
  15. package/esm/src/image.js +282 -5
  16. package/esm/src/types.d.ts +41 -1
  17. package/esm/src/utils/image_processing.d.ts +98 -0
  18. package/esm/src/utils/image_processing.js +440 -0
  19. package/esm/src/utils/metadata/xmp.d.ts +52 -0
  20. package/esm/src/utils/metadata/xmp.js +325 -0
  21. package/esm/src/utils/resize.d.ts +4 -0
  22. package/esm/src/utils/resize.js +74 -0
  23. package/package.json +1 -1
  24. package/script/mod.d.ts +2 -1
  25. package/script/mod.js +4 -2
  26. package/script/src/formats/jpeg.d.ts +12 -1
  27. package/script/src/formats/jpeg.js +633 -4
  28. package/script/src/formats/png_base.d.ts +8 -0
  29. package/script/src/formats/png_base.js +176 -3
  30. package/script/src/formats/ppm.d.ts +50 -0
  31. package/script/src/formats/ppm.js +246 -0
  32. package/script/src/formats/tiff.d.ts +10 -1
  33. package/script/src/formats/tiff.js +194 -44
  34. package/script/src/formats/webp.d.ts +9 -2
  35. package/script/src/formats/webp.js +211 -62
  36. package/script/src/image.d.ts +81 -0
  37. package/script/src/image.js +280 -3
  38. package/script/src/types.d.ts +41 -1
  39. package/script/src/utils/image_processing.d.ts +98 -0
  40. package/script/src/utils/image_processing.js +451 -0
  41. package/script/src/utils/metadata/xmp.d.ts +52 -0
  42. package/script/src/utils/metadata/xmp.js +333 -0
  43. package/script/src/utils/resize.d.ts +4 -0
  44. package/script/src/utils/resize.js +75 -0
@@ -131,6 +131,99 @@ export function adjustSaturation(data, amount) {
131
131
  }
132
132
  return result;
133
133
  }
134
+ /**
135
+ * Convert RGB to HSL color space
136
+ * @param r Red component (0-255)
137
+ * @param g Green component (0-255)
138
+ * @param b Blue component (0-255)
139
+ * @returns HSL values: [h (0-360), s (0-1), l (0-1)]
140
+ */
141
+ function rgbToHsl(r, g, b) {
142
+ r /= 255;
143
+ g /= 255;
144
+ b /= 255;
145
+ const max = Math.max(r, g, b);
146
+ const min = Math.min(r, g, b);
147
+ const delta = max - min;
148
+ let h = 0;
149
+ let s = 0;
150
+ const l = (max + min) / 2;
151
+ if (delta !== 0) {
152
+ s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
153
+ if (max === r) {
154
+ h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
155
+ }
156
+ else if (max === g) {
157
+ h = ((b - r) / delta + 2) / 6;
158
+ }
159
+ else {
160
+ h = ((r - g) / delta + 4) / 6;
161
+ }
162
+ }
163
+ return [h * 360, s, l];
164
+ }
165
+ /**
166
+ * Convert HSL to RGB color space
167
+ * @param h Hue (0-360)
168
+ * @param s Saturation (0-1)
169
+ * @param l Lightness (0-1)
170
+ * @returns RGB values: [r (0-255), g (0-255), b (0-255)]
171
+ */
172
+ function hslToRgb(h, s, l) {
173
+ h = h / 360;
174
+ let r, g, b;
175
+ if (s === 0) {
176
+ r = g = b = l; // Achromatic
177
+ }
178
+ else {
179
+ const hue2rgb = (p, q, t) => {
180
+ if (t < 0)
181
+ t += 1;
182
+ if (t > 1)
183
+ t -= 1;
184
+ if (t < 1 / 6)
185
+ return p + (q - p) * 6 * t;
186
+ if (t < 1 / 2)
187
+ return q;
188
+ if (t < 2 / 3)
189
+ return p + (q - p) * (2 / 3 - t) * 6;
190
+ return p;
191
+ };
192
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
193
+ const p = 2 * l - q;
194
+ r = hue2rgb(p, q, h + 1 / 3);
195
+ g = hue2rgb(p, q, h);
196
+ b = hue2rgb(p, q, h - 1 / 3);
197
+ }
198
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
199
+ }
200
+ /**
201
+ * Adjust hue of an image by rotating the hue wheel
202
+ * @param data Image data (RGBA)
203
+ * @param degrees Hue rotation in degrees (any value accepted, wraps at 360)
204
+ * @returns New image data with adjusted hue
205
+ */
206
+ export function adjustHue(data, degrees) {
207
+ const result = new Uint8Array(data.length);
208
+ // Normalize rotation to -180 to 180 range
209
+ const rotation = ((degrees % 360) + 360) % 360;
210
+ for (let i = 0; i < data.length; i += 4) {
211
+ const r = data[i];
212
+ const g = data[i + 1];
213
+ const b = data[i + 2];
214
+ // Convert to HSL
215
+ const [h, s, l] = rgbToHsl(r, g, b);
216
+ // Rotate hue
217
+ const newH = (h + rotation) % 360;
218
+ // Convert back to RGB
219
+ const [newR, newG, newB] = hslToRgb(newH, s, l);
220
+ result[i] = newR;
221
+ result[i + 1] = newG;
222
+ result[i + 2] = newB;
223
+ result[i + 3] = data[i + 3]; // Preserve alpha
224
+ }
225
+ return result;
226
+ }
134
227
  /**
135
228
  * Invert colors of an image
136
229
  * @param data Image data (RGBA)
@@ -229,3 +322,350 @@ export function crop(data, width, height, x, y, cropWidth, cropHeight) {
229
322
  }
230
323
  return { data: result, width: actualWidth, height: actualHeight };
231
324
  }
325
+ /**
326
+ * Apply a box blur filter to an image
327
+ * @param data Image data (RGBA)
328
+ * @param width Image width
329
+ * @param height Image height
330
+ * @param radius Blur radius (default: 1)
331
+ * @returns New image data with box blur applied
332
+ */
333
+ export function boxBlur(data, width, height, radius = 1) {
334
+ const result = new Uint8Array(data.length);
335
+ const clampedRadius = Math.max(1, Math.floor(radius));
336
+ for (let y = 0; y < height; y++) {
337
+ for (let x = 0; x < width; x++) {
338
+ let r = 0, g = 0, b = 0, a = 0;
339
+ let count = 0;
340
+ // Iterate over kernel
341
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
342
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
343
+ const px = x + kx;
344
+ const py = y + ky;
345
+ // Check bounds
346
+ if (px >= 0 && px < width && py >= 0 && py < height) {
347
+ const idx = (py * width + px) * 4;
348
+ r += data[idx];
349
+ g += data[idx + 1];
350
+ b += data[idx + 2];
351
+ a += data[idx + 3];
352
+ count++;
353
+ }
354
+ }
355
+ }
356
+ const outIdx = (y * width + x) * 4;
357
+ result[outIdx] = Math.round(r / count);
358
+ result[outIdx + 1] = Math.round(g / count);
359
+ result[outIdx + 2] = Math.round(b / count);
360
+ result[outIdx + 3] = Math.round(a / count);
361
+ }
362
+ }
363
+ return result;
364
+ }
365
+ /**
366
+ * Generate a Gaussian kernel for blur
367
+ * @param radius Kernel radius
368
+ * @param sigma Standard deviation (if not provided, calculated from radius)
369
+ * @returns Gaussian kernel as 1D array
370
+ */
371
+ function generateGaussianKernel(radius, sigma) {
372
+ const size = radius * 2 + 1;
373
+ const kernel = new Array(size);
374
+ const s = sigma ?? radius / 3;
375
+ const s2 = 2 * s * s;
376
+ let sum = 0;
377
+ for (let i = 0; i < size; i++) {
378
+ const x = i - radius;
379
+ kernel[i] = Math.exp(-(x * x) / s2);
380
+ sum += kernel[i];
381
+ }
382
+ // Normalize
383
+ for (let i = 0; i < size; i++) {
384
+ kernel[i] /= sum;
385
+ }
386
+ return kernel;
387
+ }
388
+ /**
389
+ * Apply Gaussian blur to an image
390
+ * @param data Image data (RGBA)
391
+ * @param width Image width
392
+ * @param height Image height
393
+ * @param radius Blur radius (default: 1)
394
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
395
+ * @returns New image data with Gaussian blur applied
396
+ */
397
+ export function gaussianBlur(data, width, height, radius = 1, sigma) {
398
+ const clampedRadius = Math.max(1, Math.floor(radius));
399
+ const kernel = generateGaussianKernel(clampedRadius, sigma);
400
+ // Apply horizontal pass
401
+ const temp = new Uint8Array(data.length);
402
+ for (let y = 0; y < height; y++) {
403
+ for (let x = 0; x < width; x++) {
404
+ let r = 0, g = 0, b = 0, a = 0;
405
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
406
+ const px = Math.max(0, Math.min(width - 1, x + kx));
407
+ const idx = (y * width + px) * 4;
408
+ const weight = kernel[kx + clampedRadius];
409
+ r += data[idx] * weight;
410
+ g += data[idx + 1] * weight;
411
+ b += data[idx + 2] * weight;
412
+ a += data[idx + 3] * weight;
413
+ }
414
+ const outIdx = (y * width + x) * 4;
415
+ temp[outIdx] = Math.round(r);
416
+ temp[outIdx + 1] = Math.round(g);
417
+ temp[outIdx + 2] = Math.round(b);
418
+ temp[outIdx + 3] = Math.round(a);
419
+ }
420
+ }
421
+ // Apply vertical pass
422
+ const result = new Uint8Array(data.length);
423
+ for (let y = 0; y < height; y++) {
424
+ for (let x = 0; x < width; x++) {
425
+ let r = 0, g = 0, b = 0, a = 0;
426
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
427
+ const py = Math.max(0, Math.min(height - 1, y + ky));
428
+ const idx = (py * width + x) * 4;
429
+ const weight = kernel[ky + clampedRadius];
430
+ r += temp[idx] * weight;
431
+ g += temp[idx + 1] * weight;
432
+ b += temp[idx + 2] * weight;
433
+ a += temp[idx + 3] * weight;
434
+ }
435
+ const outIdx = (y * width + x) * 4;
436
+ result[outIdx] = Math.round(r);
437
+ result[outIdx + 1] = Math.round(g);
438
+ result[outIdx + 2] = Math.round(b);
439
+ result[outIdx + 3] = Math.round(a);
440
+ }
441
+ }
442
+ return result;
443
+ }
444
+ /**
445
+ * Apply sharpen filter to an image
446
+ * @param data Image data (RGBA)
447
+ * @param width Image width
448
+ * @param height Image height
449
+ * @param amount Sharpening amount (0-1, default: 0.5)
450
+ * @returns New image data with sharpening applied
451
+ */
452
+ export function sharpen(data, width, height, amount = 0.5) {
453
+ const result = new Uint8Array(data.length);
454
+ const clampedAmount = Math.max(0, Math.min(1, amount));
455
+ // Sharpen kernel (Laplacian-based)
456
+ // Center weight is 1 + 4*amount, neighbors are -amount
457
+ const center = 1 + 4 * clampedAmount;
458
+ const neighbor = -clampedAmount;
459
+ for (let y = 0; y < height; y++) {
460
+ for (let x = 0; x < width; x++) {
461
+ const idx = (y * width + x) * 4;
462
+ let r = data[idx] * center;
463
+ let g = data[idx + 1] * center;
464
+ let b = data[idx + 2] * center;
465
+ // Apply kernel to neighbors (4-connected)
466
+ const neighbors = [
467
+ { dx: 0, dy: -1 }, // top
468
+ { dx: -1, dy: 0 }, // left
469
+ { dx: 1, dy: 0 }, // right
470
+ { dx: 0, dy: 1 }, // bottom
471
+ ];
472
+ for (const { dx, dy } of neighbors) {
473
+ const nx = x + dx;
474
+ const ny = y + dy;
475
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
476
+ const nIdx = (ny * width + nx) * 4;
477
+ r += data[nIdx] * neighbor;
478
+ g += data[nIdx + 1] * neighbor;
479
+ b += data[nIdx + 2] * neighbor;
480
+ }
481
+ }
482
+ result[idx] = Math.max(0, Math.min(255, Math.round(r)));
483
+ result[idx + 1] = Math.max(0, Math.min(255, Math.round(g)));
484
+ result[idx + 2] = Math.max(0, Math.min(255, Math.round(b)));
485
+ result[idx + 3] = data[idx + 3]; // Alpha unchanged
486
+ }
487
+ }
488
+ return result;
489
+ }
490
+ /**
491
+ * Apply sepia tone effect to an image
492
+ * @param data Image data (RGBA)
493
+ * @returns New image data with sepia tone applied
494
+ */
495
+ export function sepia(data) {
496
+ const result = new Uint8Array(data.length);
497
+ for (let i = 0; i < data.length; i += 4) {
498
+ const r = data[i];
499
+ const g = data[i + 1];
500
+ const b = data[i + 2];
501
+ // Sepia transformation matrix
502
+ result[i] = Math.min(255, Math.round(r * 0.393 + g * 0.769 + b * 0.189));
503
+ result[i + 1] = Math.min(255, Math.round(r * 0.349 + g * 0.686 + b * 0.168));
504
+ result[i + 2] = Math.min(255, Math.round(r * 0.272 + g * 0.534 + b * 0.131));
505
+ result[i + 3] = data[i + 3]; // Alpha unchanged
506
+ }
507
+ return result;
508
+ }
509
+ /**
510
+ * Apply median filter to reduce noise
511
+ * @param data Image data (RGBA)
512
+ * @param width Image width
513
+ * @param height Image height
514
+ * @param radius Filter radius (default: 1)
515
+ * @returns New image data with median filter applied
516
+ */
517
+ export function medianFilter(data, width, height, radius = 1) {
518
+ const result = new Uint8Array(data.length);
519
+ const clampedRadius = Math.max(1, Math.floor(radius));
520
+ for (let y = 0; y < height; y++) {
521
+ for (let x = 0; x < width; x++) {
522
+ const rValues = [];
523
+ const gValues = [];
524
+ const bValues = [];
525
+ const aValues = [];
526
+ // Collect values in kernel window
527
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
528
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
529
+ const px = x + kx;
530
+ const py = y + ky;
531
+ if (px >= 0 && px < width && py >= 0 && py < height) {
532
+ const idx = (py * width + px) * 4;
533
+ rValues.push(data[idx]);
534
+ gValues.push(data[idx + 1]);
535
+ bValues.push(data[idx + 2]);
536
+ aValues.push(data[idx + 3]);
537
+ }
538
+ }
539
+ }
540
+ // Sort and get median
541
+ rValues.sort((a, b) => a - b);
542
+ gValues.sort((a, b) => a - b);
543
+ bValues.sort((a, b) => a - b);
544
+ aValues.sort((a, b) => a - b);
545
+ const mid = Math.floor(rValues.length / 2);
546
+ const outIdx = (y * width + x) * 4;
547
+ result[outIdx] = rValues[mid];
548
+ result[outIdx + 1] = gValues[mid];
549
+ result[outIdx + 2] = bValues[mid];
550
+ result[outIdx + 3] = aValues[mid];
551
+ }
552
+ }
553
+ return result;
554
+ }
555
+ /**
556
+ * Rotate image 90 degrees clockwise
557
+ * @param data Image data (RGBA)
558
+ * @param width Image width
559
+ * @param height Image height
560
+ * @returns Rotated image data with swapped dimensions
561
+ */
562
+ export function rotate90(data, width, height) {
563
+ const result = new Uint8Array(data.length);
564
+ const newWidth = height;
565
+ const newHeight = width;
566
+ for (let y = 0; y < height; y++) {
567
+ for (let x = 0; x < width; x++) {
568
+ const srcIdx = (y * width + x) * 4;
569
+ const dstX = height - 1 - y;
570
+ const dstY = x;
571
+ const dstIdx = (dstY * newWidth + dstX) * 4;
572
+ result[dstIdx] = data[srcIdx];
573
+ result[dstIdx + 1] = data[srcIdx + 1];
574
+ result[dstIdx + 2] = data[srcIdx + 2];
575
+ result[dstIdx + 3] = data[srcIdx + 3];
576
+ }
577
+ }
578
+ return { data: result, width: newWidth, height: newHeight };
579
+ }
580
+ /**
581
+ * Rotate image 180 degrees
582
+ * @param data Image data (RGBA)
583
+ * @param width Image width
584
+ * @param height Image height
585
+ * @returns Rotated image data with same dimensions
586
+ */
587
+ export function rotate180(data, width, height) {
588
+ const result = new Uint8Array(data.length);
589
+ for (let y = 0; y < height; y++) {
590
+ for (let x = 0; x < width; x++) {
591
+ const srcIdx = (y * width + x) * 4;
592
+ const dstX = width - 1 - x;
593
+ const dstY = height - 1 - y;
594
+ const dstIdx = (dstY * width + dstX) * 4;
595
+ result[dstIdx] = data[srcIdx];
596
+ result[dstIdx + 1] = data[srcIdx + 1];
597
+ result[dstIdx + 2] = data[srcIdx + 2];
598
+ result[dstIdx + 3] = data[srcIdx + 3];
599
+ }
600
+ }
601
+ return result;
602
+ }
603
+ /**
604
+ * Rotate image 270 degrees clockwise (or 90 degrees counter-clockwise)
605
+ * @param data Image data (RGBA)
606
+ * @param width Image width
607
+ * @param height Image height
608
+ * @returns Rotated image data with swapped dimensions
609
+ */
610
+ export function rotate270(data, width, height) {
611
+ const result = new Uint8Array(data.length);
612
+ const newWidth = height;
613
+ const newHeight = width;
614
+ for (let y = 0; y < height; y++) {
615
+ for (let x = 0; x < width; x++) {
616
+ const srcIdx = (y * width + x) * 4;
617
+ const dstX = y;
618
+ const dstY = width - 1 - x;
619
+ const dstIdx = (dstY * newWidth + dstX) * 4;
620
+ result[dstIdx] = data[srcIdx];
621
+ result[dstIdx + 1] = data[srcIdx + 1];
622
+ result[dstIdx + 2] = data[srcIdx + 2];
623
+ result[dstIdx + 3] = data[srcIdx + 3];
624
+ }
625
+ }
626
+ return { data: result, width: newWidth, height: newHeight };
627
+ }
628
+ /**
629
+ * Flip image horizontally (mirror)
630
+ * @param data Image data (RGBA)
631
+ * @param width Image width
632
+ * @param height Image height
633
+ * @returns Flipped image data
634
+ */
635
+ export function flipHorizontal(data, width, height) {
636
+ const result = new Uint8Array(data.length);
637
+ for (let y = 0; y < height; y++) {
638
+ for (let x = 0; x < width; x++) {
639
+ const srcIdx = (y * width + x) * 4;
640
+ const dstX = width - 1 - x;
641
+ const dstIdx = (y * width + dstX) * 4;
642
+ result[dstIdx] = data[srcIdx];
643
+ result[dstIdx + 1] = data[srcIdx + 1];
644
+ result[dstIdx + 2] = data[srcIdx + 2];
645
+ result[dstIdx + 3] = data[srcIdx + 3];
646
+ }
647
+ }
648
+ return result;
649
+ }
650
+ /**
651
+ * Flip image vertically
652
+ * @param data Image data (RGBA)
653
+ * @param width Image width
654
+ * @param height Image height
655
+ * @returns Flipped image data
656
+ */
657
+ export function flipVertical(data, width, height) {
658
+ const result = new Uint8Array(data.length);
659
+ for (let y = 0; y < height; y++) {
660
+ for (let x = 0; x < width; x++) {
661
+ const srcIdx = (y * width + x) * 4;
662
+ const dstY = height - 1 - y;
663
+ const dstIdx = (dstY * width + x) * 4;
664
+ result[dstIdx] = data[srcIdx];
665
+ result[dstIdx + 1] = data[srcIdx + 1];
666
+ result[dstIdx + 2] = data[srcIdx + 2];
667
+ result[dstIdx + 3] = data[srcIdx + 3];
668
+ }
669
+ }
670
+ return result;
671
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * XMP (Extensible Metadata Platform) parsing and writing utilities
3
+ *
4
+ * This module provides utilities for reading and writing XMP metadata in image files.
5
+ * It supports Dublin Core, EXIF, and Photoshop namespaces.
6
+ */
7
+ import type { ImageMetadata } from "../../types.js";
8
+ /**
9
+ * XMP namespace URIs
10
+ */
11
+ export declare const XMP_NAMESPACES: {
12
+ readonly RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
13
+ readonly DC: "http://purl.org/dc/elements/1.1/";
14
+ readonly XMP: "http://ns.adobe.com/xap/1.0/";
15
+ readonly EXIF: "http://ns.adobe.com/exif/1.0/";
16
+ readonly TIFF: "http://ns.adobe.com/tiff/1.0/";
17
+ readonly PHOTOSHOP: "http://ns.adobe.com/photoshop/1.0/";
18
+ readonly XMP_RIGHTS: "http://ns.adobe.com/xap/1.0/rights/";
19
+ };
20
+ /**
21
+ * XMP Dublin Core field mapping to ImageMetadata
22
+ */
23
+ export interface XMPFieldMapping {
24
+ xmpPath: string;
25
+ metadataKey: keyof ImageMetadata;
26
+ namespace: string;
27
+ }
28
+ /**
29
+ * Supported XMP fields and their mappings
30
+ */
31
+ export declare const XMP_FIELD_MAPPINGS: XMPFieldMapping[];
32
+ /**
33
+ * Escape XML special characters
34
+ */
35
+ export declare function escapeXML(str: string): string;
36
+ /**
37
+ * Unescape XML special characters
38
+ */
39
+ export declare function unescapeXML(str: string): string;
40
+ /**
41
+ * Parse XMP metadata from XML string
42
+ */
43
+ export declare function parseXMP(xmpStr: string): Partial<ImageMetadata>;
44
+ /**
45
+ * Create XMP packet from metadata
46
+ */
47
+ export declare function createXMP(metadata: Partial<ImageMetadata>): string;
48
+ /**
49
+ * Get list of supported XMP metadata fields
50
+ */
51
+ export declare function getSupportedXMPFields(): Array<keyof ImageMetadata>;
52
+ //# sourceMappingURL=xmp.d.ts.map