cross-image 0.2.0 → 0.2.2

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 (62) hide show
  1. package/README.md +41 -28
  2. package/esm/mod.d.ts +4 -1
  3. package/esm/mod.js +4 -1
  4. package/esm/src/formats/apng.d.ts +50 -0
  5. package/esm/src/formats/apng.js +364 -0
  6. package/esm/src/formats/bmp.d.ts +0 -6
  7. package/esm/src/formats/bmp.js +24 -47
  8. package/esm/src/formats/dng.js +4 -4
  9. package/esm/src/formats/gif.d.ts +0 -2
  10. package/esm/src/formats/gif.js +10 -16
  11. package/esm/src/formats/ico.d.ts +41 -0
  12. package/esm/src/formats/ico.js +214 -0
  13. package/esm/src/formats/pcx.js +1 -1
  14. package/esm/src/formats/png.d.ts +2 -21
  15. package/esm/src/formats/png.js +5 -429
  16. package/esm/src/formats/png_base.d.ts +108 -0
  17. package/esm/src/formats/png_base.js +487 -0
  18. package/esm/src/formats/ppm.d.ts +50 -0
  19. package/esm/src/formats/ppm.js +242 -0
  20. package/esm/src/formats/tiff.d.ts +4 -0
  21. package/esm/src/formats/tiff.js +163 -44
  22. package/esm/src/formats/webp.d.ts +0 -1
  23. package/esm/src/formats/webp.js +4 -7
  24. package/esm/src/image.d.ts +30 -0
  25. package/esm/src/image.js +62 -1
  26. package/esm/src/utils/byte_utils.d.ts +30 -0
  27. package/esm/src/utils/byte_utils.js +50 -0
  28. package/esm/src/utils/gif_encoder.d.ts +3 -2
  29. package/esm/src/utils/gif_encoder.js +115 -48
  30. package/esm/src/utils/image_processing.d.ts +43 -0
  31. package/esm/src/utils/image_processing.js +230 -0
  32. package/package.json +1 -1
  33. package/script/mod.d.ts +4 -1
  34. package/script/mod.js +8 -2
  35. package/script/src/formats/apng.d.ts +50 -0
  36. package/script/src/formats/apng.js +368 -0
  37. package/script/src/formats/bmp.d.ts +0 -6
  38. package/script/src/formats/bmp.js +24 -47
  39. package/script/src/formats/dng.js +4 -4
  40. package/script/src/formats/gif.d.ts +0 -2
  41. package/script/src/formats/gif.js +10 -16
  42. package/script/src/formats/ico.d.ts +41 -0
  43. package/script/src/formats/ico.js +218 -0
  44. package/script/src/formats/pcx.js +1 -1
  45. package/script/src/formats/png.d.ts +2 -21
  46. package/script/src/formats/png.js +5 -429
  47. package/script/src/formats/png_base.d.ts +108 -0
  48. package/script/src/formats/png_base.js +491 -0
  49. package/script/src/formats/ppm.d.ts +50 -0
  50. package/script/src/formats/ppm.js +246 -0
  51. package/script/src/formats/tiff.d.ts +4 -0
  52. package/script/src/formats/tiff.js +163 -44
  53. package/script/src/formats/webp.d.ts +0 -1
  54. package/script/src/formats/webp.js +4 -7
  55. package/script/src/image.d.ts +30 -0
  56. package/script/src/image.js +61 -0
  57. package/script/src/utils/byte_utils.d.ts +30 -0
  58. package/script/src/utils/byte_utils.js +58 -0
  59. package/script/src/utils/gif_encoder.d.ts +3 -2
  60. package/script/src/utils/gif_encoder.js +115 -48
  61. package/script/src/utils/image_processing.d.ts +43 -0
  62. package/script/src/utils/image_processing.js +235 -0
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PPMFormat = void 0;
4
+ const security_js_1 = require("../utils/security.js");
5
+ /**
6
+ * PPM format handler
7
+ * Implements the Netpbm PPM (Portable PixMap) format.
8
+ * This is a simple uncompressed RGB format supported by many image tools.
9
+ *
10
+ * Format structure:
11
+ * - P3 (ASCII format):
12
+ * P3
13
+ * <width> <height>
14
+ * <maxval>
15
+ * R G B R G B ... (space-separated decimal values)
16
+ *
17
+ * - P6 (Binary format):
18
+ * P6
19
+ * <width> <height>
20
+ * <maxval>
21
+ * RGB RGB RGB ... (binary byte data)
22
+ */
23
+ class PPMFormat {
24
+ constructor() {
25
+ /** Format name identifier */
26
+ Object.defineProperty(this, "name", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: "ppm"
31
+ });
32
+ /** MIME type for PPM images */
33
+ Object.defineProperty(this, "mimeType", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: "image/x-portable-pixmap"
38
+ });
39
+ }
40
+ /**
41
+ * Check if the given data is a PPM image
42
+ * @param data Raw image data to check
43
+ * @returns true if data has PPM signature (P3 or P6)
44
+ */
45
+ canDecode(data) {
46
+ // Check if data has at least magic bytes
47
+ if (data.length < 3) {
48
+ return false;
49
+ }
50
+ // Check for P3 or P6 followed by whitespace
51
+ return data[0] === 0x50 && // P
52
+ (data[1] === 0x33 || data[1] === 0x36) && // 3 or 6
53
+ (data[2] === 0x0a || data[2] === 0x0d || data[2] === 0x20 ||
54
+ data[2] === 0x09); // \n, \r, space, or tab
55
+ }
56
+ /**
57
+ * Decode PPM image data to RGBA
58
+ * Supports both P3 (ASCII) and P6 (binary) formats
59
+ * @param data Raw PPM image data
60
+ * @returns Decoded image data with RGBA pixels
61
+ */
62
+ decode(data) {
63
+ if (!this.canDecode(data)) {
64
+ throw new Error("Invalid PPM signature");
65
+ }
66
+ const isBinary = data[1] === 0x36; // P6
67
+ // Parse header
68
+ let offset = 0;
69
+ let width = 0;
70
+ let height = 0;
71
+ let maxval = 0;
72
+ let headerValues = 0; // Track how many values we've parsed (need 3: width, height, maxval)
73
+ // Skip magic number and whitespace
74
+ offset = 2;
75
+ while (offset < data.length && this.isWhitespace(data[offset])) {
76
+ offset++;
77
+ }
78
+ // Parse header values (width, height, maxval)
79
+ while (headerValues < 3 && offset < data.length) {
80
+ // Skip comments (lines starting with #)
81
+ if (data[offset] === 0x23) { // #
82
+ // Skip until newline
83
+ while (offset < data.length && data[offset] !== 0x0a) {
84
+ offset++;
85
+ }
86
+ if (offset < data.length)
87
+ offset++; // Skip the newline
88
+ continue;
89
+ }
90
+ // Skip whitespace
91
+ while (offset < data.length && this.isWhitespace(data[offset])) {
92
+ offset++;
93
+ }
94
+ // Read number
95
+ let numStr = "";
96
+ while (offset < data.length &&
97
+ !this.isWhitespace(data[offset]) &&
98
+ data[offset] !== 0x23) {
99
+ numStr += String.fromCharCode(data[offset]);
100
+ offset++;
101
+ }
102
+ if (numStr) {
103
+ const num = parseInt(numStr, 10);
104
+ if (isNaN(num) || num <= 0) {
105
+ throw new Error(`Invalid PPM header value: ${numStr}`);
106
+ }
107
+ if (headerValues === 0) {
108
+ width = num;
109
+ }
110
+ else if (headerValues === 1) {
111
+ height = num;
112
+ }
113
+ else if (headerValues === 2) {
114
+ maxval = num;
115
+ }
116
+ headerValues++;
117
+ }
118
+ }
119
+ if (headerValues < 3) {
120
+ throw new Error("Incomplete PPM header");
121
+ }
122
+ // Skip single whitespace character after maxval (required by spec)
123
+ if (offset < data.length && this.isWhitespace(data[offset])) {
124
+ offset++;
125
+ }
126
+ // Validate dimensions
127
+ (0, security_js_1.validateImageDimensions)(width, height);
128
+ // Validate maxval
129
+ if (maxval > 255) {
130
+ throw new Error(`Unsupported PPM maxval: ${maxval}. Only maxval <= 255 is supported.`);
131
+ }
132
+ const pixelCount = width * height;
133
+ const rgba = new Uint8Array(pixelCount * 4);
134
+ if (isBinary) {
135
+ // P6: Binary format
136
+ const expectedDataLength = pixelCount * 3;
137
+ const actualDataLength = data.length - offset;
138
+ if (actualDataLength < expectedDataLength) {
139
+ throw new Error(`Invalid PPM data length: expected ${expectedDataLength}, got ${actualDataLength}`);
140
+ }
141
+ // Read RGB data and convert to RGBA
142
+ for (let i = 0; i < pixelCount; i++) {
143
+ const srcIndex = offset + i * 3;
144
+ const dstIndex = i * 4;
145
+ rgba[dstIndex] = data[srcIndex]; // R
146
+ rgba[dstIndex + 1] = data[srcIndex + 1]; // G
147
+ rgba[dstIndex + 2] = data[srcIndex + 2]; // B
148
+ rgba[dstIndex + 3] = 255; // A (fully opaque)
149
+ }
150
+ }
151
+ else {
152
+ // P3: ASCII format
153
+ let pixelIndex = 0;
154
+ while (pixelIndex < pixelCount * 3 && offset < data.length) {
155
+ // Skip whitespace and comments
156
+ while (offset < data.length) {
157
+ if (data[offset] === 0x23) { // #
158
+ // Skip comment
159
+ while (offset < data.length && data[offset] !== 0x0a) {
160
+ offset++;
161
+ }
162
+ if (offset < data.length)
163
+ offset++;
164
+ }
165
+ else if (this.isWhitespace(data[offset])) {
166
+ offset++;
167
+ }
168
+ else {
169
+ break;
170
+ }
171
+ }
172
+ // Read number
173
+ let numStr = "";
174
+ while (offset < data.length &&
175
+ !this.isWhitespace(data[offset]) &&
176
+ data[offset] !== 0x23) {
177
+ numStr += String.fromCharCode(data[offset]);
178
+ offset++;
179
+ }
180
+ if (numStr) {
181
+ const value = parseInt(numStr, 10);
182
+ if (isNaN(value) || value < 0 || value > maxval) {
183
+ throw new Error(`Invalid PPM pixel value: ${numStr}`);
184
+ }
185
+ // Scale to 0-255 if needed
186
+ const scaledValue = maxval === 255 ? value : Math.round((value * 255) / maxval);
187
+ const component = pixelIndex % 3;
188
+ const rgbaIndex = Math.floor(pixelIndex / 3) * 4;
189
+ if (component === 0) {
190
+ rgba[rgbaIndex] = scaledValue; // R
191
+ }
192
+ else if (component === 1) {
193
+ rgba[rgbaIndex + 1] = scaledValue; // G
194
+ }
195
+ else {
196
+ rgba[rgbaIndex + 2] = scaledValue; // B
197
+ rgba[rgbaIndex + 3] = 255; // A
198
+ }
199
+ pixelIndex++;
200
+ }
201
+ }
202
+ if (pixelIndex < pixelCount * 3) {
203
+ throw new Error(`Incomplete PPM pixel data: expected ${pixelCount * 3} values, got ${pixelIndex}`);
204
+ }
205
+ }
206
+ return Promise.resolve({ width, height, data: rgba });
207
+ }
208
+ /**
209
+ * Encode RGBA image data to PPM format (P6 binary)
210
+ * Note: Alpha channel is ignored as PPM doesn't support transparency
211
+ * @param imageData Image data to encode
212
+ * @returns Encoded PPM image bytes
213
+ */
214
+ encode(imageData) {
215
+ const { width, height, data } = imageData;
216
+ // Validate input
217
+ if (data.length !== width * height * 4) {
218
+ throw new Error(`Data length mismatch: expected ${width * height * 4}, got ${data.length}`);
219
+ }
220
+ // Create header
221
+ const header = `P6\n${width} ${height}\n255\n`;
222
+ const encoder = new TextEncoder();
223
+ const headerBytes = encoder.encode(header);
224
+ // Create output buffer (header + RGB data)
225
+ const pixelCount = width * height;
226
+ const output = new Uint8Array(headerBytes.length + pixelCount * 3);
227
+ // Write header
228
+ output.set(headerBytes, 0);
229
+ // Write RGB pixel data (discard alpha channel)
230
+ let outputOffset = headerBytes.length;
231
+ for (let i = 0; i < pixelCount; i++) {
232
+ const srcIndex = i * 4;
233
+ output[outputOffset++] = data[srcIndex]; // R
234
+ output[outputOffset++] = data[srcIndex + 1]; // G
235
+ output[outputOffset++] = data[srcIndex + 2]; // B
236
+ }
237
+ return Promise.resolve(output);
238
+ }
239
+ /**
240
+ * Check if a byte is whitespace (space, tab, CR, LF)
241
+ */
242
+ isWhitespace(byte) {
243
+ return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
244
+ }
245
+ }
246
+ exports.PPMFormat = PPMFormat;
@@ -5,6 +5,10 @@ import type { ImageData, ImageFormat, MultiFrameImageData } from "../types.js";
5
5
  export interface TIFFEncodeOptions {
6
6
  /** Compression method: "none" for uncompressed (default), "lzw" for LZW compression */
7
7
  compression?: "none" | "lzw";
8
+ /** Encode as grayscale instead of RGB/RGBA */
9
+ grayscale?: boolean;
10
+ /** Encode as RGB without alpha channel (ignored if grayscale is true) */
11
+ rgb?: boolean;
8
12
  }
