server: fix swapped dimensions on some images
- [overview] this commit fixes an issue where some images would be rendered stretched in a browser due to a swapped width and height because of intentional EXIF orientation mechanics. - [server] `func/metadata.py`: added function to resolve the image's dimensions after taking the EXIF orientation into consideration. normally, this would only be done on the client side; however, since the server takes those values into consideration for its own operations, it could also be considered a "client". for example, thumbnail generation also depends on the post's dimensions. - [server] `func/posts.py`: refactored `update_post_content`. `func/images.py` will no longer be used to determine the dimensions of a file since `func/metadata.py` is now responsible for that. - [server] refactored `func/metadata.py` - [TODO] create migration to correct post dimensions - [TODO] merge `func/metadata.py` with `func/images.py` and refactor - [TODO] fix thumbnail generation
This commit is contained in:
parent
2c1e25c158
commit
a4ea05a0e4
2 changed files with 71 additions and 31 deletions
|
@ -2,20 +2,21 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import PIPE, Popen
|
from subprocess import PIPE, Popen
|
||||||
from typing import Optional, Union
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
from exif import Image
|
from exif import Image
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
BASE_FFMPEG_COMMAND = [
|
BASE_FFPROBE_COMMAND = [
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"8",
|
"8",
|
||||||
"-print_format",
|
"-print_format",
|
||||||
"json",
|
"json",
|
||||||
"-show_format",
|
"-show_format",
|
||||||
|
"-show_streams",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,10 +29,10 @@ def _open_image(content: bytes) -> Image:
|
||||||
return tags
|
return tags
|
||||||
|
|
||||||
|
|
||||||
def _run_ffmpeg(content: Union[bytes, str]) -> Image:
|
def _run_ffprobe(content: Union[bytes, str]) -> Image:
|
||||||
if isinstance(content, bytes):
|
if isinstance(content, bytes):
|
||||||
proc = Popen(
|
proc = Popen(
|
||||||
BASE_FFMPEG_COMMAND + ["-"],
|
BASE_FFPROBE_COMMAND + ["-"],
|
||||||
stdin=PIPE,
|
stdin=PIPE,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
stderr=PIPE,
|
stderr=PIPE,
|
||||||
|
@ -40,7 +41,7 @@ def _run_ffmpeg(content: Union[bytes, str]) -> Image:
|
||||||
output = proc.communicate(input=content)[0]
|
output = proc.communicate(input=content)[0]
|
||||||
else:
|
else:
|
||||||
proc = Popen(
|
proc = Popen(
|
||||||
BASE_FFMPEG_COMMAND + [content],
|
BASE_FFPROBE_COMMAND + [content],
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
stderr=PIPE,
|
stderr=PIPE,
|
||||||
)
|
)
|
||||||
|
@ -81,7 +82,7 @@ def resolve_video_date_taken(
|
||||||
if isinstance(content, dict):
|
if isinstance(content, dict):
|
||||||
tags = content
|
tags = content
|
||||||
else:
|
else:
|
||||||
tags = _run_ffmpeg(content)
|
tags = _run_ffprobe(content)
|
||||||
|
|
||||||
creation_time = tags["creation_time"]
|
creation_time = tags["creation_time"]
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -116,7 +117,7 @@ def resolve_video_camera(content: Union[bytes, str, dict]) -> Optional[str]:
|
||||||
if isinstance(content, dict):
|
if isinstance(content, dict):
|
||||||
tags = content
|
tags = content
|
||||||
else:
|
else:
|
||||||
tags = _run_ffmpeg(content)
|
tags = _run_ffprobe(content)
|
||||||
|
|
||||||
# List of tuples where only one value can be valid
|
# List of tuples where only one value can be valid
|
||||||
option_tuples = (
|
option_tuples = (
|
||||||
|
@ -138,3 +139,45 @@ def resolve_video_camera(content: Union[bytes, str, dict]) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return " ".join(camera_string)
|
return " ".join(camera_string)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_real_image_dimensions(
|
||||||
|
content: Union[bytes, Image]
|
||||||
|
) -> Optional[Tuple[int, int]]:
|
||||||
|
try:
|
||||||
|
if isinstance(content, Image):
|
||||||
|
tags = content
|
||||||
|
else:
|
||||||
|
tags = _open_image(content)
|
||||||
|
|
||||||
|
orig_w = tags["pixel_x_dimension"]
|
||||||
|
orig_h = tags["pixel_y_dimension"]
|
||||||
|
|
||||||
|
# read: https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
||||||
|
# 8, 6, 5, 7 are orientation values where the image is rotated 90
|
||||||
|
# degrees CW or CCW. in this case, we swap the two dimensions.
|
||||||
|
if tags["orientation"] in (8, 6, 5, 7):
|
||||||
|
dimensions = (orig_h, orig_w)
|
||||||
|
else:
|
||||||
|
dimensions = (orig_w, orig_h)
|
||||||
|
except Exception:
|
||||||
|
return (0, 0)
|
||||||
|
else:
|
||||||
|
return dimensions
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_video_dimensions(
|
||||||
|
content: Union[bytes, str, dict]
|
||||||
|
) -> Optional[Tuple[int, int]]:
|
||||||
|
try:
|
||||||
|
if isinstance(content, dict):
|
||||||
|
tags = content
|
||||||
|
else:
|
||||||
|
tags = _run_ffprobe(content)
|
||||||
|
|
||||||
|
stream = tags["format"]["streams"][0]
|
||||||
|
dimensions = (stream["width"], stream["height"])
|
||||||
|
except Exception:
|
||||||
|
return (0, 0)
|
||||||
|
else:
|
||||||
|
return dimensions
|
||||||
|
|
|
@ -655,17 +655,33 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
||||||
post.signature = generate_post_signature(post, content)
|
post.signature = generate_post_signature(post, content)
|
||||||
|
|
||||||
post.file_size = len(content)
|
post.file_size = len(content)
|
||||||
|
post.date_taken = None
|
||||||
|
post.camera = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = images.Image(content)
|
if post.type == model.Post.TYPE_IMAGE:
|
||||||
post.canvas_width = image.width
|
media = metadata._open_image(content)
|
||||||
post.canvas_height = image.height
|
elif post.type == model.Post.TYPE_VIDEO:
|
||||||
except errors.ProcessingError as ex:
|
media = metadata._run_ffprobe(content)
|
||||||
|
except Exception as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
if not config.config["allow_broken_uploads"]:
|
if not config.config["allow_broken_uploads"]:
|
||||||
raise InvalidPostContentError("Unable to process image metadata")
|
raise InvalidPostContentError("Unable to process image metadata")
|
||||||
else:
|
else:
|
||||||
post.canvas_width = None
|
post.canvas_width = None
|
||||||
post.canvas_height = None
|
post.canvas_height = None
|
||||||
|
else:
|
||||||
|
if post.type == model.Post.TYPE_IMAGE:
|
||||||
|
dimensions = metadata.resolve_real_image_dimensions(media)
|
||||||
|
(post.canvas_width, post.canvas_height) = dimensions
|
||||||
|
post.date_taken = metadata.resolve_image_date_taken(media)
|
||||||
|
post.camera = metadata.resolve_image_camera(media)
|
||||||
|
elif post.type == model.Post.TYPE_VIDEO:
|
||||||
|
dimensions = metadata.resolve_video_dimensions(media)
|
||||||
|
(post.canvas_width, post.canvas_height) = dimensions
|
||||||
|
post.date_taken = metadata.resolve_video_date_taken(media)
|
||||||
|
post.camera = metadata.resolve_video_camera(media)
|
||||||
|
|
||||||
if (post.canvas_width is not None and post.canvas_width <= 0) or (
|
if (post.canvas_width is not None and post.canvas_width <= 0) or (
|
||||||
post.canvas_height is not None and post.canvas_height <= 0
|
post.canvas_height is not None and post.canvas_height <= 0
|
||||||
):
|
):
|
||||||
|
@ -676,28 +692,9 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
||||||
else:
|
else:
|
||||||
post.canvas_width = None
|
post.canvas_width = None
|
||||||
post.canvas_height = None
|
post.canvas_height = None
|
||||||
|
|
||||||
setattr(post, "__content", content)
|
setattr(post, "__content", content)
|
||||||
|
|
||||||
post.date_taken = None
|
|
||||||
post.camera = None
|
|
||||||
|
|
||||||
if post.type == model.Post.TYPE_IMAGE:
|
|
||||||
try:
|
|
||||||
image_tags = metadata._open_image(content)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
post.date_taken = metadata.resolve_image_date_taken(image_tags)
|
|
||||||
post.camera = metadata.resolve_image_camera(image_tags)
|
|
||||||
elif post.type == model.Post.TYPE_VIDEO:
|
|
||||||
try:
|
|
||||||
video_tags = metadata._run_ffmpeg(content)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
post.date_taken = metadata.resolve_video_date_taken(video_tags)
|
|
||||||
post.camera = metadata.resolve_video_camera(video_tags)
|
|
||||||
|
|
||||||
|
|
||||||
def update_post_thumbnail(
|
def update_post_thumbnail(
|
||||||
post: model.Post, content: Optional[bytes] = None
|
post: model.Post, content: Optional[bytes] = None
|
||||||
|
|
Reference in a new issue