Skip to content

Commit

Permalink
Adjusting date time text to match image size (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZachHoppinen authored May 21, 2024
1 parent a87ce73 commit 528ebd8
Show file tree
Hide file tree
Showing 7 changed files with 574 additions and 251 deletions.
4 changes: 1 addition & 3 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ build:
# Install poetry
# https://python-poetry.org/docs/#installing-manually
- pip install poetry
# Tell poetry to not use a virtual environment
- poetry config virtualenvs.create false
post_install:
# Install dependencies with 'docs' dependency group
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
- poetry install --only main,docs
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only main,docs
Binary file modified docs/capecod.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
320 changes: 185 additions & 135 deletions docs/examples.ipynb

Large diffs are not rendered by default.

110 changes: 97 additions & 13 deletions geogif/gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def _validate_arr_for_gif(
cmap: str | matplotlib.colors.Colormap | None,
date_format: str | None,
date_position: Literal["ul", "ur", "ll", "lr"],
date_size: int | float,
) -> tuple[xr.DataArray, matplotlib.colors.Colormap | None]:
if arr.ndim not in (3, 4):
raise ValueError(
Expand Down Expand Up @@ -60,16 +61,71 @@ def _validate_arr_for_gif(
f"Coordinates for the {time_coord.name} dimension are not datetimes, or don't support `strftime`. "
"Set `date_format=False`"
)
assert date_position in (
"ul",
"ur",
"ll",
"lr",
assert (
date_position
in (
"ul",
"ur",
"ll",
"lr",
)
), f"date_position must be one of ('ul', 'ur', 'll', 'lr'), not {date_position}."

if isinstance(date_size, int):
assert date_size > 0, f"date_size must be >0, not {date_size}"
elif isinstance(date_size, float):
assert (
0 < date_size < 1
), f"date_size must be greater than 0 and less than 1, not {date_size}"
else:
raise TypeError(
f"date_size must be int or float, not {type(date_size)}: {date_size}"
)

return (arr, cmap)


def _get_font(
date_size: int | float, labels: list[str], image_width: int
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
"""
Get an appropriately-sized font for the requested ``date_size``
When ``date_size`` is a float, figure out a font size that will make the width of the
text that fraction of the width of the image.
"""
if isinstance(date_size, int):
# absolute font size
return ImageFont.load_default(size=date_size)

target_width = date_size * image_width
test_label = max(labels, key=len)

fnt = ImageFont.load_default(size=5)
if isinstance(fnt, ImageFont.FreeTypeFont):
# NOTE: if Pillow doesn't have FreeType support, we won't get a font
# with adjustable size. So trying to increase the size would just
# loop infinitely.

def text_width(font) -> int:
bbox = font.getbbox(test_label)
return bbox[2] - bbox[0]

if text_width(fnt) > 0:
# check for zero-width so we don't loop forever.
# (could happen if `test_label` is an empty string or non-printable character)

# increment font until you get large enough text
while text_width(fnt) <= target_width:
try:
size = fnt.size + 1
fnt = ImageFont.load_default(size)
except OSError as e:
raise RuntimeError(f"Invalid font size: {size}") from e

return fnt


def gif(
arr: xr.DataArray,
*,
Expand All @@ -83,6 +139,7 @@ def gif(
date_position: Literal["ul", "ur", "ll", "lr"] = "ul",
date_color: tuple[int, int, int] = (255, 255, 255),
date_bg: tuple[int, int, int] | None = (0, 0, 0),
date_size: int | float = 0.15,
) -> IPython.display.Image | None:
"""
Render a `~xarray.DataArray` timestack (``time``, ``band``, ``y``, ``x``) into a GIF.
Expand Down Expand Up @@ -140,6 +197,14 @@ def gif(
date_bg:
Fill color to draw behind the timestamp (for legibility), as an RGB 3-tuple.
Default: ``(0, 0, 0)`` (black). Set to None to disable.
date_size:
If a float, make the label this fraction of the width of the image.
If an int, use this absolute font size for the label.
Default: 0.15 (so the label is 15% of the image width).
Note that if Pillow does not have FreeType support, the font size
cannot be adjusted, and the text will be whatever size Pillow's
default basic font is (usually rather small).
Returns
-------
Expand Down Expand Up @@ -167,7 +232,7 @@ def gif(
if isinstance(arr.data, da.Array):
raise TypeError("DataArray contains delayed data; use `dgif` instead.")

arr, cmap = _validate_arr_for_gif(arr, cmap, date_format, date_position)
arr, cmap = _validate_arr_for_gif(arr, cmap, date_format, date_position, date_size)

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

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

if date_bg:
d.rectangle((x, y, x + t_width, y + t_height), fill=date_bg)
# draw text
d.multiline_text((x, y), label, font=fnt, fill=date_color)
pad = 0.1 * t_height # looks nicer
d.rectangle(
(x - pad, y - pad, x + t_width + pad, y + t_height + pad),
fill=date_bg,
)

# NOTE: sometimes the text seems to incorporate its own internal offset.
# This will show up in the first two coordinates of `t_bbox`, so we
# "de-offset" by these to make the rectangle and text align.
d.multiline_text(
(x - t_bbox[0], y - t_bbox[1]), label, font=fnt, fill=date_color
)

out = to if to is not None else io.BytesIO()
imgs[0].save(
Expand All @@ -244,7 +318,7 @@ def gif(
loop=False,
)
if to is None and isinstance(out, io.BytesIO):
# second `isinstace` is just for the typechecker
# second `isinstance` is just for the typechecker
try:
import IPython.display
except ImportError:
Expand Down Expand Up @@ -274,7 +348,7 @@ def dgif(
arr: xr.DataArray,
*,
bytes=False,
fps: int = 10,
fps: int = 16,
robust: bool = True,
vmin: float | None = None,
vmax: float | None = None,
Expand All @@ -283,6 +357,7 @@ def dgif(
date_position: Literal["ul", "ur", "ll", "lr"] = "ul",
date_color: tuple[int, int, int] = (255, 255, 255),
date_bg: tuple[int, int, int] | None = (0, 0, 0),
date_size: int | float = 0.15,
) -> Delayed:
"""
Turn a dask-backed `~xarray.DataArray` timestack into a GIF, as a `~dask.delayed.Delayed` object.
Expand Down Expand Up @@ -350,6 +425,14 @@ def dgif(
date_bg:
Fill color to draw behind the timestamp (for legibility), as an RGB 3-tuple.
Default: ``(0, 0, 0)`` (black). Set to None to disable.
date_size:
If a float, make the label this fraction of the width of the image.
If an int, use this absolute font size for the label.
Default: 0.15 (so the label is 15% of the image width).
Note that if Pillow does not have FreeType support, the font size
cannot be adjusted, and the text will be whatever size Pillow's
default basic font is (usually rather small).
Returns
-------
Expand Down Expand Up @@ -381,7 +464,7 @@ def dgif(
)

# Do some quick sanity checks to save you a lot of compute
_validate_arr_for_gif(arr, cmap, date_format, date_position)
_validate_arr_for_gif(arr, cmap, date_format, date_position, date_size)

if not bytes:
try:
Expand Down Expand Up @@ -414,4 +497,5 @@ def dgif(
date_position=date_position,
date_color=date_color,
date_bg=date_bg,
date_size=date_size,
)
Loading

0 comments on commit 528ebd8

Please sign in to comment.