Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CBDT and COLR fonts #4955

Merged
merged 13 commits into from Oct 12, 2020
4 changes: 4 additions & 0 deletions .github/workflows/test-windows.yml
Expand Up @@ -105,6 +105,10 @@ jobs:
- name: Build dependencies / WebP
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libwebp.cmd"
# for FreeType CBDT font support
- name: Build dependencies / libpng
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd"
- name: Build dependencies / FreeType
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_freetype.cmd"
Expand Down
Binary file added Tests/fonts/BungeeColor-Regular_colr_Windows.ttf
Binary file not shown.
Binary file added Tests/fonts/DejaVuSans-24-1-stripped.ttf
Binary file not shown.
Binary file added Tests/fonts/DejaVuSans-24-2-stripped.ttf
Binary file not shown.
Binary file added Tests/fonts/DejaVuSans-24-4-stripped.ttf
Binary file not shown.
Binary file added Tests/fonts/DejaVuSans-24-8-stripped.ttf
Binary file not shown.
Binary file removed Tests/fonts/DejaVuSans-bitmap.ttf
Binary file not shown.
5 changes: 5 additions & 0 deletions Tests/fonts/LICENSE.txt
Expand Up @@ -2,14 +2,19 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/
BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee

All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.


DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range.


10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base

"Public domain font. Share and enjoy."
Binary file added Tests/fonts/NotoColorEmoji.ttf
Binary file not shown.
11 changes: 11 additions & 0 deletions Tests/helper.py
Expand Up @@ -11,6 +11,7 @@
from io import BytesIO

import pytest
from packaging.version import parse as parse_version

from PIL import Image, ImageMath, features

Expand Down Expand Up @@ -162,6 +163,16 @@ def skip_unless_feature(feature):
return pytest.mark.skipif(not features.check(feature), reason=reason)


def skip_unless_feature_version(feature, version_required, reason=None):
if not features.check(feature):
return pytest.mark.skip(f"{feature} not available")
if reason is None:
reason = f"{feature} is older than {version_required}"
version_required = parse_version(version_required)
version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason)


@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS")
class PillowLeakTestCase:
# requires unix/macOS
Expand Down
Binary file added Tests/images/bitmap_font_1_basic.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/bitmap_font_1_raqm.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/bitmap_font_2_basic.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/bitmap_font_2_raqm.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/bitmap_font_4_basic.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/bitmap_font_4_raqm.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/bitmap_font_8_basic.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/bitmap_font_8_raqm.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/cbdt_notocoloremoji.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/cbdt_notocoloremoji_mask.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/colr_bungee.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/colr_bungee_mask.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/standard_embedded.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 106 additions & 3 deletions Tests/test_imagefont.py
Expand Up @@ -18,6 +18,7 @@
is_pypy,
is_win32,
skip_unless_feature,
skip_unless_feature_version,
)

FONT_PATH = "Tests/fonts/FreeMono.ttf"
Expand Down Expand Up @@ -840,18 +841,120 @@ def test_anchor_invalid(self):
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
)

@skip_unless_feature("freetype2")
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
def test_bitmap_font(self, bpp):
text = "Bitmap Font"
layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE]
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
font = ImageFont.truetype(
f"Tests/fonts/DejaVuSans-24-{bpp}-stripped.ttf",
24,
layout_engine=self.LAYOUT_ENGINE,
)

im = Image.new("RGB", (160, 35), "white")
draw = ImageDraw.Draw(im)
draw.text((2, 2), text, "black", font)

assert_image_equal_tofile(im, target)

def test_standard_embedded_color(self):
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE)
ttf.getsize(txt)

im = Image.new("RGB", (300, 64), "white")
d = ImageDraw.Draw(im)
d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)

with Image.open("Tests/images/standard_embedded.png") as expected:
assert_image_similar(im, expected, max(self.metrics["multiline"], 3))

@skip_unless_feature_version("freetype2", "2.5.0")
@pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm")
def test_cbdt(self):
try:
font = ImageFont.truetype(
"Tests/fonts/NotoColorEmoji.ttf",
size=109,
layout_engine=self.LAYOUT_ENGINE,
)

im = Image.new("RGB", (150, 150), "white")
d = ImageDraw.Draw(im)

d.text((10, 10), "\U0001f469", embedded_color=True, font=font)

with Image.open("Tests/images/cbdt_notocoloremoji.png") as expected:
assert_image_similar(im, expected, self.metrics["multiline"])
except IOError as e:
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or unsupported")

@skip_unless_feature_version("freetype2", "2.5.0")
@pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm")
def test_cbdt_mask(self):
try:
font = ImageFont.truetype(
"Tests/fonts/NotoColorEmoji.ttf",
size=109,
layout_engine=self.LAYOUT_ENGINE,
)

im = Image.new("RGB", (150, 150), "white")
d = ImageDraw.Draw(im)

d.text((10, 10), "\U0001f469", "black", font=font)

with Image.open("Tests/images/cbdt_notocoloremoji_mask.png") as expected:
assert_image_similar(im, expected, self.metrics["multiline"])
except IOError as e:
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or unsupported")

