Compress images in a separate process

Compression with Pillow can take long, especially with large
clipboard PNG images.
Doing this in a separate process prevents the async event loop from
getting blocked, and allows multiple compression operations to run in
parallel.
This commit is contained in:
miruka 2020-07-21 23:35:16 -04:00
parent 286b7a951a
commit 0088fadddd
3 changed files with 40 additions and 14 deletions

View File

@ -4,7 +4,6 @@
- hflickable: support kinetic scrolling disabler
- settings
- compress png in a thread
- verify upload cancellation
- clipboard preview doesn't update when copied image changes until second time
- Avatar tooltip can get displayed in front of presence menu

View File

@ -597,11 +597,11 @@ class MatrixClient(nio.AsyncClient):
with NamedTemporaryFile(prefix=prefix, suffix=".png") as temp:
async def get_path() -> Path:
with io.BytesIO(image) as inp, io.BytesIO() as buffer:
PILImage.open(inp).save(buffer, "PNG")
# optimize is too slow for large images
compressed = await utils.compress_image(image, optimize=False)
async with aiofiles.open(temp.name, "wb") as file:
await file.write(buffer.getvalue())
await file.write(compressed)
return Path(temp.name)
@ -1308,15 +1308,14 @@ class MatrixClient(nio.AsyncClient):
if not small:
thumb.thumbnail((800, 600))
with io.BytesIO() as out:
if thumb.mode in png_modes:
thumb.save(out, "PNG", optimize=True)
thumb_data = await utils.compress_image(thumb)
mime = "image/png"
else:
thumb.convert("RGB").save(out, "JPEG", optimize=True)
thumb = thumb.convert("RGB")
thumb_data = await utils.compress_image(thumb, "JPEG")
mime = "image/jpeg"
thumb_data = out.getvalue()
thumb_size = len(thumb_data)
if thumb_size >= len(data) and is_jpg_png and not is_svg:

View File

@ -2,6 +2,7 @@
"""Various utilities that are used throughout the package."""
import asyncio
import collections
import html
import inspect
@ -9,6 +10,7 @@ import io
import json
import sys
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
from concurrent.futures import ProcessPoolExecutor
from datetime import datetime, timedelta
from enum import Enum
from enum import auto as autostr
@ -24,6 +26,8 @@ import aiofiles
import filetype
from aiofiles.threadpool.binary import AsyncBufferedReader
from aiofiles.threadpool.text import AsyncTextIOWrapper
from PIL import Image as PILImage
from nio.crypto import AsyncDataT as File
from nio.crypto import async_generator_from_data
@ -34,8 +38,11 @@ else:
AsyncOpenFile = Union[AsyncTextIOWrapper, AsyncBufferedReader]
Size = Tuple[int, int]
BytesOrPIL = Union[bytes, PILImage.Image]
auto = autostr
COMPRESSION_POOL = ProcessPoolExecutor()
class AutoStrEnum(Enum):
"""An Enum where auto() assigns the member's name instead of an integer.
@ -236,3 +243,24 @@ async def atomic_write(
temp_path.replace(path)
else:
temp_path.unlink()
def _compress(image: BytesOrPIL, fmt: str, optimize: bool) -> bytes:
if isinstance(image, bytes):
pil_image = PILImage.open(io.BytesIO(image))
else:
pil_image = image
with io.BytesIO() as buffer:
pil_image.save(buffer, fmt, optimize=optimize)
return buffer.getvalue()
async def compress_image(
image: BytesOrPIL, fmt: str = "PNG", optimize: bool = True,
) -> bytes:
"""Compress image in a separate process, without blocking event loop."""
return await asyncio.get_event_loop().run_in_executor(
COMPRESSION_POOL, _compress, image, fmt, optimize,
)