cross-image 0.4.0 → 0.4.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.
Files changed (50) hide show
  1. package/README.md +109 -1
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +2 -0
  4. package/esm/src/formats/apng.d.ts +5 -3
  5. package/esm/src/formats/apng.js +11 -4
  6. package/esm/src/formats/avif.d.ts +2 -2
  7. package/esm/src/formats/avif.js +11 -1
  8. package/esm/src/formats/gif.d.ts +3 -3
  9. package/esm/src/formats/gif.js +4 -4
  10. package/esm/src/formats/heic.d.ts +2 -2
  11. package/esm/src/formats/heic.js +11 -1
  12. package/esm/src/formats/png.d.ts +3 -2
  13. package/esm/src/formats/png.js +8 -2
  14. package/esm/src/formats/png_base.d.ts +42 -1
  15. package/esm/src/formats/png_base.js +198 -5
  16. package/esm/src/formats/tiff.js +76 -6
  17. package/esm/src/image.d.ts +15 -0
  18. package/esm/src/image.js +29 -1
  19. package/esm/src/types.d.ts +66 -0
  20. package/esm/src/utils/base64.d.ts +32 -0
  21. package/esm/src/utils/base64.js +173 -0
  22. package/esm/src/utils/gif_encoder.d.ts +3 -1
  23. package/esm/src/utils/gif_encoder.js +4 -2
  24. package/esm/src/utils/image_processing.d.ts +31 -0
  25. package/esm/src/utils/image_processing.js +88 -0
  26. package/package.json +1 -1
  27. package/script/mod.d.ts +3 -1
  28. package/script/mod.js +11 -1
  29. package/script/src/formats/apng.d.ts +5 -3
  30. package/script/src/formats/apng.js +11 -4
  31. package/script/src/formats/avif.d.ts +2 -2
  32. package/script/src/formats/avif.js +11 -1
  33. package/script/src/formats/gif.d.ts +3 -3
  34. package/script/src/formats/gif.js +4 -4
  35. package/script/src/formats/heic.d.ts +2 -2
  36. package/script/src/formats/heic.js +11 -1
  37. package/script/src/formats/png.d.ts +3 -2
  38. package/script/src/formats/png.js +8 -2
  39. package/script/src/formats/png_base.d.ts +42 -1
  40. package/script/src/formats/png_base.js +198 -5
  41. package/script/src/formats/tiff.js +76 -6
  42. package/script/src/image.d.ts +15 -0
  43. package/script/src/image.js +28 -0
  44. package/script/src/types.d.ts +66 -0
  45. package/script/src/utils/base64.d.ts +32 -0
  46. package/script/src/utils/base64.js +179 -0
  47. package/script/src/utils/gif_encoder.d.ts +3 -1
  48. package/script/src/utils/gif_encoder.js +4 -2
  49. package/script/src/utils/image_processing.d.ts +31 -0
  50. package/script/src/utils/image_processing.js +92 -0
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Base64 utilities.
4
+ *
5
+ * Designed to work across Deno, Node.js, and Bun without external dependencies.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.encodeBase64 = encodeBase64;
9
+ exports.decodeBase64 = decodeBase64;
10
+ exports.toDataUrl = toDataUrl;
11
+ exports.parseDataUrl = parseDataUrl;
12
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
13
+ const DECODE_TABLE = (() => {
14
+ const table = new Uint8Array(256);
15
+ table.fill(0xff);
16
+ for (let i = 0; i < BASE64_ALPHABET.length; i++) {
17
+ table[BASE64_ALPHABET.charCodeAt(i)] = i;
18
+ }
19
+ return table;
20
+ })();
21
+ function stripWhitespace(input) {
22
+ // Avoid regex to keep this fast and allocation-light.
23
+ let out = "";
24
+ for (let i = 0; i < input.length; i++) {
25
+ const code = input.charCodeAt(i);
26
+ // Space, tab, CR, LF
27
+ if (code === 0x20 || code === 0x09 || code === 0x0d || code === 0x0a)
28
+ continue;
29
+ out += input[i];
30
+ }
31
+ return out;
32
+ }
33
+ function normalizeBase64(input) {
34
+ // Accept URL-safe alphabet in decode paths.
35
+ let s = stripWhitespace(input.trim());
36
+ if (s.length === 0)
37
+ return s;
38
+ // Map base64url to base64.
39
+ if (s.includes("-") || s.includes("_")) {
40
+ s = s.replaceAll("-", "+").replaceAll("_", "/");
41
+ }
42
+ const remainder = s.length % 4;
43
+ if (remainder === 1) {
44
+ throw new Error("Invalid base64: length must not be 1 (mod 4)");
45
+ }
46
+ if (remainder === 2)
47
+ s += "==";
48
+ if (remainder === 3)
49
+ s += "=";
50
+ return s;
51
+ }
52
+ /**
53
+ * Encode bytes into a standard Base64 string.
54
+ */
55
+ function encodeBase64(bytes) {
56
+ if (bytes.length === 0)
57
+ return "";
58
+ const outLen = Math.ceil(bytes.length / 3) * 4;
59
+ const out = new Array(outLen);
60
+ let i = 0;
61
+ let o = 0;
62
+ for (; i + 2 < bytes.length; i += 3) {
63
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
64
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
65
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
66
+ out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
67
+ out[o++] = BASE64_ALPHABET[n & 63];
68
+ }
69
+ const remaining = bytes.length - i;
70
+ if (remaining === 1) {
71
+ const n = bytes[i] << 16;
72
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
73
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
74
+ out[o++] = "=";
75
+ out[o++] = "=";
76
+ }
77
+ else if (remaining === 2) {
78
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
79
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
80
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
81
+ out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
82
+ out[o++] = "=";
83
+ }
84
+ return out.join("");
85
+ }
86
+ /**
87
+ * Decode a Base64 (or Base64URL) string into bytes.
88
+ *
89
+ * - Whitespace is ignored.
90
+ * - Missing padding is tolerated.
91
+ * - `-`/`_` are accepted as URL-safe variants.
92
+ */
93
+ function decodeBase64(base64) {
94
+ const s = normalizeBase64(base64);
95
+ if (s.length === 0)
96
+ return new Uint8Array(0);
97
+ let padding = 0;
98
+ if (s.endsWith("=="))
99
+ padding = 2;
100
+ else if (s.endsWith("="))
101
+ padding = 1;
102
+ const quadCount = s.length / 4;
103
+ const outLen = quadCount * 3 - padding;
104
+ const out = new Uint8Array(outLen);
105
+ let o = 0;
106
+ for (let i = 0; i < s.length; i += 4) {
107
+ const c0 = s.charCodeAt(i);
108
+ const c1 = s.charCodeAt(i + 1);
109
+ const c2 = s.charCodeAt(i + 2);
110
+ const c3 = s.charCodeAt(i + 3);
111
+ const v0 = DECODE_TABLE[c0];
112
+ const v1 = DECODE_TABLE[c1];
113
+ if (v0 === 0xff || v1 === 0xff) {
114
+ throw new Error("Invalid base64: invalid character");
115
+ }
116
+ const isPad2 = c2 === 0x3d; // '='
117
+ const isPad3 = c3 === 0x3d;
118
+ const v2 = isPad2 ? 0 : DECODE_TABLE[c2];
119
+ const v3 = isPad3 ? 0 : DECODE_TABLE[c3];
120
+ if ((!isPad2 && v2 === 0xff) || (!isPad3 && v3 === 0xff)) {
121
+ throw new Error("Invalid base64: invalid character");
122
+ }
123
+ const n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
124
+ out[o++] = (n >> 16) & 0xff;
125
+ if (!isPad2) {
126
+ if (o >= out.length)
127
+ break;
128
+ out[o++] = (n >> 8) & 0xff;
129
+ }
130
+ if (!isPad3) {
131
+ if (o >= out.length)
132
+ break;
133
+ out[o++] = n & 0xff;
134
+ }
135
+ if (isPad2 || isPad3) {
136
+ // Padding is only valid in the final quartet.
137
+ if (i + 4 !== s.length) {
138
+ throw new Error("Invalid base64: padding can only appear at the end");
139
+ }
140
+ // If third char is padding, fourth must also be padding.
141
+ if (isPad2 && !isPad3) {
142
+ throw new Error("Invalid base64: invalid padding");
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ /**
149
+ * Create a base64 data URL.
150
+ */
151
+ function toDataUrl(mime, bytes) {
152
+ if (!mime)
153
+ throw new Error("mime is required");
154
+ return `data:${mime};base64,${encodeBase64(bytes)}`;
155
+ }
156
+ /**
157
+ * Parse a base64 data URL.
158
+ *
159
+ * Supports `data:<mime>;base64,<payload>`.
160
+ */
161
+ function parseDataUrl(url) {
162
+ if (!url.startsWith("data:")) {
163
+ throw new Error("Invalid data URL: must start with 'data:'");
164
+ }
165
+ const commaIndex = url.indexOf(",");
166
+ if (commaIndex === -1) {
167
+ throw new Error("Invalid data URL: missing ',' separator");
168
+ }
169
+ const meta = url.slice(5, commaIndex);
170
+ const payload = url.slice(commaIndex + 1);
171
+ // We only support base64 payloads.
172
+ const metaParts = meta.split(";");
173
+ const mime = metaParts[0] || "application/octet-stream";
174
+ const isBase64 = metaParts.some((p) => p.toLowerCase() === "base64");
175
+ if (!isBase64) {
176
+ throw new Error("Invalid data URL: only base64 payloads are supported");
177
+ }
178
+ return { mime, bytes: decodeBase64(payload) };
179
+ }
@@ -25,6 +25,8 @@ export declare class GIFEncoder {
25
25
  private quantize;
26
26
  private nextPowerOf2;
27
27
  private getBitsPerColor;
28
- encode(): Uint8Array;
28
+ encode(options?: {
29
+ loop?: number;
30
+ }): Uint8Array;
29
31
  }
30
32
  //# sourceMappingURL=gif_encoder.d.ts.map
@@ -168,11 +168,13 @@ class GIFEncoder {
168
168
  }
169
169
  return Math.max(2, bits);
170
170
  }
171
- encode() {
171
+ encode(options) {
172
172
  if (this.frames.length === 0) {
173
173
  throw new Error("No frames to encode");
174
174
  }
175
175
  const output = [];
176
+ // Get loop count from options (default to 0 = infinite)
177
+ const loopCount = options?.loop ?? 0;
176
178
  // Quantize first frame for Global Color Table
177
179
  const firstFrame = this.frames[0];
178
180
  const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
@@ -209,7 +211,7 @@ class GIFEncoder {
209
211
  this.writeString(output, "NETSCAPE2.0");
210
212
  output.push(3); // Sub-block Size
211
213
  output.push(1); // Loop Indicator (1 = loop)
212
- this.writeUint16LE(output, 0); // Loop Count (0 = infinite)
214
+ this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
213
215
  output.push(0); // Block Terminator
214
216
  }
215
217
  // Encode frames
@@ -186,4 +186,35 @@ export declare function flipHorizontal(data: Uint8Array, width: number, height:
186
186
  * @returns Flipped image data
187
187
  */
188
188
  export declare function flipVertical(data: Uint8Array, width: number, height: number): Uint8Array;
189
+ /**
190
+ * Convert RGB color to CMYK color space
191
+ * @param r Red component (0-255)
192
+ * @param g Green component (0-255)
193
+ * @param b Blue component (0-255)
194
+ * @returns CMYK values: [c (0-1), m (0-1), y (0-1), k (0-1)]
195
+ */
196
+ export declare function rgbToCmyk(r: number, g: number, b: number): [number, number, number, number];
197
+ /**
198
+ * Convert CMYK color to RGB color space
199
+ * @param c Cyan component (0-1)
200
+ * @param m Magenta component (0-1)
201
+ * @param y Yellow component (0-1)
202
+ * @param k Key/Black component (0-1)
203
+ * @returns RGB values: [r (0-255), g (0-255), b (0-255)]
204
+ */
205
+ export declare function cmykToRgb(c: number, m: number, y: number, k: number): [number, number, number];
206
+ /**
207
+ * Convert RGBA image data to CMYK representation
208
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
209
+ * @param data Image data (RGBA)
210
+ * @returns CMYK image data as Float32Array (4 values per pixel)
211
+ */
212
+ export declare function rgbaToCmyk(data: Uint8Array): Float32Array;
213
+ /**
214
+ * Convert CMYK image data to RGBA representation
215
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
216
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
217
+ * @returns RGBA image data
218
+ */
219
+ export declare function cmykToRgba(cmykData: Float32Array, alpha?: number): Uint8Array;
189
220
  //# sourceMappingURL=image_processing.d.ts.map
@@ -24,6 +24,10 @@ exports.rotate180 = rotate180;
24
24
  exports.rotate270 = rotate270;
25
25
  exports.flipHorizontal = flipHorizontal;
26
26
  exports.flipVertical = flipVertical;
27
+ exports.rgbToCmyk = rgbToCmyk;
28
+ exports.cmykToRgb = cmykToRgb;
29
+ exports.rgbaToCmyk = rgbaToCmyk;
30
+ exports.cmykToRgba = cmykToRgba;
27
31
  /**
28
32
  * Detect system endianness
29
33
  * Returns true if little-endian (most common), false if big-endian
@@ -765,3 +769,91 @@ function flipVertical(data, width, height) {
765
769
  }
766
770
  return result;
767
771
  }
772
+ /**
773
+ * Convert RGB color to CMYK color space
774
+ * @param r Red component (0-255)
775
+ * @param g Green component (0-255)
776
+ * @param b Blue component (0-255)
777
+ * @returns CMYK values: [c (0-1), m (0-1), y (0-1), k (0-1)]
778
+ */
779
+ function rgbToCmyk(r, g, b) {
780
+ // Normalize RGB values to 0-1 range
781
+ const rNorm = r / 255;
782
+ const gNorm = g / 255;
783
+ const bNorm = b / 255;
784
+ // Calculate K (key/black)
785
+ const k = 1 - Math.max(rNorm, gNorm, bNorm);
786
+ // If K is 1 (pure black), CMY are undefined but conventionally set to 0
787
+ if (k === 1) {
788
+ return [0, 0, 0, 1];
789
+ }
790
+ // Calculate CMY components
791
+ const c = (1 - rNorm - k) / (1 - k);
792
+ const m = (1 - gNorm - k) / (1 - k);
793
+ const y = (1 - bNorm - k) / (1 - k);
794
+ return [c, m, y, k];
795
+ }
796
+ /**
797
+ * Convert CMYK color to RGB color space
798
+ * @param c Cyan component (0-1)
799
+ * @param m Magenta component (0-1)
800
+ * @param y Yellow component (0-1)
801
+ * @param k Key/Black component (0-1)
802
+ * @returns RGB values: [r (0-255), g (0-255), b (0-255)]
803
+ */
804
+ function cmykToRgb(c, m, y, k) {
805
+ // Convert CMYK to RGB
806
+ const r = 255 * (1 - c) * (1 - k);
807
+ const g = 255 * (1 - m) * (1 - k);
808
+ const b = 255 * (1 - y) * (1 - k);
809
+ return [
810
+ Math.round(Math.max(0, Math.min(255, r))),
811
+ Math.round(Math.max(0, Math.min(255, g))),
812
+ Math.round(Math.max(0, Math.min(255, b))),
813
+ ];
814
+ }
815
+ /**
816
+ * Convert RGBA image data to CMYK representation
817
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
818
+ * @param data Image data (RGBA)
819
+ * @returns CMYK image data as Float32Array (4 values per pixel)
820
+ */
821
+ function rgbaToCmyk(data) {
822
+ const pixelCount = data.length / 4;
823
+ const result = new Float32Array(pixelCount * 4);
824
+ for (let i = 0; i < data.length; i += 4) {
825
+ const r = data[i];
826
+ const g = data[i + 1];
827
+ const b = data[i + 2];
828
+ const [c, m, y, k] = rgbToCmyk(r, g, b);
829
+ const outIdx = (i / 4) * 4;
830
+ result[outIdx] = c;
831
+ result[outIdx + 1] = m;
832
+ result[outIdx + 2] = y;
833
+ result[outIdx + 3] = k;
834
+ }
835
+ return result;
836
+ }
837
+ /**
838
+ * Convert CMYK image data to RGBA representation
839
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
840
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
841
+ * @returns RGBA image data
842
+ */
843
+ function cmykToRgba(cmykData, alpha = 255) {
844
+ const pixelCount = cmykData.length / 4;
845
+ const result = new Uint8Array(pixelCount * 4);
846
+ for (let i = 0; i < cmykData.length; i += 4) {
847
+ const c = cmykData[i];
848
+ const m = cmykData[i + 1];
849
+ const y = cmykData[i + 2];
850
+ const k = cmykData[i + 3];
851
+ const [r, g, b] = cmykToRgb(c, m, y, k);
852
+ const outIdx = i;
853
+ result[outIdx] = r;
854
+ result[outIdx + 1] = g;
855
+ result[outIdx + 2] = b;
856
+ result[outIdx + 3] = alpha;
857
+ }
858
+ return result;
859
+ }