From 6e2a3eaf927aa0e9d97040250a7760b99fe0acbb Mon Sep 17 00:00:00 2001 From: Amras Date: Wed, 8 Nov 2023 17:24:40 +0100 Subject: [PATCH] Rotate images based on EXIF Orientation This change resolves https://github.com/rr-/szurubooru/issues/470 As well as correcting a related issue with thumbnail rotation. Based on the EXIF Orientation data, we correctly size the scaled image in the post view, and rotate the image before creating its thumbnail. --- server/szurubooru/func/images.py | 67 ++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index 93b66710..1d55762b 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -16,6 +16,30 @@ from szurubooru import errors from szurubooru.func import mime, util logger = logging.getLogger(__name__) +logger.setLevel(level=logging.INFO) + +# Refer to: +# https://exiftool.org/TagNames/EXIF.html +# https://ffmpeg.org/ffmpeg-filters.html#transpose-1 +# https://www.impulseadventure.com/photo/images/orient_flag.gif +ORIENTATION_FILTER = { + "Horizontal (normal)": "null", + "Mirror Horizontal": "transpose=clock_flip,transpose=cclock", + "Rotate 180": "transpose=clock,transpose=clock", + "Mirror vertical": "transpose=clock_flip,transpose=clock", + "Mirror horizontal and rotate 270 CW": "transpose=cclock_flip,transpose=clock,transpose=clock", + "Rotate 90 CW": "transpose=clock", + "Mirror horizontal and rotate 90 CW": "transpose=clock_flip,transpose=clock,transpose=clock", + "Rotate 270 CW": "transpose=cclock", +} + + +ORTHOGONAL_ORIENTATIONS = ( + "Mirror horizontal and rotate 270 CW", + "Rotate 90 CW", + "Mirror horizontal and rotate 90 CW", + "Rotate 270 CW", +) def convert_heif_to_png(content: bytes) -> bytes: @@ -32,10 +56,14 @@ class Image: @property def width(self) -> int: + if self._is_orthogonal(): + return self.info["ImageHeight"] return self.info["ImageWidth"] @property def height(self) -> int: + if self._is_orthogonal(): + return self.info["ImageWidth"] return self.info["ImageHeight"] @property @@ -66,17 +94,32 @@ class Image: logger.warning("Unexpected time format(duration=%r)", duration_data) return None + def _orientation_filter(self) -> str: + try: + return ORIENTATION_FILTER[self.info["Orientation"]] + except KeyError: + return "null" + + def _is_orthogonal(self) -> bool: + try: + return self.info["Orientation"] in ORTHOGONAL_ORIENTATIONS + except KeyError: + return False + def resize_fill(self, width: int, height: int) -> None: width_greater = self.width > self.height width, height = (-1, height) if width_greater else (width, -1) + filters = "{orientation},scale='{width}:{height}'".format( + orientation=self._orientation_filter(), width=width, height=height) + cli = [ "-i", "{path}", "-f", "image2", "-filter:v", - "scale='{width}:{height}'".format(width=width, height=height), + filters, "-map", "0:v:0", "-vframes", @@ -86,9 +129,7 @@ class Image: "-", ] duration = self.duration - if ( - duration is not None and self.info["FileType"] != "SWF" - ): + if duration is not None and self.info["FileType"] != "SWF": total_seconds = duration.total_seconds() if total_seconds > 3: cli = [ @@ -108,6 +149,8 @@ class Image: "{path}", "-f", "image2", + "-filter:v", + self._orientation_filter(), "-map", "0:v:0", "-vframes", @@ -130,7 +173,7 @@ class Image: "-f", "image2", "-filter_complex", - "overlay", + "overlay," + self._orientation_filter(), "-map", "0:v:0", "-vframes", @@ -142,6 +185,7 @@ class Image: ) def to_webm(self) -> bytes: + filters = self._orientation_filter() with util.create_temp_file_path(suffix=".log") as phase_log_path: # Pass 1 self._execute( @@ -152,6 +196,8 @@ class Image: "1", "-passlogfile", phase_log_path, + "-filter:v", + filters, "-vcodec", "libvpx-vp9", "-crf", @@ -176,6 +222,8 @@ class Image: "2", "-passlogfile", phase_log_path, + "-filter:v", + filters, "-vcodec", "libvpx-vp9", "-crf", @@ -204,6 +252,10 @@ class Image: height = self.height - 1 altered_dimensions = True + filters = self._orientation_filter() + if altered_dimensions: + filters += ",scale='%d:%d'" % (width, height) + args = [ "-i", "{path}", @@ -223,11 +275,10 @@ class Image: "aac", "-f", "mp4", + "-filter:v", + filters, ] - if altered_dimensions: - args += ["-filter:v", "scale='%d:%d'" % (width, height)] - self._execute(args + ["-y", mp4_temp_path]) with open(mp4_temp_path, "rb") as mp4_temp: