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.
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # @cross/image
2
2
 
3
3
  A pure JavaScript, dependency-free, cross-runtime image processing library for
4
- Deno, Node.js, and Bun.
4
+ Deno, Node.js, and Bun. Decode, encode, manipulate, and process images in
5
+ multiple formats including PNG, JPEG, WebP, GIF, and moreβ€”all without native
6
+ dependencies.
5
7
 
6
8
  πŸ“š **[Full Documentation](https://cross-image.56k.guru/)**
7
9
 
@@ -11,10 +13,10 @@ Deno, Node.js, and Bun.
11
13
  - πŸ”Œ **Pluggable formats** - Easy to extend with custom formats
12
14
  - πŸ“¦ **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
13
15
  - 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG,
14
- PAM, PCX and ASCII support
16
+ PAM, PPM, PCX and ASCII support
15
17
  - βœ‚οΈ **Image manipulation** - Resize, crop, composite, and more
16
- - πŸŽ›οΈ **Image processing** - Chainable `brightness`, `contrast`, `saturation`,
17
- and `exposure` helpers
18
+ - πŸŽ›οΈ **Image processing** - Chainable filters including `brightness`,
19
+ `contrast`, `saturation`, `exposure`, `blur`, `sharpen`, `sepia`, and more
18
20
  - πŸ–ŒοΈ **Drawing operations** - Create, fill, and manipulate pixels
19
21
  - 🧩 **Multi-frame** - Decode/encode animated GIFs, APNGs and multi-page TIFFs
20
22
  - πŸ”§ **Simple API** - Easy to use, intuitive interface
@@ -66,11 +68,13 @@ const canvas = Image.create(800, 600, 255, 255, 255); // white background
66
68
  // Composite the loaded image on top
67
69
  canvas.composite(image, 50, 50);
68
70
 
69
- // Apply image processing
71
+ // Apply image processing filters
70
72
  canvas
71
73
  .brightness(0.1)
72
74
  .contrast(0.2)
73
- .saturation(-0.1);
75
+ .saturation(-0.1)
76
+ .blur(1)
77
+ .sharpen(0.3);
74
78
 
75
79
  // Encode in a different format
76
80
  const jpeg = await canvas.encode("jpeg");
@@ -99,37 +103,40 @@ await writeFile("output.jpg", jpeg);
99
103
 
100
104
  ## Supported Formats
101
105
 
102
- | Format | Pure-JS | Notes |
103
- | ------ | ----------- | ------------------------------- |
104
- | PNG | βœ… Full | Complete pure-JS implementation |
105
- | APNG | βœ… Full | Animated PNG with multi-frame |
106
- | BMP | βœ… Full | Complete pure-JS implementation |
107
- | ICO | βœ… Full | Windows Icon format |
108
- | GIF | βœ… Full | Animated GIF with multi-frame |
109
- | DNG | βœ… Full | Linear DNG (Uncompressed RGBA) |
110
- | PAM | βœ… Full | Netpbm PAM format |
111
- | PCX | βœ… Full | ZSoft PCX (RLE compressed) |
112
- | ASCII | βœ… Full | Text-based ASCII art |
113
- | JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
114
- | WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
115
- | TIFF | ⚠️ Basic | Pure-JS uncompressed + LZW |
106
+ | Format | Pure-JS | Notes |
107
+ | ------ | ----------- | -------------------------------------- |
108
+ | PNG | βœ… Full | Complete pure-JS implementation |
109
+ | APNG | βœ… Full | Animated PNG with multi-frame |
110
+ | BMP | βœ… Full | Complete pure-JS implementation |
111
+ | ICO | βœ… Full | Windows Icon format |
112
+ | GIF | βœ… Full | Animated GIF with multi-frame |
113
+ | DNG | βœ… Full | Linear DNG (Uncompressed RGBA) |
114
+ | PAM | βœ… Full | Netpbm PAM format |
115
+ | PPM | βœ… Full | Netpbm PPM format (P3/P6) |
116
+ | PCX | βœ… Full | ZSoft PCX (RLE compressed) |
117
+ | ASCII | βœ… Full | Text-based ASCII art |
118
+ | JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
119
+ | WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
120
+ | TIFF | ⚠️ Basic | Pure-JS uncompressed, LZW, & grayscale |
116
121
 
117
122
  See the
118
- [full format support documentation](https://cross-image.56k.guru/formats.html)
119
- for detailed compatibility information.
123
+ [full format support documentation](https://cross-image.56k.guru/formats/) for
124
+ detailed compatibility information.
120
125
 
121
126
  ## Documentation
122
127
 
123
- - **[API Reference](https://cross-image.56k.guru/api.html)** - Complete API
128
+ - **[API Reference](https://cross-image.56k.guru/api/)** - Complete API
124
129
  documentation
125
- - **[Examples](https://cross-image.56k.guru/examples.html)** - Usage examples
126
- for common tasks
127
- - **[Format Support](https://cross-image.56k.guru/formats.html)** - Supported
130
+ - **[Examples](https://cross-image.56k.guru/examples/)** - Usage examples for
131
+ common tasks
132
+ - **[Format Support](https://cross-image.56k.guru/formats/)** - Supported
128
133
  formats and specifications
129
- - **[JPEG Implementation](https://cross-image.56k.guru/implementation/jpeg-implementation.html)** -
134
+ - **[JPEG Implementation](https://cross-image.56k.guru/implementation/jpeg-implementation/)** -
130
135
  Technical details for JPEG
131
- - **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation.html)** -
136
+ - **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation/)** -
132
137
  Technical details for WebP
138
+ - **[TIFF Implementation](https://cross-image.56k.guru/implementation/tiff-implementation/)** -
139
+ Technical details for TIFF
133
140
 
134
141
  ## Development
135
142
 
package/esm/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/esm/mod.js 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
@@ -54,4 +54,5 @@ export { ICOFormat } from "./src/formats/ico.js";
54
54
  export { DNGFormat } from "./src/formats/dng.js";
55
55
  export { PAMFormat } from "./src/formats/pam.js";
56
56
  export { PCXFormat } from "./src/formats/pcx.js";
57
+ export { PPMFormat } from "./src/formats/ppm.js";
57
58
  export { ASCIIFormat } from "./src/formats/ascii.js";
@@ -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,242 @@
1
+ import { validateImageDimensions } from "../utils/security.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 class PPMFormat {
21
+ constructor() {
22
+ /** Format name identifier */
23
+ Object.defineProperty(this, "name", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: "ppm"
28
+ });
29
+ /** MIME type for PPM images */
30
+ Object.defineProperty(this, "mimeType", {
31
+ enumerable: true,
32
+ configurable: true,
33
+ writable: true,
34
+ value: "image/x-portable-pixmap"
35
+ });
36
+ }
37
+ /**
38
+ * Check if the given data is a PPM image
39
+ * @param data Raw image data to check
40
+ * @returns true if data has PPM signature (P3 or P6)
41
+ */
42
+ canDecode(data) {
43
+ // Check if data has at least magic bytes
44
+ if (data.length < 3) {
45
+ return false;
46
+ }
47
+ // Check for P3 or P6 followed by whitespace
48
+ return data[0] === 0x50 && // P
49
+ (data[1] === 0x33 || data[1] === 0x36) && // 3 or 6
50
+ (data[2] === 0x0a || data[2] === 0x0d || data[2] === 0x20 ||
51
+ data[2] === 0x09); // \n, \r, space, or tab
52
+ }
53
+ /**
54
+ * Decode PPM image data to RGBA
55
+ * Supports both P3 (ASCII) and P6 (binary) formats
56
+ * @param data Raw PPM image data
57
+ * @returns Decoded image data with RGBA pixels
58
+ */
59
+ decode(data) {
60
+ if (!this.canDecode(data)) {
61
+ throw new Error("Invalid PPM signature");
62
+ }
63
+ const isBinary = data[1] === 0x36; // P6
64
+ // Parse header
65
+ let offset = 0;
66
+ let width = 0;
67
+ let height = 0;
68
+ let maxval = 0;
69
+ let headerValues = 0; // Track how many values we've parsed (need 3: width, height, maxval)
70
+ // Skip magic number and whitespace
71
+ offset = 2;
72
+ while (offset < data.length && this.isWhitespace(data[offset])) {
73
+ offset++;
74
+ }
75
+ // Parse header values (width, height, maxval)
76
+ while (headerValues < 3 && offset < data.length) {
77
+ // Skip comments (lines starting with #)
78
+ if (data[offset] === 0x23) { // #
79
+ // Skip until newline
80
+ while (offset < data.length && data[offset] !== 0x0a) {
81
+ offset++;
82
+ }
83
+ if (offset < data.length)
84
+ offset++; // Skip the newline
85
+ continue;
86
+ }
87
+ // Skip whitespace
88
+ while (offset < data.length && this.isWhitespace(data[offset])) {
89
+ offset++;
90
+ }
91
+ // Read number
92
+ let numStr = "";
93
+ while (offset < data.length &&
94
+ !this.isWhitespace(data[offset]) &&
95
+ data[offset] !== 0x23) {
96
+ numStr += String.fromCharCode(data[offset]);
97
+ offset++;
98
+ }
99
+ if (numStr) {
100
+ const num = parseInt(numStr, 10);
101
+ if (isNaN(num) || num <= 0) {
102
+ throw new Error(`Invalid PPM header value: ${numStr}`);
103
+ }
104
+ if (headerValues === 0) {
105
+ width = num;
106
+ }
107
+ else if (headerValues === 1) {
108
+ height = num;
109
+ }
110
+ else if (headerValues === 2) {
111
+ maxval = num;
112
+ }
113
+ headerValues++;
114
+ }
115
+ }
116
+ if (headerValues < 3) {
117
+ throw new Error("Incomplete PPM header");
118
+ }
119
+ // Skip single whitespace character after maxval (required by spec)
120
+ if (offset < data.length && this.isWhitespace(data[offset])) {
121
+ offset++;
122
+ }
123
+ // Validate dimensions
124
+ validateImageDimensions(width, height);
125
+ // Validate maxval
126
+ if (maxval > 255) {
127
+ throw new Error(`Unsupported PPM maxval: ${maxval}. Only maxval <= 255 is supported.`);
128
+ }
129
+ const pixelCount = width * height;
130
+ const rgba = new Uint8Array(pixelCount * 4);
131
+ if (isBinary) {
132
+ // P6: Binary format
133
+ const expectedDataLength = pixelCount * 3;
134
+ const actualDataLength = data.length - offset;
135
+ if (actualDataLength < expectedDataLength) {
136
+ throw new Error(`Invalid PPM data length: expected ${expectedDataLength}, got ${actualDataLength}`);
137
+ }
138
+ // Read RGB data and convert to RGBA
139
+ for (let i = 0; i < pixelCount; i++) {
140
+ const srcIndex = offset + i * 3;
141
+ const dstIndex = i * 4;
142
+ rgba[dstIndex] = data[srcIndex]; // R
143
+ rgba[dstIndex + 1] = data[srcIndex + 1]; // G
144
+ rgba[dstIndex + 2] = data[srcIndex + 2]; // B
145
+ rgba[dstIndex + 3] = 255; // A (fully opaque)
146
+ }
147
+ }
148
+ else {
149
+ // P3: ASCII format
150
+ let pixelIndex = 0;
151
+ while (pixelIndex < pixelCount * 3 && offset < data.length) {
152
+ // Skip whitespace and comments
153
+ while (offset < data.length) {
154
+ if (data[offset] === 0x23) { // #
155
+ // Skip comment
156
+ while (offset < data.length && data[offset] !== 0x0a) {
157
+ offset++;
158
+ }
159
+ if (offset < data.length)
160
+ offset++;
161
+ }
162
+ else if (this.isWhitespace(data[offset])) {
163
+ offset++;
164
+ }
165
+ else {
166
+ break;
167
+ }
168
+ }
169
+ // Read number
170
+ let numStr = "";
171
+ while (offset < data.length &&
172
+ !this.isWhitespace(data[offset]) &&
173
+ data[offset] !== 0x23) {
174
+ numStr += String.fromCharCode(data[offset]);
175
+ offset++;
176
+ }
177
+ if (numStr) {
178
+ const value = parseInt(numStr, 10);
179
+ if (isNaN(value) || value < 0 || value > maxval) {
180
+ throw new Error(`Invalid PPM pixel value: ${numStr}`);
181
+ }
182
+ // Scale to 0-255 if needed
183
+ const scaledValue = maxval === 255 ? value : Math.round((value * 255) / maxval);
184
+ const component = pixelIndex % 3;
185
+ const rgbaIndex = Math.floor(pixelIndex / 3) * 4;
186
+ if (component === 0) {
187
+ rgba[rgbaIndex] = scaledValue; // R
188
+ }
189
+ else if (component === 1) {
190
+ rgba[rgbaIndex + 1] = scaledValue; // G
191
+ }
192
+ else {
193
+ rgba[rgbaIndex + 2] = scaledValue; // B
194
+ rgba[rgbaIndex + 3] = 255; // A
195
+ }
196
+ pixelIndex++;
197
+ }
198
+ }
199
+ if (pixelIndex < pixelCount * 3) {
200
+ throw new Error(`Incomplete PPM pixel data: expected ${pixelCount * 3} values, got ${pixelIndex}`);
201
+ }
202
+ }
203
+ return Promise.resolve({ width, height, data: rgba });
204
+ }
205
+ /**
206
+ * Encode RGBA image data to PPM format (P6 binary)
207
+ * Note: Alpha channel is ignored as PPM doesn't support transparency
208
+ * @param imageData Image data to encode
209
+ * @returns Encoded PPM image bytes
210
+ */
211
+ encode(imageData) {
212
+ const { width, height, data } = imageData;
213
+ // Validate input
214
+ if (data.length !== width * height * 4) {
215
+ throw new Error(`Data length mismatch: expected ${width * height * 4}, got ${data.length}`);
216
+ }
217
+ // Create header
218
+ const header = `P6\n${width} ${height}\n255\n`;
219
+ const encoder = new TextEncoder();
220
+ const headerBytes = encoder.encode(header);
221
+ // Create output buffer (header + RGB data)
222
+ const pixelCount = width * height;
223
+ const output = new Uint8Array(headerBytes.length + pixelCount * 3);
224
+ // Write header
225
+ output.set(headerBytes, 0);
226
+ // Write RGB pixel data (discard alpha channel)
227
+ let outputOffset = headerBytes.length;
228
+ for (let i = 0; i < pixelCount; i++) {
229
+ const srcIndex = i * 4;
230
+ output[outputOffset++] = data[srcIndex]; // R
231
+ output[outputOffset++] = data[srcIndex + 1]; // G
232
+ output[outputOffset++] = data[srcIndex + 2]; // B
233
+ }
234
+ return Promise.resolve(output);
235
+ }
236
+ /**
237
+ * Check if a byte is whitespace (space, tab, CR, LF)
238
+ */
239
+ isWhitespace(byte) {
240
+ return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
241
+ }
242
+ }
@@ -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