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 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
|
# this is to cater for Python 2, is it really needed?
try:
from inspect import getfullargspec
except ImportError:
from inspect import getargspec as getfullargspec
try:
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
from tkinter import PhotoImage, TclError
import sys
class GUIZeroException(Exception):
pass
# holds details about the configuration guizero is using
class SystemConfig():
def __init__(self):
"""
Holds details about the system configuration guizero is using
"""
self._platform = sys.platform
if self._platform.startswith("linux"):
self._platform = "linux"
if PIL_AVAILABLE:
self._supported_image_types = ["GIF", "Animated GIF", "BMP", "ICO", "PNG", "JPG", "TIF"]
else:
self._supported_image_types = ["GIF", "PNG"]
if self._platform == "darwin":
#MacOS only supports GIF with PIL
self._supported_image_types = ["GIF"]
@property
def PIL_available(self):
"""
Returns `True` if PIL (Python Imaging Library) is available.
"""
return PIL_AVAILABLE
@property
def supported_image_types(self):
"""
Returns a list of images types supported by this system
e.g. ('GIF', 'PNG', 'JPG')
"""
return self._supported_image_types
@property
def platform(self):
"""
Returns the current platform ("linux", "darwin", "win32")
"""
return self._platform
system_config = SystemConfig()
class GUIZeroImage():
def __init__(self, image_source, width, height):
"""
GUIZeroImage manages an "image" for guizero widgets, parsing its
contents, sizing it accordingly and managing environment.
:param string image_source:
The source of the image, a file path, PIL or
Tk image object.
:param int width:
The required width of the image, set to `None`, to keep the
original image width
:param int height:
The required height of the image, set to `None`, to keep the
original image width.
"""
self._image_source = image_source
self._pil_image = None
self._tk_image = None
self._tk_frames = []
self._width = width
self._height = height
self._current_frame = 0
self._animation = False
self._animation_running = False
# open the image
self._setup_image()
@property
def image_source(self):
"""
Returns the original source of the image, be that a file path, PIL or
Tk image object.
"""
return self._image_source
@property
def tk_image(self):
"""
Returns the Tk PhotoImage object.
"""
return self._tk_image
@property
def pil_image(self):
"""
Returns the PIL Image object.
"""
return self._pil_image
@property
def width(self):
"""
Returns the image width.
"""
return int(self._width)
@property
def height(self):
"""
Returns the image height.
"""
return int(self._height)
@property
def animation(self):
"""
Returns `True` if the image contains more than 1 frame (i.e. is an
animation)
"""
return self._animation
@property
def tk_frames(self):
"""
Returns a list of frames as Tk PhotoImage objects which make up this
image.
"""
return self._tk_frames
def _setup_image(self):
try:
# open image
self._open_image_source()
# size image
self._size_image()
# open frames
self._open_image_frames()
except Exception as e:
error_text = "Image import error - '{}'\n".format(e)
error_text += "Check the file path and image type is {}".format("/".join(system_config.supported_image_types))
raise_error(error_text)
def _open_image_source(self):
if system_config.PIL_available:
if isinstance(self._image_source, str):
# the source is a string, so try and open as a path
self._pil_image = Image.open(self._image_source)
self._tk_image = ImageTk.PhotoImage(self._pil_image)
elif Image.isImageType(self._image_source):
# the source is a PIL Image
self._pil_image = self._image_source
self._tk_image = ImageTk.PhotoImage(self._pil_image)
elif isinstance(self._image_source, (PhotoImage, ImageTk.PhotoImage)):
self._tk_image = self._image_source
else:
raise Exception("Image must be a file path, PIL.Image or tkinter.PhotoImage")
else:
if isinstance(self._image_source, str):
self._tk_image = PhotoImage(file=self._image_source)
elif isinstance(self._image_source, PhotoImage):
self._tk_image = self._image_source
else:
raise Exception("Image must be a file path or tkinter.PhotoImage")
def _size_image(self):
# if there is no size, set it to the image width
if self._width is None:
self._width = self._tk_image.width()
if self._height is None:
self._height = self._tk_image.height()
# does it need resizing?
if self._width != self._tk_image.width() or self._height != self._tk_image.height():
if self._pil_image:
resized_image = self._pil_image.resize((self._width, self._height), Image.ANTIALIAS)
self._tk_image = ImageTk.PhotoImage(resized_image)
else:
error_format("Image resizing - cannot scale the image as PIL is not available.")
def _open_image_frames(self):
if self._pil_image:
frame_count = 0
try:
while True:
self._pil_image.seek(frame_count)
tk_frame = ImageTk.PhotoImage(self._pil_image.resize((self._width, self._height), Image.ANTIALIAS))
try:
delay = self._pil_image.info['duration']
except:
delay = 100
self._tk_frames.append((tk_frame, delay))
frame_count += 1
except EOFError as e:
# end of frames
pass
if frame_count > 1:
self._animation = True
class AnimationPlayer():
def __init__(self, master, guizero_image, update_image_callback):
"""
AnimationPlayer manages the playing of a animated gif for guizero
widgets.
:param Widget master:
The widget which is displaying the animation.
:param GUIZeroImage guizero_image:
The image object which contains the animation.
:param function update_image_callback:
A function which should be called when the Image needs updating.
The function will be called and passed a reference to the next
Tk PhotoImage object in the animation.
"""
self._master = master
self._guizero_image = guizero_image
self._update_image_callback = update_image_callback
self._current_frame = 0
self._running = False
self.start()
@property
def running(self):
"""
Returns `True` if the animation is running
"""
return self._running
def start(self):
"""
Starts the animation.
"""
if not self._running:
self._running = True
self._show_frame()
def stop(self):
"""
Stops the animation
"""
self._running = False
def _show_frame(self):
if self.running:
# get the frame
frame_data = self._guizero_image.tk_frames[self._current_frame]
frame = frame_data[0]
delay = frame_data[1]
# give it to the call back
self._update_image_callback(frame)
# increment the frame
self._current_frame += 1
if self._current_frame == len(self._guizero_image.tk_frames):
self._current_frame = 0
# call again after the delay
self._master.after(delay, self._show_frame)
# Lambda-izer for making it easy to pass arguments with function calls
# without having to know what lambda does
def with_args( func_name, *args):
return lambda: func_name(*args)
# Gets the number of args a function expects
def no_args_expected(func_name):
args = getfullargspec(func_name).args
if len(args) > 0:
# if someone names the first arg of a class function to something
# other than self, this will fail! or if they name the first argument
# of a non class function to self this will fail!
if args[0] == "self":
return len(args) - 1
else:
return len(args)
else:
return 0
# Format errors in a pretty way
def error_format(message):
print("------------------------------------------------------------")
print("*** GUIZERO WARNING ***")
print(message)
print("------------------------------------------------------------")
# Raise error in a pretty way
def raise_error(message):
error_message = "\n------------------------------------------------------------\n"
error_message += "*** GUIZERO ERROR ***\n"
error_message += message + "\n"
error_message += "------------------------------------------------------------\n"
raise GUIZeroException(error_message)
def deprecated(message):
print("*** DEPRECATED: " + message)
def convert_color(color):
"""
Converts a color from "color", (255, 255, 255) or "#ffffff" into a color tk
should understand.
"""
if color is not None:
# is the color a string
if isinstance(color, str):
# strip the color of white space
color = color.strip()
# if it starts with a # check it is a valid color
if color[0] == "#":
# check its format
if len(color) != 7:
raise ValueError("{} is not a valid # color, it must be in the format #ffffff.".format(color))
else:
# split the color into its hex values
hex_colors = (color[1:3], color[3:5], color[5:7])
# check hex values are between 00 and ff
for hex_color in hex_colors:
try:
int_color = int(hex_color, 16)
except:
raise ValueError("{} is not a valid value, it must be hex 00 - ff".format(hex_color))
if not (0 <= int_color <= 255):
raise ValueError("{} is not a valid color value, it must be 00 - ff".format(hex_color))
# if the color is not a string, try and convert it
else:
# get the number of colors and check it is iterable
try:
no_of_colors = len(color)
except:
raise ValueError("A color must be a list or tuple of 3 values (red, green, blue)")
if no_of_colors != 3:
raise ValueError("A color must contain 3 values (red, green, blue)")
# check the color values are between 0 and 255
for c in color:
if not (0 <= c <= 255):
raise ValueError("{} is not a valid color value, it must be 0 - 255")
# convert to #ffffff format
color = "#{:02x}{:02x}{:02x}".format(color[0], color[1], color[2])
return color
|