@skip_unless_feature_version("freetype2", "2.10.0")
def test_colr(self):
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
layout_engine=self.LAYOUT_ENGINE,
)

im = Image.new("RGB", (300, 75), "white")
d = ImageDraw.Draw(im)

d.text((15, 5), "Bungee", embedded_color=True, font=font)

with Image.open("Tests/images/colr_bungee.png") as expected:
assert_image_similar(im, expected, 21)

@skip_unless_feature_version("freetype2", "2.10.0")
def test_colr_mask(self):
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
layout_engine=self.LAYOUT_ENGINE,
)

im = Image.new("RGB", (300, 75), "white")
d = ImageDraw.Draw(im)

d.text((15, 5), "Bungee", "black", font=font)

with Image.open("Tests/images/colr_bungee_mask.png") as expected:
assert_image_similar(im, expected, 22)


@skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont):
LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM


@skip_unless_feature_version("freetype2", "2.4", "Different metrics")
def test_render_mono_size():
# issue 4177

if parse_version(ImageFont.core.freetype2_version) < parse_version("2.4"):
pytest.skip("Different metrics")

im = Image.new("P", (100, 30), "white")
draw = ImageDraw.Draw(im)
ttf = ImageFont.truetype(
Expand Down
42 changes: 0 additions & 42 deletions Tests/test_imagefont_bitmap.py

This file was deleted.

16 changes: 10 additions & 6 deletions Tests/test_imagefontctl.py
Expand Up @@ -3,7 +3,11 @@

from PIL import Image, ImageDraw, ImageFont, features

from .helper import assert_image_similar, skip_unless_feature
from .helper import (
assert_image_similar,
skip_unless_feature,
skip_unless_feature_version,
)

FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans.ttf"
Expand Down Expand Up @@ -209,13 +213,13 @@ def test_language():
assert_image_similar(im, target_img, 0.5)


# FreeType 2.5.1 README: Miscellaneous Changes:
# Improved computation of emulated vertical metrics for TrueType fonts.
@skip_unless_feature_version(
"freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics"
)
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
def test_anchor_ttb(anchor):
if parse_version(features.version_module("freetype2")) < parse_version("2.5.1"):
# FreeType 2.5.1 README: Miscellaneous Changes:
# Improved computation of emulated vertical metrics for TrueType fonts.
pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")

text = "f"
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
Expand Down
22 changes: 20 additions & 2 deletions docs/reference/ImageDraw.rst
Expand Up @@ -291,7 +291,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)
.. 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)

Draws the string at the given position.

Expand Down Expand Up @@ -352,7 +352,12 @@ Methods

.. versionadded:: 6.2.0

.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None)
:param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT).

.. versionadded:: 8.0.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)

Draws the string at the given position.

Expand Down Expand Up @@ -399,6 +404,19 @@ Methods

.. versionadded:: 6.0.0

:param stroke_width: The width of the text stroke.

.. versionadded:: 6.2.0

:param stroke_fill: Color to use for the text stroke. If not given, will default to
the ``fill`` parameter.

.. versionadded:: 6.2.0

:param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT).

.. versionadded:: 8.0.0

.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)

Return the size of the given string, in pixels.
Expand Down
26 changes: 23 additions & 3 deletions src/PIL/ImageDraw.py
Expand Up @@ -282,6 +282,7 @@ def text(
language=None,
stroke_width=0,
stroke_fill=None,
embedded_color=False,
*args,
**kwargs,
):
Expand All @@ -299,8 +300,12 @@ def text(
language,
stroke_width,
stroke_fill,
embedded_color,
)

if embedded_color and self.mode not in ("RGB", "RGBA"):
raise ValueError("Embedded color supported only in RGB and RGBA modes")

if font is None:
font = self.getfont()

Expand All @@ -311,16 +316,20 @@ def getink(fill):
return ink

def draw_text(ink, stroke_width=0, stroke_offset=None):
mode = self.fontmode
if stroke_width == 0 and embedded_color:
mode = "RGBA"
coord = xy
try:
mask, offset = font.getmask2(
text,
self.fontmode,
mode,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
*args,
**kwargs,
)
Expand All @@ -329,20 +338,29 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
try:
mask = font.getmask(
text,
self.fontmode,
mode,
direction,
features,
language,
stroke_width,
anchor,
ink,
*args,
**kwargs,
)
except TypeError:
mask = font.getmask(text)
if stroke_offset:
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
self.draw.draw_bitmap(coord, mask, ink)
if mode == "RGBA":
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF)
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
self.im.paste(color, coord + coord2, mask)
else:
self.draw.draw_bitmap(coord, mask, ink)

ink = getink(fill)
if ink is not None:
Expand Down Expand Up @@ -374,6 +392,7 @@ def multiline_text(
language=None,
stroke_width=0,
stroke_fill=None,
embedded_color=False,
):
if direction == "ttb":
raise ValueError("ttb direction is unsupported for multiline text")
Expand Down Expand Up @@ -440,6 +459,7 @@ def multiline_text(
language=language,
stroke_width=stroke_width,
stroke_fill=stroke_fill,
embedded_color=embedded_color,
)
top += line_spacing

Expand Down