cross-image 0.2.1 → 0.2.3

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 (44) hide show
  1. package/README.md +160 -32
  2. package/esm/mod.d.ts +2 -1
  3. package/esm/mod.js +2 -1
  4. package/esm/src/formats/jpeg.d.ts +12 -1
  5. package/esm/src/formats/jpeg.js +633 -4
  6. package/esm/src/formats/png_base.d.ts +8 -0
  7. package/esm/src/formats/png_base.js +176 -3
  8. package/esm/src/formats/ppm.d.ts +50 -0
  9. package/esm/src/formats/ppm.js +242 -0
  10. package/esm/src/formats/tiff.d.ts +10 -1
  11. package/esm/src/formats/tiff.js +194 -44
  12. package/esm/src/formats/webp.d.ts +9 -2
  13. package/esm/src/formats/webp.js +211 -62
  14. package/esm/src/image.d.ts +81 -0
  15. package/esm/src/image.js +282 -5
  16. package/esm/src/types.d.ts +41 -1
  17. package/esm/src/utils/image_processing.d.ts +98 -0
  18. package/esm/src/utils/image_processing.js +440 -0
  19. package/esm/src/utils/metadata/xmp.d.ts +52 -0
  20. package/esm/src/utils/metadata/xmp.js +325 -0
  21. package/esm/src/utils/resize.d.ts +4 -0
  22. package/esm/src/utils/resize.js +74 -0
  23. package/package.json +1 -1
  24. package/script/mod.d.ts +2 -1
  25. package/script/mod.js +4 -2
  26. package/script/src/formats/jpeg.d.ts +12 -1
  27. package/script/src/formats/jpeg.js +633 -4
  28. package/script/src/formats/png_base.d.ts +8 -0
  29. package/script/src/formats/png_base.js +176 -3
  30. package/script/src/formats/ppm.d.ts +50 -0
  31. package/script/src/formats/ppm.js +246 -0
  32. package/script/src/formats/tiff.d.ts +10 -1
  33. package/script/src/formats/tiff.js +194 -44
  34. package/script/src/formats/webp.d.ts +9 -2
  35. package/script/src/formats/webp.js +211 -62
  36. package/script/src/image.d.ts +81 -0
  37. package/script/src/image.js +280 -3
  38. package/script/src/types.d.ts +41 -1
  39. package/script/src/utils/image_processing.d.ts +98 -0
  40. package/script/src/utils/image_processing.js +451 -0
  41. package/script/src/utils/metadata/xmp.d.ts +52 -0
  42. package/script/src/utils/metadata/xmp.js +333 -0
  43. package/script/src/utils/resize.d.ts +4 -0
  44. package/script/src/utils/resize.js +75 -0
@@ -1,5 +1,6 @@
1
1
  import { validateImageDimensions } from "../utils/security.js";
2
2
  import { readUint32LE } from "../utils/byte_utils.js";
3
+ import { createXMP, parseXMP } from "../utils/metadata/xmp.js";
3
4
  // Default quality for WebP encoding when not specified
4
5
  const DEFAULT_WEBP_QUALITY = 90;
