mirror of https://github.com/zulip/zulip.git
thumbnail: Store the post-orientation-transformation dimensions.
Modern browsers respect the EXIF orientation information of images, applying rotation and/or mirroring as specified in those tags. The the `width="..."` and `height="..."` tags are to size the image _after_ applying those orientation transformations. The `.width` and `.height` properties of libvips' images are _before_ any transformations are applied. Since we intend to use these to hint to rendering clients the size that the image should be _rendered at_, change to storing (and providing to clients) the dimensions of the rendered image, not the stored bytes.
This commit is contained in:
parent
e7ac62aad7
commit
e4a8304f57
|
@ -86,7 +86,8 @@ previews:
|
||||||
available. Clients that would like to size the lightbox based on the
|
available. Clients that would like to size the lightbox based on the
|
||||||
size of the original image can use the `data-original-dimensions`
|
size of the original image can use the `data-original-dimensions`
|
||||||
attribute, which encodes the dimensions of the original image as
|
attribute, which encodes the dimensions of the original image as
|
||||||
`{width}x{height}`, to do so.
|
`{width}x{height}`, to do so. These dimensions are for the image as
|
||||||
|
rendered, _after_ any EXIF rotation and mirroring has been applied.
|
||||||
- Animated images will have a `data-animated` attribute on the `img`
|
- Animated images will have a `data-animated` attribute on the `img`
|
||||||
tag. As detailed in `server_thumbnail_formats`, both animated and
|
tag. As detailed in `server_thumbnail_formats`, both animated and
|
||||||
still images are available for clients to use, depending on their
|
still images are available for clients to use, depending on their
|
||||||
|
|
|
@ -295,11 +295,23 @@ def maybe_thumbnail(attachment: AbstractAttachment, content: bytes) -> ImageAtta
|
||||||
try:
|
try:
|
||||||
# This only attempts to read the header, not the full image content
|
# This only attempts to read the header, not the full image content
|
||||||
with libvips_check_image(content) as image:
|
with libvips_check_image(content) as image:
|
||||||
|
# "original_width_px" and "original_height_px" here are
|
||||||
|
# _as rendered_, after applying the orientation
|
||||||
|
# information which the image may contain.
|
||||||
|
if (
|
||||||
|
"orientation" in image.get_fields()
|
||||||
|
and image.get("orientation") >= 5
|
||||||
|
and image.get("orientation") <= 8
|
||||||
|
):
|
||||||
|
(width, height) = (image.height, image.width)
|
||||||
|
else:
|
||||||
|
(width, height) = (image.width, image.height)
|
||||||
|
|
||||||
image_row = ImageAttachment.objects.create(
|
image_row = ImageAttachment.objects.create(
|
||||||
realm_id=attachment.realm_id,
|
realm_id=attachment.realm_id,
|
||||||
path_id=attachment.path_id,
|
path_id=attachment.path_id,
|
||||||
original_width_px=image.width,
|
original_width_px=width,
|
||||||
original_height_px=image.height,
|
original_height_px=height,
|
||||||
frames=image.get_n_pages(),
|
frames=image.get_n_pages(),
|
||||||
thumbnail_metadata=[],
|
thumbnail_metadata=[],
|
||||||
)
|
)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
|
@ -398,6 +398,56 @@ class TestStoreThumbnail(ZulipTestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_image_orientation(self) -> None:
|
||||||
|
self.login_user(self.example_user("hamlet"))
|
||||||
|
|
||||||
|
with (
|
||||||
|
self.thumbnail_formats(ThumbnailFormat("webp", 100, 75, animated=False)),
|
||||||
|
self.captureOnCommitCallbacks(execute=True),
|
||||||
|
):
|
||||||
|
with get_test_image_file("orientation.jpg") as image_file:
|
||||||
|
response = self.assert_json_success(
|
||||||
|
self.client_post("/json/user_uploads", {"file": image_file})
|
||||||
|
)
|
||||||
|
path_id = re.sub(r"/user_uploads/", "", response["url"])
|
||||||
|
self.assertEqual(Attachment.objects.filter(path_id=path_id).count(), 1)
|
||||||
|
|
||||||
|
image_attachment = ImageAttachment.objects.get(path_id=path_id)
|
||||||
|
# The bytes in this image are 100 wide, and 600 tall --
|
||||||
|
# however, it has EXIF orientation information which says
|
||||||
|
# to rotate it 270 degrees counter-clockwise.
|
||||||
|
self.assertEqual(image_attachment.original_height_px, 100)
|
||||||
|
self.assertEqual(image_attachment.original_width_px, 600)
|
||||||
|
|
||||||
|
# The worker triggers when we exit this block and call the pending callbacks
|
||||||
|
image_attachment = ImageAttachment.objects.get(path_id=path_id)
|
||||||
|
self.assert_length(image_attachment.thumbnail_metadata, 1)
|
||||||
|
generated_thumbnail = StoredThumbnailFormat(**image_attachment.thumbnail_metadata[0])
|
||||||
|
|
||||||
|
# The uploaded original content is technically "tall", not "wide", with a 270 CCW rotation set.
|
||||||
|
with BytesIO() as fh:
|
||||||
|
save_attachment_contents(path_id, fh)
|
||||||
|
thumbnailed_bytes = fh.getvalue()
|
||||||
|
with pyvips.Image.new_from_buffer(thumbnailed_bytes, "") as thumbnailed_image:
|
||||||
|
self.assertEqual(thumbnailed_image.get("vips-loader"), "jpegload_buffer")
|
||||||
|
self.assertEqual(thumbnailed_image.width, 100)
|
||||||
|
self.assertEqual(thumbnailed_image.height, 600)
|
||||||
|
self.assertEqual(thumbnailed_image.get("orientation"), 8) # 270 CCW rotation
|
||||||
|
|
||||||
|
# The generated thumbnail should be wide, not tall, with the default orientation
|
||||||
|
self.assertEqual(str(generated_thumbnail), "100x75.webp")
|
||||||
|
self.assertEqual(generated_thumbnail.width, 100)
|
||||||
|
self.assertEqual(generated_thumbnail.height, 17)
|
||||||
|
|
||||||
|
with BytesIO() as fh:
|
||||||
|
save_attachment_contents(f"thumbnail/{path_id}/100x75.webp", fh)
|
||||||
|
thumbnailed_bytes = fh.getvalue()
|
||||||
|
with pyvips.Image.new_from_buffer(thumbnailed_bytes, "") as thumbnailed_image:
|
||||||
|
self.assertEqual(thumbnailed_image.get("vips-loader"), "webpload_buffer")
|
||||||
|
self.assertEqual(thumbnailed_image.width, 100)
|
||||||
|
self.assertEqual(thumbnailed_image.height, 17)
|
||||||
|
self.assertEqual(thumbnailed_image.get("orientation"), 1)
|
||||||
|
|
||||||
def test_big_upload(self) -> None:
|
def test_big_upload(self) -> None:
|
||||||
# We decline to treat as an image a large single-frame image
|
# We decline to treat as an image a large single-frame image
|
||||||
self.login_user(self.example_user("hamlet"))
|
self.login_user(self.example_user("hamlet"))
|
||||||
|
|
Loading…
Reference in New Issue