mirror of https://github.com/seebye/ueberzug
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
8.3 KiB
Python
265 lines
8.3 KiB
Python
"""Modul which implements class and functions
|
|
all about scaling images.
|
|
"""
|
|
import abc
|
|
import enum
|
|
|
|
import ueberzug.geometry as geometry
|
|
|
|
|
|
class ImageScaler(metaclass=abc.ABCMeta):
|
|
"""Describes the structure used to define image scaler classes.
|
|
|
|
Defines a general interface used to implement different ways
|
|
of scaling images to specific sizes.
|
|
"""
|
|
|
|
@staticmethod
|
|
@abc.abstractmethod
|
|
def get_scaler_name():
|
|
"""Returns:
|
|
str: the constant name which is associated to this scaler.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@staticmethod
|
|
@abc.abstractmethod
|
|
def is_indulgent_resizing():
|
|
"""This method specifies whether the
|
|
algorithm returns noticeable different results for
|
|
the same image with different sizes (bigger than the
|
|
maximum size which is passed to the scale method).
|
|
|
|
Returns:
|
|
bool: False if the results differ
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def calculate_resolution(self, image, width: int, height: int):
|
|
"""Calculates the final resolution of the scaled image.
|
|
|
|
Args:
|
|
image (PIL.Image): the image which should be scaled
|
|
width (int): maximum width that can be taken
|
|
height (int): maximum height that can be taken
|
|
|
|
Returns:
|
|
tuple: final width: int, final height: int
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def scale(self, image, position: geometry.Point,
|
|
width: int, height: int):
|
|
"""Scales the image according to the respective implementation.
|
|
|
|
Args:
|
|
image (PIL.Image): the image which should be scaled
|
|
position (geometry.Position): the centered position, if possible
|
|
Specified as factor of the image size,
|
|
so it should be an element of [0, 1].
|
|
width (int): maximum width that can be taken
|
|
height (int): maximum height that can be taken
|
|
|
|
Returns:
|
|
PIL.Image: the scaled image
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class OffsetImageScaler(ImageScaler, metaclass=abc.ABCMeta):
|
|
"""Extension of the ImageScaler class by Offset specific functions."""
|
|
# pylint can't detect abstract subclasses
|
|
# pylint: disable=abstract-method
|
|
|
|
@staticmethod
|
|
def get_offset(position: float, target_size: float, image_size: float):
|
|
"""Calculates a offset which contains the position
|
|
in a range from offset to offset + target_size.
|
|
|
|
Args:
|
|
position (float): the centered position, if possible
|
|
Specified as factor of the image size,
|
|
so it should be an element of [0, 1].
|
|
target_size (int): the image size of the wanted result
|
|
image_size (int): the image size
|
|
|
|
Returns:
|
|
int: the offset
|
|
"""
|
|
return int(min(max(0, position * image_size - target_size / 2),
|
|
image_size - target_size))
|
|
|
|
|
|
class MinSizeImageScaler(ImageScaler):
|
|
"""Partial implementation of an ImageScaler.
|
|
Subclasses calculate the final resolution of the scaled image
|
|
as the minimum value of the image size and the maximum size.
|
|
"""
|
|
# pylint: disable=abstract-method
|
|
|
|
def calculate_resolution(self, image, width: int, height: int):
|
|
return (min(width, image.width),
|
|
min(height, image.height))
|
|
|
|
|
|
class CropImageScaler(MinSizeImageScaler, OffsetImageScaler):
|
|
"""Implementation of the ImageScaler
|
|
which crops out the maximum image size.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "crop"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return False
|
|
|
|
def scale(self, image, position: geometry.Point,
|
|
width: int, height: int):
|
|
width, height = self.calculate_resolution(image, width, height)
|
|
image_width, image_height = image.width, image.height
|
|
offset_x = self.get_offset(position.x, width, image_width)
|
|
offset_y = self.get_offset(position.y, height, image_height)
|
|
return image \
|
|
.crop((offset_x, offset_y,
|
|
offset_x + width, offset_y + height))
|
|
|
|
|
|
class DistortImageScaler(ImageScaler):
|
|
"""Implementation of the ImageScaler
|
|
which distorts the image to the maximum image size.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "distort"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return True
|
|
|
|
def calculate_resolution(self, image, width: int, height: int):
|
|
return width, height
|
|
|
|
def scale(self, image, position: geometry.Point,
|
|
width: int, height: int):
|
|
import PIL.Image
|
|
width, height = self.calculate_resolution(image, width, height)
|
|
return image.resize((width, height), PIL.Image.ANTIALIAS)
|
|
|
|
|
|
class FitContainImageScaler(DistortImageScaler):
|
|
"""Implementation of the ImageScaler
|
|
which resizes the image that either
|
|
the width matches the maximum width
|
|
or the height matches the maximum height
|
|
while keeping the image ratio.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "fit_contain"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return True
|
|
|
|
def calculate_resolution(self, image, width: int, height: int):
|
|
factor = min(width / image.width, height / image.height)
|
|
return int(image.width * factor), int(image.height * factor)
|
|
|
|
|
|
class ContainImageScaler(FitContainImageScaler):
|
|
"""Implementation of the ImageScaler
|
|
which resizes the image to a size <= the maximum size
|
|
while keeping the image ratio.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "contain"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return True
|
|
|
|
def calculate_resolution(self, image, width: int, height: int):
|
|
return super().calculate_resolution(
|
|
image, min(width, image.width), min(height, image.height))
|
|
|
|
|
|
class ForcedCoverImageScaler(DistortImageScaler, OffsetImageScaler):
|
|
"""Implementation of the ImageScaler
|
|
which resizes the image to cover the entire area which should be filled
|
|
while keeping the image ratio.
|
|
If the image is smaller than the desired size
|
|
it will be stretched to reach the desired size.
|
|
If the ratio of the area differs
|
|
from the image ratio the edges will be cut off.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "forced_cover"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return True
|
|
|
|
def scale(self, image, position: geometry.Point,
|
|
width: int, height: int):
|
|
import PIL.Image
|
|
width, height = self.calculate_resolution(image, width, height)
|
|
image_width, image_height = image.width, image.height
|
|
if width / image_width > height / image_height:
|
|
image_height = int(image_height * width / image_width)
|
|
image_width = width
|
|
else:
|
|
image_width = int(image_width * height / image_height)
|
|
image_height = height
|
|
offset_x = self.get_offset(position.x, width, image_width)
|
|
offset_y = self.get_offset(position.y, height, image_height)
|
|
|
|
return image \
|
|
.resize((image_width, image_height), PIL.Image.ANTIALIAS) \
|
|
.crop((offset_x, offset_y,
|
|
offset_x + width, offset_y + height))
|
|
|
|
|
|
class CoverImageScaler(MinSizeImageScaler, ForcedCoverImageScaler):
|
|
"""The same as ForcedCoverImageScaler but images won't be stretched
|
|
if they are smaller than the area which should be filled.
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_scaler_name():
|
|
return "cover"
|
|
|
|
@staticmethod
|
|
def is_indulgent_resizing():
|
|
return True
|
|
|
|
|
|
@enum.unique
|
|
class ScalerOption(str, enum.Enum):
|
|
"""Enum which lists the useable ImageScaler classes."""
|
|
DISTORT = DistortImageScaler
|
|
CROP = CropImageScaler
|
|
FIT_CONTAIN = FitContainImageScaler
|
|
CONTAIN = ContainImageScaler
|
|
FORCED_COVER = ForcedCoverImageScaler
|
|
COVER = CoverImageScaler
|
|
|
|
def __new__(cls, scaler_class):
|
|
inst = str.__new__(cls)
|
|
# Based on an official example
|
|
# https://docs.python.org/3/library/enum.html#using-a-custom-new
|
|
# So.. stfu pylint
|
|
# pylint: disable=protected-access
|
|
inst._value_ = scaler_class.get_scaler_name()
|
|
inst.scaler_class = scaler_class
|
|
return inst
|