9
13
  /**
10
14
  * TIFF format handler
@@ -135,18 +135,49 @@ class TIFFFormat {
135
135
  const { width, height, data, metadata } = imageData;
136
136
  const opts = options;
137
137
  const compression = opts?.compression ?? "none";
138
+ const grayscale = opts?.grayscale ?? false;
139
+ const rgb = opts?.rgb ?? false;
140
+ // Convert RGBA to grayscale if requested
141
+ let sourceData;
142
+ let samplesPerPixel;
143
+ if (grayscale) {
144
+ sourceData = new Uint8Array(width * height);
145
+ samplesPerPixel = 1;
146
+ for (let i = 0; i < width * height; i++) {
147
+ const r = data[i * 4];
148
+ const g = data[i * 4 + 1];
149
+ const b = data[i * 4 + 2];
150
+ // Use standard luminance formula
151
+ sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
152
+ }
153
+ }
154
+ else if (rgb) {
155
+ // Convert RGBA to RGB (strip alpha channel)
156
+ sourceData = new Uint8Array(width * height * 3);
157
+ samplesPerPixel = 3;
158
+ for (let i = 0; i < width * height; i++) {
159
+ sourceData[i * 3] = data[i * 4]; // R
160
+ sourceData[i * 3 + 1] = data[i * 4 + 1]; // G
161
+ sourceData[i * 3 + 2] = data[i * 4 + 2]; // B
162
+ }
163
+ }
164
+ else {
165
+ // Keep as RGBA
166
+ sourceData = data;
167
+ samplesPerPixel = 4;
168
+ }
138
169
  // Prepare pixel data (compress if needed)
139
170
  let pixelData;
140
171
  let compressionCode;
141
172
  if (compression === "lzw") {
142
173
  // LZW compress the pixel data
143
174
  const encoder = new tiff_lzw_js_1.TIFFLZWEncoder();
144
- pixelData = encoder.compress(data);
175
+ pixelData = encoder.compress(sourceData);
145
176
  compressionCode = 5;
146
177
  }
147
178
  else {
148
179
  // Uncompressed
149
- pixelData = data;
180
+ pixelData = sourceData;
150
181
  compressionCode = 1;
151
182
  }
152
183
  const result = [];
@@ -164,7 +195,10 @@ class TIFFFormat {
164
195
  // IFD (Image File Directory)
165
196
  const ifdStart = result.length;
166
197
  // Count number of entries (including metadata)
167
- let numEntries = 12; // Base entries (including ExtraSamples)
198
+ // Grayscale: 10 entries (no ExtraSamples)
199
+ // RGB: 11 entries (no ExtraSamples)
200
+ // RGBA: 12 entries (includes ExtraSamples)
201
+ let numEntries = grayscale ? 10 : (rgb ? 11 : 12);
168
202
  if (metadata?.description)
169
203
  numEntries++;
170
204
  if (metadata?.author)
@@ -182,16 +216,28 @@ class TIFFFormat {
182
216
  // ImageHeight (0x0101)
183
217
  this.writeIFDEntry(result, 0x0101, 4, 1, height);
184
218
  // BitsPerSample (0x0102) - 8 bits per channel
185
- this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
186
- dataOffset += 8; // 4 x 2-byte values
219
+ if (grayscale) {
220
+ // Single value for grayscale
221
+ this.writeIFDEntry(result, 0x0102, 3, 1, 8);
222
+ }
223
+ else if (rgb) {
224
+ // 3 values for RGB
225
+ this.writeIFDEntry(result, 0x0102, 3, 3, dataOffset);
226
+ dataOffset += 6; // 3 x 2-byte values
227
+ }
228
+ else {
229
+ // 4 values for RGBA
230
+ this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
231
+ dataOffset += 8; // 4 x 2-byte values
232
+ }
187
233
  // Compression (0x0103) - 1 = uncompressed, 5 = LZW
188
234
  this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
189
- // PhotometricInterpretation (0x0106) - 2 = RGB
190
- this.writeIFDEntry(result, 0x0106, 3, 1, 2);
235
+ // PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
236
+ this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : 2);
191
237
  // StripOffsets (0x0111)
192
238
  this.writeIFDEntry(result, 0x0111, 4, 1, 8);
193
- // SamplesPerPixel (0x0115) - 4 (RGBA)
194
- this.writeIFDEntry(result, 0x0115, 3, 1, 4);
239
+ // SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
240
+ this.writeIFDEntry(result, 0x0115, 3, 1, samplesPerPixel);
195
241
  // RowsPerStrip (0x0116)
196
242
  this.writeIFDEntry(result, 0x0116, 4, 1, height);
197
243
  // StripByteCounts (0x0117)
@@ -204,8 +250,10 @@ class TIFFFormat {
204
250
  const yResOffset = dataOffset;
205
251
  this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
206
252
  dataOffset += 8;
207
- // ExtraSamples (0x0152) - 2 = unassociated alpha
208
- this.writeIFDEntry(result, 0x0152, 3, 1, 2);
253
+ // ExtraSamples (0x0152) - 2 = unassociated alpha (only for RGBA)
254
+ if (!grayscale && !rgb) {
255
+ this.writeIFDEntry(result, 0x0152, 3, 1, 2);
256
+ }
209
257
  // Optional metadata entries
210
258
  if (metadata?.description) {
211
259
  const descBytes = new TextEncoder().encode(metadata.description + "\0");
@@ -232,11 +280,18 @@ class TIFFFormat {
232
280
  // Next IFD offset (0 = no more IFDs)
233
281
  this.writeUint32LE(result, 0);
234
282
  // Write variable-length data
235
- // BitsPerSample values (must be written first to match offset calculation)
236
- this.writeUint16LE(result, 8);
237
- this.writeUint16LE(result, 8);
238
- this.writeUint16LE(result, 8);
239
- this.writeUint16LE(result, 8);
283
+ // BitsPerSample values (only for RGB and RGBA, not for grayscale)
284
+ if (rgb) {
285
+ this.writeUint16LE(result, 8);
286
+ this.writeUint16LE(result, 8);
287
+ this.writeUint16LE(result, 8);
288
+ }
289
+ else if (!grayscale) {
290
+ this.writeUint16LE(result, 8);
291
+ this.writeUint16LE(result, 8);
292
+ this.writeUint16LE(result, 8);
293
+ this.writeUint16LE(result, 8);
294
+ }
240
295
  // XResolution value (rational)
241
296
  const dpiX = metadata?.dpiX ?? DEFAULT_DPI;
242
297
  this.writeUint32LE(result, dpiX);
@@ -699,14 +754,23 @@ class TIFFFormat {
699
754
  }
700
755
  // Check photometric interpretation
701
756
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
702
- if (photometric !== 2) {
703
- // Only support RGB (photometric = 2)
757
+ if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
758
+ // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
704
759
  return null;
705
760
  }
706
761
  // Get samples per pixel
707
762
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
708
- if (!samplesPerPixel || (samplesPerPixel !== 3 && samplesPerPixel !== 4)) {
709
- // Only support RGB (3) or RGBA (4)
763
+ // For grayscale (photometric 0 or 1), expect 1 sample per pixel
764
+ // For RGB, expect 3 or 4 samples per pixel
765
+ if (!samplesPerPixel) {
766
+ return null;
767
+ }
768
+ if ((photometric === 0 || photometric === 1) && samplesPerPixel !== 1) {
769
+ // Grayscale requires 1 sample per pixel
770
+ return null;
771
+ }
772
+ if (photometric === 2 && samplesPerPixel !== 3 && samplesPerPixel !== 4) {
773
+ // RGB requires 3 or 4 samples per pixel
710
774
  return null;
711
775
  }
712
776
  // Get strip offset
@@ -736,17 +800,40 @@ class TIFFFormat {
736
800
  // Convert to RGBA
737
801
  const rgba = new Uint8Array(width * height * 4);
738
802
  let srcPos = 0;
739
- for (let y = 0; y < height; y++) {
740
- for (let x = 0; x < width; x++) {
741
- const dstIdx = (y * width + x) * 4;
742
- if (srcPos + samplesPerPixel > pixelData.length) {
743
- return null; // Not enough data
803
+ if (photometric === 0 || photometric === 1) {
804
+ // Grayscale image
805
+ for (let y = 0; y < height; y++) {
806
+ for (let x = 0; x < width; x++) {
807
+ const dstIdx = (y * width + x) * 4;
808
+ if (srcPos >= pixelData.length) {
809
+ return null; // Not enough data
810
+ }
811
+ let gray = pixelData[srcPos++];
812
+ // WhiteIsZero (0) means 0=white, 255=black, so invert
813
+ if (photometric === 0) {
814
+ gray = 255 - gray;
815
+ }
816
+ rgba[dstIdx] = gray; // R
817
+ rgba[dstIdx + 1] = gray; // G
818
+ rgba[dstIdx + 2] = gray; // B
819
+ rgba[dstIdx + 3] = 255; // A (opaque)
820
+ }
821
+ }
822
+ }
823
+ else {
824
+ // RGB/RGBA image
825
+ for (let y = 0; y < height; y++) {
826
+ for (let x = 0; x < width; x++) {
827
+ const dstIdx = (y * width + x) * 4;
828
+ if (srcPos + samplesPerPixel > pixelData.length) {
829
+ return null; // Not enough data
830
+ }
831
+ // TIFF stores RGB(A) in order
832
+ rgba[dstIdx] = pixelData[srcPos++]; // R
833
+ rgba[dstIdx + 1] = pixelData[srcPos++]; // G
834
+ rgba[dstIdx + 2] = pixelData[srcPos++]; // B
835
+ rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
744
836
  }
745
- // TIFF stores RGB(A) in order
746
- rgba[dstIdx] = pixelData[srcPos++]; // R
747
- rgba[dstIdx + 1] = pixelData[srcPos++]; // G
748
- rgba[dstIdx + 2] = pixelData[srcPos++]; // B
749
- rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
750
837
  }
751
838
  }
752
839
  return rgba;
@@ -764,14 +851,23 @@ class TIFFFormat {
764
851
  }
765
852
  // Check photometric interpretation
766
853
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
767
- if (photometric !== 2) {
768
- // Only support RGB (photometric = 2)
854
+ if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
855
+ // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
769
856
  return null;
770
857
  }
771
858
  // Get samples per pixel
772
859
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
773
- if (!samplesPerPixel || (samplesPerPixel !== 3 && samplesPerPixel !== 4)) {
774
- // Only support RGB (3) or RGBA (4)
860
+ // For grayscale (photometric 0 or 1), expect 1 sample per pixel
861
+ // For RGB, expect 3 or 4 samples per pixel
862
+ if (!samplesPerPixel) {
863
+ return null;
864
+ }
865
+ if ((photometric === 0 || photometric === 1) && samplesPerPixel !== 1) {
866
+ // Grayscale requires 1 sample per pixel
867
+ return null;
868
+ }
869
+ if (photometric === 2 && samplesPerPixel !== 3 && samplesPerPixel !== 4) {
870
+ // RGB requires 3 or 4 samples per pixel
775
871
  return null;
776
872
  }
777
873
  // Get strip offset
@@ -801,17 +897,40 @@ class TIFFFormat {
801
897
  // Convert to RGBA
802
898
  const rgba = new Uint8Array(width * height * 4);
803
899
  let srcPos = 0;
804
- for (let y = 0; y < height; y++) {
805
- for (let x = 0; x < width; x++) {
806
- const dstIdx = (y * width + x) * 4;
807
- if (srcPos + samplesPerPixel > pixelData.length) {
808
- return null; // Not enough data
900
+ if (photometric === 0 || photometric === 1) {
901
+ // Grayscale image
902
+ for (let y = 0; y < height; y++) {
903
+ for (let x = 0; x < width; x++) {
904
+ const dstIdx = (y * width + x) * 4;
905
+ if (srcPos >= pixelData.length) {
906
+ return null; // Not enough data
907
+ }
908
+ let gray = pixelData[srcPos++];
909
+ // WhiteIsZero (0) means 0=white, 255=black, so invert
910
+ if (photometric === 0) {
911
+ gray = 255 - gray;
912
+ }
913
+ rgba[dstIdx] = gray; // R
914
+ rgba[dstIdx + 1] = gray; // G
915
+ rgba[dstIdx + 2] = gray; // B
916
+ rgba[dstIdx + 3] = 255; // A (opaque)
917
+ }
918
+ }
919
+ }
920
+ else {
921
+ // RGB/RGBA image
922
+ for (let y = 0; y < height; y++) {
923
+ for (let x = 0; x < width; x++) {
924
+ const dstIdx = (y * width + x) * 4;
925
+ if (srcPos + samplesPerPixel > pixelData.length) {
926
+ return null; // Not enough data
927
+ }
928
+ // TIFF stores RGB(A) in order
929
+ rgba[dstIdx] = pixelData[srcPos++]; // R
930
+ rgba[dstIdx + 1] = pixelData[srcPos++]; // G
931
+ rgba[dstIdx + 2] = pixelData[srcPos++]; // B
932
+ rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
809
933
  }
810
- // TIFF stores RGB(A) in order
811
- rgba[dstIdx] = pixelData[srcPos++]; // R
812
- rgba[dstIdx + 1] = pixelData[srcPos++]; // G
813
- rgba[dstIdx + 2] = pixelData[srcPos++]; // B
814
- rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
815
934
  }
816
935
  }
817
936
  return rgba;
@@ -27,7 +27,6 @@ export declare class WebPFormat implements ImageFormat {
27
27
  * @returns Encoded WebP image bytes
28
28
  */
