From 8f322ffb8ac87f511644c45b825695cb880bb8ca Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Tue, 6 Aug 2024 16:09:59 +1200 Subject: [PATCH] Change cover option to contain to avoid double negative, fix contain to not upscale images --- README.md | 8 +++----- easy_images/core.py | 2 +- easy_images/engine.py | 24 ++++++++++++------------ easy_images/models.py | 2 +- easy_images/options.py | 10 +++++----- easy_images/types_.py | 3 +-- tests/test_engine.py | 9 ++++++++- tests/test_options.py | 4 ++-- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4fc3cfb..e1f3a93 100644 --- a/README.md +++ b/README.md @@ -182,13 +182,11 @@ You can also use the following keywords: `tl` (top left), `tr` (top right), `bl` If crop is `False`, the image will be resized so that it will cover the requested ratio but not cropped down. This is useful when you want to handle positioning in CSS using `object-fit`. -#### `cover` +#### `contain` -Whether to resize the image to cover the requested ratio or to contain it (when not cropping). +When resizing the image (and not cropping), contain the image within the requested ratio. This ensures it will always fit within the requested dimensions. It also stops the image from being upscaled. -The default is `True`, meaning the image will be resized down to cover the requested ratio (which means the image dimensions may be larger than the requested dimensions). - -To rezise the image to always fit within the requested dimensions, set `cover=False`. +The default is `False`, meaning the image will be resized down to cover the requested ratio (which means the image dimensions may be larger than the requested dimensions). #### `focal_window` diff --git a/easy_images/core.py b/easy_images/core.py index 09f7fd4..a51d673 100644 --- a/easy_images/core.py +++ b/easy_images/core.py @@ -23,7 +23,7 @@ "quality": 80, "ratio": "video", "crop": True, - "cover": False, + "contain": True, "densities": [2], "format": "webp", } diff --git a/easy_images/engine.py b/easy_images/engine.py index a6f22c2..0b2a989 100644 --- a/easy_images/engine.py +++ b/easy_images/engine.py @@ -22,24 +22,24 @@ def scale_image( target: tuple[int, int], /, crop: tuple[float, float] | bool | None = None, - cover: bool = True, + contain: bool = False, focal_window: tuple[float, float, float, float] | None = None, ): """ - Scale an image to cover the given dimensions, optionally cropping it around a focal - point or a focal window. + Scale an image to the given dimensions, optionally cropping it around a focal point + or a focal window. """ w, h = img.width, img.height if crop: - cover = True + contain = False - if cover: - # Size image down to cover the dimensions - scale = max(target[0] / w, target[1] / h) + if contain: + # Size image to contain the dimensions, also avoiding upscaling + scale = min(target[0] / w, target[1] / h, 1) else: - # Size image to contain the dimensions - scale = min(target[0] / w, target[1] / h) + # Scale the image to cover the dimensions + scale = max(target[0] / w, target[1] / h) # Focal window scaling if focal_window: @@ -52,10 +52,10 @@ def scale_image( if f_right - f_left > target[0] and f_bottom - f_top > target[1]: img = img.extract_area(f_left, f_top, f_right - f_left, f_bottom - f_top) w, h = img.width, h - if cover: - scale = max(target[0] / w, target[1] / h) + if contain: + scale = min(target[0] / w, target[1] / h, 1) else: - scale = min(target[0] / w, target[1] / h) + scale = max(target[0] / w, target[1] / h) focal_window = None # Otherwise, if cropping then set the crop focal point to the center of the # focal window. diff --git a/easy_images/models.py b/easy_images/models.py index deaa0ec..4d14e6f 100644 --- a/easy_images/models.py +++ b/easy_images/models.py @@ -139,7 +139,7 @@ def build( size, focal_window=options.window, crop=options.crop, - cover=options.cover, + contain=options.contain, ) else: img = source_img diff --git a/easy_images/options.py b/easy_images/options.py index 6c00823..a49e95e 100644 --- a/easy_images/options.py +++ b/easy_images/options.py @@ -39,17 +39,17 @@ class ParsedOptions: - __slots__ = ("quality", "crop", "cover", "window", "width", "ratio", "mimetype") + __slots__ = ("quality", "crop", "contain", "window", "width", "ratio", "mimetype") quality: int crop: tuple[float, float] | None - cover: bool + contain: bool window: tuple[float, float, float, float] | None width: int | None ratio: float | None mimetype: str | None - _defaults = {"cover": True} + _defaults = {"contain": False} def __init__(self, bound=None, string="", /, **options): if string: @@ -112,9 +112,9 @@ def parse_crop(value, **options) -> tuple[float, float] | None: raise ValueError(f"Invalid crop value {value}") @staticmethod - def parse_cover(value, **options) -> bool: + def parse_contain(value, **options) -> bool: if not isinstance(value, bool): - raise ValueError(f"Invalid cover value {value}") + raise ValueError(f"Invalid contain value {value}") return value @staticmethod diff --git a/easy_images/types_.py b/easy_images/types_.py index afef0ae..0515883 100644 --- a/easy_images/types_.py +++ b/easy_images/types_.py @@ -32,7 +32,6 @@ "golden", "golden_vertical", ] -FitChoices: TypeAlias = Literal["contain", "cover"] alternative_re = re.compile(r"^(\d+w|\d(?:\.\d)?x)$") @@ -42,7 +41,7 @@ class Options(TypedDict, total=False): quality: int crop: tuple[float, float] | CropChoices | bool - cover: bool + contain: bool window: tuple[float, float, float, float] | None width: int | WidthChoices | None ratio: float | tuple[float, float] | RatioChoices | None diff --git a/tests/test_engine.py b/tests/test_engine.py index abf1492..2e823b3 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -57,7 +57,14 @@ def test_scale(): source = Image.black(1000, 1000) scaled_cover = scale_image(source, (400, 500)) assert (scaled_cover.width, scaled_cover.height) == (500, 500) - scaled = scale_image(source, (400, 500), cover=False) + scaled = scale_image(source, (400, 500), contain=True) assert (scaled.width, scaled.height) == (400, 400) cropped = scale_image(source, (400, 500), crop=True) assert (cropped.width, cropped.height) == (400, 500) + + small_src = Image.black(100, 100) + cropped_upscale = scale_image(small_src, (400, 500), crop=True) + assert (cropped_upscale.width, cropped_upscale.height) == (400, 500) + + scaled_not_upscale = scale_image(small_src, (400, 500), contain=True) + assert (scaled_not_upscale.width, scaled_not_upscale.height) == (100, 100) diff --git a/tests/test_options.py b/tests/test_options.py index 3368c66..2d4283b 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -58,6 +58,6 @@ def test_str(): == '{"crop": null, "mimetype": null, "quality": 80, "ratio": 1.7777777777777777, "width": 100, "window": null}' ) assert ( - str(ParsedOptions(width=100, cover=False)) - == '{"cover": false, "crop": null, "mimetype": null, "quality": 80, "ratio": null, "width": 100, "window": null}' + str(ParsedOptions(width=100, contain=True)) + == '{"contain": true, "crop": null, "mimetype": null, "quality": 80, "ratio": null, "width": 100, "window": null}' )