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.
This commit is contained in:
Amras 2023-11-08 17:24:40 +01:00
parent 3a4a94bdb3
commit 6e2a3eaf92

View file

@ -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: