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

Fixes: #41
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;