5
6
  /**
@@ -219,6 +220,7 @@ export class WebPFormat {
219
220
  const numEntries = littleEndian
220
221
  ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
221
222
  : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
223
+ let gpsIfdOffset = 0;
222
224
  // Parse basic EXIF tags (simplified version)
223
225
  for (let i = 0; i < numEntries && i < 50; i++) {
224
226
  const entryOffset = ifd0Offset + 2 + i * 12;
@@ -245,32 +247,103 @@ export class WebPFormat {
245
247
  }
246
248
  }
247
249
  }
250
+ // GPS IFD Pointer tag (0x8825)
251
+ if (tag === 0x8825) {
252
+ gpsIfdOffset = littleEndian
253
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
254
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
255
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
256
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
257
+ }
258
+ }
259
+ // Parse GPS IFD if present
260
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
261
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
248
262
  }
249
263
  }
250
264
  catch (_e) {
251
265
  // Ignore EXIF parsing errors
252
266
  }
253
267
  }
268
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
269
+ try {
270
+ const numEntries = littleEndian
271
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
272
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
273
+ let latRef = "";
274
+ let lonRef = "";
275
+ let latitude;
276
+ let longitude;
277
+ for (let i = 0; i < numEntries; i++) {
278
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
279
+ if (entryOffset + 12 > data.length)
280
+ break;
281
+ const tag = littleEndian
282
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
283
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
284
+ const type = littleEndian
285
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
286
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
287
+ const valueOffset = littleEndian
288
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
289
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
290
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
291
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
292
+ // GPSLatitudeRef (0x0001)
293
+ if (tag === 0x0001 && type === 2) {
294
+ latRef = String.fromCharCode(data[entryOffset + 8]);
295
+ }
296
+ // GPSLatitude (0x0002)
297
+ if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
298
+ const degrees = this.readRational(data, valueOffset, littleEndian);
299
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
300
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
301
+ latitude = degrees + minutes / 60 + seconds / 3600;
302
+ }
303
+ // GPSLongitudeRef (0x0003)
304
+ if (tag === 0x0003 && type === 2) {
305
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
306
+ }
307
+ // GPSLongitude (0x0004)
308
+ if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
309
+ const degrees = this.readRational(data, valueOffset, littleEndian);
310
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
311
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
312
+ longitude = degrees + minutes / 60 + seconds / 3600;
313
+ }
314
+ }
315
+ // Apply hemisphere references
316
+ if (latitude !== undefined && latRef) {
317
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
318
+ }
319
+ if (longitude !== undefined && lonRef) {
320
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
321
+ }
322
+ }
323
+ catch (_e) {
324
+ // Ignore GPS parsing errors
325
+ }
326
+ }
327
+ readRational(data, offset, littleEndian) {
328
+ const numerator = littleEndian
329
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
330
+ (data[offset + 3] << 24)
331
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
332
+ (data[offset + 2] << 8) | data[offset + 3];
333
+ const denominator = littleEndian
334
+ ? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
335
+ (data[offset + 7] << 24)
336
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
337
+ (data[offset + 6] << 8) | data[offset + 7];
338
+ return denominator !== 0 ? numerator / denominator : 0;
339
+ }
254
340
  parseXMP(data, metadata) {
255
- // XMP is XML-based metadata - simple parsing for common fields
341
+ // Parse XMP using the centralized utility
256
342
  try {
257
343
  const xmpStr = new TextDecoder().decode(data);
258
- // Extract title
259
- const titleMatch = xmpStr.match(/<dc:title[^>]*>([^<]+)<\/dc:title>/);
260
- if (titleMatch)
261
- metadata.title = titleMatch[1].trim();
262
- // Extract description
263
- const descMatch = xmpStr.match(/<dc:description[^>]*>([^<]+)<\/dc:description>/);
264
- if (descMatch)
265
- metadata.description = descMatch[1].trim();
266
- // Extract creator/author
267
- const creatorMatch = xmpStr.match(/<dc:creator[^>]*>([^<]+)<\/dc:creator>/);
268
- if (creatorMatch)
269
- metadata.author = creatorMatch[1].trim();
270
- // Extract rights/copyright
271
- const rightsMatch = xmpStr.match(/<dc:rights[^>]*>([^<]+)<\/dc:rights>/);
272
- if (rightsMatch)
273
- metadata.copyright = rightsMatch[1].trim();
344
+ const parsedMetadata = parseXMP(xmpStr);
345
+ // Merge parsed metadata into the existing metadata object
346
+ Object.assign(metadata, parsedMetadata);
274
347
  }
275
348
  catch (_e) {
276
349
  // Ignore XMP parsing errors
@@ -284,8 +357,9 @@ export class WebPFormat {
284
357
  chunks.push(webpData.slice(0, 12));
285
358
  // Create metadata chunks
286
359
  const metadataChunks = [];
287
- // Create EXIF chunk if we have date or other EXIF data
288
- if (metadata.creationDate) {
360
+ // Create EXIF chunk if we have date or GPS data
361
+ if (metadata.creationDate ||
362
+ (metadata.latitude !== undefined && metadata.longitude !== undefined)) {
289
363
  const exifData = this.createEXIFChunk(metadata);
290
364
  if (exifData) {
291
365
  metadataChunks.push(exifData);
@@ -341,7 +415,10 @@ export class WebPFormat {
341
415
  return finalData;
342
416
  }
343
417
  createEXIFChunk(metadata) {
344
- if (!metadata.creationDate)
418
+ const hasDate = metadata.creationDate !== undefined;
419
+ const hasGPS = metadata.latitude !== undefined &&
420
+ metadata.longitude !== undefined;
421
+ if (!hasDate && !hasGPS)
345
422
  return null;
346
423
  const exifData = [];
347
424
  // Byte order marker (little endian)
@@ -349,21 +426,47 @@ export class WebPFormat {
349
426
  exifData.push(0x2a, 0x00); // 42
350
427
  // IFD0 offset
351
428
  exifData.push(0x08, 0x00, 0x00, 0x00);
352
- // Number of entries
353
- exifData.push(0x01, 0x00);
354
- // DateTime entry
355
- const date = metadata.creationDate;
356
- const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
357
- const dateBytes = new TextEncoder().encode(dateStr);
358
- // Tag 0x0132, Type 2 (ASCII), Count, Offset
359
- exifData.push(0x32, 0x01, 0x02, 0x00);
360
- exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
361
- exifData.push(0x12, 0x00, 0x00, 0x00); // Offset to data
429
+ // Number of entries (DateTime + GPS IFD pointer if needed)
430
+ const numEntries = (hasDate ? 1 : 0) + (hasGPS ? 1 : 0);
431
+ exifData.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
432
+ let dataOffset = 8 + 2 + numEntries * 12 + 4;
433
+ // DateTime entry (if present)
434
+ if (hasDate) {
435
+ const date = metadata.creationDate;
436
+ const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
437
+ const dateBytes = new TextEncoder().encode(dateStr);
438
+ // Tag 0x0132, Type 2 (ASCII), Count, Offset
439
+ exifData.push(0x32, 0x01, 0x02, 0x00);
440
+ exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
441
+ exifData.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
442
+ dataOffset += dateBytes.length;
443
+ }
444
+ // GPS IFD pointer (if present)
445
+ let gpsIfdOffset = 0;
446
+ if (hasGPS) {
447
+ gpsIfdOffset = dataOffset;
448
+ exifData.push(0x25, 0x88); // Tag 0x8825
449
+ exifData.push(0x04, 0x00); // Type LONG
450
+ exifData.push(0x01, 0x00, 0x00, 0x00); // Count 1
451
+ exifData.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
452
+ }
362
453
  // Next IFD
363
454
  exifData.push(0x00, 0x00, 0x00, 0x00);
364
- // Date string data
365
- for (const byte of dateBytes) {
366
- exifData.push(byte);
455
+ // Date string data (if present)
456
+ if (hasDate) {
457
+ const date = metadata.creationDate;
458
+ const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
459
+ const dateBytes = new TextEncoder().encode(dateStr);
460
+ for (const byte of dateBytes) {
461
+ exifData.push(byte);
462
+ }
463
+ }
464
+ // GPS IFD data (if present)
465
+ if (hasGPS) {
466
+ const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
467
+ for (const byte of gpsIfd) {
468
+ exifData.push(byte);
469
+ }
367
470
  }
368
471
  // Create chunk header
369
472
  const chunkData = new Uint8Array(exifData);
@@ -376,29 +479,59 @@ export class WebPFormat {
376
479
  chunk.set(chunkData, 8);
377
480
  return chunk;
378
481
  }
482
+ createGPSIFD(metadata, gpsIfdStart) {
483
+ const gps = [];
484
+ const numEntries = 4;
485
+ gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
486
+ const latitude = metadata.latitude;
487
+ const longitude = metadata.longitude;
488
+ const absLat = Math.abs(latitude);
489
+ const absLon = Math.abs(longitude);
490
+ const latDeg = Math.floor(absLat);
491
+ const latMin = Math.floor((absLat - latDeg) * 60);
492
+ const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
493
+ const lonDeg = Math.floor(absLon);
494
+ const lonMin = Math.floor((absLon - lonDeg) * 60);
495
+ const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
496
+ let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
497
+ // GPSLatitudeRef
498
+ gps.push(0x01, 0x00);
499
+ gps.push(0x02, 0x00);
500
+ gps.push(0x02, 0x00, 0x00, 0x00);
501
+ gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
502
+ // GPSLatitude
503
+ gps.push(0x02, 0x00);
504
+ gps.push(0x05, 0x00);
505
+ gps.push(0x03, 0x00, 0x00, 0x00);
506
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
507
+ dataOffset += 24;
508
+ // GPSLongitudeRef
509
+ gps.push(0x03, 0x00);
510
+ gps.push(0x02, 0x00);
511
+ gps.push(0x02, 0x00, 0x00, 0x00);
512
+ gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
513
+ // GPSLongitude
514
+ gps.push(0x04, 0x00);
515
+ gps.push(0x05, 0x00);
516
+ gps.push(0x03, 0x00, 0x00, 0x00);
517
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
518
+ gps.push(0x00, 0x00, 0x00, 0x00);
519
+ // Write rationals
520
+ this.writeRational(gps, latDeg, 1);
521
+ this.writeRational(gps, latMin, 1);
522
+ this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
523
+ this.writeRational(gps, lonDeg, 1);
524
+ this.writeRational(gps, lonMin, 1);
525
+ this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
526
+ return gps;
527
+ }
528
+ writeRational(output, numerator, denominator) {
529
+ output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
530
+ output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
531
+ }
379
532
  createXMPChunk(metadata) {
380
- const xmpParts = [];
381
- xmpParts.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
382
- xmpParts.push('<x:xmpmeta xmlns:x="adobe:ns:meta/">');
383
- xmpParts.push('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
384
- xmpParts.push('<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">');
385
- if (metadata.title) {
386
- xmpParts.push(`<dc:title>${this.escapeXML(metadata.title)}</dc:title>`);
387
- }
388
- if (metadata.description) {
389
- xmpParts.push(`<dc:description>${this.escapeXML(metadata.description)}</dc:description>`);
390
- }
391
- if (metadata.author) {
392
- xmpParts.push(`<dc:creator>${this.escapeXML(metadata.author)}</dc:creator>`);
393
- }
394
- if (metadata.copyright) {
395
- xmpParts.push(`<dc:rights>${this.escapeXML(metadata.copyright)}</dc:rights>`);
396
- }
397
- xmpParts.push("</rdf:Description>");
398
- xmpParts.push("</rdf:RDF>");
399
- xmpParts.push("</x:xmpmeta>");
400
- xmpParts.push('<?xpacket end="w"?>');
401
- const xmpStr = xmpParts.join("\n");
533
+ // Use the centralized XMP utility to create the XMP packet
534
+ const xmpStr = createXMP(metadata);
402
535
  const xmpData = new TextEncoder().encode(xmpStr);
403
536
  // Create chunk
404
537
  const chunk = new Uint8Array(8 + xmpData.length);
@@ -410,12 +543,28 @@ export class WebPFormat {
410
543
  chunk.set(xmpData, 8);
411
544
  return chunk;
412
545
  }
413
- escapeXML(str) {
414
- return str
415
- .replace(/&/g, "&amp;")
416
- .replace(/</g, "&lt;")
417
- .replace(/>/g, "&gt;")
418
- .replace(/"/g, "&quot;")
419
- .replace(/'/g, "&apos;");
546
+ /**
547
+ * Get the list of metadata fields supported by WebP format
548
+ */
549
+ getSupportedMetadata() {
550
+ return [
551
+ // EXIF chunk
552
+ "creationDate",
553
+ "latitude",
554
+ "longitude",
555
+ // XMP chunk (enhanced support)
556
+ "title",
557
+ "description",
558
+ "author",
559
+ "copyright",
560
+ "cameraMake",
561
+ "cameraModel",
562
+ "orientation",
563
+ "software",
564
+ "iso",
565
+ "exposureTime",
566
+ "fNumber",
567
+ "focalLength",
568
+ ];
420
569
  }
