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
|
||||
size of the original image can use the `data-original-dimensions`
|
||||
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`
|
||||
tag. As detailed in `server_thumbnail_formats`, both animated and
|
||||
still images are available for clients to use, depending on their
|
||||
|
|
|
@ -295,11 +295,23 @@ def maybe_thumbnail(attachment: AbstractAttachment, content: bytes) -> ImageAtta
|
|||
try:
|
||||
# This only attempts to read the header, not the full image content
|
||||
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(
|
||||
realm_id=attachment.realm_id,
|
||||
path_id=attachment.path_id,
|
||||
original_width_px=image.width,
|
||||
original_height_px=image.height,
|
||||
original_width_px=width,
|
||||
original_height_px=height,
|
||||
frames=image.get_n_pages(),
|
||||
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:
|
||||
# We decline to treat as an image a large single-frame image
|
||||
self.login_user(self.example_user("hamlet"))
|
||||
|
|
Loading…
Reference in New Issue