|
4 | 4 | from typing import TYPE_CHECKING, Literal |
5 | 5 |
|
6 | 6 | import numpy as np |
| 7 | +from PIL import Image |
7 | 8 | from PIL.Image import Image as PILImage |
8 | 9 |
|
9 | 10 | from ..themes.theme import theme |
@@ -70,8 +71,8 @@ def __init__( |
70 | 71 | ): |
71 | 72 | from matplotlib.transforms import Bbox |
72 | 73 |
|
73 | | - self._image = image |
74 | | - self._image_size = _image_size(image) # (W, H) px |
| 74 | + self._image = _to_pil_image(image) |
| 75 | + self._image_size = self._image.size # (W, H) px |
75 | 76 | self._frac_bbox = Bbox.unit() |
76 | 77 | self._anchor = _resolve_anchor(anchor) |
77 | 78 | self.theme = theme() |
@@ -150,16 +151,22 @@ def _draw_plot_background(self): |
150 | 151 | self.theme.targets.plot_background = self.patch |
151 | 152 |
|
152 | 153 |
|
153 | | -def _image_size(obj: PILImage | np.ndarray) -> tuple[int, int]: |
| 154 | +def _to_pil_image(obj: PILImage | np.ndarray) -> PILImage: |
154 | 155 | """ |
155 | | - Return the (width, height) of a PIL image or ndarray in pixels |
| 156 | + Normalise an image input to a PIL Image for resampling |
| 157 | +
|
| 158 | + A `PIL.Image` is returned unchanged. A uint8 ndarray (L / RGB / |
| 159 | + RGBA) is wrapped directly. A float ndarray follows matplotlib's |
| 160 | + `[0, 1]` convention and is clipped then scaled to uint8; the final |
| 161 | + inset output is an 8-bit raster regardless, so nothing visible is |
| 162 | + lost. |
156 | 163 | """ |
157 | 164 | if isinstance(obj, PILImage): |
158 | | - return obj.size # PIL exposes (W, H) |
159 | | - |
| 165 | + return obj |
160 | 166 | arr = np.asarray(obj) |
161 | | - h, w = arr.shape[:2] # ndarray is HWC or HW |
162 | | - return w, h |
| 167 | + if arr.dtype != np.uint8: |
| 168 | + arr = (np.clip(arr, 0, 1) * 255).round().astype(np.uint8) |
| 169 | + return Image.fromarray(arr) |
163 | 170 |
|
164 | 171 |
|
165 | 172 | # Named anchors → (h, v) fractions in [0, 1]², where `h = 0` |
|
0 commit comments