import tkinter.ttk as ttk
from .base import Base
from .canvasview import CanvasView
from .modulesmanager import ModulesManager
[docs]
class Image(Base):
"""Widget for displaying a PIL image in a tkinter label."""
def __init__(self, filepath=None, url=None, pil_image=None):
Base.__init__(self)
self.pil_image = pil_image
if self.pil_image is None:
try:
self.pil_image = self.read_pil_image(filepath=filepath, url=url)
except Exception:
self.pil_image = self.PILImage.new("RGB", size=(100, 100))
self._displayed_tkimage = None
self.is_rescalable = False
self.add_observer(self, "is_rescalable")
self.resize_update_delay = 0
self.parent_grid_cell = None
@property
def width(self):
"""The width of the source PIL image in pixels."""
if self.pil_image is not None:
return self.pil_image.width
return None
@property
def height(self):
"""The height of the source PIL image in pixels."""
if self.pil_image is not None:
return self.pil_image.height
return None
@property
def displayed_width(self):
"""The width of the currently displayed image in pixels."""
if self._displayed_tkimage is not None:
return self._displayed_tkimage.width
return None
@property
def displayed_height(self):
"""The height of the currently displayed image in pixels."""
if self._displayed_tkimage is not None:
return self._displayed_tkimage.height
return None
[docs]
def is_environment_valid(self):
"""Check that Pillow and its submodules are installed and importable."""
ModulesManager.install_and_import_modules_if_absent(
{
"Pillow": "PIL",
"ImageTk": "PIL.ImageTk",
"PILImage": "PIL.Image",
"ImageDraw": "PIL.ImageDraw",
}
)
self.PIL = ModulesManager.imported["Pillow"]
self.PILImage = ModulesManager.imported["PILImage"]
self.ImageDraw = ModulesManager.imported["ImageDraw"]
self.ImageTk = ModulesManager.imported["ImageTk"]
return all(
v is not None
for v in [self.ImageTk, self.PIL, self.PILImage, self.ImageDraw]
)
[docs]
def read_pil_image(self, filepath=None, url=None):
"""Load a PIL image from a local file path or a URL."""
if filepath is not None:
pil_image = self.PILImage.open(filepath)
elif url is not None:
from io import BytesIO
import requests
response = requests.get(url)
pil_image = self.PILImage.open(BytesIO(response.content))
pil_image.load()
return pil_image
[docs]
def observed_property_changed(
self, observed_object, observed_property_name, new_value, context
):
"""Handle changes to observed properties such as is_rescalable."""
if observed_property_name == "is_rescalable":
if self.is_rescalable:
self.resize_image_to_fit_widget()
else:
self.update_display()
super().observed_property_changed(
observed_object, observed_property_name, new_value, context
)
[docs]
def event_resized(self, event):
"""Resize the image if is_rescalable, throttling to avoid infinite loops."""
if self.is_rescalable:
if self.resize_update_delay > 0:
if len(self.scheduled_tasks) == 0:
self.after(
self.resize_update_delay,
self.resize_image_to_fit_widget,
)
else:
self.resize_image_to_fit_widget()
else:
self.update_display()
[docs]
def update_display(self, image_to_display=None):
"""Update the widget to show the given image, or the source image if None."""
if self.widget is None:
return
if image_to_display is None:
image_to_display = self.pil_image
if image_to_display is not None and self.ImageTk is not None:
self._displayed_tkimage = self.ImageTk.PhotoImage(
image=image_to_display
)
else:
self._displayed_tkimage = None
self.widget.configure(image=self._displayed_tkimage)
[docs]
class ImageWithGrid(Image):
"""Image widget with an optional grid overlay drawn on top."""
def __init__(self, filepath=None, url=None, pil_image=None):
super().__init__(filepath=filepath, url=url, pil_image=pil_image)
self.is_grid_showing = True
self.grid_count = 5
self.add_observer(self, "is_grid_showing")
self.add_observer(self, "grid_count")
[docs]
def observed_property_changed(
self, observed_object, observed_property_name, new_value, context
):
"""Handle changes to grid visibility or grid count."""
super().observed_property_changed(
observed_object, observed_property_name, new_value, context
)
if observed_property_name == "is_grid_showing" or observed_property_name == "grid_count":
if self.is_rescalable:
self.resize_image_to_fit_widget()
else:
self.update_display()
[docs]
def update_display(self, image_to_display=None):
"""Update the widget, adding a grid overlay if enabled."""
if self.widget is None:
return
if image_to_display is None:
image_to_display = self.pil_image
if self.is_grid_showing:
image_to_display = self.image_with_grid_overlay(image_to_display)
if image_to_display is not None and self.ImageTk is not None:
self._displayed_tkimage = self.ImageTk.PhotoImage(
image=image_to_display
)
else:
self._displayed_tkimage = None
self.widget.configure(image=self._displayed_tkimage)
[docs]
def image_with_grid_overlay(self, pil_image):
"""Return a copy of the image with grid lines drawn on it."""
if pil_image is not None:
# from
# https://randomgeekery.org/post/2017/11/drawing-grids-with-python-and-pillow/
image = pil_image.copy()
draw = self.ImageDraw.Draw(image)
y_start = 0
y_end = image.height
step_size = int(image.width / self.grid_count)
if step_size > 0:
for x in range(0, image.width, step_size):
line = ((x, y_start), (x, y_end))
draw.line(line, fill=255)
x_start = 0
x_end = image.width
for y in range(0, image.height, step_size):
line = ((x_start, y), (x_end, y))
draw.line(line, fill=255)
return image
else:
return None
[docs]
class DynamicImage(CanvasView):
"""Canvas-based image that can be redrawn dynamically."""
def __init__(self, width=200, height=200):
super().__init__(width=width, height=height)
[docs]
def draw_canvas(self):
"""Draw the canvas contents. Override in subclasses."""
pass