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:
parent
286b7a951a
commit
0088fadddd
1
TODO.md
1
TODO.md
|
@ -4,7 +4,6 @@
|
||||||
- hflickable: support kinetic scrolling disabler
|
- hflickable: support kinetic scrolling disabler
|
||||||
- settings
|
- settings
|
||||||
|
|
||||||
- compress png in a thread
|
|
||||||
- verify upload cancellation
|
- verify upload cancellation
|
||||||
- clipboard preview doesn't update when copied image changes until second time
|
- clipboard preview doesn't update when copied image changes until second time
|
||||||
- Avatar tooltip can get displayed in front of presence menu
|
- Avatar tooltip can get displayed in front of presence menu
|
||||||
|
|
|
@ -597,11 +597,11 @@ class MatrixClient(nio.AsyncClient):
|
||||||
with NamedTemporaryFile(prefix=prefix, suffix=".png") as temp:
|
with NamedTemporaryFile(prefix=prefix, suffix=".png") as temp:
|
||||||
|
|
||||||
async def get_path() -> Path:
|
async def get_path() -> Path:
|
||||||
with io.BytesIO(image) as inp, io.BytesIO() as buffer:
|
# optimize is too slow for large images
|
||||||
PILImage.open(inp).save(buffer, "PNG")
|
compressed = await utils.compress_image(image, optimize=False)
|
||||||
|
|
||||||
async with aiofiles.open(temp.name, "wb") as file:
|
async with aiofiles.open(temp.name, "wb") as file:
|
||||||
await file.write(buffer.getvalue())
|
await file.write(compressed)
|
||||||
|
|
||||||
return Path(temp.name)
|
return Path(temp.name)
|
||||||
|
|
||||||
|
@ -1308,15 +1308,14 @@ class MatrixClient(nio.AsyncClient):
|
||||||
if not small:
|
if not small:
|
||||||
thumb.thumbnail((800, 600))
|
thumb.thumbnail((800, 600))
|
||||||
|
|
||||||
with io.BytesIO() as out:
|
|
||||||
if thumb.mode in png_modes:
|
if thumb.mode in png_modes:
|
||||||
thumb.save(out, "PNG", optimize=True)
|
thumb_data = await utils.compress_image(thumb)
|
||||||
mime = "image/png"
|
mime = "image/png"
|
||||||
else:
|
else:
|
||||||
thumb.convert("RGB").save(out, "JPEG", optimize=True)
|
thumb = thumb.convert("RGB")
|
||||||
|
thumb_data = await utils.compress_image(thumb, "JPEG")
|
||||||
mime = "image/jpeg"
|
mime = "image/jpeg"
|
||||||
|
|
||||||
thumb_data = out.getvalue()
|
|
||||||
thumb_size = len(thumb_data)
|
thumb_size = len(thumb_data)
|
||||||
|
|
||||||
if thumb_size >= len(data) and is_jpg_png and not is_svg:
|
if thumb_size >= len(data) and is_jpg_png and not is_svg:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
"""Various utilities that are used throughout the package."""
|
"""Various utilities that are used throughout the package."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import collections
|
import collections
|
||||||
import html
|
import html
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -9,6 +10,7 @@ import io
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
|
import xml.etree.cElementTree as xml_etree # FIXME: bandit warning
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from enum import auto as autostr
|
from enum import auto as autostr
|
||||||
|
@ -24,6 +26,8 @@ import aiofiles
|
||||||
import filetype
|
import filetype
|
||||||
from aiofiles.threadpool.binary import AsyncBufferedReader
|
from aiofiles.threadpool.binary import AsyncBufferedReader
|
||||||
from aiofiles.threadpool.text import AsyncTextIOWrapper
|
from aiofiles.threadpool.text import AsyncTextIOWrapper
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
from nio.crypto import AsyncDataT as File
|
from nio.crypto import AsyncDataT as File
|
||||||
from nio.crypto import async_generator_from_data
|
from nio.crypto import async_generator_from_data
|
||||||
|
|
||||||
|
@ -34,8 +38,11 @@ else:
|
||||||
|
|
||||||
AsyncOpenFile = Union[AsyncTextIOWrapper, AsyncBufferedReader]
|
AsyncOpenFile = Union[AsyncTextIOWrapper, AsyncBufferedReader]
|
||||||
Size = Tuple[int, int]
|
Size = Tuple[int, int]
|
||||||
|
BytesOrPIL = Union[bytes, PILImage.Image]
|
||||||
auto = autostr
|
auto = autostr
|
||||||
|
|
||||||
|
COMPRESSION_POOL = ProcessPoolExecutor()
|
||||||
|
|
||||||
|
|
||||||
class AutoStrEnum(Enum):
|
class AutoStrEnum(Enum):
|
||||||
"""An Enum where auto() assigns the member's name instead of an integer.
|
"""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)
|
temp_path.replace(path)
|
||||||
else:
|
else:
|
||||||
temp_path.unlink()
|
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,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user