Support pasp, clap, irot, imir metadata for encode/decode

Fixes: #41
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f013e5d..0ab20d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 - Update default rav1e version to v0.3.1
 - `avifRGBImage` structure and associated routines (BREAKING CHANGE)
 - avifImage alphaRange support
+- Support pasp, clap, irot, imir metadata for encode/decode
 
 ### Changed
 - Large RGB conversion refactor (BREAKING CHANGE), see README for new examples
diff --git a/apps/avifenc.c b/apps/avifenc.c
index fe428bd..9c4fa46 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -21,31 +21,35 @@
 {
     printf("Syntax: avifenc [options] input.y4m output.avif\n");
     printf("Options:\n");
-    printf("    -h,--help         : Show syntax help\n");
-    printf("    -j,--jobs J       : Number of jobs (worker threads, default: 1)\n");
-    printf("    -n,--nclx P/T/M/R : Set nclx colr box values (4 raw numbers)\n");
-    printf("                        P = enum avifNclxColourPrimaries\n");
-    printf("                        T = enum avifNclxTransferCharacteristics\n");
-    printf("                        M = enum avifNclxMatrixCoefficients\n");
-    printf("                        R = avifNclxRangeFlag (any nonzero value becomes AVIF_NCLX_FULL_RANGE)\n");
-    printf("    --min Q           : Set min quantizer for color (%d-%d, where %d is lossless)\n",
+    printf("    -h,--help                         : Show syntax help\n");
+    printf("    -j,--jobs J                       : Number of jobs (worker threads, default: 1)\n");
+    printf("    -n,--nclx P/T/M/R                 : Set nclx colr box values (4 raw numbers)\n");
+    printf("                                        P = enum avifNclxColourPrimaries\n");
+    printf("                                        T = enum avifNclxTransferCharacteristics\n");
+    printf("                                        M = enum avifNclxMatrixCoefficients\n");
+    printf("                                        R = avifNclxRangeFlag (any nonzero value becomes AVIF_NCLX_FULL_RANGE)\n");
+    printf("    --min Q                           : Set min quantizer for color (%d-%d, where %d is lossless)\n",
            AVIF_QUANTIZER_BEST_QUALITY,
            AVIF_QUANTIZER_WORST_QUALITY,
            AVIF_QUANTIZER_LOSSLESS);
-    printf("    --max Q           : Set max quantizer for color (%d-%d, where %d is lossless)\n",
+    printf("    --max Q                           : Set max quantizer for color (%d-%d, where %d is lossless)\n",
            AVIF_QUANTIZER_BEST_QUALITY,
            AVIF_QUANTIZER_WORST_QUALITY,
            AVIF_QUANTIZER_LOSSLESS);
-    printf("    --minalpha Q      : Set min quantizer for alpha (%d-%d, where %d is lossless)\n",
+    printf("    --minalpha Q                      : Set min quantizer for alpha (%d-%d, where %d is lossless)\n",
            AVIF_QUANTIZER_BEST_QUALITY,
            AVIF_QUANTIZER_WORST_QUALITY,
            AVIF_QUANTIZER_LOSSLESS);
-    printf("    --maxalpha Q      : Set max quantizer for alpha (%d-%d, where %d is lossless)\n",
+    printf("    --maxalpha Q                      : Set max quantizer for alpha (%d-%d, where %d is lossless)\n",
            AVIF_QUANTIZER_BEST_QUALITY,
            AVIF_QUANTIZER_WORST_QUALITY,
            AVIF_QUANTIZER_LOSSLESS);
-    printf("    -s,--speed S      : Encoder speed (%d-%d, slowest to fastest)\n", AVIF_SPEED_SLOWEST, AVIF_SPEED_FASTEST);
-    printf("    -c,--codec C      : AV1 codec to use (choose from versions list below)\n");
+    printf("    -s,--speed S                      : Encoder speed (%d-%d, slowest to fastest)\n", AVIF_SPEED_SLOWEST, AVIF_SPEED_FASTEST);
+    printf("    -c,--codec C                      : AV1 codec to use (choose from versions list below)\n");
+    printf("    --pasp H,V                        : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n");
+    printf("    --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in num/denom pairs)\n");
+    printf("    --irot ANGLE                      : Add irot property (rotation). [0-3], makes (90 * ANGLE) degree rotation anti-clockwise\n");
+    printf("    --imir AXIS                       : Add imir property (mirroring). 0=vertical, 1=horizontal\n");
     printf("\n");
     avifPrintVersions();
     return 0;
@@ -98,6 +102,27 @@
     return AVIF_FALSE;
 }
 
+// Returns the count of uint32_t (up to 8)
+static int parseU32List(uint32_t output[8], const char * arg)
+{
+    char buffer[128];
+    strncpy(buffer, arg, 127);
+    buffer[127] = 0;
+
+    int index = 0;
+    char * token = strtok(buffer, ",");
+    while (token != NULL) {
+        output[index] = (uint32_t)atoi(token);
+        ++index;
+        if (index >= 8) {
+            break;
+        }
+
+        token = strtok(NULL, ",");
+    }
+    return index;
+}
+
 int main(int argc, char * argv[])
 {
     const char * inputFilename = NULL;
@@ -113,6 +138,12 @@
     int minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
     int maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
     int speed = AVIF_SPEED_DEFAULT;
+    int paspCount = 0;
+    uint32_t paspValues[8]; // only the first two are used
+    int clapCount = 0;
+    uint32_t clapValues[8];
+    uint8_t irotAngle = 0xff; // sentinel value indicating "unused"
+    uint8_t imirAxis = 0xff;  // sentinel value indicating "unused"
     avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO;
     avifBool nclxSet = AVIF_FALSE;
     avifEncoder * encoder = NULL;
@@ -196,6 +227,34 @@
                     return 1;
                 }
             }
+        } else if (!strcmp(arg, "--pasp")) {
+            NEXTARG();
+            paspCount = parseU32List(paspValues, arg);
+            if (paspCount != 2) {
+                fprintf(stderr, "ERROR: Invalid pasp values: %s\n", arg);
+                return 1;
+            }
+        } else if (!strcmp(arg, "--clap")) {
+            NEXTARG();
+            clapCount = parseU32List(clapValues, arg);
+            if (clapCount != 8) {
+                fprintf(stderr, "ERROR: Invalid clap values: %s\n", arg);
+                return 1;
+            }
+        } else if (!strcmp(arg, "--irot")) {
+            NEXTARG();
+            irotAngle = (uint8_t)atoi(arg);
+            if (irotAngle > 3) {
+                fprintf(stderr, "ERROR: Invalid irot angle: %s\n", arg);
+                return 1;
+            }
+        } else if (!strcmp(arg, "--imir")) {
+            NEXTARG();
+            imirAxis = (uint8_t)atoi(arg);
+            if (imirAxis > 1) {
+                fprintf(stderr, "ERROR: Invalid imir axis: %s\n", arg);
+                return 1;
+            }
         } else {
             // Positional argument
             if (!inputFilename) {
@@ -230,6 +289,31 @@
         memcpy(&avif->nclx, &nclx, sizeof(nclx));
     }
 
+    if (paspCount == 2) {
+        avif->transformFlags |= AVIF_TRANSFORM_PASP;
+        avif->pasp.hSpacing = paspValues[0];
+        avif->pasp.vSpacing = paspValues[1];
+    }
+    if (clapCount == 8) {
+        avif->transformFlags |= AVIF_TRANSFORM_CLAP;
+        avif->clap.widthN = clapValues[0];
+        avif->clap.widthD = clapValues[1];
+        avif->clap.heightN = clapValues[2];
+        avif->clap.heightD = clapValues[3];
+        avif->clap.horizOffN = clapValues[4];
+        avif->clap.horizOffD = clapValues[5];
+        avif->clap.vertOffN = clapValues[6];
+        avif->clap.vertOffD = clapValues[7];
+    }
+    if (irotAngle != 0xff) {
+        avif->transformFlags |= AVIF_TRANSFORM_IROT;
+        avif->irot.angle = irotAngle;
+    }
+    if (imirAxis != 0xff) {
+        avif->transformFlags |= AVIF_TRANSFORM_IMIR;
+        avif->imir.axis = imirAxis;
+    }
+
     printf("AVIF to be written:\n");
     avifImageDump(avif);
 
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 9df8b96..3aa3cd2 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -4,28 +4,56 @@
 #include "avifutil.h"
 
 #include <stdio.h>
+#include <string.h>
 
 void avifImageDump(avifImage * avif)
 {
-    printf(" * Resolution   : %dx%d\n", avif->width, avif->height);
-    printf(" * Bit Depth    : %d\n", avif->depth);
-    printf(" * Format       : %s\n", avifPixelFormatToString(avif->yuvFormat));
-    printf(" * Alpha        : %s\n", (avif->alphaPlane && (avif->alphaRowBytes > 0)) ? "Present" : "Absent");
+    printf(" * Resolution     : %dx%d\n", avif->width, avif->height);
+    printf(" * Bit Depth      : %d\n", avif->depth);
+    printf(" * Format         : %s\n", avifPixelFormatToString(avif->yuvFormat));
+    printf(" * Alpha          : %s\n", (avif->alphaPlane && (avif->alphaRowBytes > 0)) ? "Present" : "Absent");
     switch (avif->profileFormat) {
         case AVIF_PROFILE_FORMAT_NONE:
-            printf(" * Color Profile: None\n");
+            printf(" * Color Profile  : None\n");
             break;
         case AVIF_PROFILE_FORMAT_ICC:
-            printf(" * Color Profile: ICC (%zu bytes)\n", avif->icc.size);
+            printf(" * Color Profile  : ICC (%zu bytes)\n", avif->icc.size);
             break;
         case AVIF_PROFILE_FORMAT_NCLX:
-            printf(" * Color Profile: nclx - P:%d / T:%d / M:%d / R:%s\n",
+            printf(" * Color Profile  : nclx - P:%d / T:%d / M:%d / R:%s\n",
                    avif->nclx.colourPrimaries,
                    avif->nclx.transferCharacteristics,
                    avif->nclx.matrixCoefficients,
-                   avif->nclx.fullRangeFlag ? "full" : "limited");
+                   avif->nclx.fullRangeFlag ? "Full" : "Limited");
             break;
     }
+
+    if (avif->transformFlags == AVIF_TRANSFORM_NONE) {
+        printf(" * Transformations: None\n");
+    } else {
+        printf(" * Transformations:\n");
+
+        if (avif->transformFlags & AVIF_TRANSFORM_PASP) {
+            printf("    * pasp (Aspect Ratio)  : %d/%d\n", (int)avif->pasp.hSpacing, (int)avif->pasp.vSpacing);
+        }
+        if (avif->transformFlags & AVIF_TRANSFORM_CLAP) {
+            printf("    * clap (Clean Aperture): W: %d/%d, H: %d/%d, hOff: %d/%d, vOff: %d/%d\n",
+                   (int)avif->clap.widthN,
+                   (int)avif->clap.widthD,
+                   (int)avif->clap.heightN,
+                   (int)avif->clap.heightD,
+                   (int)avif->clap.horizOffN,
+                   (int)avif->clap.horizOffD,
+                   (int)avif->clap.vertOffN,
+                   (int)avif->clap.vertOffD);
+        }
+        if (avif->transformFlags & AVIF_TRANSFORM_IROT) {
+            printf("    * irot (Rotation)      : %u\n", avif->irot.angle);
+        }
+        if (avif->transformFlags & AVIF_TRANSFORM_IMIR) {
+            printf("    * imir (Mirror)        : %u (%s)\n", avif->imir.axis, (avif->imir.axis == 0) ? "Vertical" : "Horizontal");
+        }
+    }
     printf("\n");
 }
 
diff --git a/include/avif/avif.h b/include/avif/avif.h
index fb05654..e2e4cbb 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -274,6 +274,65 @@
 } avifProfileFormat;
 
 // ---------------------------------------------------------------------------
+// Optional transformation structs
+
+typedef enum avifTransformationFlags
+{
+    AVIF_TRANSFORM_NONE = 0,
+
+    AVIF_TRANSFORM_PASP = (1 << 0),
+    AVIF_TRANSFORM_CLAP = (1 << 1),
+    AVIF_TRANSFORM_IROT = (1 << 2),
+    AVIF_TRANSFORM_IMIR = (1 << 3)
+} avifTransformationFlags;
+
+typedef struct avifPixelAspectRatioBox
+{
+    // 'pasp' from ISO/IEC 14496-12:2015 12.1.4.3
+
+    // define the relative width and height of a pixel
+    uint32_t hSpacing;
+    uint32_t vSpacing;
+} avifPixelAspectRatioBox;
+
+typedef struct avifCleanApertureBox
+{
+    // 'clap' from ISO/IEC 14496-12:2015 12.1.4.3
+
+    // a fractional number which defines the exact clean aperture width, in counted pixels, of the video image
+    uint32_t widthN;
+    uint32_t widthD;
+
+    // a fractional number which defines the exact clean aperture height, in counted pixels, of the video image
+    uint32_t heightN;
+    uint32_t heightD;
+
+    // a fractional number which defines the horizontal offset of clean aperture centre minus (width‐1)/2. Typically 0.
+    uint32_t horizOffN;
+    uint32_t horizOffD;
+
+    // a fractional number which defines the vertical offset of clean aperture centre minus (height‐1)/2. Typically 0.
+    uint32_t vertOffN;
+    uint32_t vertOffD;
+} avifCleanApertureBox;
+
+typedef struct avifImageRotation
+{
+    // 'irot' from ISO/IEC 23008-12:2017 6.5.10
+
+    // angle * 90 specifies the angle (in anti-clockwise direction) in units of degrees.
+    uint8_t angle; // legal values: [0-3]
+} avifImageRotation;
+
+typedef struct avifImageMirror
+{
+    // 'imir' from ISO/IEC 23008-12:2017 6.5.12
+
+    // axis specifies a vertical (axis = 0) or horizontal (axis = 1) axis for the mirroring operation.
+    uint8_t axis; // legal values: [0, 1]
+} avifImageMirror;
+
+// ---------------------------------------------------------------------------
 // avifImage
 
 typedef struct avifImage
@@ -299,6 +358,20 @@
     avifRWData icc;
     avifNclxColorProfile nclx;
 
+    // Transformations - These metadata values are encoded/decoded when transformFlags are set
+    // appropriately, but do not impact/adjust the actual pixel buffers used (images won't be
+    // pre-cropped or mirrored upon decode). Basic explanations from the standards are offered in
+    // comments above, but for detailed explanations, please refer to the HEIF standard (ISO/IEC
+    // 23008-12:2017) and the BMFF standard (ISO/IEC 14496-12:2015).
+    //
+    // To encode any of these boxes, set the values in the associated box, then enable the flag in
+    // transformFlags. On decode, only honor the values in boxes with the associated transform flag set.
+    uint32_t transformFlags;
+    avifPixelAspectRatioBox pasp;
+    avifCleanApertureBox clap;
+    avifImageRotation irot;
+    avifImageMirror imir;
+
     // Metadata - set with avifImageSetMetadata*() before write, check .size>0 for existence after read
     avifRWData exif;
     avifRWData xmp;
diff --git a/src/avif.c b/src/avif.c
index 9894461..70a748c 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -132,6 +132,12 @@
     dstImage->yuvRange = srcImage->yuvRange;
     dstImage->alphaRange = srcImage->alphaRange;
 
+    dstImage->transformFlags = srcImage->transformFlags;
+    memcpy(&dstImage->pasp, &srcImage->pasp, sizeof(dstImage->pasp));
+    memcpy(&dstImage->clap, &srcImage->clap, sizeof(dstImage->clap));
+    memcpy(&dstImage->irot, &srcImage->irot, sizeof(dstImage->irot));
+    memcpy(&dstImage->imir, &srcImage->imir, sizeof(dstImage->pasp));
+
     if (srcImage->profileFormat == AVIF_PROFILE_FORMAT_ICC) {
         avifImageSetProfileICC(dstImage, srcImage->icc.data, srcImage->icc.size);
     } else if (srcImage->profileFormat == AVIF_PROFILE_FORMAT_NCLX) {
diff --git a/src/read.c b/src/read.c
index 5088e8d..8a0d3e4 100644
--- a/src/read.c
+++ b/src/read.c
@@ -91,6 +91,14 @@
     avifColourInformationBox colr;
     avifBool av1CPresent;
     avifCodecConfigurationBox av1C;
+    avifBool paspPresent;
+    avifPixelAspectRatioBox pasp;
+    avifBool clapPresent;
+    avifCleanApertureBox clap;
+    avifBool irotPresent;
+    avifImageRotation irot;
+    avifBool imirPresent;
+    avifImageMirror imir;
     uint32_t thumbnailForID; // if non-zero, this item is a thumbnail for Item #{thumbnailForID}
     uint32_t auxForID;       // if non-zero, this item is an auxC plane for Item #{auxForID}
     uint32_t descForID;      // if non-zero, this item is a content description for Item #{descForID}
@@ -106,6 +114,10 @@
     avifAuxiliaryType auxC;
     avifColourInformationBox colr;
     avifCodecConfigurationBox av1C;
+    avifPixelAspectRatioBox pasp;
+    avifCleanApertureBox clap;
+    avifImageRotation irot;
+    avifImageMirror imir;
 } avifProperty;
 AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop);
 
@@ -890,6 +902,58 @@
     return avifParseAV1CodecConfigurationBox(raw, rawLen, &data->properties.prop[propertyIndex].av1C);
 }
 
