From 1555a4717efcdb7e5dccfdb98ac17c0ff40ac1d3 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 9 Dec 2018 01:51:58 -0300 Subject: [PATCH] attachments: add gif resize support - images: strip IconManager.resize_gif into its own function --- litecord/blueprints/attachments.py | 30 ++++++- litecord/images.py | 121 ++++++++++++++--------------- 2 files changed, 87 insertions(+), 64 deletions(-) diff --git a/litecord/blueprints/attachments.py b/litecord/blueprints/attachments.py index cffe495..2ece3e0 100644 --- a/litecord/blueprints/attachments.py +++ b/litecord/blueprints/attachments.py @@ -22,15 +22,34 @@ from pathlib import Path from quart import Blueprint, send_file, current_app as app, request from PIL import Image +from litecord.images import resize_gif + bp = Blueprint('attachments', __name__) ATTACHMENTS = Path.cwd() / 'attachments' -async def _resize(image, attach_id: str, ext: str, +async def _resize_gif(attach_id: int, resized_path: Path, + width: int, height: int) -> str: + """Resize a GIF attachment.""" + + # get original gif bytes + orig_path = ATTACHMENTS / f'{attach_id}.gif' + orig_bytes = orig_path.read_bytes() + + # give them and the target size to the + # image module's resize_gif + + _data_fd, raw_data = await resize_gif(orig_bytes, (width, height)) + + # write raw_data to the destination + resized_path.write_bytes(raw_data) + + return str(resized_path) + + +async def _resize(image, attach_id: int, ext: str, width: int, height: int) -> str: """Resize an image.""" - # TODO: gif support - # check if we have it on the folder resized_path = ATTACHMENTS / f'{attach_id}_{width}_{height}.{ext}' @@ -44,6 +63,11 @@ async def _resize(image, attach_id: str, ext: str, # if we dont, we need to generate it off the # given image instance. + # the process is different for gif files because we need + # gifsicle. doing it manually is too troublesome. + if ext == 'gif': + return await _resize_gif(attach_id, resized_path, width, height) + # NOTE: this is the same resize mode for icons. resized = image.resize((width, height), resample=Image.LANCZOS) resized.save(resized_path_s, format=ext) diff --git a/litecord/images.py b/litecord/images.py index d0cf305..d1395aa 100644 --- a/litecord/images.py +++ b/litecord/images.py @@ -194,6 +194,65 @@ def _try_unlink(path: str): pass +async def resize_gif(raw_data: bytes, target: tuple) -> tuple: + """Resize a GIF image.""" + # generate a temporary file to call gifsticle to and from. + input_fd, input_path = tempfile.mkstemp(suffix='.gif') + _, output_path = tempfile.mkstemp(suffix='.gif') + + input_handler = os.fdopen(input_fd, 'wb') + + # make sure its valid image data + data_fd = BytesIO(raw_data) + image = Image.open(data_fd) + image.close() + + log.info('resizing a GIF from {} to {}', + image.size, target) + + # insert image info on input_handler + # close it to make it ready for consumption by gifsicle + input_handler.write(raw_data) + input_handler.close() + + # call gifsicle under subprocess + log.debug('input: {}', input_path) + log.debug('output: {}', output_path) + + process = await asyncio.create_subprocess_shell( + f'gifsicle --resize {target[0]}x{target[1]} ' + f'{input_path} > {output_path}', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + # run it, etc. + out, err = await process.communicate() + + log.debug('out + err from gifsicle: {}', out + err) + + # write over an empty data_fd + data_fd = BytesIO() + output_handler = open(output_path, 'rb') + data_fd.write(output_handler.read()) + + # close unused handlers + output_handler.close() + + # delete the files we created with mkstemp + _try_unlink(input_path) + _try_unlink(output_path) + + # reseek, save to raw_data, reseek again. + # TODO: remove raw_data altogether as its inefficient + # to have two representations of the same bytes + data_fd.seek(0) + raw_data = data_fd.read() + data_fd.seek(0) + + return data_fd, raw_data + + class IconManager: """Main icon manager.""" def __init__(self, app): @@ -257,65 +316,6 @@ class IconManager: return await self.generic_get( 'guild', guild_id, icon_hash, **kwargs) - async def _resize_gif(self, scope: str, key, - raw_data: bytes, target: tuple) -> tuple: - """Resize a GIF image.""" - # generate a temporary file to call gifsticle to and from. - input_fd, input_path = tempfile.mkstemp(suffix='.gif') - _, output_path = tempfile.mkstemp(suffix='.gif') - - input_handler = os.fdopen(input_fd, 'wb') - - # make sure its valid image data - data_fd = BytesIO(raw_data) - image = Image.open(data_fd) - image.close() - - log.info('resizing a GIF from {} to {}', - image.size, target) - - # insert image info on input_handler - # close it to make it ready for consumption by gifsicle - input_handler.write(raw_data) - input_handler.close() - - # call gifsicle under subprocess - log.debug('input: {}', input_path) - log.debug('output: {}', output_path) - - process = await asyncio.create_subprocess_shell( - f'gifsicle --resize {target[0]}x{target[1]} ' - f'{input_path} > {output_path}', - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - # run it, etc. - await process.communicate() - - # write over an empty data_fd - data_fd = BytesIO() - output_handler = open(output_path, 'rb') - data_fd.write(output_handler.read()) - - # close unused handlers - output_handler.close() - - # delete the files we created with mkstemp - _try_unlink(input_path) - _try_unlink(output_path) - - # reseek, save to raw_data, reseek again. - # TODO: remove raw_data altogether as its inefficient - # to have two representations of the same bytes - data_fd.seek(0) - raw_data = data_fd.read() - data_fd.seek(0) - - - return data_fd, raw_data - - async def put(self, scope: str, key: str, b64_data: str, **kwargs) -> Icon: """Insert an icon.""" @@ -336,8 +336,7 @@ class IconManager: # size management is different for gif files # as they're composed of multiple frames. if 'size' in kwargs and mime == 'image/gif': - data_fd, raw_data = await self._resize_gif( - scope, key, raw_data, kwargs['size']) + data_fd, raw_data = await resize_gif(raw_data, kwargs['size']) elif 'size' in kwargs: image = Image.open(data_fd)