162 lines
5 KiB
Python
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)
|