421
570
  }
@@ -79,6 +79,12 @@ export declare class Image {
79
79
  * @returns Image instance
80
80
  */
81
81
  static decode(data: Uint8Array, format?: string): Promise<Image>;
82
+ /**
83
+ * Get supported metadata fields for a specific format
84
+ * @param format Format name (e.g., "jpeg", "png", "webp")
85
+ * @returns Array of supported metadata field names, or undefined if format doesn't support metadata
86
+ */
87
+ static getSupportedMetadata(format: string): Array<keyof ImageMetadata> | undefined;
82
88
  /**
83
89
  * Read an image from bytes
84
90
  * @deprecated Use `decode()` instead. This method will be removed in a future version.
@@ -197,6 +203,12 @@ export declare class Image {
197
203
  * @returns This image instance for chaining
198
204
  */
199
205
  saturation(amount: number): this;
206
+ /**
207
+ * Adjust hue of the image by rotating the color wheel
208
+ * @param degrees Hue rotation in degrees (any value accepted, wraps at 360)
209
+ * @returns This image instance for chaining
210
+ */
211
+ hue(degrees: number): this;
200
212
  /**
201
213
  * Invert colors of the image
202
214
  * @returns This image instance for chaining
@@ -207,6 +219,36 @@ export declare class Image {
207
219
  * @returns This image instance for chaining
208
220
  */
209
221
  grayscale(): this;
222
+ /**
223
+ * Apply sepia tone effect to the image
224
+ * @returns This image instance for chaining
225
+ */
226
+ sepia(): this;
227
+ /**
228
+ * Apply box blur filter to the image
229
+ * @param radius Blur radius (default: 1)
230
+ * @returns This image instance for chaining
231
+ */
232
+ blur(radius?: number): this;
233
+ /**
234
+ * Apply Gaussian blur filter to the image
235
+ * @param radius Blur radius (default: 1)
236
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
237
+ * @returns This image instance for chaining
238
+ */
239
+ gaussianBlur(radius?: number, sigma?: number): this;
240
+ /**
241
+ * Apply sharpen filter to the image
242
+ * @param amount Sharpening amount (0-1, default: 0.5)
243
+ * @returns This image instance for chaining
244
+ */
245
+ sharpen(amount?: number): this;
246
+ /**
247
+ * Apply median filter to reduce noise
248
+ * @param radius Filter radius (default: 1)
249
+ * @returns This image instance for chaining
250
+ */
251
+ medianFilter(radius?: number): this;
210
252
  /**
211
253
  * Fill a rectangular region with a color
212
254
  * @param x Starting X position
@@ -252,5 +294,44 @@ export declare class Image {
252
294
  * @returns This image instance for chaining
253
295
  */
254
296
  setPixel(x: number, y: number, r: number, g: number, b: number, a?: number): this;
297
+ /**
298
+ * Rotate the image 90 degrees clockwise
299
+ * @returns This image instance for chaining
300
+ */
301
+ rotate90(): this;
302
+ /**
303
+ * Rotate the image 180 degrees
304
+ * @returns This image instance for chaining
305
+ */
306
+ rotate180(): this;
307
+ /**
308
+ * Rotate the image 270 degrees clockwise (or 90 degrees counter-clockwise)
309
+ * @returns This image instance for chaining
310
+ */
311
+ rotate270(): this;
312
+ /**
313
+ * Rotate the image by the specified angle in degrees
314
+ * @param degrees Rotation angle in degrees (positive = clockwise, negative = counter-clockwise)
315
+ * @returns This image instance for chaining
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * image.rotate(90); // Rotate 90° clockwise
320
+ * image.rotate(-90); // Rotate 90° counter-clockwise
321
+ * image.rotate(180); // Rotate 180°
322
+ * image.rotate(45); // Rotate 45° clockwise (rounded to nearest 90°)
323
+ * ```
324
+ */
325
+ rotate(degrees: number): this;
326
+ /**
327
+ * Flip the image horizontally (mirror)
328
+ * @returns This image instance for chaining
329
+ */
330
+ flipHorizontal(): this;
331
+ /**
332
+ * Flip the image vertically
333
+ * @returns This image instance for chaining
334
+ */
335
+ flipVertical(): this;
255
336
  }
256
337
  //# sourceMappingURL=image.d.ts.map