+static avifBool avifParsePixelAspectRatioBoxProperty(avifData * data, const uint8_t * raw, size_t rawLen, int propertyIndex)
+{
+    BEGIN_STREAM(s, raw, rawLen);
+
+    avifPixelAspectRatioBox * pasp = &data->properties.prop[propertyIndex].pasp;
+    CHECK(avifROStreamReadU32(&s, &pasp->hSpacing)); // unsigned int(32) hSpacing;
+    CHECK(avifROStreamReadU32(&s, &pasp->vSpacing)); // unsigned int(32) vSpacing;
+    return AVIF_TRUE;
+}
+
+static avifBool avifParseCleanApertureBoxProperty(avifData * data, const uint8_t * raw, size_t rawLen, int propertyIndex)
+{
+    BEGIN_STREAM(s, raw, rawLen);
+
+    avifCleanApertureBox * clap = &data->properties.prop[propertyIndex].clap;
+    CHECK(avifROStreamReadU32(&s, &clap->widthN));    // unsigned int(32) cleanApertureWidthN;
+    CHECK(avifROStreamReadU32(&s, &clap->widthD));    // unsigned int(32) cleanApertureWidthD;
+    CHECK(avifROStreamReadU32(&s, &clap->heightN));   // unsigned int(32) cleanApertureHeightN;
+    CHECK(avifROStreamReadU32(&s, &clap->heightD));   // unsigned int(32) cleanApertureHeightD;
+    CHECK(avifROStreamReadU32(&s, &clap->horizOffN)); // unsigned int(32) horizOffN;
+    CHECK(avifROStreamReadU32(&s, &clap->horizOffD)); // unsigned int(32) horizOffD;
+    CHECK(avifROStreamReadU32(&s, &clap->vertOffN));  // unsigned int(32) vertOffN;
+    CHECK(avifROStreamReadU32(&s, &clap->vertOffD));  // unsigned int(32) vertOffD;
+    return AVIF_TRUE;
+}
+
+static avifBool avifParseImageRotationProperty(avifData * data, const uint8_t * raw, size_t rawLen, int propertyIndex)
+{
+    BEGIN_STREAM(s, raw, rawLen);
+
+    avifImageRotation * irot = &data->properties.prop[propertyIndex].irot;
+    CHECK(avifROStreamRead(&s, &irot->angle, 1)); // unsigned int (6) reserved = 0; unsigned int (2) angle;
+    if ((irot->angle & 0xfc) != 0) {
+        // reserved bits must be 0
+        return AVIF_FALSE;
+    }
+    return AVIF_TRUE;
+}
+
+static avifBool avifParseImageMirrorProperty(avifData * data, const uint8_t * raw, size_t rawLen, int propertyIndex)
+{
+    BEGIN_STREAM(s, raw, rawLen);
+
+    avifImageMirror * imir = &data->properties.prop[propertyIndex].imir;
+    CHECK(avifROStreamRead(&s, &imir->axis, 1)); // unsigned int (7) reserved = 0; unsigned int (1) axis;
+    if ((imir->axis & 0xfe) != 0) {
+        // reserved bits must be 0
+        return AVIF_FALSE;
+    }
+    return AVIF_TRUE;
+}
+
 static avifBool avifParseItemPropertyContainerBox(avifData * data, const uint8_t * raw, size_t rawLen)
 {
     BEGIN_STREAM(s, raw, rawLen);
@@ -912,6 +976,18 @@
         if (!memcmp(header.type, "av1C", 4)) {
             CHECK(avifParseAV1CodecConfigurationBoxProperty(data, avifROStreamCurrent(&s), header.size, propertyIndex));
         }
+        if (!memcmp(header.type, "pasp", 4)) {
+            CHECK(avifParsePixelAspectRatioBoxProperty(data, avifROStreamCurrent(&s), header.size, propertyIndex));
+        }
+        if (!memcmp(header.type, "clap", 4)) {
+            CHECK(avifParseCleanApertureBoxProperty(data, avifROStreamCurrent(&s), header.size, propertyIndex));
+        }
+        if (!memcmp(header.type, "irot", 4)) {
+            CHECK(avifParseImageRotationProperty(data, avifROStreamCurrent(&s), header.size, propertyIndex));
+        }
+        if (!memcmp(header.type, "imir", 4)) {
+            CHECK(avifParseImageMirrorProperty(data, avifROStreamCurrent(&s), header.size, propertyIndex));
+        }
 
         CHECK(avifROStreamSkip(&s, header.size));
     }
