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)