From 685f75c7b0bcac7fe2353de274cb2f6716fe7a4c Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Mon, 8 Jan 2024 19:53:33 -0500 Subject: [PATCH] fix: Convert EXIF orientation to avif irot and imir (#40) * fix: Convert EXIF orientation to AVIF irot and imir * Disable mmap (see python-pillow/pillow#7565) * pin older cmake for python 2.7 --- src/pillow_avif/AvifImagePlugin.py | 20 ++++- src/pillow_avif/_avif.c | 117 ++++++++++++++++++++++++++++- wheelbuild/config.sh | 2 + 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/pillow_avif/AvifImagePlugin.py b/src/pillow_avif/AvifImagePlugin.py index c0c2f03..6aa6bd7 100644 --- a/src/pillow_avif/AvifImagePlugin.py +++ b/src/pillow_avif/AvifImagePlugin.py @@ -3,7 +3,7 @@ from io import BytesIO import sys -from PIL import Image, ImageFile +from PIL import ExifTags, Image, ImageFile try: from pillow_avif import _avif @@ -56,6 +56,9 @@ class AvifImageFile(ImageFile.ImageFile): __loaded = -1 __frame = 0 + def load_seek(self, pos): + pass + def _open(self): self._decoder = _avif.AvifDecoder( self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING @@ -146,6 +149,20 @@ def _save(im, fp, filename, save_all=False): exif = info.get("exif", im.info.get("exif")) if isinstance(exif, Image.Exif): exif = exif.tobytes() + + exif_orientation = 0 + if exif: + exif_data = Image.Exif() + try: + exif_data.load(exif) + except SyntaxError: + pass + else: + orientation_tag = next( + k for k, v in ExifTags.TAGS.items() if v == "Orientation" + ) + exif_orientation = exif_data.get(orientation_tag) or 0 + xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp")) if isinstance(xmp, text_type): @@ -187,6 +204,7 @@ def _save(im, fp, filename, save_all=False): autotiling, icc_profile or b"", exif or b"", + exif_orientation, xmp or b"", advanced, ) diff --git a/src/pillow_avif/_avif.c b/src/pillow_avif/_avif.c index c35b3bb..3b2970d 100644 --- a/src/pillow_avif/_avif.c +++ b/src/pillow_avif/_avif.c @@ -139,6 +139,118 @@ exc_type_for_avif_result(avifResult result) { } } +static void +exif_orientation_to_irot_imir(avifImage *image, int orientation) { + const avifTransformFlags otherFlags = + image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR); + + // + // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A + // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021 + // sections 6.5.10 and 6.5.12. + switch (orientation) { + case 1: // The 0th row is at the visual top of the image, and the 0th column is + // the visual left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 2: // The 0th row is at the visual top of the image, and the 0th column is + // the visual right-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 1; +#else + image->imir.mode = 1; +#endif + return; + case 3: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual right-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 2; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 4: // The 0th row is at the visual bottom of the image, and the 0th column + // is the visual left-hand side. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 5: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = + otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 1; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 6: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual top. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 3; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + case 7: // The 0th row is the visual right-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = + otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; + image->irot.angle = 3; // applied before imir according to MIAF spec + // ISO/IEC 28002-12:2021 - section 7.3.6.7 +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; +#else + image->imir.mode = 0; +#endif + return; + case 8: // The 0th row is the visual left-hand side of the image, and the 0th + // column is the visual bottom. + image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT; + image->irot.angle = 1; +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif + return; + default: // reserved + break; + } + + // The orientation tag is not mandatory (only recommended) according to JEITA + // CP-3451C section 4.6.8.A. The default value is 1 if the orientation tag is + // missing, meaning: + // The 0th row is at the visual top of the image, and the 0th column is the visual + // left-hand side. + image->transformFlags = otherFlags; + image->irot.angle = 0; // ignored +#if AVIF_VERSION_MAJOR >= 1 + image->imir.axis = 0; // ignored +#else + image->imir.mode = 0; // ignored +#endif +} + static int _codec_available(const char *name, uint32_t flags) { avifCodecChoice codec = avifCodecChoiceFromName(name); @@ -208,6 +320,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { int qmax = 10; // "High Quality", but not lossless int quality = 75; int speed = 8; + int exif_orientation = 0; PyObject *icc_bytes; PyObject *exif_bytes; PyObject *xmp_bytes; @@ -223,7 +336,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { if (!PyArg_ParseTuple( args, - "IIsiiiissiiOOSSSO", + "IIsiiiissiiOOSSiSO", &width, &height, &subsampling, @@ -239,6 +352,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { &autotiling, &icc_bytes, &exif_bytes, + &exif_orientation, &xmp_bytes, &advanced)) { return NULL; @@ -404,6 +518,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) { (uint8_t *)PyBytes_AS_STRING(xmp_bytes), PyBytes_GET_SIZE(xmp_bytes)); } + exif_orientation_to_irot_imir(image, exif_orientation); self->image = image; self->frame_index = -1; diff --git a/wheelbuild/config.sh b/wheelbuild/config.sh index 64d8476..4a1ffbb 100644 --- a/wheelbuild/config.sh +++ b/wheelbuild/config.sh @@ -521,6 +521,8 @@ function install_cmake { else if [[ "$MB_ML_VER" == "1" ]]; then $PYTHON_EXE -m pip install 'cmake<3.23' + elif [ "$MB_PYTHON_VERSION" == "2.7" ]; then + $PYTHON_EXE -m pip install 'cmake==3.27.7' else $PYTHON_EXE -m pip install cmake fi