# -*- coding: utf-8 -*-
# Copyright (c) 2017-2022 Richard Hull and contributors
# See LICENSE.rst for details.
"""
Composes scrollable, positionable images into another
Image
.. versionadded:: 1.1.0
"""
from PIL import Image, ImageDraw
[docs]
class ComposableImage(object):
"""
This class encapsulates an image and its attributes
that can be rendered onto an :py:class:`ImageComposition`.
"""
def __init__(self, image, position=(0, 0), offset=(0, 0)):
"""
Instantiates a new ``ComposableImage``.
:param image: The composable image.
:type image: PIL.Image
:param position: The initial position of the image
within the composition.
:type position: tuple
:param offset: The initial offset within the image,
from which it should be drawn.
:type offset: tuple
"""
assert image
self._image = image
self._position = position
self._offset = offset
@property
def position(self):
"""
Getter for position
:returns: A tuple containing the ``x,y`` position.
:rtype: tuple
"""
return self._position
@position.setter
def position(self, value):
"""
Indicates where the image is to be rendered in an image
composition.
:param value: The ``x,y`` position tuple.
:type value: tuple
"""
self._position = value
@property
def offset(self):
"""
Getter for offset.
:returns: A tuple containing the top,left position.
:rtype: tuple
"""
return self._offset
@offset.setter
def offset(self, value):
"""
Indicates the top left position within the image,
as of which it is is to be rendered in the
image composition.
:param value: The top,left position.
:type value: tuple
"""
self._offset = value
@property
def width(self):
"""
:returns: The actual width of the image, regardless
its position or offset within the image composition.
:rtype: int
"""
return self._image.width
@property
def height(self):
"""
:returns: The actual height of the image, regardless
its position or offset within the image composition.
:rtype: int
"""
return self._image.height
[docs]
def image(self, size):
"""
:param size: The width, height of the image composition.
:type size: tuple
:returns: An image, cropped to the boundaries specified
by ``size``.
:rtype: PIL.Image.Image
"""
assert size[0]
assert size[1]
return self._image.crop(box=self._crop_box(size))
def _crop_box(self, size):
"""
Helper that calculates the crop box for the offset within the image.
:param size: The width and height of the image composition.
:type size: tuple
:returns: The bounding box of the image, given ``size``.
:rtype: tuple
"""
(left, top) = self.offset
right = left + min(size[0], self.width)
bottom = top + min(size[1], self.height)
return (left, top, right, bottom)
[docs]
class ImageComposition(object):
"""
Manages a composition of :py:class:`ComposableImage` instances that
can be rendered onto a single :py:class:`PIL.Image.Image`.
"""
def __init__(self, device):
"""
Instantiates a new ``ImageComposition``
:param device: The device on which to render.
:type device: luma.core.device.device
"""
self._device = device
self._background_image = Image.new(device.mode, device.size)
self.composed_images = []
[docs]
def add_image(self, image):
"""
Adds an image to the composition.
:param image: The image to add.
:type image: PIL.Image.Image
"""
assert image
self.composed_images.append(image)
[docs]
def remove_image(self, image):
"""
Removes an image from the composition.
:param image: The image to be removed.
:type image: PIL.Image.Image
"""
assert image
self.composed_images.remove(image)
def __call__(self):
"""
Returns the current composition.
:rtype: PIL.Image.Image
"""
return self._background_image
[docs]
def refresh(self):
"""
Clears the composition and renders all the images
taking into account their position and offset.
"""
self._clear()
for img in self.composed_images:
self._background_image.paste(img.image(self._device.size),
img.position)
self._background_image.crop(box=self._device.bounding_box)
def _clear(self):
"""
Helper that clears the composition.
"""
draw = ImageDraw.Draw(self._background_image)
draw.rectangle(self._device.bounding_box,
fill="black")
del draw