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 +35 -28
- package/esm/mod.d.ts +2 -1
- package/esm/mod.js +2 -1
- package/esm/src/formats/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +4 -0
- package/esm/src/formats/tiff.js +163 -44
- package/esm/src/image.d.ts +30 -0
- package/esm/src/image.js +58 -1
- package/esm/src/utils/image_processing.d.ts +43 -0
- package/esm/src/utils/image_processing.js +230 -0
- package/package.json +1 -1
- package/script/mod.d.ts +2 -1
- package/script/mod.js +4 -2
- package/script/src/formats/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +4 -0
- package/script/src/formats/tiff.js +163 -44
- package/script/src/image.d.ts +30 -0
- package/script/src/image.js +57 -0
- package/script/src/utils/image_processing.d.ts +43 -0
- package/script/src/utils/image_processing.js +235 -0
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
|
|
17
|
-
|
|
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
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
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
|
|
119
|
-
|
|
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
|
|
128
|
+
- **[API Reference](https://cross-image.56k.guru/api/)** - Complete API
|
|
124
129
|
documentation
|
|
125
|
-
- **[Examples](https://cross-image.56k.guru/examples
|
|
126
|
-
|
|
127
|
-
- **[Format Support](https://cross-image.56k.guru/formats
|
|
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
|
|
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
|
|
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
|