29
29
  encode(imageData: ImageData, options?: WebPEncodeOptions): Promise<Uint8Array>;
30
- private readUint32LE;
31
30
  private readUint24LE;
32
31
  private decodeUsingRuntime;
33
32
  private parseEXIF;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.WebPFormat = void 0;
37
37
  const security_js_1 = require("../utils/security.js");
38
+ const byte_utils_js_1 = require("../utils/byte_utils.js");
38
39
  // Default quality for WebP encoding when not specified
39
40
  const DEFAULT_WEBP_QUALITY = 90;
40
41
  /**
@@ -88,7 +89,7 @@ class WebPFormat {
88
89
  // Read all chunks to extract metadata
89
90
  while (pos + 8 <= data.length) {
90
91
  const chunkType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
91
- const chunkSize = this.readUint32LE(data, pos + 4);
92
+ const chunkSize = (0, byte_utils_js_1.readUint32LE)(data, pos + 4);
92
93
  pos += 8;
93
94
  // Stop if we've gone past the end
94
95
  if (pos + chunkSize > data.length)
@@ -110,7 +111,7 @@ class WebPFormat {
110
111
  else if (chunkType === "VP8L") {
111
112
  // Lossless format - extract dimensions
112
113
  if (chunkData.length >= 5 && chunkData[0] === 0x2f) {
113
- const bits = this.readUint32LE(chunkData, 1);
114
+ const bits = (0, byte_utils_js_1.readUint32LE)(chunkData, 1);
114
115
  width = (bits & 0x3fff) + 1;
115
116
  height = ((bits >> 14) & 0x3fff) + 1;
116
117
  }
@@ -202,10 +203,6 @@ class WebPFormat {
202
203
  }
203
204
  return encoded;
204
205
  }
205
- readUint32LE(data, offset) {
206
- return data[offset] | (data[offset + 1] << 8) |
207
- (data[offset + 2] << 16) | (data[offset + 3] << 24);
208
- }
209
206
  readUint24LE(data, offset) {
210
207
  return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
211
208
  }
@@ -342,7 +339,7 @@ class WebPFormat {
342
339
  let pos = 12;
343
340
  while (pos + 8 <= webpData.length) {
344
341
  const chunkType = String.fromCharCode(webpData[pos], webpData[pos + 1], webpData[pos + 2], webpData[pos + 3]);
345
- const chunkSize = this.readUint32LE(webpData, pos + 4);
342
+ const chunkSize = (0, byte_utils_js_1.readUint32LE)(webpData, pos + 4);
346
343
  // Don't copy existing EXIF/XMP chunks (we'll add new ones)
347
344
  if (chunkType !== "EXIF" && chunkType !== "XMP ") {
348
345
  const chunkEnd = pos + 8 + chunkSize + (chunkSize % 2);
@@ -207,6 +207,36 @@ export declare class Image {
207
207
  * @returns This image instance for chaining
208
208
  */
209
209
  grayscale(): this;
210
+ /**
211
+ * Apply sepia tone effect to the image
212
+ * @returns This image instance for chaining
213
+ */
214
+ sepia(): this;
215
+ /**
216
+ * Apply box blur filter to the image
217
+ * @param radius Blur radius (default: 1)
218
+ * @returns This image instance for chaining
219
+ */
220
+ blur(radius?: number): this;
221
+ /**
222
+ * Apply Gaussian blur filter to the image
223
+ * @param radius Blur radius (default: 1)
224
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
225
+ * @returns This image instance for chaining
226
+ */
227
+ gaussianBlur(radius?: number, sigma?: number): this;
228
+ /**
229
+ * Apply sharpen filter to the image
230
+ * @param amount Sharpening amount (0-1, default: 0.5)
231
+ * @returns This image instance for chaining
232
+ */
233
+ sharpen(amount?: number): this;
234
+ /**
235
+ * Apply median filter to reduce noise
236
+ * @param radius Filter radius (default: 1)
237
+ * @returns This image instance for chaining
238
+ */
239
+ medianFilter(radius?: number): this;
210
240
  /**
211
241
  * Fill a rectangular region with a color
212
242
  * @param x Starting X position