From 1e1ba142474b9a08d34c25d8e02c19b1efc6929e Mon Sep 17 00:00:00 2001 From: Ramnath0521 Date: Wed, 2 Sep 2026 00:25:34 +0530 Subject: [PATCH 1/2] fix: pass (width, height) to Pillow in the Resize transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resize()` handed a tuple size straight to `PIL.Image.resize()`. fastembed keeps sizes as (height, width) — `Transform.from_config` builds the tuple as `(size["height"], size["width"])` — while Pillow takes (width, height), so a non-square image processor configuration produced a transposed image: Resize(size=(100, 200))(Image.new("RGB", (300, 300)))[0].size # (100, 200), expected (200, 100) Square sizes are unaffected, which is why this went unnoticed. The int branch of `resize()` already emits Pillow order and is untouched, as are `resize_ndarray()`'s callers, which pass (width, height) explicitly. `Resize.__call__` is the only caller of this function and always supplies fastembed's height-first order, so converting here is safe. Co-Authored-By: Claude Opus 5 --- fastembed/image/transform/functional.py | 7 +++- tests/test_image_transform.py | 45 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/test_image_transform.py diff --git a/fastembed/image/transform/functional.py b/fastembed/image/transform/functional.py index 9d9e2197..3018e203 100644 --- a/fastembed/image/transform/functional.py +++ b/fastembed/image/transform/functional.py @@ -98,7 +98,12 @@ def resize( resample: int | Image.Resampling = Image.Resampling.BILINEAR, ) -> Image.Image: if isinstance(size, tuple): - return image.resize(size, resample) + # fastembed keeps sizes as (height, width) — `Transform.from_config` builds + # the tuple as (size["height"], size["width"]) — while Pillow's resize takes + # (width, height). The two agree for square sizes, so this only shows up on + # a non-square image processor configuration. + height, width = size + return image.resize((width, height), resample) height, width = image.height, image.width short, long = (width, height) if width <= height else (height, width) diff --git a/tests/test_image_transform.py b/tests/test_image_transform.py new file mode 100644 index 00000000..3ba643ed --- /dev/null +++ b/tests/test_image_transform.py @@ -0,0 +1,45 @@ +from PIL import Image + +from fastembed.image.transform.functional import resize +from fastembed.image.transform.operators import Resize + + +def test_resize_tuple_converts_from_height_width_to_pillow_order(): + """A ``(height, width)`` size must reach Pillow as ``(width, height)``. + + ``Transform.from_config`` builds the tuple as ``(size["height"], size["width"])``, + so every tuple reaching ``resize`` is in fastembed's height-first order, while + ``PIL.Image.resize`` takes width first. The two agree for square sizes, which is + why this went unnoticed. + """ + image = Image.new("RGB", (300, 300)) + + resized = resize(image, size=(100, 200)) + + assert resized.size == (200, 100) # PIL reports (width, height) + + +def test_resize_operator_produces_requested_height_and_width(): + image = Image.new("RGB", (300, 300)) + + resized = Resize(size=(100, 200))([image])[0] + + width, height = resized.size + assert (height, width) == (100, 200) + + +def test_resize_square_tuple_is_unchanged(): + """The square case behaved correctly before and must keep doing so.""" + image = Image.new("RGB", (300, 200)) + + assert resize(image, size=(224, 224)).size == (224, 224) + + +def test_resize_int_keeps_shortest_edge_behaviour(): + """The int branch already emitted Pillow order; it must not be disturbed.""" + landscape = Image.new("RGB", (400, 200)) + portrait = Image.new("RGB", (200, 400)) + + # size sets the shortest edge, and the aspect ratio is preserved. + assert resize(landscape, size=100).size == (200, 100) + assert resize(portrait, size=100).size == (100, 200) From 0ab382f695b3a82f9d562c332b60320065b49708 Mon Sep 17 00:00:00 2001 From: George Panchuk Date: Mon, 21 Sep 2026 22:18:25 +0700 Subject: [PATCH 2/2] tests: simplify tests --- fastembed/image/transform/functional.py | 8 ++--- tests/test_image_transform.py | 43 ++++++++----------------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/fastembed/image/transform/functional.py b/fastembed/image/transform/functional.py index 3018e203..af28fb0b 100644 --- a/fastembed/image/transform/functional.py +++ b/fastembed/image/transform/functional.py @@ -98,10 +98,10 @@ def resize( resample: int | Image.Resampling = Image.Resampling.BILINEAR, ) -> Image.Image: if isinstance(size, tuple): - # fastembed keeps sizes as (height, width) — `Transform.from_config` builds - # the tuple as (size["height"], size["width"]) — while Pillow's resize takes - # (width, height). The two agree for square sizes, so this only shows up on - # a non-square image processor configuration. + # fastembed keeps sizes as (height, width) — `Compose.from_config` builds the + # tuple as (size["height"], size["width"]) — while Pillow's resize takes + # (width, height). The two agree for square sizes, so this only shows up on a + # non-square image processor configuration. height, width = size return image.resize((width, height), resample) diff --git a/tests/test_image_transform.py b/tests/test_image_transform.py index 3ba643ed..6a203238 100644 --- a/tests/test_image_transform.py +++ b/tests/test_image_transform.py @@ -1,41 +1,24 @@ +import pytest from PIL import Image from fastembed.image.transform.functional import resize -from fastembed.image.transform.operators import Resize -def test_resize_tuple_converts_from_height_width_to_pillow_order(): - """A ``(height, width)`` size must reach Pillow as ``(width, height)``. +@pytest.mark.parametrize( + ("size", "expected"), + [ + ((100, 200), (200, 100)), # the bug: a non-square size came back transposed + ((224, 224), (224, 224)), # the square path every shipped model takes + ], +) +def test_resize_tuple_is_height_width(size: tuple[int, int], expected: tuple[int, int]) -> None: + """A ``(height, width)`` size must reach Pillow as ``(width, height)``.""" + resized = resize(Image.new("RGB", (300, 300)), size=size) - ``Transform.from_config`` builds the tuple as ``(size["height"], size["width"])``, - so every tuple reaching ``resize`` is in fastembed's height-first order, while - ``PIL.Image.resize`` takes width first. The two agree for square sizes, which is - why this went unnoticed. - """ - image = Image.new("RGB", (300, 300)) + assert resized.size == expected # PIL reports (width, height) - resized = resize(image, size=(100, 200)) - assert resized.size == (200, 100) # PIL reports (width, height) - - -def test_resize_operator_produces_requested_height_and_width(): - image = Image.new("RGB", (300, 300)) - - resized = Resize(size=(100, 200))([image])[0] - - width, height = resized.size - assert (height, width) == (100, 200) - - -def test_resize_square_tuple_is_unchanged(): - """The square case behaved correctly before and must keep doing so.""" - image = Image.new("RGB", (300, 200)) - - assert resize(image, size=(224, 224)).size == (224, 224) - - -def test_resize_int_keeps_shortest_edge_behaviour(): +def test_resize_int_keeps_shortest_edge_behaviour() -> None: """The int branch already emitted Pillow order; it must not be disturbed.""" landscape = Image.new("RGB", (400, 200)) portrait = Image.new("RGB", (200, 400))