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.
- package/README.md +41 -28
- package/esm/mod.d.ts +4 -1
- package/esm/mod.js +4 -1
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.js +4 -4
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pcx.js +1 -1
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- 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/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.d.ts +30 -0
- package/esm/src/image.js +62 -1
- package/esm/src/utils/byte_utils.d.ts +30 -0
- package/esm/src/utils/byte_utils.js +50 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -2
- package/esm/src/utils/gif_encoder.js +115 -48
- 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 +4 -1
- package/script/mod.js +8 -2
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.js +4 -4
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pcx.js +1 -1
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- 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/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.d.ts +30 -0
- package/script/src/image.js +61 -0
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
- package/script/src/utils/image_processing.d.ts +43 -0
- 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(
|
|
175
|
+
pixelData = encoder.compress(sourceData);
|
|
145
176
|
compressionCode = 5;
|
|
146
177
|
}
|
|
147
178
|
else {
|
|
148
179
|
// Uncompressed
|
|
149
|
-
pixelData =
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
|
194
|
-
this.writeIFDEntry(result, 0x0115, 3, 1,
|
|
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
|
-
|
|
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 (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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);
|
package/script/src/image.d.ts
CHANGED
|
@@ -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
|