@@ -983,6 +1059,18 @@
             } else if (!memcmp(prop->type, "av1C", 4)) {
                 item->av1CPresent = AVIF_TRUE;
                 memcpy(&item->av1C, &prop->av1C, sizeof(avifCodecConfigurationBox));
+            } else if (!memcmp(prop->type, "pasp", 4)) {
+                item->paspPresent = AVIF_TRUE;
+                memcpy(&item->pasp, &prop->pasp, sizeof(avifPixelAspectRatioBox));
+            } else if (!memcmp(prop->type, "clap", 4)) {
+                item->clapPresent = AVIF_TRUE;
+                memcpy(&item->clap, &prop->clap, sizeof(avifCleanApertureBox));
+            } else if (!memcmp(prop->type, "irot", 4)) {
+                item->irotPresent = AVIF_TRUE;
+                memcpy(&item->irot, &prop->irot, sizeof(avifImageRotation));
+            } else if (!memcmp(prop->type, "imir", 4)) {
+                item->imirPresent = AVIF_TRUE;
+                memcpy(&item->imir, &prop->imir, sizeof(avifImageMirror));
             }
         }
     }
@@ -1793,9 +1881,12 @@
     memset(&data->colorGrid, 0, sizeof(data->colorGrid));
     memset(&data->alphaGrid, 0, sizeof(data->alphaGrid));
     avifDataClearTiles(data);
