Skip to content

Commit

Permalink
Merge pull request #7354 from radarhere/load_default
Browse files Browse the repository at this point in the history
Added TrueType default font to allow for different sizes
  • Loading branch information
hugovk committed Oct 14, 2023
2 parents e154e97 + c2d5088 commit 7bf1a87
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 61 deletions.
Binary file added Tests/images/default_font_freetype.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/imagedraw_default_font_size.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 29 additions & 2 deletions Tests/test_imagedraw.py
@@ -1,8 +1,9 @@
import contextlib
import os.path

import pytest

from PIL import Image, ImageColor, ImageDraw, ImageFont
from PIL import Image, ImageColor, ImageDraw, ImageFont, features

from .helper import (
assert_image_equal,
Expand Down Expand Up @@ -1353,7 +1354,33 @@ def test_setting_default_font():
assert draw.getfont() == font
finally:
ImageDraw.ImageDraw.font = None
assert isinstance(draw.getfont(), ImageFont.ImageFont)
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)


def test_default_font_size():
freetype_support = features.check_module("freetype2")
text = "Default font at a specific size."

im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")

with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.textlength(text, font_size=16) == 216

with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)

im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")

with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)


@pytest.mark.parametrize("bbox", BBOX)
Expand Down
23 changes: 5 additions & 18 deletions Tests/test_imagefont.py
Expand Up @@ -453,16 +453,19 @@ def test_load_non_font_bytes():

def test_default_font():
# Arrange
txt = 'This is a "better than nothing" default font.'
txt = "This is a default font using FreeType support."
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)

# Act
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)

larger_default_font = ImageFont.load_default(size=14)
draw.text((10, 60), txt, font=larger_default_font)

# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")


@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
Expand All @@ -485,14 +488,6 @@ def test_render_empty(font):
assert_image_equal(im, target)


def test_unicode_pilfont():
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("’")


def test_unicode_extended(layout_engine):
# issue #3777
text = "A\u278A\U0001F12B"
Expand Down Expand Up @@ -722,14 +717,6 @@ def test_variation_set_by_axes(font):
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)


def test_textbbox_non_freetypefont():
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)


@pytest.mark.parametrize(
"anchor, left, top",
(
Expand Down
45 changes: 45 additions & 0 deletions Tests/test_imagefontpil.py
@@ -0,0 +1,45 @@
import pytest

from PIL import Image, ImageDraw, ImageFont, features

from .helper import assert_image_equal_tofile

pytestmark = pytest.mark.skipif(
features.check_module("freetype2"),
reason="PILfont superseded if FreeType is supported",
)


def test_default_font():
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)

# Act
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)

# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")


def test_size_without_freetype():
with pytest.raises(ImportError):
ImageFont.load_default(size=14)


def test_unicode():
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("’")


def test_textbbox():
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
40 changes: 35 additions & 5 deletions docs/reference/ImageDraw.rst
Expand Up @@ -351,7 +351,7 @@ Methods

Draw a shape.

.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position.

Expand Down Expand Up @@ -416,8 +416,14 @@ Methods

.. versionadded:: 8.0.0

:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.

.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
.. versionadded:: 10.1.0


.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position.

Expand Down Expand Up @@ -477,7 +483,13 @@ Methods

.. versionadded:: 8.0.0

.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.

.. versionadded:: 10.1.0

.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None)
Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language.
Expand Down Expand Up @@ -538,9 +550,15 @@ Methods
It should be a `BCP 47 language code`_.
Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.

.. versionadded:: 10.1.0

:return: Either width for horizontal text, or height for vertical text.

.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
Expand Down Expand Up @@ -588,9 +606,15 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.

.. versionadded:: 10.1.0

:return: ``(left, top, right, bottom)`` bounding box

.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language.
Expand Down Expand Up @@ -632,6 +656,12 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.

.. versionadded:: 10.1.0

:return: ``(left, top, right, bottom)`` bounding box

.. py:method:: getdraw(im=None, hints=None)
Expand Down
36 changes: 27 additions & 9 deletions docs/releasenotes/10.1.0.rst
Expand Up @@ -41,15 +41,6 @@ to be specified, rather than a single number for both dimensions. ::
API Additions
=============

ImageOps.cover
^^^^^^^^^^^^^^

Returns a resized version of the image, so that the requested size is covered,
while maintaining the original aspect ratio.

See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
methods.

EpsImagePlugin.gs_binary
^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -69,6 +60,33 @@ channel, a palette with an alpha channel, or a "transparency" key in the
Even if this attribute is true, the image might still appear solid, if all of
the values shown within are opaque.

ImageOps.cover
^^^^^^^^^^^^^^

Returns a resized version of the image, so that the requested size is covered,
while maintaining the original aspect ratio.

See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
methods.

size and font_size arguments when using default font
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Pillow has had a "better than nothing" default font, which can only be drawn at
one font size. Now, if FreeType support is available, a version of
`Aileron Regular <https://dotcolon.net/font/aileron>`_ is loaded, which can be
drawn at chosen font sizes.

The following ``size`` and ``font_size`` arguments can now be used to specify a
font size for this new builtin font::

ImageFont.load_default(size=24)
draw.text((0, 0), "test", font_size=24)
draw.textlength((0, 0), "test", font_size=24)
draw.textbbox((0, 0), "test", font_size=24)
draw.multiline_text((0, 0), "test", font_size=24)
draw.multiline_textbbox((0, 0), "test", font_size=24)

Other Changes
=============

Expand Down

0 comments on commit 7bf1a87

Please sign in to comment.