summary refs log tree commit diff
path: root/synapse/rest/media/v1/thumbnailer.py
diff options
context:
space:
mode:
authorPatrick Cloke <clokep@users.noreply.github.com>2023-02-27 08:26:05 -0500
committerGitHub <noreply@github.com>2023-02-27 08:26:05 -0500
commit4fc8875876374ec8f97a3b3cc344a4e3abcf769f (patch)
tree41b920427c0f62cd1463324c89fe7f5ce3d15164 /synapse/rest/media/v1/thumbnailer.py
parentSmall fixes to `MatrixFederationHttpClient` docstrings (#15148) (diff)
downloadsynapse-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.py221
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()