+
+    // Prepare / cleanup decoded image state
     if (!decoder->image) {
         decoder->image = avifImageCreateEmpty();
     }
+    decoder->image->transformFlags = AVIF_TRANSFORM_NONE;
 
     memset(&decoder->ioStats, 0, sizeof(decoder->ioStats));
 
@@ -2055,6 +2146,24 @@
             }
         }
 
+        // Transformations
+        if (colorOBUItem->paspPresent) {
+            decoder->image->transformFlags |= AVIF_TRANSFORM_PASP;
+            memcpy(&decoder->image->pasp, &colorOBUItem->pasp, sizeof(avifPixelAspectRatioBox));
+        }
+        if (colorOBUItem->clapPresent) {
+            decoder->image->transformFlags |= AVIF_TRANSFORM_CLAP;
+            memcpy(&decoder->image->clap, &colorOBUItem->clap, sizeof(avifCleanApertureBox));
+        }
+        if (colorOBUItem->irotPresent) {
+            decoder->image->transformFlags |= AVIF_TRANSFORM_IROT;
+            memcpy(&decoder->image->irot, &colorOBUItem->irot, sizeof(avifImageRotation));
+        }
+        if (colorOBUItem->imirPresent) {
+            decoder->image->transformFlags |= AVIF_TRANSFORM_IMIR;
+            memcpy(&decoder->image->imir, &colorOBUItem->imir, sizeof(avifImageMirror));
+        }
+
         if (exifData.data && exifData.size) {
             avifImageSetMetadataExif(decoder->image, exifData.data, exifData.size);
         }
