attachments: add gif resize support

- images: strip IconManager.resize_gif into its own function
This commit is contained in:
Luna 2018-12-09 01:51:58 -03:00
parent 35967cb714
commit 1555a4717e
2 changed files with 87 additions and 64 deletions

View File

@ -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)

View File

@ -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)