cross-image 0.2.1 → 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.
@@ -229,3 +229,233 @@ export function crop(data, width, height, x, y, cropWidth, cropHeight) {
229
229
  }
230
230
  return { data: result, width: actualWidth, height: actualHeight };
231
231
  }
232
+ /**
233
+ * Apply a box blur filter to an image
234
+ * @param data Image data (RGBA)
235
+ * @param width Image width
236
+ * @param height Image height
237
+ * @param radius Blur radius (default: 1)
238
+ * @returns New image data with box blur applied
239
+ */
240
+ export function boxBlur(data, width, height, radius = 1) {
241
+ const result = new Uint8Array(data.length);
242
+ const clampedRadius = Math.max(1, Math.floor(radius));
243
+ for (let y = 0; y < height; y++) {
244
+ for (let x = 0; x < width; x++) {
245
+ let r = 0, g = 0, b = 0, a = 0;
246
+ let count = 0;
247
+ // Iterate over kernel
248
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
249
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
250
+ const px = x + kx;
251
+ const py = y + ky;
252
+ // Check bounds
253
+ if (px >= 0 && px < width && py >= 0 && py < height) {
254
+ const idx = (py * width + px) * 4;
255
+ r += data[idx];
256
+ g += data[idx + 1];
257
+ b += data[idx + 2];
258
+ a += data[idx + 3];
259
+ count++;
260
+ }
261
+ }
262
+ }
263
+ const outIdx = (y * width + x) * 4;
264
+ result[outIdx] = Math.round(r / count);
265
+ result[outIdx + 1] = Math.round(g / count);
266
+ result[outIdx + 2] = Math.round(b / count);
267
+ result[outIdx + 3] = Math.round(a / count);
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+ /**
273
+ * Generate a Gaussian kernel for blur
274
+ * @param radius Kernel radius
275
+ * @param sigma Standard deviation (if not provided, calculated from radius)
276
+ * @returns Gaussian kernel as 1D array
277
+ */
278
+ function generateGaussianKernel(radius, sigma) {
279
+ const size = radius * 2 + 1;
280
+ const kernel = new Array(size);
281
+ const s = sigma ?? radius / 3;
282
+ const s2 = 2 * s * s;
283
+ let sum = 0;
284
+ for (let i = 0; i < size; i++) {
285
+ const x = i - radius;
286
+ kernel[i] = Math.exp(-(x * x) / s2);
287
+ sum += kernel[i];
288
+ }
289
+ // Normalize
290
+ for (let i = 0; i < size; i++) {
291
+ kernel[i] /= sum;
292
+ }
293
+ return kernel;
294
+ }
295
+ /**
296
+ * Apply Gaussian blur to an image
297
+ * @param data Image data (RGBA)
298
+ * @param width Image width
299
+ * @param height Image height
300
+ * @param radius Blur radius (default: 1)
301
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
302
+ * @returns New image data with Gaussian blur applied
303
+ */
304
+ export function gaussianBlur(data, width, height, radius = 1, sigma) {
305
+ const clampedRadius = Math.max(1, Math.floor(radius));
306
+ const kernel = generateGaussianKernel(clampedRadius, sigma);
307
+ // Apply horizontal pass
308
+ const temp = new Uint8Array(data.length);
309
+ for (let y = 0; y < height; y++) {
310
+ for (let x = 0; x < width; x++) {
311
+ let r = 0, g = 0, b = 0, a = 0;
312
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
313
+ const px = Math.max(0, Math.min(width - 1, x + kx));
314
+ const idx = (y * width + px) * 4;
315
+ const weight = kernel[kx + clampedRadius];
316
+ r += data[idx] * weight;
317
+ g += data[idx + 1] * weight;
318
+ b += data[idx + 2] * weight;
319
+ a += data[idx + 3] * weight;
320
+ }
321
+ const outIdx = (y * width + x) * 4;
322
+ temp[outIdx] = Math.round(r);
323
+ temp[outIdx + 1] = Math.round(g);
324
+ temp[outIdx + 2] = Math.round(b);
325
+ temp[outIdx + 3] = Math.round(a);
326
+ }
327
+ }
328
+ // Apply vertical pass
329
+ const result = new Uint8Array(data.length);
330
+ for (let y = 0; y < height; y++) {
331
+ for (let x = 0; x < width; x++) {
332
+ let r = 0, g = 0, b = 0, a = 0;
333
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
334
+ const py = Math.max(0, Math.min(height - 1, y + ky));
335
+ const idx = (py * width + x) * 4;
336
+ const weight = kernel[ky + clampedRadius];
337
+ r += temp[idx] * weight;
338
+ g += temp[idx + 1] * weight;
339
+ b += temp[idx + 2] * weight;
340
+ a += temp[idx + 3] * weight;
341
+ }
342
+ const outIdx = (y * width + x) * 4;
343
+ result[outIdx] = Math.round(r);
344
+ result[outIdx + 1] = Math.round(g);
345
+ result[outIdx + 2] = Math.round(b);
346
+ result[outIdx + 3] = Math.round(a);
347
+ }
348
+ }
349
+ return result;
350
+ }
351
+ /**
352
+ * Apply sharpen filter to an image
353
+ * @param data Image data (RGBA)
354
+ * @param width Image width
355
+ * @param height Image height
356
+ * @param amount Sharpening amount (0-1, default: 0.5)
357
+ * @returns New image data with sharpening applied
358
+ */
359
+ export function sharpen(data, width, height, amount = 0.5) {
360
+ const result = new Uint8Array(data.length);
361
+ const clampedAmount = Math.max(0, Math.min(1, amount));
362
+ // Sharpen kernel (Laplacian-based)
363
+ // Center weight is 1 + 4*amount, neighbors are -amount
364
+ const center = 1 + 4 * clampedAmount;
365
+ const neighbor = -clampedAmount;
366
+ for (let y = 0; y < height; y++) {
367
+ for (let x = 0; x < width; x++) {
368
+ const idx = (y * width + x) * 4;
369
+ let r = data[idx] * center;
370
+ let g = data[idx + 1] * center;
371
+ let b = data[idx + 2] * center;
372
+ // Apply kernel to neighbors (4-connected)
373
+ const neighbors = [
374
+ { dx: 0, dy: -1 }, // top
375
+ { dx: -1, dy: 0 }, // left
376
+ { dx: 1, dy: 0 }, // right
377
+ { dx: 0, dy: 1 }, // bottom
378
+ ];
379
+ for (const { dx, dy } of neighbors) {
380
+ const nx = x + dx;
381
+ const ny = y + dy;
382
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
383
+ const nIdx = (ny * width + nx) * 4;
384
+ r += data[nIdx] * neighbor;
385
+ g += data[nIdx + 1] * neighbor;
386
+ b += data[nIdx + 2] * neighbor;
387
+ }
388
+ }
389
+ result[idx] = Math.max(0, Math.min(255, Math.round(r)));
390
+ result[idx + 1] = Math.max(0, Math.min(255, Math.round(g)));
391
+ result[idx + 2] = Math.max(0, Math.min(255, Math.round(b)));
392
+ result[idx + 3] = data[idx + 3]; // Alpha unchanged
393
+ }
394
+ }
395
+ return result;
396
+ }
397
+ /**
398
+ * Apply sepia tone effect to an image
399
+ * @param data Image data (RGBA)
400
+ * @returns New image data with sepia tone applied
401
+ */
402
+ export function sepia(data) {
403
+ const result = new Uint8Array(data.length);
404
+ for (let i = 0; i < data.length; i += 4) {
405
+ const r = data[i];
406
+ const g = data[i + 1];
407
+ const b = data[i + 2];
408
+ // Sepia transformation matrix
409
+ result[i] = Math.min(255, Math.round(r * 0.393 + g * 0.769 + b * 0.189));
410
+ result[i + 1] = Math.min(255, Math.round(r * 0.349 + g * 0.686 + b * 0.168));
411
+ result[i + 2] = Math.min(255, Math.round(r * 0.272 + g * 0.534 + b * 0.131));
412
+ result[i + 3] = data[i + 3]; // Alpha unchanged
413
+ }
414
+ return result;
415
+ }
416
+ /**
417
+ * Apply median filter to reduce noise
418
+ * @param data Image data (RGBA)
419
+ * @param width Image width
420
+ * @param height Image height
421
+ * @param radius Filter radius (default: 1)
422
+ * @returns New image data with median filter applied
423
+ */
424
+ export function medianFilter(data, width, height, radius = 1) {
425
+ const result = new Uint8Array(data.length);
426
+ const clampedRadius = Math.max(1, Math.floor(radius));
427
+ for (let y = 0; y < height; y++) {
428
+ for (let x = 0; x < width; x++) {
429
+ const rValues = [];
430
+ const gValues = [];
431
+ const bValues = [];
432
+ const aValues = [];
433
+ // Collect values in kernel window
434
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
435
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
436
+ const px = x + kx;
437
+ const py = y + ky;
438
+ if (px >= 0 && px < width && py >= 0 && py < height) {
439
+ const idx = (py * width + px) * 4;
440
+ rValues.push(data[idx]);
441
+ gValues.push(data[idx + 1]);
442
+ bValues.push(data[idx + 2]);
443
+ aValues.push(data[idx + 3]);
444
+ }
445
+ }
446
+ }
447
+ // Sort and get median
448
+ rValues.sort((a, b) => a - b);
449
+ gValues.sort((a, b) => a - b);
450
+ bValues.sort((a, b) => a - b);
451
+ aValues.sort((a, b) => a - b);
452
+ const mid = Math.floor(rValues.length / 2);
453
+ const outIdx = (y * width + x) * 4;
454
+ result[outIdx] = rValues[mid];
455
+ result[outIdx + 1] = gValues[mid];
456
+ result[outIdx + 2] = bValues[mid];
457
+ result[outIdx + 3] = aValues[mid];
458
+ }
459
+ }
460
+ return result;
461
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-image",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and Bun.",
5
5
  "keywords": [
6
6
  "image",
package/script/mod.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @module @cross/image
3
3
  *
4
4
  * A pure JavaScript, dependency-free, cross-runtime image processing library.
5
- * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PCX).
5
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX).
6
6
  * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
