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
|
||||
from datetime import datetime
|
||||
from subprocess import PIPE, Popen
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from exif import Image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE_FFMPEG_COMMAND = [
|
||||
BASE_FFPROBE_COMMAND = [
|
||||
"ffprobe",
|
||||
"-loglevel",
|
||||
"8",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
]
|
||||
|
||||
|
||||
|
@ -28,10 +29,10 @@ def _open_image(content: bytes) -> Image:
|
|||
return tags
|
||||
|
||||
|
||||
def _run_ffmpeg(content: Union[bytes, str]) -> Image:
|
||||
def _run_ffprobe(content: Union[bytes, str]) -> Image:
|
||||
if isinstance(content, bytes):
|
||||
proc = Popen(
|
||||
BASE_FFMPEG_COMMAND + ["-"],
|
||||
BASE_FFPROBE_COMMAND + ["-"],
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
|
@ -40,7 +41,7 @@ def _run_ffmpeg(content: Union[bytes, str]) -> Image:
|
|||
output = proc.communicate(input=content)[0]
|
||||
else:
|
||||
proc = Popen(
|
||||
BASE_FFMPEG_COMMAND + [content],
|
||||
BASE_FFPROBE_COMMAND + [content],
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
)
|
||||
|
@ -81,7 +82,7 @@ def resolve_video_date_taken(
|
|||
if isinstance(content, dict):
|
||||
tags = content
|
||||
else:
|
||||
tags = _run_ffmpeg(content)
|
||||
tags = _run_ffprobe(content)
|
||||
|
||||
creation_time = tags["creation_time"]
|
||||
except Exception:
|
||||
|
@ -116,7 +117,7 @@ def resolve_video_camera(content: Union[bytes, str, dict]) -> Optional[str]:
|
|||
if isinstance(content, dict):
|
||||
tags = content
|
||||
else:
|
||||
tags = _run_ffmpeg(content)
|
||||
tags = _run_ffprobe(content)
|
||||
|
||||
# List of tuples where only one value can be valid
|
||||
option_tuples = (
|
||||
|
@ -138,3 +139,45 @@ def resolve_video_camera(content: Union[bytes, str, dict]) -> Optional[str]:
|
|||
return None
|
||||
else:
|
||||
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.file_size = len(content)
|
||||
post.date_taken = None
|
||||
post.camera = None
|
||||
|
||||
try:
|
||||
image = images.Image(content)
|
||||
post.canvas_width = image.width
|
||||
post.canvas_height = image.height
|
||||
except errors.ProcessingError as ex:
|
||||
if post.type == model.Post.TYPE_IMAGE:
|
||||
media = metadata._open_image(content)
|
||||
elif post.type == model.Post.TYPE_VIDEO:
|
||||
media = metadata._run_ffprobe(content)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
if not config.config["allow_broken_uploads"]:
|
||||
raise InvalidPostContentError("Unable to process image metadata")
|
||||
else:
|
||||
post.canvas_width = 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 (
|
||||
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:
|
||||
post.canvas_width = None
|
||||
post.canvas_height = None
|
||||
|
||||
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(
|
||||
post: model.Post, content: Optional[bytes] = None
|
||||
|
|
Reference in a new issue