diff --git a/synapse/config/repository.py b/synapse/config/repository.py
index 6891abd71d..64644b9a7a 100644
--- a/synapse/config/repository.py
+++ b/synapse/config/repository.py
@@ -14,6 +14,39 @@
# limitations under the License.
from ._base import Config
+from collections import namedtuple
+
+ThumbnailRequirement = namedtuple(
+ "ThumbnailRequirement", ["width", "height", "method", "media_type"]
+)
+
+
+def parse_thumbnail_requirements(thumbnail_sizes):
+ """ Takes a list of dictionaries with "width", "height", and "method" keys
+ and creates a map from image media types to the thumbnail size, thumnailing
+ method, and thumbnail media type to precalculate
+
+ Args:
+ thumbnail_sizes(list): List of dicts with "width", "height", and
+ "method" keys
+ Returns:
+ Dictionary mapping from media type string to list of
+ ThumbnailRequirement tuples.
+ """
+ requirements = {}
+ for size in thumbnail_sizes:
+ width = size["width"]
+ height = size["height"]
+ method = size["method"]
+ jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg")
+ png_thumbnail = ThumbnailRequirement(width, height, method, "image/png")
+ requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail)
+ requirements.setdefault("image/gif", []).append(png_thumbnail)
+ requirements.setdefault("image/png", []).append(png_thumbnail)
+ return {
+ media_type: tuple(thumbnails)
+ for media_type, thumbnails in requirements.items()
+ }
class ContentRepositoryConfig(Config):
@@ -22,6 +55,10 @@ class ContentRepositoryConfig(Config):
self.max_image_pixels = self.parse_size(config["max_image_pixels"])
self.media_store_path = self.ensure_directory(config["media_store_path"])
self.uploads_path = self.ensure_directory(config["uploads_path"])
+ self.dynamic_thumbnails = config["dynamic_thumbnails"]
+ self.thumbnail_requirements = parse_thumbnail_requirements(
+ config["thumbnail_sizes"]
+ )
def default_config(self, config_dir_path, server_name):
media_store = self.default_path("media_store")
@@ -38,4 +75,26 @@ class ContentRepositoryConfig(Config):
# Maximum number of pixels that will be thumbnailed
max_image_pixels: "32M"
+
+ # Whether to generate new thumbnails on the fly to precisely match
+ # the resolution requested by the client. If true then whenever
+ # a new resolution is requested by the client the server will
+ # generate a new thumbnail. If false the server will pick a thumbnail
+ # from a precalcualted list.
+ dynamic_thumbnails: false
+
+ # List of thumbnail to precalculate when an image is uploaded.
+ thumbnail_sizes:
+ - width: 32
+ height: 32
+ method: crop
+ - width: 96
+ height: 96
+ method: crop
+ - width: 320
+ height: 240
+ method: scale
+ - width: 640
+ height: 480
+ method: scale
""" % locals()
diff --git a/synapse/rest/media/v1/base_resource.py b/synapse/rest/media/v1/base_resource.py
index 84e1961a21..4e21527c3d 100644
--- a/synapse/rest/media/v1/base_resource.py
+++ b/synapse/rest/media/v1/base_resource.py
@@ -69,6 +69,8 @@ class BaseMediaResource(Resource):
self.filepaths = filepaths
self.version_string = hs.version_string
self.downloads = {}
+ self.dynamic_thumbnails = hs.config.dynamic_thumbnails
+ self.thumbnail_requirements = hs.config.thumbnail_requirements
def _respond_404(self, request):
respond_with_json(
@@ -208,22 +210,74 @@ class BaseMediaResource(Resource):
self._respond_404(request)
def _get_thumbnail_requirements(self, media_type):
- if media_type == "image/jpeg":
- return (
- (32, 32, "crop", "image/jpeg"),
- (96, 96, "crop", "image/jpeg"),
- (320, 240, "scale", "image/jpeg"),
- (640, 480, "scale", "image/jpeg"),
- )
- elif (media_type == "image/png") or (media_type == "image/gif"):
- return (
- (32, 32, "crop", "image/png"),
- (96, 96, "crop", "image/png"),
- (320, 240, "scale", "image/png"),
- (640, 480, "scale", "image/png"),
+ return self.thumbnail_requirements.get(media_type, ())
+
+ def _generate_thumbnail(self, input_path, t_path, t_width, t_height,
+ t_method, t_type):
+ thumbnailer = Thumbnailer(input_path)
+ m_width = thumbnailer.width
+ m_height = thumbnailer.height
+
+ if m_width * m_height >= self.max_image_pixels:
+ logger.info(
+ "Image too large to thumbnail %r x %r > %r",
+ m_width, m_height, self.max_image_pixels
)
+ return
+
+ if t_method == "crop":
+ t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
+ elif t_method == "scale":
+ t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
else:
- return ()
+ t_len = None
+
+ return t_len
+
+ @defer.inlineCallbacks
+ def _generate_local_exact_thumbnail(self, media_id, t_width, t_height,
+ t_method, t_type):
+ input_path = self.filepaths.local_media_filepath(media_id)
+
+ t_path = self.filepaths.local_media_thumbnail(
+ media_id, t_width, t_height, t_type, t_method
+ )
+ self._makedirs(t_path)
+
+ t_len = yield threads.deferToThread(
+ self._generate_thumbnail,
+ input_path, t_path, t_width, t_height, t_method, t_type
+ )
+
+ if t_len:
+ yield self.store.store_local_thumbnail(
+ media_id, t_width, t_height, t_type, t_method, t_len
+ )
+
+ defer.returnValue(t_path)
+
+ @defer.inlineCallbacks
+ def _generate_remote_exact_thumbnail(self, server_name, file_id, media_id,
+ t_width, t_height, t_method, t_type):
+ input_path = self.filepaths.remote_media_filepath(server_name, file_id)
+
+ t_path = self.filepaths.remote_media_thumbnail(
+ server_name, file_id, t_width, t_height, t_type, t_method
+ )
+ self._makedirs(t_path)
+
+ t_len = yield threads.deferToThread(
+ self._generate_thumbnail,
+ input_path, t_path, t_width, t_height, t_method, t_type
+ )
+
+ if t_len:
+ yield self.store.store_remote_media_thumbnail(
+ server_name, media_id, file_id,
+ t_width, t_height, t_type, t_method, t_len
+ )
+
+ defer.returnValue(t_path)
@defer.inlineCallbacks
def _generate_local_thumbnails(self, media_id, media_info):
diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py
index 61f88e486e..e506dad934 100644
--- a/synapse/rest/media/v1/thumbnail_resource.py
+++ b/synapse/rest/media/v1/thumbnail_resource.py
@@ -43,14 +43,25 @@ class ThumbnailResource(BaseMediaResource):
m_type = parse_string(request, "type", "image/png")
if server_name == self.server_name:
- yield self._respond_local_thumbnail(
- request, media_id, width, height, method, m_type
- )
+ if self.dynamic_thumbnails:
+ yield self._select_or_generate_local_thumbnail(
+ request, media_id, width, height, method, m_type
+ )
+ else:
+ yield self._respond_local_thumbnail(
+ request, media_id, width, height, method, m_type
+ )
else:
- yield self._respond_remote_thumbnail(
- request, server_name, media_id,
- width, height, method, m_type
- )
+ if self.dynamic_thumbnails:
+ yield self._select_or_generate_remote_thumbnail(
+ request, server_name, media_id,
+ width, height, method, m_type
+ )
+ else:
+ yield self._respond_remote_thumbnail(
+ request, server_name, media_id,
+ width, height, method, m_type
+ )
@defer.inlineCallbacks
def _respond_local_thumbnail(self, request, media_id, width, height,
@@ -83,6 +94,87 @@ class ThumbnailResource(BaseMediaResource):
)
@defer.inlineCallbacks
+ def _select_or_generate_local_thumbnail(self, request, media_id, desired_width,
+ desired_height, desired_method,
+ desired_type):
+ media_info = yield self.store.get_local_media(media_id)
+
+ if not media_info:
+ self._respond_404(request)
+ return
+
+ thumbnail_infos = yield self.store.get_local_media_thumbnails(media_id)
+ for info in thumbnail_infos:
+ t_w = info["thumbnail_width"] == desired_width
+ t_h = info["thumbnail_height"] == desired_height
+ t_method = info["thumbnail_method"] == desired_method
+ t_type = info["thumbnail_type"] == desired_type
+
+ if t_w and t_h and t_method and t_type:
+ file_path = self.filepaths.local_media_thumbnail(
+ media_id, desired_width, desired_height, desired_type, desired_method,
+ )
+ yield self._respond_with_file(request, desired_type, file_path)
+ return
+
+ logger.debug("We don't have a local thumbnail of that size. Generating")
+
+ # Okay, so we generate one.
+ file_path = yield self._generate_local_exact_thumbnail(
+ media_id, desired_width, desired_height, desired_method, desired_type
+ )
+
+ if file_path:
+ yield self._respond_with_file(request, desired_type, file_path)
+ else:
+ yield self._respond_default_thumbnail(
+ request, media_info, desired_width, desired_height,
+ desired_method, desired_type,
+ )
+
+ @defer.inlineCallbacks
+ def _select_or_generate_remote_thumbnail(self, request, server_name, media_id,
+ desired_width, desired_height,
+ desired_method, desired_type):
+ media_info = yield self._get_remote_media(server_name, media_id)
+
+ thumbnail_infos = yield self.store.get_remote_media_thumbnails(
+ server_name, media_id,
+ )
+
+ file_id = media_info["filesystem_id"]
+
+ for info in thumbnail_infos:
+ t_w = info["thumbnail_width"] == desired_width
+ t_h = info["thumbnail_height"] == desired_height
+ t_method = info["thumbnail_method"] == desired_method
+ t_type = info["thumbnail_type"] == desired_type
+
+ if t_w and t_h and t_method and t_type:
+ file_path = self.filepaths.remote_media_thumbnail(
+ server_name, file_id, desired_width, desired_height,
+ desired_type, desired_method,
+ )
+ yield self._respond_with_file(request, desired_type, file_path)
+ return
+
+ logger.debug("We don't have a local thumbnail of that size. Generating")
+
+ # Okay, so we generate one.
+ file_path = yield self._generate_remote_exact_thumbnail(
+ server_name, file_id, media_id, desired_width,
+ desired_height, desired_method, desired_type
+ )
+
+ if file_path:
+ yield self._respond_with_file(request, desired_type, file_path)
+ else:
+ yield self._respond_default_thumbnail(
+ request, media_info, desired_width, desired_height,
+ desired_method, desired_type,
+ )
+
+ @defer.inlineCallbacks
def _respond_remote_thumbnail(self, request, server_name, media_id, width,
height, method, m_type):
# TODO: Don't download the whole remote file
|