Skip to content

Commit 685f75c

Browse files
authored
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
1 parent 8f0a315 commit 685f75c

File tree

3 files changed

+137
-2
lines changed

3 files changed

+137
-2
lines changed

src/pillow_avif/AvifImagePlugin.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from io import BytesIO
44
import sys
55

6-
from PIL import Image, ImageFile
6+
from PIL import ExifTags, Image, ImageFile
77

88
try:
99
from pillow_avif import _avif
@@ -56,6 +56,9 @@ class AvifImageFile(ImageFile.ImageFile):
5656
__loaded = -1
5757
__frame = 0
5858

59+
def load_seek(self, pos):
60+
pass
61+
5962
def _open(self):
6063
self._decoder = _avif.AvifDecoder(
6164
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING
@@ -146,6 +149,20 @@ def _save(im, fp, filename, save_all=False):
146149
exif = info.get("exif", im.info.get("exif"))
147150
if isinstance(exif, Image.Exif):
148151
exif = exif.tobytes()
152+
153+
exif_orientation = 0
154+
if exif:
155+
exif_data = Image.Exif()
156+
try:
157+
exif_data.load(exif)
158+
except SyntaxError:
159+
pass
160+
else:
161+
orientation_tag = next(
162+
k for k, v in ExifTags.TAGS.items() if v == "Orientation"
163+
)
164+
exif_orientation = exif_data.get(orientation_tag) or 0
165+
149166
xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp"))
150167

151168
if isinstance(xmp, text_type):
@@ -187,6 +204,7 @@ def _save(im, fp, filename, save_all=False):
187204
autotiling,
188205
icc_profile or b"",
189206
exif or b"",
207+
exif_orientation,
190208
xmp or b"",
191209
advanced,
192210
)

src/pillow_avif/_avif.c

+116-1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,118 @@ exc_type_for_avif_result(avifResult result) {
139139
}
140140
}
141141