7
7
  *
8
8
  * @example
@@ -55,5 +55,6 @@ export { ICOFormat } from "./src/formats/ico.js";
55
55
  export { DNGFormat } from "./src/formats/dng.js";
56
56
  export { PAMFormat } from "./src/formats/pam.js";
57
57
  export { PCXFormat } from "./src/formats/pcx.js";
58
+ export { PPMFormat } from "./src/formats/ppm.js";
58
59
  export { ASCIIFormat } from "./src/formats/ascii.js";
59
60
  //# sourceMappingURL=mod.d.ts.map
package/script/mod.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module @cross/image
4
4
  *
5
5
  * A pure JavaScript, dependency-free, cross-runtime image processing library.
6
- * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PCX).
6
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX).
7
7
  * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
8
8
  *
9
9
  * @example
@@ -44,7 +44,7 @@
44
44
  * ```
45
45
  */
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.ASCIIFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
47
+ exports.ASCIIFormat = exports.PPMFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
48
48
  var image_js_1 = require("./src/image.js");
49
49
  Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return image_js_1.Image; } });
50
50
  var png_js_1 = require("./src/formats/png.js");
@@ -69,5 +69,7 @@ var pam_js_1 = require("./src/formats/pam.js");
69
69
  Object.defineProperty(exports, "PAMFormat", { enumerable: true, get: function () { return pam_js_1.PAMFormat; } });
70
70
  var pcx_js_1 = require("./src/formats/pcx.js");
71
71
  Object.defineProperty(exports, "PCXFormat", { enumerable: true, get: function () { return pcx_js_1.PCXFormat; } });
72
+ var ppm_js_1 = require("./src/formats/ppm.js");
73
+ Object.defineProperty(exports, "PPMFormat", { enumerable: true, get: function () { return ppm_js_1.PPMFormat; } });
72
74
  var ascii_js_1 = require("./src/formats/ascii.js");
73
75
  Object.defineProperty(exports, "ASCIIFormat", { enumerable: true, get: function () { return ascii_js_1.ASCIIFormat; } });
@@ -0,0 +1,50 @@
1
+ import type { ImageData, ImageFormat } from "../types.js";
2
+ /**
3
+ * PPM format handler
4
+ * Implements the Netpbm PPM (Portable PixMap) format.
5
+ * This is a simple uncompressed RGB format supported by many image tools.
6
+ *
7
+ * Format structure:
8
+ * - P3 (ASCII format):
9
+ * P3
10
+ * <width> <height>
11
+ * <maxval>
12
+ * R G B R G B ... (space-separated decimal values)
13
+ *
14
+ * - P6 (Binary format):
15
+ * P6
16
+ * <width> <height>
17
+ * <maxval>
18
+ * RGB RGB RGB ... (binary byte data)
19
+ */
20
+ export declare class PPMFormat implements ImageFormat {
21
+ /** Format name identifier */
22
+ readonly name = "ppm";
23
+ /** MIME type for PPM images */
24
+ readonly mimeType = "image/x-portable-pixmap";
25
+ /**
26
+ * Check if the given data is a PPM image
27
+ * @param data Raw image data to check
28
+ * @returns true if data has PPM signature (P3 or P6)
29
+ */
30
+ canDecode(data: Uint8Array): boolean;
31
+ /**
32
+ * Decode PPM image data to RGBA
33
+ * Supports both P3 (ASCII) and P6 (binary) formats
34
+ * @param data Raw PPM image data
35
+ * @returns Decoded image data with RGBA pixels
36
+ */
37
+ decode(data: Uint8Array): Promise<ImageData>;
38
+ /**
39
+ * Encode RGBA image data to PPM format (P6 binary)
40
+ * Note: Alpha channel is ignored as PPM doesn't support transparency
41
+ * @param imageData Image data to encode
42
+ * @returns Encoded PPM image bytes
43
+ */
44
+ encode(imageData: ImageData): Promise<Uint8Array>;
45
+ /**
46
+ * Check if a byte is whitespace (space, tab, CR, LF)
47
+ */
48
+ private isWhitespace;
49
+ }
50
+ //# sourceMappingURL=ppm.d.ts.map
@@ -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