Skip to content

Commit

Permalink
fix: Convert EXIF orientation to avif irot and imir (#40)
Browse files Browse the repository at this point in the history
* fix: Convert EXIF orientation to AVIF irot and imir
* Disable mmap (see python-pillow/Pillow#7565)
* pin older cmake for python 2.7
  • Loading branch information
fdintino authored Jan 9, 2024
1 parent 8f0a315 commit 685f75c
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 2 deletions.
20 changes: 19 additions & 1 deletion src/pillow_avif/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
)
Expand Down
117 changes: 116 additions & 1 deletion src/pillow_avif/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -223,7 +336,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {

if (!PyArg_ParseTuple(
args,
"IIsiiiissiiOOSSSO",
"IIsiiiissiiOOSSiSO",
&width,
&height,
&subsampling,
Expand All @@ -239,6 +352,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
&autotiling,
&icc_bytes,
&exif_bytes,
&exif_orientation,
&xmp_bytes,
&advanced)) {
return NULL;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions wheelbuild/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 685f75c

Please sign in to comment.