Skip to content

Commit 528ebd8

Browse files
authored
Adjusting date time text to match image size (#22)
1 parent a87ce73 commit 528ebd8

File tree

7 files changed

+574
-251
lines changed

7 files changed

+574
-251
lines changed

.readthedocs.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ build:
1313
# Install poetry
1414
# https://python-poetry.org/docs/#installing-manually
1515
- pip install poetry
16-
# Tell poetry to not use a virtual environment
17-
- poetry config virtualenvs.create false
1816
post_install:
1917
# Install dependencies with 'docs' dependency group
2018
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
21-
- poetry install --only main,docs
19+
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only main,docs

docs/capecod.gif

-8.51 MB
Loading

docs/examples.ipynb

Lines changed: 185 additions & 135 deletions
Large diffs are not rendered by default.

geogif/gif.py

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def _validate_arr_for_gif(
2424
cmap: str | matplotlib.colors.Colormap | None,
2525
date_format: str | None,
2626
date_position: Literal["ul", "ur", "ll", "lr"],
27+
date_size: int | float,
2728
) -> tuple[xr.DataArray, matplotlib.colors.Colormap | None]:
2829
if arr.ndim not in (3, 4):
2930
raise ValueError(
@@ -60,16 +61,71 @@ def _validate_arr_for_gif(
6061
f"Coordinates for the {time_coord.name} dimension are not datetimes, or don't support `strftime`. "
6162
"Set `date_format=False`"
6263
)
63-
assert date_position in (
64-
"ul",
65-
"ur",
66-
"ll",
67-
"lr",
64+
assert (
65+
date_position
66+
in (
67+
"ul",
68+
"ur",
69+
"ll",
70+
"lr",
71+
)
6872
), f"date_position must be one of ('ul', 'ur', 'll', 'lr'), not {date_position}."
6973

74+
if isinstance(date_size, int):
75+
assert date_size > 0, f"date_size must be >0, not {date_size}"
76+
elif isinstance(date_size, float):
77+
assert (
78+
0 < date_size < 1
79+
), f"date_size must be greater than 0 and less than 1, not {date_size}"
80+
else:
81+
raise TypeError(
82+
f"date_size must be int or float, not {type(date_size)}: {date_size}"
83+
)
84+
7085
return (arr, cmap)
7186

7287

88+
def _get_font(
89+
date_size: int | float, labels: list[str], image_width: int
90+
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
91+
"""
92+
Get an appropriately-sized font for the requested ``date_size``
93+
94+
When ``date_size`` is a float, figure out a font size that will make the width of the
95+
text that fraction of the width of the image.
96+
"""
97+
if isinstance(date_size, int):
98+
# absolute font size
99+
return ImageFont.load_default(size=date_size)
100+
101+
target_width = date_size * image_width
102+
test_label = max(labels, key=len)
103+
104+
fnt = ImageFont.load_default(size=5)
105+
if isinstance(fnt, ImageFont.FreeTypeFont):
106+
# NOTE: if Pillow doesn't have FreeType support, we won't get a font
107+
# with adjustable size. So trying to increase the size would just
108+
# loop infinitely.
109+
110+
def text_width(font) -> int:
111+
bbox = font.getbbox(test_label)
112+
return bbox[2] - bbox[0]
113+
114+
if text_width(fnt) > 0:
115+
# check for zero-width so we don't loop forever.
116+
# (could happen if `test_label` is an empty string or non-printable character)
117+
118+
# increment font until you get large enough text
119+
while text_width(fnt) <= target_width:
120+
try:
121+
size = fnt.size + 1
122+
fnt = ImageFont.load_default(size)
123+
except OSError as e:
124+
raise RuntimeError(f"Invalid font size: {size}") from e
125+
126+
return fnt
127+
128+
73129
def gif(
74130
arr: xr.DataArray,
75131
*,
@@ -83,6 +139,7 @@ def gif(
83139
date_position: Literal["ul", "ur", "ll", "lr"] = "ul",
84140
date_color: tuple[int, int, int] = (255, 255, 255),
85141
date_bg: tuple[int, int, int] | None = (0, 0, 0),
142+
date_size: int | float = 0.15,
86143
) -> IPython.display.Image | None:
87144
"""
88145
Render a `~xarray.DataArray` timestack (``time``, ``band``, ``y``, ``x``) into a GIF.
@@ -140,6 +197,14 @@ def gif(
140197
date_bg:
141198
Fill color to draw behind the timestamp (for legibility), as an RGB 3-tuple.
142199
Default: ``(0, 0, 0)`` (black). Set to None to disable.
200+
date_size:
201+
If a float, make the label this fraction of the width of the image.
202+
If an int, use this absolute font size for the label.
203+
Default: 0.15 (so the label is 15% of the image width).
204+
205+
Note that if Pillow does not have FreeType support, the font size
206+
cannot be adjusted, and the text will be whatever size Pillow's
207+
default basic font is (usually rather small).
143208
144209
Returns
145210
-------
@@ -167,7 +232,7 @@ def gif(
167232
if isinstance(arr.data, da.Array):
168233
raise TypeError("DataArray contains delayed data; use `dgif` instead.")
169234

170-
arr, cmap = _validate_arr_for_gif(arr, cmap, date_format, date_position)
235+
arr, cmap = _validate_arr_for_gif(arr, cmap, date_format, date_position, date_size)
171236

172237
# Rescale
173238
if arr.dtype.kind == "b":
@@ -207,7 +272,7 @@ def gif(
207272
time_coord = arr[arr.dims[0]]
208273
labels = time_coord.dt.strftime(date_format).data
209274

210-
fnt = ImageFont.load_default()
275+
fnt = _get_font(date_size, labels, imgs[0].size[0])
211276
for label, img in zip(labels, imgs):
212277
# get a drawing context
213278
d = ImageDraw.Draw(img)
@@ -230,9 +295,18 @@ def gif(
230295
x = width - t_width - offset
231296

232297
if date_bg:
233-
d.rectangle((x, y, x + t_width, y + t_height), fill=date_bg)
234-
# draw text
235-
d.multiline_text((x, y), label, font=fnt, fill=date_color)
298+
pad = 0.1 * t_height # looks nicer
299+
d.rectangle(
300+
(x - pad, y - pad, x + t_width + pad, y + t_height + pad),
301+
fill=date_bg,
302+
)
303+
304+
# NOTE: sometimes the text seems to incorporate its own internal offset.
305+
# This will show up in the first two coordinates of `t_bbox`, so we
306+
# "de-offset" by these to make the rectangle and text align.
307+
d.multiline_text(
308+
(x - t_bbox[0], y - t_bbox[1]), label, font=fnt, fill=date_color
309+
)
236310

237311
out = to if to is not None else io.BytesIO()
238312
imgs[0].save(
@@ -244,7 +318,7 @@ def gif(
244318
loop=False,
245319
)
246320
if to is None and isinstance(out, io.BytesIO):
247-
# second `isinstace` is just for the typechecker
321+
# second `isinstance` is just for the typechecker
248322
try:
249323
import IPython.display
250324
except ImportError:
@@ -274,7 +348,7 @@ def dgif(
274348
arr: xr.DataArray,
275349
*,
276350
bytes=False,
277-
fps: int = 10,
351+
fps: int = 16,
278352
robust: bool = True,
279353
vmin: float | None = None,
280354
vmax: float | None = None,
@@ -283,6 +357,7 @@ def dgif(
283357
date_position: Literal["ul", "ur", "ll", "lr"] = "ul",
284358
date_color: tuple[int, int, int] = (255, 255, 255),
285359
date_bg: tuple[int, int, int] | None = (0, 0, 0),
360+
date_size: int | float = 0.15,
286361
) -> Delayed:
287362
"""
288363
Turn a dask-backed `~xarray.DataArray` timestack into a GIF, as a `~dask.delayed.Delayed` object.
@@ -350,6 +425,14 @@ def dgif(
350425
date_bg:
351426
Fill color to draw behind the timestamp (for legibility), as an RGB 3-tuple.
352427
Default: ``(0, 0, 0)`` (black). Set to None to disable.
428+
date_size:
429+
If a float, make the label this fraction of the width of the image.
430+
If an int, use this absolute font size for the label.
431+
Default: 0.15 (so the label is 15% of the image width).
432+
433+
Note that if Pillow does not have FreeType support, the font size
434+
cannot be adjusted, and the text will be whatever size Pillow's
435+
default basic font is (usually rather small).
353436
354437
Returns
355438
-------
@@ -381,7 +464,7 @@ def dgif(
381464
)
382465

383466
# Do some quick sanity checks to save you a lot of compute
384-
_validate_arr_for_gif(arr, cmap, date_format, date_position)
467+
_validate_arr_for_gif(arr, cmap, date_format, date_position, date_size)
385468

386469
if not bytes:
387470
try:
@@ -414,4 +497,5 @@ def dgif(
414497
date_position=date_position,
415498
date_color=date_color,
416499
date_bg=date_bg,
500+
date_size=date_size,
417501
)

0 commit comments

Comments
 (0)