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:
parent
3a4a94bdb3
commit
6e2a3eaf92
1 changed files with 59 additions and 8 deletions
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue