|
|
|
@ -89,6 +89,49 @@ class ImageHolder:
|
|
|
|
|
return self.image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PostLoadImageProcessor(metaclass=abc.ABCMeta):
|
|
|
|
|
"""Describes the structure used to define callbacks which
|
|
|
|
|
will be invoked after loading an image.
|
|
|
|
|
"""
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
def on_loaded(self, image):
|
|
|
|
|
"""Postprocessor of an loaded image.
|
|
|
|
|
The returned image will be assigned to the image holder.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
image (PIL.Image): the loaded image
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
PIL.Image:
|
|
|
|
|
the image which will be assigned
|
|
|
|
|
to the image holder of this loading process
|
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoverPostLoadImageProcessor(PostLoadImageProcessor):
|
|
|
|
|
"""Implementation of PostLoadImageProcessor
|
|
|
|
|
which resizes an image (if possible -> needs to be bigger)
|
|
|
|
|
such that it covers only just a given resolution.
|
|
|
|
|
"""
|
|
|
|
|
def __init__(self, width, height):
|
|
|
|
|
self.width = width
|
|
|
|
|
self.height = height
|
|
|
|
|
|
|
|
|
|
def on_loaded(self, image):
|
|
|
|
|
import PIL.Image
|
|
|
|
|
resize_ratio = max(min(1, self.width / image.width),
|
|
|
|
|
min(1, self.height / image.height))
|
|
|
|
|
|
|
|
|
|
if resize_ratio != 1:
|
|
|
|
|
image = image.resize(
|
|
|
|
|
(int(resize_ratio * image.width),
|
|
|
|
|
int(resize_ratio * image.height)),
|
|
|
|
|
PIL.Image.ANTIALIAS)
|
|
|
|
|
|
|
|
|
|
return image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImageLoader(metaclass=abc.ABCMeta):
|
|
|
|
|
"""Describes the structure used to define image loading strategies.
|
|
|
|
|
|
|
|
|
@ -113,7 +156,7 @@ class ImageLoader(metaclass=abc.ABCMeta):
|
|
|
|
|
self.error_handler = None
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
def load(self, path, upper_bound_size):
|
|
|
|
|
def load(self, path, upper_bound_size, post_load_processor=None):
|
|
|
|
|
"""Starts the image loading procedure for the passed path.
|
|
|
|
|
How and when an image get's loaded depends on the implementation
|
|
|
|
|
of the used ImageLoader class.
|
|
|
|
@ -122,6 +165,8 @@ class ImageLoader(metaclass=abc.ABCMeta):
|
|
|
|
|
path (str): the path to the image which should be loaded
|
|
|
|
|
upper_bound_size (tuple of (width: int, height: int)):
|
|
|
|
|
the maximal size to load data for
|
|
|
|
|
post_load_processor (PostLoadImageProcessor):
|
|
|
|
|
allows to apply changes to the recently loaded image
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
ImageHolder: which the image will be assigned to
|
|
|
|
@ -162,7 +207,7 @@ class SynchronousImageLoader(ImageLoader):
|
|
|
|
|
def get_loader_name():
|
|
|
|
|
return "synchronous"
|
|
|
|
|
|
|
|
|
|
def load(self, path, upper_bound_size):
|
|
|
|
|
def load(self, path, upper_bound_size, post_load_processor=None):
|
|
|
|
|
image = None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
@ -170,6 +215,9 @@ class SynchronousImageLoader(ImageLoader):
|
|
|
|
|
except OSError as exception:
|
|
|
|
|
self.process_error(exception)
|
|
|
|
|
|
|
|
|
|
if image and post_load_processor:
|
|
|
|
|
image = post_load_processor.on_loaded(image)
|
|
|
|
|
|
|
|
|
|
return ImageHolder(path, image or self.PLACEHOLDER)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -192,7 +240,7 @@ class AsynchronousImageLoader(ImageLoader):
|
|
|
|
|
self.__queue_low_priority = queue.Queue()
|
|
|
|
|
self.__waiter_low_priority = threading.Condition()
|
|
|
|
|
|
|
|
|
|
def _enqueue(self, queue, image_holder, upper_bound_size):
|
|
|
|
|
def _enqueue(self, queue, image_holder, upper_bound_size, post_load_processor):
|
|
|
|
|
"""Enqueues the image holder weakly referenced.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
@ -201,8 +249,11 @@ class AsynchronousImageLoader(ImageLoader):
|
|
|
|
|
the image holder for which an image should be loaded
|
|
|
|
|
upper_bound_size (tuple of (width: int, height: int)):
|
|
|
|
|
the maximal size to load data for
|
|
|
|
|
post_load_processor (PostLoadImageProcessor):
|
|
|
|
|
allows to apply changes to the recently loaded image
|
|
|
|
|
"""
|
|
|
|
|
queue.put((weakref.ref(image_holder), upper_bound_size))
|
|
|
|
|
queue.put((
|
|
|
|
|
weakref.ref(image_holder), upper_bound_size, post_load_processor))
|
|
|
|
|
|
|
|
|
|
def _dequeue(self, queue):
|
|
|
|
|
"""Removes queue entries till an alive reference was found.
|
|
|
|
@ -214,21 +265,25 @@ class AsynchronousImageLoader(ImageLoader):
|
|
|
|
|
queue (queue.Queue): the queue to operate on
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple of (ImageHolder, tuple of (width: int, height: int)):
|
|
|
|
|
an queued image holder or None, upper bound size or None
|
|
|
|
|
tuple of (ImageHolder, tuple of (width: int, height: int),
|
|
|
|
|
PostLoadImageProcessor):
|
|
|
|
|
an queued image holder or None, upper bound size or None,
|
|
|
|
|
the post load image processor or None
|
|
|
|
|
"""
|
|
|
|
|
holder_reference = None
|
|
|
|
|
image_holder = None
|
|
|
|
|
upper_bound_size = None
|
|
|
|
|
post_load_processor = None
|
|
|
|
|
|
|
|
|
|
while not queue.empty():
|
|
|
|
|
holder_reference, upper_bound_size = queue.get_nowait()
|
|
|
|
|
holder_reference, upper_bound_size, post_load_processor = \
|
|
|
|
|
queue.get_nowait()
|
|
|
|
|
image_holder = holder_reference and holder_reference()
|
|
|
|
|
if (holder_reference is None or
|
|
|
|
|
image_holder is not None):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
return image_holder, upper_bound_size
|
|
|
|
|
return image_holder, upper_bound_size, post_load_processor
|
|
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
|
def _schedule(self, function, priority):
|
|
|
|
@ -243,16 +298,22 @@ class AsynchronousImageLoader(ImageLoader):
|
|
|
|
|
"""
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
def _load_image(self, path, upper_bound_size):
|
|
|
|
|
def _load_image(self, path, upper_bound_size, post_load_processor):
|
|
|
|
|
"""Wrapper for calling load_image.
|
|
|
|
|
Behaves like calling it directly,
|
|
|
|
|
but allows e.g. executing the function in other processes.
|
|
|
|
|
"""
|
|
|
|
|
return load_image(path, upper_bound_size)
|
|
|
|
|
image, *other_data = load_image(path, upper_bound_size)
|
|
|
|
|
|
|
|
|
|
if image and post_load_processor:
|
|
|
|
|
image = post_load_processor.on_loaded(image)
|
|
|
|
|
|
|
|
|
|
return (image, *other_data)
|
|
|
|
|
|
|
|
|
|
def load(self, path, upper_bound_size):
|
|
|
|
|
def load(self, path, upper_bound_size, post_load_processor=None):
|
|
|
|
|
holder = ImageHolder(path)
|
|
|
|
|
self._enqueue(self.__queue, holder, upper_bound_size)
|
|
|
|
|
self._enqueue(
|
|
|
|
|
self.__queue, holder, upper_bound_size, post_load_processor)
|
|
|
|
|
self._schedule(self.__process_high_priority_entry,
|
|
|
|
|
self.Priority.HIGH)
|
|
|
|
|
return holder
|
|
|
|
@ -290,15 +351,18 @@ class AsynchronousImageLoader(ImageLoader):
|
|
|
|
|
queue (queue.Queue): the queue to operate on
|
|
|
|
|
"""
|
|
|
|
|
image = None
|
|
|
|
|
image_holder, upper_bound_size = self._dequeue(queue)
|
|
|
|
|
image_holder, upper_bound_size, post_load_processor = \
|
|
|
|
|
self._dequeue(queue)
|
|
|
|
|
if image_holder is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
image, downscaled = self._load_image(
|
|
|
|
|
image_holder.path, upper_bound_size)
|
|
|
|
|
image_holder.path, upper_bound_size, post_load_processor)
|
|
|
|
|
if upper_bound_size and downscaled:
|
|
|
|
|
self._enqueue(self.__queue_low_priority, image_holder, None)
|
|
|
|
|
self._enqueue(
|
|
|
|
|
self.__queue_low_priority,
|
|
|
|
|
image_holder, None, post_load_processor)
|
|
|
|
|
self._schedule(self.__process_low_priority_entry,
|
|
|
|
|
self.Priority.LOW)
|
|
|
|
|
except OSError as exception:
|
|
|
|
@ -366,20 +430,25 @@ class ProcessImageLoader(ThreadImageLoader):
|
|
|
|
|
.result()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _load_image_extern(path, upper_bound_size):
|
|
|
|
|
def _load_image_extern(path, upper_bound_size, post_load_processor):
|
|
|
|
|
"""This function is a wrapper for the image loading function
|
|
|
|
|
as sometimes pillow restores decoded images
|
|
|
|
|
received from other processes wrongly.
|
|
|
|
|
E.g. a PNG is reported as webp (-> crash on using an image function)
|
|
|
|
|
So this function is a workaround which prevents these crashs to happen.
|
|
|
|
|
"""
|
|
|
|
|
image, downscaled = load_image(path, upper_bound_size)
|
|
|
|
|
return image.mode, image.size, image.tobytes(), downscaled
|
|
|
|
|
image, *other_data = load_image(path, upper_bound_size)
|
|
|
|
|
|
|
|
|
|
if image and post_load_processor:
|
|
|
|
|
image = post_load_processor.on_loaded(image)
|
|
|
|
|
|
|
|
|
|
return (image.mode, image.size, image.tobytes(), *other_data)
|
|
|
|
|
|
|
|
|
|
def _load_image(self, path, upper_bound_size):
|
|
|
|
|
def _load_image(self, path, upper_bound_size, post_load_processor=None):
|
|
|
|
|
import PIL.Image
|
|
|
|
|
future = self.__executor_loader.submit(
|
|
|
|
|
ProcessImageLoader._load_image_extern, path, upper_bound_size)
|
|
|
|
|
ProcessImageLoader._load_image_extern,
|
|
|
|
|
path, upper_bound_size, post_load_processor)
|
|
|
|
|
mode, size, data, downscaled = future.result()
|
|
|
|
|
return PIL.Image.frombytes(mode, size, data), downscaled
|
|
|
|
|
|
|
|
|
|