File: image_filter.py

package info (click to toggle)
streamdeck-ui 2.0.15-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,456 kB
  • sloc: python: 2,167; makefile: 3
file content (119 lines) | stat: -rw-r--r-- 4,351 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import itertools
from fractions import Fraction
from io import BytesIO
from typing import Callable, Tuple

import cairosvg
import filetype
from PIL import Image, ImageSequence

from streamdeck_ui.display.filter import Filter


class ImageFilter(Filter):
    """
    Represents a static image. It transforms the input image by replacing it with a static image.
    """

    def __init__(self, file: str):
        super(ImageFilter, self).__init__()
        self.file = file

    def initialize(self, size: Tuple[int, int]):
        # Each frame needs to have a unique hashcode. Start with file name as baseline.
        image_hash = hash((self.__class__, self.file))
        frame_duration = []
        frame_hash = []

        try:
            kind = filetype.guess(self.file)
            if kind is None:
                svg_code = open(self.file).read()
                png = cairosvg.svg2png(svg_code, output_height=size[1], output_width=size[0])
                image_file = BytesIO(png)
                image = Image.open(image_file)
                frame_duration.append(-1)
                frame_hash.append(image_hash)
            else:
                image = Image.open(self.file)
                image.seek(0)
                # Frame number is used to create unique hash
                frame_number = 1
                while True:
                    try:
                        frame_duration.append(image.info["duration"])
                        # Create tuple and hash it, to combine the image and frame hashcodes
                        frame_hash.append(hash((image_hash, frame_number)))
                        image.seek(image.tell() + 1)
                        frame_number += 1
                    except EOFError:
                        # Reached the final frame
                        break
                    except KeyError:
                        # If the key 'duration' can't be found, it's not an animation
                        frame_duration.append(-1)
                        frame_hash.append(image_hash)
                        break

        except OSError as icon_error:
            # FIXME: caller should handle this?
            print(f"Unable to load icon {self.file} with error {icon_error}")
            image = Image.new("RGB", size)
            frame_duration.append(-1)
            frame_hash.append(image_hash)

        frames = ImageSequence.Iterator(image)

        # Scale all the frames to the target size
        self.frames = []
        for frame, milliseconds, hashcode in zip(frames, frame_duration, frame_hash):
            frame = frame.copy()
            frame.thumbnail(size, Image.LANCZOS)
            self.frames.append((frame, milliseconds, hashcode))

        self.frame_cycle = itertools.cycle(self.frames)
        self.current_frame = next(self.frame_cycle)
        self.frame_time = Fraction()

    def transform(self, get_input: Callable[[], Image.Image], get_output: Callable[[int], Image.Image], input_changed: bool, time: Fraction) -> Tuple[Image.Image, int]:
        """
        The transformation returns the loaded image, ando overwrites whatever came before.
        """

        # Unpack tuple to make code a bit easier to understand
        frame, duration, hashcode = self.current_frame

        if duration >= 0 and time - self.frame_time > duration / 1000:
            self.frame_time = time
            self.current_frame = next(self.frame_cycle)

            # Unpack updated value
            frame, duration, hashcode = self.current_frame

            image = get_output(hashcode)
            if image:
                return (image, hashcode)

            input = get_input()
            if frame.mode == "RGBA":
                # Use the transparency mask of the image to paste
                input.paste(frame, frame)
            else:
                input.paste(frame)
            return (input, hashcode)

        if input_changed:
            image = get_output(hashcode)
            if image:
                return (image, hashcode)

            input = get_input()

            if frame.mode == "RGBA":
                # Use the transparency mask of the image to paste
                input.paste(frame, frame)
            else:
                input.paste(frame)
            return (input, hashcode)
        else:
            return (None, hashcode)