diff options
author | Patrick Cloke <clokep@users.noreply.github.com> | 2023-02-27 08:26:05 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-27 08:26:05 -0500 |
commit | 4fc8875876374ec8f97a3b3cc344a4e3abcf769f (patch) | |
tree | 41b920427c0f62cd1463324c89fe7f5ce3d15164 /synapse/rest/media/v1/thumbnailer.py | |
parent | Small fixes to `MatrixFederationHttpClient` docstrings (#15148) (diff) | |
download | synapse-4fc8875876374ec8f97a3b3cc344a4e3abcf769f.tar.xz |
Refactor media modules. (#15146)
* Removes the `v1` directory from `test.rest.media.v1`. * Moves the non-REST code from `synapse.rest.media.v1` to `synapse.media`. * Flatten the `v1` directory from `synapse.rest.media`, but leave compatiblity with 3rd party media repositories and spam checkers.
Diffstat (limited to 'synapse/rest/media/v1/thumbnailer.py')
-rw-r--r-- | synapse/rest/media/v1/thumbnailer.py | 221 |
1 files changed, 0 insertions, 221 deletions
diff --git a/synapse/rest/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py deleted file mode 100644 index f909a4fb9a..0000000000 --- a/synapse/rest/media/v1/thumbnailer.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2020-2021 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import logging -from io import BytesIO -from types import TracebackType -from typing import Optional, Tuple, Type - -from PIL import Image - -logger = logging.getLogger(__name__) - -EXIF_ORIENTATION_TAG = 0x0112 -EXIF_TRANSPOSE_MAPPINGS = { - 2: Image.FLIP_LEFT_RIGHT, - 3: Image.ROTATE_180, - 4: Image.FLIP_TOP_BOTTOM, - 5: Image.TRANSPOSE, - 6: Image.ROTATE_270, - 7: Image.TRANSVERSE, - 8: Image.ROTATE_90, -} - - -class ThumbnailError(Exception): - """An error occurred generating a thumbnail.""" - - -class Thumbnailer: - FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"} - - @staticmethod - def set_limits(max_image_pixels: int) -> None: - Image.MAX_IMAGE_PIXELS = max_image_pixels - - def __init__(self, input_path: str): - # Have we closed the image? - self._closed = False - - try: - self.image = Image.open(input_path) - except OSError as e: - # If an error occurs opening the image, a thumbnail won't be able to - # be generated. - raise ThumbnailError from e - except Image.DecompressionBombError as e: - # If an image decompression bomb error occurs opening the image, - # then the image exceeds the pixel limit and a thumbnail won't - # be able to be generated. - raise ThumbnailError from e - - self.width, self.height = self.image.size - self.transpose_method = None - try: - # We don't use ImageOps.exif_transpose since it crashes with big EXIF - # - # Ignore safety: Pillow seems to acknowledge that this method is - # "private, experimental, but generally widely used". Pillow 6 - # includes a public getexif() method (no underscore) that we might - # consider using instead when we can bump that dependency. - # - # At the time of writing, Debian buster (currently oldstable) - # provides version 5.4.1. It's expected to EOL in mid-2022, see - # https://wiki.debian.org/DebianReleases#Production_Releases - image_exif = self.image._getexif() # type: ignore - if image_exif is not None: - image_orientation = image_exif.get(EXIF_ORIENTATION_TAG) - assert type(image_orientation) is int - self.transpose_method = EXIF_TRANSPOSE_MAPPINGS.get(image_orientation) - except Exception as e: - # A lot of parsing errors can happen when parsing EXIF - logger.info("Error parsing image EXIF information: %s", e) - - def transpose(self) -> Tuple[int, int]: - """Transpose the image using its EXIF Orientation tag - - Returns: - A tuple containing the new image size in pixels as (width, height). - """ - if self.transpose_method is not None: - # Safety: `transpose` takes an int rather than e.g. an IntEnum. - # self.transpose_method is set above to be a value in - # EXIF_TRANSPOSE_MAPPINGS, and that only contains correct values. - with self.image: - self.image = self.image.transpose(self.transpose_method) # type: ignore[arg-type] - self.width, self.height = self.image.size - self.transpose_method = None - # We don't need EXIF any more - self.image.info["exif"] = None - return self.image.size - - def aspect(self, max_width: int, max_height: int) -> Tuple[int, int]: - """Calculate the largest size that preserves aspect ratio which - fits within the given rectangle:: - - (w_in / h_in) = (w_out / h_out) - w_out = max(min(w_max, h_max * (w_in / h_in)), 1) - h_out = max(min(h_max, w_max * (h_in / w_in)), 1) - - Args: - max_width: The largest possible width. - max_height: The largest possible height. - """ - - if max_width * self.height < max_height * self.width: - return max_width, max((max_width * self.height) // self.width, 1) - else: - return max((max_height * self.width) // self.height, 1), max_height - - def _resize(self, width: int, height: int) -> Image.Image: - # 1-bit or 8-bit color palette images need converting to RGB - # otherwise they will be scaled using nearest neighbour which - # looks awful. - # - # If the image has transparency, use RGBA instead. - if self.image.mode in ["1", "L", "P"]: - if self.image.info.get("transparency", None) is not None: - with self.image: - self.image = self.image.convert("RGBA") - else: - with self.image: - self.image = self.image.convert("RGB") - return self.image.resize((width, height), Image.ANTIALIAS) - - def scale(self, width: int, height: int, output_type: str) -> BytesIO: - """Rescales the image to the given dimensions. - - Returns: - The bytes of the encoded image ready to be written to disk - """ - with self._resize(width, height) as scaled: - return self._encode_image(scaled, output_type) - - def crop(self, width: int, height: int, output_type: str) -> BytesIO: - """Rescales and crops the image to the given dimensions preserving - aspect:: - (w_in / h_in) = (w_scaled / h_scaled) - w_scaled = max(w_out, h_out * (w_in / h_in)) - h_scaled = max(h_out, w_out * (h_in / w_in)) - - Args: - max_width: The largest possible width. - max_height: The largest possible height. - - Returns: - The bytes of the encoded image ready to be written to disk - """ - if width * self.height > height * self.width: - scaled_width = width - scaled_height = (width * self.height) // self.width - crop_top = (scaled_height - height) // 2 - crop_bottom = height + crop_top - crop = (0, crop_top, width, crop_bottom) - else: - scaled_width = (height * self.width) // self.height - scaled_height = height - crop_left = (scaled_width - width) // 2 - crop_right = width + crop_left - crop = (crop_left, 0, crop_right, height) - - with self._resize(scaled_width, scaled_height) as scaled_image: - with scaled_image.crop(crop) as cropped: - return self._encode_image(cropped, output_type) - - def _encode_image(self, output_image: Image.Image, output_type: str) -> BytesIO: - output_bytes_io = BytesIO() - fmt = self.FORMATS[output_type] - if fmt == "JPEG": - output_image = output_image.convert("RGB") - output_image.save(output_bytes_io, fmt, quality=80) - return output_bytes_io - - def close(self) -> None: - """Closes the underlying image file. - - Once closed no other functions can be called. - - Can be called multiple times. - """ - - if self._closed: - return - - self._closed = True - - # Since we run this on the finalizer then we need to handle `__init__` - # raising an exception before it can define `self.image`. - image = getattr(self, "image", None) - if image is None: - return - - image.close() - - def __enter__(self) -> "Thumbnailer": - """Make `Thumbnailer` a context manager that calls `close` on - `__exit__`. - """ - return self - - def __exit__( - self, - type: Optional[Type[BaseException]], - value: Optional[BaseException], - traceback: Optional[TracebackType], - ) -> None: - self.close() - - def __del__(self) -> None: - # Make sure we actually do close the image, rather than leak data. - self.close() |