diff --git a/src/write.c b/src/write.c
index 8f2fbac..5ca31df 100644
--- a/src/write.c
+++ b/src/write.c
@@ -403,6 +403,46 @@
             ++ipcoIndex;
             ipmaPush(&ipmaColor, ipcoIndex);
 
+            // Write (Optional) Transformations
+            if (image->transformFlags & AVIF_TRANSFORM_PASP) {
+                avifBoxMarker pasp = avifRWStreamWriteBox(&s, "pasp", -1, 0);
+                avifRWStreamWriteU32(&s, image->pasp.hSpacing); // unsigned int(32) hSpacing;
+                avifRWStreamWriteU32(&s, image->pasp.vSpacing); // unsigned int(32) vSpacing;
+                avifRWStreamFinishBox(&s, pasp);
+                ++ipcoIndex;
+                ipmaPush(&ipmaColor, ipcoIndex);
+            }
+            if (image->transformFlags & AVIF_TRANSFORM_CLAP) {
+                avifBoxMarker clap = avifRWStreamWriteBox(&s, "clap", -1, 0);
+                avifRWStreamWriteU32(&s, image->clap.widthN);    // unsigned int(32) cleanApertureWidthN;
+                avifRWStreamWriteU32(&s, image->clap.widthD);    // unsigned int(32) cleanApertureWidthD;
+                avifRWStreamWriteU32(&s, image->clap.heightN);   // unsigned int(32) cleanApertureHeightN;
+                avifRWStreamWriteU32(&s, image->clap.heightD);   // unsigned int(32) cleanApertureHeightD;
+                avifRWStreamWriteU32(&s, image->clap.horizOffN); // unsigned int(32) horizOffN;
+                avifRWStreamWriteU32(&s, image->clap.horizOffD); // unsigned int(32) horizOffD;
+                avifRWStreamWriteU32(&s, image->clap.vertOffN);  // unsigned int(32) vertOffN;
+                avifRWStreamWriteU32(&s, image->clap.vertOffD);  // unsigned int(32) vertOffD;
+                avifRWStreamFinishBox(&s, clap);
+                ++ipcoIndex;
+                ipmaPush(&ipmaColor, ipcoIndex);
+            }
+            if (image->transformFlags & AVIF_TRANSFORM_IROT) {
+                avifBoxMarker irot = avifRWStreamWriteBox(&s, "irot", -1, 0);
+                uint8_t angle = image->irot.angle & 0x3;
+                avifRWStreamWrite(&s, &angle, 1); // unsigned int (6) reserved = 0; unsigned int (2) angle;
+                avifRWStreamFinishBox(&s, irot);
+                ++ipcoIndex;
+                ipmaPush(&ipmaColor, ipcoIndex);
+            }
+            if (image->transformFlags & AVIF_TRANSFORM_IMIR) {
+                avifBoxMarker imir = avifRWStreamWriteBox(&s, "imir", -1, 0);
+                uint8_t axis = image->imir.axis & 0x1;
+                avifRWStreamWrite(&s, &axis, 1); // unsigned int (7) reserved = 0; unsigned int (1) axis;
+                avifRWStreamFinishBox(&s, imir);
+                ++ipcoIndex;
+                ipmaPush(&ipmaColor, ipcoIndex);
+            }
+
             if (hasAlpha) {
                 avifBoxMarker pixiA = avifRWStreamWriteBox(&s, "pixi", 0, 0);
                 avifRWStreamWriteU8(&s, 1);                     // unsigned int (8) num_channels;