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
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.WebPFormat = void 0;
37
37
  const security_js_1 = require("../utils/security.js");
38
38
  const byte_utils_js_1 = require("../utils/byte_utils.js");
39
+ const xmp_js_1 = require("../utils/metadata/xmp.js");
39
40
  // Default quality for WebP encoding when not specified
40
41
  const DEFAULT_WEBP_QUALITY = 90;
41
42
  /**
@@ -255,6 +256,7 @@ class WebPFormat {
255
256
  const numEntries = littleEndian
256
257
  ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
257
258
  : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
259
+ let gpsIfdOffset = 0;
258
260
  // Parse basic EXIF tags (simplified version)
259
261
  for (let i = 0; i < numEntries && i < 50; i++) {
260
262
  const entryOffset = ifd0Offset + 2 + i * 12;
@@ -281,32 +283,103 @@ class WebPFormat {
281
283
  }
282
284
  }
283
285
  }
286
+ // GPS IFD Pointer tag (0x8825)
287
+ if (tag === 0x8825) {
288
+ gpsIfdOffset = littleEndian
289
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
290
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
291
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
292
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
293
+ }
294
+ }
295
+ // Parse GPS IFD if present
296
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
297
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
284
298
  }
285
299
  }
286
300
  catch (_e) {
287
301
  // Ignore EXIF parsing errors
288
302
  }
289
303
  }
304
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
305
+ try {
306
+ const numEntries = littleEndian
307
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
308
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
309
+ let latRef = "";
310
+ let lonRef = "";
311
+ let latitude;
312
+ let longitude;
313
+ for (let i = 0; i < numEntries; i++) {
314
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
315
+ if (entryOffset + 12 > data.length)
316
+ break;
317
+ const tag = littleEndian
318
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
319
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
320
+ const type = littleEndian
321
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
322
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
323
+ const valueOffset = littleEndian
324
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
325
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
326
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
327
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
328
+ // GPSLatitudeRef (0x0001)
329
+ if (tag === 0x0001 && type === 2) {
330
+ latRef = String.fromCharCode(data[entryOffset + 8]);
331
+ }
332
+ // GPSLatitude (0x0002)
333
+ if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
334
+ const degrees = this.readRational(data, valueOffset, littleEndian);
335
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
336
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
337
+ latitude = degrees + minutes / 60 + seconds / 3600;
338
+ }
339
+ // GPSLongitudeRef (0x0003)
340
+ if (tag === 0x0003 && type === 2) {
341
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
342
+ }
343
+ // GPSLongitude (0x0004)
344
+ if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
345
+ const degrees = this.readRational(data, valueOffset, littleEndian);
346
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
347
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
348
+ longitude = degrees + minutes / 60 + seconds / 3600;
349
+ }
350
+ }
351
+ // Apply hemisphere references
352
+ if (latitude !== undefined && latRef) {
353
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
354
+ }
355
+ if (longitude !== undefined && lonRef) {
356
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
357
+ }
358
+ }
359
+ catch (_e) {
360
+ // Ignore GPS parsing errors
361
+ }
362
+ }
363
+ readRational(data, offset, littleEndian) {
364
+ const numerator = littleEndian
365
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
366
+ (data[offset + 3] << 24)
367
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
368
+ (data[offset + 2] << 8) | data[offset + 3];
369
+ const denominator = littleEndian
370
+ ? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
371
+ (data[offset + 7] << 24)
372
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
373
+ (data[offset + 6] << 8) | data[offset + 7];
374
+ return denominator !== 0 ? numerator / denominator : 0;
375
+ }
290
376
  parseXMP(data, metadata) {
291
- // XMP is XML-based metadata - simple parsing for common fields
377
+ // Parse XMP using the centralized utility
292
378
  try {
293
379
  const xmpStr = new TextDecoder().decode(data);
294
- // Extract title
295
- const titleMatch = xmpStr.match(/<dc:title[^>]*>([^<]+)<\/dc:title>/);
296
- if (titleMatch)
297
- metadata.title = titleMatch[1].trim();
298
- // Extract description
299
- const descMatch = xmpStr.match(/<dc:description[^>]*>([^<]+)<\/dc:description>/);
300
- if (descMatch)
301
- metadata.description = descMatch[1].trim();
302
- // Extract creator/author
303
- const creatorMatch = xmpStr.match(/<dc:creator[^>]*>([^<]+)<\/dc:creator>/);
304
- if (creatorMatch)
305
- metadata.author = creatorMatch[1].trim();
306
- // Extract rights/copyright
307
- const rightsMatch = xmpStr.match(/<dc:rights[^>]*>([^<]+)<\/dc:rights>/);
308
- if (rightsMatch)
309
- metadata.copyright = rightsMatch[1].trim();
380
+ const parsedMetadata = (0, xmp_js_1.parseXMP)(xmpStr);
381
+ // Merge parsed metadata into the existing metadata object
382
+ Object.assign(metadata, parsedMetadata);
310
383
  }
311
384
  catch (_e) {
312
385
  // Ignore XMP parsing errors
@@ -320,8 +393,9 @@ class WebPFormat {
320
393
  chunks.push(webpData.slice(0, 12));
321
394
  // Create metadata chunks
322
395
  const metadataChunks = [];
323
- // Create EXIF chunk if we have date or other EXIF data
324
- if (metadata.creationDate) {
396
+ // Create EXIF chunk if we have date or GPS data
397
+ if (metadata.creationDate ||
398
+ (metadata.latitude !== undefined && metadata.longitude !== undefined)) {
325
399
  const exifData = this.createEXIFChunk(metadata);
326
400
  if (exifData) {
327
401
  metadataChunks.push(exifData);
@@ -377,7 +451,10 @@ class WebPFormat {
377
451
  return finalData;
378
452
  }
379
453
  createEXIFChunk(metadata) {
380
- if (!metadata.creationDate)
454
+ const hasDate = metadata.creationDate !== undefined;
455
+ const hasGPS = metadata.latitude !== undefined &&
456
+ metadata.longitude !== undefined;
457
+ if (!hasDate && !hasGPS)
381
458
  return null;
382
459
  const exifData = [];
383
460
  // Byte order marker (little endian)
@@ -385,21 +462,47 @@ class WebPFormat {
385
462
  exifData.push(0x2a, 0x00); // 42
386
463
  // IFD0 offset
387
464
  exifData.push(0x08, 0x00, 0x00, 0x00);
388
- // Number of entries
389
- exifData.push(0x01, 0x00);
390
- // DateTime entry
391
- const date = metadata.creationDate;
392
- 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`;
393
- const dateBytes = new TextEncoder().encode(dateStr);
394
- // Tag 0x0132, Type 2 (ASCII), Count, Offset
395
- exifData.push(0x32, 0x01, 0x02, 0x00);
396
- exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
397
- exifData.push(0x12, 0x00, 0x00, 0x00); // Offset to data
465
+ // Number of entries (DateTime + GPS IFD pointer if needed)
466
+ const numEntries = (hasDate ? 1 : 0) + (hasGPS ? 1 : 0);
467
+ exifData.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
468
+ let dataOffset = 8 + 2 + numEntries * 12 + 4;
469
+ // DateTime entry (if present)
470
+ if (hasDate) {
471
+ const date = metadata.creationDate;
472
+ 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`;
473
+ const dateBytes = new TextEncoder().encode(dateStr);
474
+ // Tag 0x0132, Type 2 (ASCII), Count, Offset
475
+ exifData.push(0x32, 0x01, 0x02, 0x00);
476
+ exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
477
+ exifData.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
478
+ dataOffset += dateBytes.length;
479
+ }
480
+ // GPS IFD pointer (if present)
481
+ let gpsIfdOffset = 0;
482
+ if (hasGPS) {
483
+ gpsIfdOffset = dataOffset;
484
+ exifData.push(0x25, 0x88); // Tag 0x8825
485
+ exifData.push(0x04, 0x00); // Type LONG
486
+ exifData.push(0x01, 0x00, 0x00, 0x00); // Count 1
487
+ exifData.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
488
+ }
398
489
  // Next IFD
399
490
  exifData.push(0x00, 0x00, 0x00, 0x00);
400
- // Date string data
401
- for (const byte of dateBytes) {
402
- exifData.push(byte);
491
+ // Date string data (if present)
492
+ if (hasDate) {
493
+ const date = metadata.creationDate;
494
+ 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`;
495
+ const dateBytes = new TextEncoder().encode(dateStr);
496
+ for (const byte of dateBytes) {
497
+ exifData.push(byte);
498
+ }
499
+ }
500
+ // GPS IFD data (if present)
501
+ if (hasGPS) {
502
+ const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
503
+ for (const byte of gpsIfd) {
504
+ exifData.push(byte);
505
+ }
403
506
  }
404
507
  // Create chunk header
405
508
  const chunkData = new Uint8Array(exifData);
@@ -412,29 +515,59 @@ class WebPFormat {
412
515
  chunk.set(chunkData, 8);
413
516
  return chunk;
414
517
  }
518
+ createGPSIFD(metadata, gpsIfdStart) {
519
+ const gps = [];
520
+ const numEntries = 4;
521
+ gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
522
+ const latitude = metadata.latitude;
523
+ const longitude = metadata.longitude;
524
+ const absLat = Math.abs(latitude);
525
+ const absLon = Math.abs(longitude);
526
+ const latDeg = Math.floor(absLat);
527
+ const latMin = Math.floor((absLat - latDeg) * 60);
528
+ const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
529
+ const lonDeg = Math.floor(absLon);
530
+ const lonMin = Math.floor((absLon - lonDeg) * 60);
531
+ const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
532
+ let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
533
+ // GPSLatitudeRef
534
+ gps.push(0x01, 0x00);
535
+ gps.push(0x02, 0x00);
536
+ gps.push(0x02, 0x00, 0x00, 0x00);
537
+ gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
538
+ // GPSLatitude
539
+ gps.push(0x02, 0x00);
540
+ gps.push(0x05, 0x00);
541
+ gps.push(0x03, 0x00, 0x00, 0x00);
542
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
543
+ dataOffset += 24;
544
+ // GPSLongitudeRef
545
+ gps.push(0x03, 0x00);
546
+ gps.push(0x02, 0x00);
547
+ gps.push(0x02, 0x00, 0x00, 0x00);
548
+ gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
549
+ // GPSLongitude
550
+ gps.push(0x04, 0x00);
551
+ gps.push(0x05, 0x00);
552
+ gps.push(0x03, 0x00, 0x00, 0x00);
553
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
554
+ gps.push(0x00, 0x00, 0x00, 0x00);
555
+ // Write rationals
556
+ this.writeRational(gps, latDeg, 1);
557
+ this.writeRational(gps, latMin, 1);
558
+ this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
559
+ this.writeRational(gps, lonDeg, 1);
560
+ this.writeRational(gps, lonMin, 1);
561
+ this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
562
+ return gps;
563
+ }
564
+ writeRational(output, numerator, denominator) {
565
+ output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
566
+ output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
567
+ }
415
568
  createXMPChunk(metadata) {
416
- const xmpParts = [];
417
- xmpParts.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
418
- xmpParts.push('<x:xmpmeta xmlns:x="adobe:ns:meta/">');
419
- xmpParts.push('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
420
- xmpParts.push('<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">');
421
- if (metadata.title) {
422
- xmpParts.push(`<dc:title>${this.escapeXML(metadata.title)}</dc:title>`);
423
- }
424
- if (metadata.description) {
425
- xmpParts.push(`<dc:description>${this.escapeXML(metadata.description)}</dc:description>`);
426
- }
427
- if (metadata.author) {
428
- xmpParts.push(`<dc:creator>${this.escapeXML(metadata.author)}</dc:creator>`);
429
- }
430
- if (metadata.copyright) {
431
- xmpParts.push(`<dc:rights>${this.escapeXML(metadata.copyright)}</dc:rights>`);
432
- }
433
- xmpParts.push("</rdf:Description>");
434
- xmpParts.push("</rdf:RDF>");
435
- xmpParts.push("</x:xmpmeta>");
436
- xmpParts.push('<?xpacket end="w"?>');
437
- const xmpStr = xmpParts.join("\n");
569
+ // Use the centralized XMP utility to create the XMP packet
570
+ const xmpStr = (0, xmp_js_1.createXMP)(metadata);
438
571
  const xmpData = new TextEncoder().encode(xmpStr);
439
572
  // Create chunk
440
573
  const chunk = new Uint8Array(8 + xmpData.length);
@@ -446,13 +579,29 @@ class WebPFormat {
446
579
  chunk.set(xmpData, 8);
447
580
  return chunk;
448
581
  }
449
- escapeXML(str) {
450
- return str
451
- .replace(/&/g, "&amp;")
452
- .replace(/</g, "&lt;")
453
- .replace(/>/g, "&gt;")
454
- .replace(/"/g, "&quot;")
455
- .replace(/'/g, "&apos;");
582
+ /**
583
+ * Get the list of metadata fields supported by WebP format
584
+ */
585
+ getSupportedMetadata() {
586
+ return [
587
+ // EXIF chunk
588
+ "creationDate",
589
+ "latitude",
590
+ "longitude",
591
+ // XMP chunk (enhanced support)
592
+ "title",
593
+ "description",
594
+ "author",
595
+ "copyright",
596
+ "cameraMake",
597
+ "cameraModel",
598
+ "orientation",
599
+ "software",
600
+ "iso",
601
+ "exposureTime",
602
+ "fNumber",
603
+ "focalLength",
604
+ ];
456
605
  }
457
606
  }
458
607
  exports.WebPFormat = WebPFormat;
@@ -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