142+
static void
143+
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
144+
const avifTransformFlags otherFlags =
145+
image->transformFlags & ~(AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR);
146+
147+
//
148+
// Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A
149+
// Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021
150+
// sections 6.5.10 and 6.5.12.
151+
switch (orientation) {
152+
case 1: // The 0th row is at the visual top of the image, and the 0th column is
153+
// the visual left-hand side.
154+
image->transformFlags = otherFlags;
155+
image->irot.angle = 0; // ignored
156+
#if AVIF_VERSION_MAJOR >= 1
157+
image->imir.axis = 0; // ignored
158+
#else
159+
image->imir.mode = 0; // ignored
160+
#endif
161+
return;
162+
case 2: // The 0th row is at the visual top of the image, and the 0th column is
163+
// the visual right-hand side.
164+
image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
165+
image->irot.angle = 0; // ignored
166+
#if AVIF_VERSION_MAJOR >= 1
167+
image->imir.axis = 1;
168+
#else
169+
image->imir.mode = 1;
170+
#endif
171+
return;
172+
case 3: // The 0th row is at the visual bottom of the image, and the 0th column
173+
// is the visual right-hand side.
174+
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
175+
image->irot.angle = 2;
176+
#if AVIF_VERSION_MAJOR >= 1
177+
image->imir.axis = 0; // ignored
178+
#else
179+
image->imir.mode = 0; // ignored
180+
#endif
181+
return;
182+
case 4: // The 0th row is at the visual bottom of the image, and the 0th column
183+
// is the visual left-hand side.
184+
image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
185+
image->irot.angle = 0; // ignored
186+
#if AVIF_VERSION_MAJOR >= 1
187+
image->imir.axis = 0;
188+
#else
189+
image->imir.mode = 0;
190+
#endif
191+
return;
192+
case 5: // The 0th row is the visual left-hand side of the image, and the 0th
193+
// column is the visual top.
194+
image->transformFlags =
195+
otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
196+
image->irot.angle = 1; // applied before imir according to MIAF spec
197+
// ISO/IEC 28002-12:2021 - section 7.3.6.7
198+
#if AVIF_VERSION_MAJOR >= 1
199+
image->imir.axis = 0;
200+
#else
201+
image->imir.mode = 0;
202+
#endif
203+
return;
204+
case 6: // The 0th row is the visual right-hand side of the image, and the 0th
205+
// column is the visual top.
206+
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
207+
image->irot.angle = 3;
208+
#if AVIF_VERSION_MAJOR >= 1
209+
image->imir.axis = 0; // ignored
210+
#else
211+
image->imir.mode = 0; // ignored
212+
#endif
213+
return;
214+
case 7: // The 0th row is the visual right-hand side of the image, and the 0th
215+
// column is the visual bottom.
216+
image->transformFlags =
217+
otherFlags | AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
218+
image->irot.angle = 3; // applied before imir according to MIAF spec
219+
// ISO/IEC 28002-12:2021 - section 7.3.6.7
220+
#if AVIF_VERSION_MAJOR >= 1
221+
image->imir.axis = 0;
222+
#else
223+
image->imir.mode = 0;
224+
#endif
225+
return;
226+
case 8: // The 0th row is the visual left-hand side of the image, and the 0th
227+
// column is the visual bottom.
228+
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
229+
image->irot.angle = 1;
230+
#if AVIF_VERSION_MAJOR >= 1
231+
image->imir.axis = 0; // ignored
232+
#else
233+
image->imir.mode = 0; // ignored
234+
#endif
235+
return;
236+
default: // reserved
237+
break;
238+
}
239+
240+
// The orientation tag is not mandatory (only recommended) according to JEITA
241+
// CP-3451C section 4.6.8.A. The default value is 1 if the orientation tag is
242+
// missing, meaning:
243+
// The 0th row is at the visual top of the image, and the 0th column is the visual
244+
// left-hand side.
245+
image->transformFlags = otherFlags;
246+
image->irot.angle = 0; // ignored
247+
#if AVIF_VERSION_MAJOR >= 1
248+
image->imir.axis = 0; // ignored
249+
#else
250+
image->imir.mode = 0; // ignored
251+
#endif
252+
}
253+
142254
static int
143255
_codec_available(const char *name, uint32_t flags) {
144256
avifCodecChoice codec = avifCodecChoiceFromName(name);
@@ -208,6 +320,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
208320
int qmax = 10; // "High Quality", but not lossless
209321
int quality = 75;
210322
int speed = 8;
323+
int exif_orientation = 0;
211324
PyObject *icc_bytes;
212325
PyObject *exif_bytes;
213326
PyObject *xmp_bytes;
@@ -223,7 +336,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
223336

224337
if (!PyArg_ParseTuple(
225338
args,
226-
"IIsiiiissiiOOSSSO",
339+
"IIsiiiissiiOOSSiSO",
227340
&width,
228341
&height,
229342
&subsampling,
@@ -239,6 +352,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
239352
&autotiling,
240353
&icc_bytes,
241354
&exif_bytes,
355+
&exif_orientation,
242356
&xmp_bytes,
243357
&advanced)) {
244358
return NULL;
@@ -404,6 +518,7 @@ AvifEncoderNew(PyObject *self_, PyObject *args) {
404518
(uint8_t *)PyBytes_AS_STRING(xmp_bytes),
405519
PyBytes_GET_SIZE(xmp_bytes));
406520
}
521+
exif_orientation_to_irot_imir(image, exif_orientation);
407522

408523
self->image = image;
409524
self->frame_index = -1;

wheelbuild/config.sh

+2
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,8 @@ function install_cmake {
521521
else
522522
if [[ "$MB_ML_VER" == "1" ]]; then
523523
$PYTHON_EXE -m pip install 'cmake<3.23'
524+
elif [ "$MB_PYTHON_VERSION" == "2.7" ]; then
525+
$PYTHON_EXE -m pip install 'cmake==3.27.7'
524526
else
525527
$PYTHON_EXE -m pip install cmake
526528
fi

0 commit comments

Comments
 (0)