Skip to content

fix: Convert EXIF orientation to avif irot and imir #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/pillow_avif/AvifImagePlugin.py
Original file line number Diff line number Diff line change
@@ -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,
)
117 changes: 116 additions & 1 deletion src/pillow_avif/_avif.c
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions wheelbuild/config.sh
Original file line number Diff line number Diff line change
@@ -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