greendeck/greendeck/lib/images/__init__.py
2022-12-13 14:25:50 -06:00

162 lines
5 KiB
Python

import asyncio
import io
from PIL import Image
from PIL.Image import Image as ImageType
from pydantic.color import Color
from greendeck.lib.elgato.streamdeck import StreamDeck
from greendeck.lib.images.materialdesignicons import get_material_design_icon
__all__ = [
"create_image",
"create_scaled_image",
"render_key_image",
"to_native_format",
]
def _create_image(deck: StreamDeck, background: Color = Color("black")) -> ImageType:
"""
Creates a new PIL Image with the correct image dimensions for the given
StreamDeck device's keys.
.. seealso:: See :func:`~PILHelper.to_native_format` method for converting a
PIL image instance to the native image format of a given
StreamDeck device.
:param StreamDeck deck: StreamDeck device to generate a compatible image for.
:param str background: Background color to use, compatible with `PIL.Image.new()`.
:rtype: PIL.Image
:return: Created PIL image
"""
image_format = deck.key_image_format()
return Image.new("RGBA", image_format["size"], background.as_rgb_tuple())
async def create_image(
deck: StreamDeck, background: Color = Color("black")
) -> ImageType:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, _create_image, deck, background)
def _create_scaled_image(
deck: StreamDeck,
image: ImageType,
margins: tuple[int, int, int, int] = (0, 0, 0, 0),
background: Color = Color("black"),
) -> ImageType:
if len(margins) != 4:
raise ValueError("Margins should be given as an array of four integers.")
final_image = create_image(deck, background=background.as_rgb_tuple())
thumbnail_max_width = final_image.width - (margins[1] + margins[3])
thumbnail_max_height = final_image.height - (margins[0] + margins[2])
thumbnail = image.convert("RGBA")
thumbnail.thumbnail((thumbnail_max_width, thumbnail_max_height), Image.LANCZOS)
thumbnail_x = margins[3] + (thumbnail_max_width - thumbnail.width) // 2
thumbnail_y = margins[0] + (thumbnail_max_height - thumbnail.height) // 2
final_image.paste(thumbnail, (thumbnail_x, thumbnail_y), thumbnail)
return final_image
async def create_scaled_image(
deck: StreamDeck,
image: ImageType,
margins: tuple[int, int, int, int] = (0, 0, 0, 0),
background: Color = Color("black"),
) -> ImageType:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, _create_scaled_image, deck, image, margins, background
)
def _to_native_format(deck: StreamDeck, image: ImageType) -> bytes:
image_format = deck.key_image_format()
if image_format["rotation"]:
image = image.rotate(image_format["rotation"])
if image_format["flip"][0]:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
if image_format["flip"][1]:
image = image.transpose(Image.FLIP_TOP_BOTTOM)
if image.size != image_format["size"]:
image.thumbnail(image_format["size"], Image.LANCZOS)
image = image.convert("RGB")
# We want a compressed image in a given codec, convert.
compressed_image = io.BytesIO()
image.save(compressed_image, image_format["format"], quality=100)
return compressed_image.getvalue()
async def to_native_format(deck: StreamDeck, image: ImageType) -> bytes:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, _to_native_format, deck, image)
async def render_key_image(
deck: StreamDeck, name: str, foreground: Color, background: Color
) -> bytes:
loop = asyncio.get_running_loop()
icon: ImageType
foreground: ImageType
background: ImageType
icon, foreground, background = await asyncio.gather(
get_material_design_icon(name, deck.KEY_PIXEL_WIDTH, deck.KEY_PIXEL_HEIGHT),
create_image(deck, foreground),
create_image(deck, background),
)
def _compose(
foreground: ImageType, background: ImageType, icon: ImageType
) -> ImageType:
image = background.copy()
image.paste(foreground, None, icon)
return image
image = await loop.run_in_executor(None, _compose, foreground, background, icon)
def _convert(image: ImageType):
return image.convert(mode="RGB")
image = await loop.run_in_executor(None, _convert, image)
# Resize the source image asset to best-fit the dimensions of a single key,
# leaving a margin at the bottom so that we can draw the key title
# afterwards.
# icon = Image.open(icon_filename)
# image = PILHelper.create_scaled_image(deck, icon, margins=[0, 0, 20, 0])
# # Load a custom TrueType font and use it to overlay the key index, draw key
# # label onto the image a few pixels from the bottom of the key.
# draw = ImageDraw.Draw(image)
# font = ImageFont.truetype(font_filename, 14)
# draw.text(
# (image.width / 2, image.height - 5),
# text=label_text,
# font=font,
# anchor="ms",
# fill="white",
# )
return await to_native_format(deck, image)