Skip to content

Commit

Permalink
Merge pull request #4955 from nulano/ft-color3
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Oct 12, 2020
2 parents fee6baf + 39ae5d6 commit 43c3f4d
Show file tree
Hide file tree
Showing 31 changed files with 384 additions and 100 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Loading
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 43c3f4d

Please sign in to comment.