Merge pull request #4955 from nulano/ft-color3
4
.github/workflows/test-windows.yml
vendored
|
@ -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"
|
||||
|
|
BIN
Tests/fonts/BungeeColor-Regular_colr_Windows.ttf
Normal file
BIN
Tests/fonts/DejaVuSans-24-1-stripped.ttf
Normal file
BIN
Tests/fonts/DejaVuSans-24-2-stripped.ttf
Normal file
BIN
Tests/fonts/DejaVuSans-24-4-stripped.ttf
Normal file
BIN
Tests/fonts/DejaVuSans-24-8-stripped.ttf
Normal file
|
@ -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."
|
||||
|
|
BIN
Tests/fonts/NotoColorEmoji.ttf
Normal file
|
@ -11,6 +11,7 @@ import tempfile
|
|||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, ImageMath, features
|
||||
|
||||
|
@ -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
|
||||
|
|
BIN
Tests/images/bitmap_font_1_basic.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
Tests/images/bitmap_font_1_raqm.png
Normal file
After Width: | Height: | Size: 480 B |
BIN
Tests/images/bitmap_font_2_basic.png
Normal file
After Width: | Height: | Size: 661 B |
BIN
Tests/images/bitmap_font_2_raqm.png
Normal file
After Width: | Height: | Size: 658 B |
BIN
Tests/images/bitmap_font_4_basic.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Tests/images/bitmap_font_4_raqm.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Tests/images/bitmap_font_8_basic.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
Tests/images/bitmap_font_8_raqm.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
Tests/images/cbdt_notocoloremoji.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/cbdt_notocoloremoji_mask.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
Tests/images/colr_bungee.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
Tests/images/colr_bungee_mask.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
Tests/images/standard_embedded.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
|
@ -18,6 +18,7 @@ from .helper import (
|
|||
is_pypy,
|
||||
is_win32,
|
||||
skip_unless_feature,
|
||||
skip_unless_feature_version,
|
||||
)
|
||||
|
||||
FONT_PATH = "Tests/fonts/FreeMono.ttf"
|
||||
|
@ -840,18 +841,120 @@ class TestImageFont:
|
|||
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(
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from .helper import assert_image_similar
|
||||
|
||||
image_font_installed = True
|
||||
try:
|
||||
ImageFont.core.getfont
|
||||
except ImportError:
|
||||
image_font_installed = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not image_font_installed, reason="Image font not installed")
|
||||
def test_similar():
|
||||
text = "EmbeddedBitmap"
|
||||
font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24)
|
||||
font_bitmap = ImageFont.truetype(font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24)
|
||||
size_outline = font_outline.getsize(text)
|
||||
size_bitmap = font_bitmap.getsize(text)
|
||||
size_final = (
|
||||
max(size_outline[0], size_bitmap[0]),
|
||||
max(size_outline[1], size_bitmap[1]),
|
||||
)
|
||||
im_bitmap = Image.new("RGB", size_final, (255, 255, 255))
|
||||
im_outline = im_bitmap.copy()
|
||||
draw_bitmap = ImageDraw.Draw(im_bitmap)
|
||||
draw_outline = ImageDraw.Draw(im_outline)
|
||||
|
||||
# Metrics are different on the bitmap and TTF fonts,
|
||||
# more so on some platforms and versions of FreeType than others.
|
||||
# Mac has a 1px difference, Linux doesn't.
|
||||
draw_bitmap.text(
|
||||
(0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap
|
||||
)
|
||||
draw_outline.text(
|
||||
(0, size_final[1] - size_outline[1]),
|
||||
text,
|
||||
fill=(0, 0, 0),
|
||||
font=font_outline,
|
||||
)
|
||||
assert_image_similar(im_bitmap, im_outline, 20)
|
|
@ -3,7 +3,11 @@ from packaging.version import parse as parse_version
|
|||
|
||||
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"
|
||||
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
|
|
@ -282,6 +282,7 @@ class ImageDraw:
|
|||
language=None,
|
||||
stroke_width=0,
|
||||
stroke_fill=None,
|
||||
embedded_color=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
@ -299,8 +300,12 @@ class ImageDraw:
|
|||
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()
|
||||
|
||||
|
@ -311,16 +316,20 @@ class ImageDraw:
|
|||
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,
|
||||
)
|
||||
|
@ -329,12 +338,13 @@ class ImageDraw:
|
|||
try:
|
||||
mask = font.getmask(
|
||||
text,
|
||||
self.fontmode,
|
||||
mode,
|
||||
direction,
|
||||
features,
|
||||
language,
|
||||
stroke_width,
|
||||
anchor,
|
||||
ink,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
@ -342,7 +352,15 @@ class ImageDraw:
|
|||
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:
|
||||
|
@ -374,6 +392,7 @@ class ImageDraw:
|
|||
language=None,
|
||||
stroke_width=0,
|
||||
stroke_fill=None,
|
||||
embedded_color=False,
|
||||
):
|
||||
if direction == "ttb":
|
||||
raise ValueError("ttb direction is unsupported for multiline text")
|
||||
|
@ -440,6 +459,7 @@ class ImageDraw:
|
|||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
stroke_fill=stroke_fill,
|
||||
embedded_color=embedded_color,
|
||||
)
|
||||
top += line_spacing
|
||||
|
||||
|
|
|
@ -261,7 +261,7 @@ class FreeTypeFont:
|
|||
"""
|
||||
# vertical offset is added for historical reasons
|
||||
# see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929
|
||||
size, offset = self.font.getsize(text, False, direction, features, language)
|
||||
size, offset = self.font.getsize(text, "L", direction, features, language)
|
||||
return (
|
||||
size[0] + stroke_width * 2,
|
||||
size[1] + stroke_width * 2 + offset[1],
|
||||
|
@ -348,12 +348,14 @@ class FreeTypeFont:
|
|||
language=None,
|
||||
stroke_width=0,
|
||||
anchor=None,
|
||||
ink=0,
|
||||
):
|
||||
"""
|
||||
Create a bitmap for the text.
|
||||
|
||||
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
||||
maximum value of 255. Otherwise, it should have mode ``1``.
|
||||
maximum value of 255. If the font has embedded color data, the bitmap
|
||||
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
||||
|
||||
:param text: Text to render.
|
||||
:param mode: Used by some graphics drivers to indicate what mode the
|
||||
|
@ -402,6 +404,10 @@ class FreeTypeFont:
|
|||
|
||||
.. versionadded:: 8.0.0
|
||||
|
||||
:param ink: Foreground ink for rendering in RGBA mode.
|
||||
|
||||
.. versionadded:: 8.0.0
|
||||
|
||||
:return: An internal PIL storage memory instance as defined by the
|
||||
:py:mod:`PIL.Image.core` interface module.
|
||||
"""
|
||||
|
@ -413,6 +419,7 @@ class FreeTypeFont:
|
|||
language=language,
|
||||
stroke_width=stroke_width,
|
||||
anchor=anchor,
|
||||
ink=ink,
|
||||
)[0]
|
||||
|
||||
def getmask2(
|
||||
|
@ -425,6 +432,7 @@ class FreeTypeFont:
|
|||
language=None,
|
||||
stroke_width=0,
|
||||
anchor=None,
|
||||
ink=0,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
@ -432,7 +440,8 @@ class FreeTypeFont:
|
|||
Create a bitmap for the text.
|
||||
|
||||
If the font uses antialiasing, the bitmap should have mode ``L`` and use a
|
||||
maximum value of 255. Otherwise, it should have mode ``1``.
|
||||
maximum value of 255. If the font has embedded color data, the bitmap
|
||||
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
|
||||
|
||||
:param text: Text to render.
|
||||
:param mode: Used by some graphics drivers to indicate what mode the
|
||||
|
@ -481,18 +490,22 @@ class FreeTypeFont:
|
|||
|
||||
.. versionadded:: 8.0.0
|
||||
|
||||
:param ink: Foreground ink for rendering in RGBA mode.
|
||||
|
||||
.. versionadded:: 8.0.0
|
||||
|
||||
:return: A tuple of an internal PIL storage memory instance as defined by the
|
||||
:py:mod:`PIL.Image.core` interface module, and the text offset, the
|
||||
gap between the starting coordinate and the first marking
|
||||
"""
|
||||
size, offset = self.font.getsize(
|
||||
text, mode == "1", direction, features, language, anchor
|
||||
text, mode, direction, features, language, anchor
|
||||
)
|
||||
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
|
||||
offset = offset[0] - stroke_width, offset[1] - stroke_width
|
||||
im = fill("L", size, 0)
|
||||
im = fill("RGBA" if mode == "RGBA" else "L", size, 0)
|
||||
self.font.render(
|
||||
text, im.id, mode == "1", direction, features, language, stroke_width
|
||||
text, im.id, mode, direction, features, language, stroke_width, ink
|
||||
)
|
||||
return im, offset
|
||||
|
||||
|
|
204
src/_imagingft.c
|
@ -25,9 +25,13 @@
|
|||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_GLYPH_H
|
||||
#include FT_BITMAP_H
|
||||
#include FT_STROKER_H
|
||||
#include FT_MULTIPLE_MASTERS_H
|
||||
#include FT_SFNT_NAMES_H
|
||||
#ifdef FT_COLOR_H
|
||||
#include FT_COLOR_H
|
||||
#endif
|
||||
|
||||
#define KEEP_PY_UNICODE
|
||||
|
||||
|
@ -350,7 +354,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
|
|||
|
||||
static size_t
|
||||
text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features,
|
||||
const char* lang, GlyphInfo **glyph_info, int mask)
|
||||
const char* lang, GlyphInfo **glyph_info, int mask, int color)
|
||||
{
|
||||
size_t i = 0, count = 0, start = 0;
|
||||
raqm_t *rq;
|
||||
|
@ -529,7 +533,7 @@ failed:
|
|||
|
||||
static size_t
|
||||
text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features,
|
||||
const char* lang, GlyphInfo **glyph_info, int mask)
|
||||
const char* lang, GlyphInfo **glyph_info, int mask, int color)
|
||||
{
|
||||
int error, load_flags;
|
||||
FT_ULong ch;
|
||||
|
@ -561,10 +565,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje
|
|||
return 0;
|
||||
}
|
||||
|
||||
load_flags = FT_LOAD_NO_BITMAP;
|
||||
load_flags = FT_LOAD_DEFAULT;
|
||||
if (mask) {
|
||||
load_flags |= FT_LOAD_TARGET_MONO;
|
||||
}
|
||||
#ifdef FT_LOAD_COLOR
|
||||
if (color) {
|
||||
load_flags |= FT_LOAD_COLOR;
|
||||
}
|
||||
#endif
|
||||
for (i = 0; font_getchar(string, i, &ch); i++) {
|
||||
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch);
|
||||
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags);
|
||||
|
@ -595,14 +604,14 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje
|
|||
|
||||
static size_t
|
||||
text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features,
|
||||
const char* lang, GlyphInfo **glyph_info, int mask)
|
||||
const char* lang, GlyphInfo **glyph_info, int mask, int color)
|
||||
{
|
||||
size_t count;
|
||||
|
||||
if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) {
|
||||
count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask);
|
||||
count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask, color);
|
||||
} else {
|
||||
count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask);
|
||||
count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask, color);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
@ -624,6 +633,8 @@ font_getsize(FontObject* self, PyObject* args)
|
|||
size_t i, count; /* glyph_info index and length */
|
||||
int horizontal_dir; /* is primary axis horizontal? */
|
||||
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
|
||||
int color = 0; /* is FT_LOAD_COLOR enabled? */
|
||||
const char *mode = NULL;
|
||||
const char *dir = NULL;
|
||||
const char *lang = NULL;
|
||||
const char *anchor = NULL;
|
||||
|
@ -632,12 +643,15 @@ font_getsize(FontObject* self, PyObject* args)
|
|||
|
||||
/* calculate size and bearing for a given string */
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O|izOzz:getsize", &string, &mask, &dir, &features, &lang, &anchor)) {
|
||||
if (!PyArg_ParseTuple(args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
|
||||
|
||||
mask = mode && strcmp(mode, "1") == 0;
|
||||
color = mode && strcmp(mode, "RGBA") == 0;
|
||||
|
||||
if (anchor == NULL) {
|
||||
anchor = horizontal_dir ? "la" : "lt";
|
||||
}
|
||||
|
@ -645,18 +659,20 @@ font_getsize(FontObject* self, PyObject* args)
|
|||
goto bad_anchor;
|
||||
}
|
||||
|
||||
count = text_layout(string, self, dir, features, lang, &glyph_info, mask);
|
||||
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
|
||||
if (PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960
|
||||
* Yifu Yu<root@jackyyf.com>, 2014-10-15
|
||||
*/
|
||||
load_flags = FT_LOAD_NO_BITMAP;
|
||||
load_flags = FT_LOAD_DEFAULT;
|
||||
if (mask) {
|
||||
load_flags |= FT_LOAD_TARGET_MONO;
|
||||
}
|
||||
#ifdef FT_LOAD_COLOR
|
||||
if (color) {
|
||||
load_flags |= FT_LOAD_COLOR;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* text bounds are given by:
|
||||
|
@ -822,19 +838,26 @@ font_render(FontObject* self, PyObject* args)
|
|||
FT_Glyph glyph;
|
||||
FT_GlyphSlot glyph_slot;
|
||||
FT_Bitmap bitmap;
|
||||
FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */
|
||||
FT_BitmapGlyph bitmap_glyph;
|
||||
FT_Stroker stroker = NULL;
|
||||
int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */
|
||||
GlyphInfo *glyph_info = NULL; /* computed text layout */
|
||||
size_t i, count; /* glyph_info index and length */
|
||||
int xx, yy; /* pixel offset of current glyph bitmap */
|
||||
int x0, x1; /* horizontal bounds of glyph bitmap to copy */
|
||||
unsigned int bitmap_y; /* glyph bitmap y index */
|
||||
unsigned char *source; /* glyph bitmap source buffer */
|
||||
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
|
||||
Imaging im;
|
||||
Py_ssize_t id;
|
||||
int horizontal_dir; /* is primary axis horizontal? */
|
||||
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
|
||||
int color = 0; /* is FT_LOAD_COLOR enabled? */
|
||||
int stroke_width = 0;
|
||||
PY_LONG_LONG foreground_ink_long = 0;
|
||||
unsigned int foreground_ink;
|
||||
const char *mode = NULL;
|
||||
const char *dir = NULL;
|
||||
const char *lang = NULL;
|
||||
PyObject *features = Py_None;
|
||||
|
@ -843,14 +866,31 @@ font_render(FontObject* self, PyObject* args)
|
|||
/* render string into given buffer (the buffer *must* have
|
||||
the right size, or this will crash) */
|
||||
|
||||
if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang,
|
||||
&stroke_width)) {
|
||||
if (!PyArg_ParseTuple(args, "On|zzOziL:render", &string, &id, &mode, &dir, &features, &lang,
|
||||
&stroke_width, &foreground_ink_long)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
|
||||
|
||||
count = text_layout(string, self, dir, features, lang, &glyph_info, mask);
|
||||
mask = mode && strcmp(mode, "1") == 0;
|
||||
color = mode && strcmp(mode, "RGBA") == 0;
|
||||
|
||||
foreground_ink = foreground_ink_long;
|
||||
|
||||
#ifdef FT_COLOR_H
|
||||
if (color) {
|
||||
FT_Color foreground_color;
|
||||
FT_Byte* ink = (FT_Byte*)&foreground_ink;
|
||||
foreground_color.red = ink[0];
|
||||
foreground_color.green = ink[1];
|
||||
foreground_color.blue = ink[2];
|
||||
foreground_color.alpha = (FT_Byte) 255; /* ink alpha is handled in ImageDraw.text */
|
||||
FT_Palette_Set_Foreground_Color(self->face, foreground_color);
|
||||
}
|
||||
#endif
|
||||
|
||||
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
|
||||
if (PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
|
@ -868,12 +908,15 @@ font_render(FontObject* self, PyObject* args)
|
|||
}
|
||||
|
||||
im = (Imaging) id;
|
||||
|
||||
/* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */
|
||||
load_flags = FT_LOAD_NO_BITMAP;
|
||||
load_flags = FT_LOAD_DEFAULT;
|
||||
if (mask) {
|
||||
load_flags |= FT_LOAD_TARGET_MONO;
|
||||
}
|
||||
#ifdef FT_LOAD_COLOR
|
||||
if (color) {
|
||||
load_flags |= FT_LOAD_COLOR;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* calculate x_min and y_max
|
||||
|
@ -945,6 +988,55 @@ font_render(FontObject* self, PyObject* args)
|
|||
yy = -(py + glyph_slot->bitmap_top);
|
||||
}
|
||||
|
||||
/* convert non-8bpp bitmaps */
|
||||
switch (bitmap.pixel_mode) {
|
||||
case FT_PIXEL_MODE_MONO:
|
||||
convert_scale = 255;
|
||||
break;
|
||||
case FT_PIXEL_MODE_GRAY2:
|
||||
convert_scale = 255 / 3;
|
||||
break;
|
||||
case FT_PIXEL_MODE_GRAY4:
|
||||
convert_scale = 255 / 15;
|
||||
break;
|
||||
default:
|
||||
convert_scale = 1;
|
||||
}
|
||||
switch (bitmap.pixel_mode) {
|
||||
case FT_PIXEL_MODE_MONO:
|
||||
case FT_PIXEL_MODE_GRAY2:
|
||||
case FT_PIXEL_MODE_GRAY4:
|
||||
if (!bitmap_converted_ready) {
|
||||
|
||||
#if FREETYPE_MAJOR > 2 ||\
|
||||
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6)
|
||||
FT_Bitmap_Init(&bitmap_converted);
|
||||
#else
|
||||
FT_Bitmap_New(&bitmap_converted);
|
||||
#endif
|
||||
bitmap_converted_ready = 1;
|
||||
}
|
||||
error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1);
|
||||
if (error) {
|
||||
geterror(error);
|
||||
goto glyph_error;
|
||||
}
|
||||
bitmap = bitmap_converted;
|
||||
/* bitmap is now FT_PIXEL_MODE_GRAY, fall through */
|
||||
case FT_PIXEL_MODE_GRAY:
|
||||
break;
|
||||
#ifdef FT_LOAD_COLOR
|
||||
case FT_PIXEL_MODE_BGRA:
|
||||
if (color) {
|
||||
break;
|
||||
}
|
||||
/* we didn't ask for color, fall through to default */
|
||||
#endif
|
||||
default:
|
||||
PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
|
||||
goto glyph_error;
|
||||
}
|
||||
|
||||
/* clip glyph bitmap width to target image bounds */
|
||||
x0 = 0;
|
||||
x1 = bitmap.width;
|
||||
|
@ -959,28 +1051,54 @@ font_render(FontObject* self, PyObject* args)
|
|||
for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) {
|
||||
/* clip glyph bitmap height to target image bounds */
|
||||
if (yy >= 0 && yy < im->ysize) {
|
||||
// blend this glyph into the buffer
|
||||
unsigned char *target = im->image8[yy] + xx;
|
||||
if (mask) {
|
||||
// use monochrome mask (on palette images, etc)
|
||||
int j, k, m = 128;
|
||||
for (j = k = 0; j < x1; j++) {
|
||||
if (j >= x0 && (source[k] & m)) {
|
||||
target[j] = 255;
|
||||
/* blend this glyph into the buffer */
|
||||
int k;
|
||||
unsigned char v;
|
||||
unsigned char* target;
|
||||
if (color) {
|
||||
/* target[RGB] returns the color, target[A] returns the mask */
|
||||
/* target bands get split again in ImageDraw.text */
|
||||
target = im->image[yy] + xx * 4;
|
||||
} else {
|
||||
target = im->image8[yy] + xx;
|
||||
}
|
||||
#ifdef FT_LOAD_COLOR
|
||||
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
|
||||
/* paste color glyph */
|
||||
for (k = x0; k < x1; k++) {
|
||||
if (target[k * 4 + 3] < source[k * 4 + 3]) {
|
||||
/* unpremultiply BGRa to RGBA */
|
||||
target[k * 4 + 0] = CLIP8((255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]);
|
||||
target[k * 4 + 1] = CLIP8((255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]);
|
||||
target[k * 4 + 2] = CLIP8((255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]);
|
||||
target[k * 4 + 3] = source[k * 4 + 3];
|
||||
}
|
||||
if (!(m >>= 1)) {
|
||||
m = 128;
|
||||
k++;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
|
||||
if (color) {
|
||||
unsigned char* ink = (unsigned char*)&foreground_ink;
|
||||
for (k = x0; k < x1; k++) {
|
||||
v = source[k] * convert_scale;
|
||||
if (target[k * 4 + 3] < v) {
|
||||
target[k * 4 + 0] = ink[0];
|
||||
target[k * 4 + 1] = ink[1];
|
||||
target[k * 4 + 2] = ink[2];
|
||||
target[k * 4 + 3] = v;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (k = x0; k < x1; k++) {
|
||||
v = source[k] * convert_scale;
|
||||
if (target[k] < v) {
|
||||
target[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// use antialiased rendering
|
||||
int k;
|
||||
for (k = x0; k < x1; k++) {
|
||||
if (target[k] < source[k]) {
|
||||
target[k] = source[k];
|
||||
}
|
||||
}
|
||||
PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
|
||||
goto glyph_error;
|
||||
}
|
||||
}
|
||||
source += bitmap.pitch;
|
||||
|
@ -992,9 +1110,23 @@ font_render(FontObject* self, PyObject* args)
|
|||
}
|
||||
}
|
||||
|
||||
if (bitmap_converted_ready) {
|
||||
FT_Bitmap_Done(library, &bitmap_converted);
|
||||
}
|
||||
FT_Stroker_Done(stroker);
|
||||
PyMem_Del(glyph_info);
|
||||
Py_RETURN_NONE;
|
||||
|
||||
glyph_error:
|
||||
if (stroker != NULL) {
|
||||
FT_Done_Glyph(glyph);
|
||||
}
|
||||
if (bitmap_converted_ready) {
|
||||
FT_Bitmap_Done(library, &bitmap_converted);
|
||||
}
|
||||
FT_Stroker_Done(stroker);
|
||||
PyMem_Del(glyph_info);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#if FREETYPE_MAJOR > 2 ||\
|
||||
|
|
|
@ -169,6 +169,20 @@ deps = {
|
|||
],
|
||||
"libs": [r"output\release-static\{architecture}\lib\*.lib"],
|
||||
},
|
||||
"libpng": {
|
||||
"url": SF_MIRROR + "/project/libpng/libpng16/1.6.37/lpng1637.zip",
|
||||
"filename": "lpng1637.zip",
|
||||
"dir": "lpng1637",
|
||||
"build": [
|
||||
# lint: do not inline
|
||||
cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")),
|
||||
cmd_nmake(target="clean"),
|
||||
cmd_nmake(),
|
||||
cmd_copy("libpng16_static.lib", "libpng16.lib"),
|
||||
],
|
||||
"headers": [r"png*.h"],
|
||||
"libs": [r"libpng16.lib"],
|
||||
},
|
||||
"freetype": {
|
||||
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.2.tar.gz", # noqa: E501
|
||||
"filename": "freetype-2.10.2.tar.gz",
|
||||
|
@ -181,8 +195,10 @@ deps = {
|
|||
'<PropertyGroup Label="Globals">': '<PropertyGroup Label="Globals">\n <WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>', # noqa: E501
|
||||
},
|
||||
r"builds\windows\vc2010\freetype.user.props": {
|
||||
"<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_USE_HARFBUZZ</UserDefines>", # noqa: E501
|
||||
"<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src</UserIncludeDirectories>", # noqa: E501
|
||||
"<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ</UserDefines>", # noqa: E501
|
||||
"<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src;{inc_dir}</UserIncludeDirectories>", # noqa: E501
|
||||
"<UserLibraryDirectories></UserLibraryDirectories>": "<UserLibraryDirectories>{lib_dir}</UserLibraryDirectories>", # noqa: E501
|
||||
"<UserDependencies></UserDependencies>": "<UserDependencies>zlib.lib;libpng16.lib</UserDependencies>", # noqa: E501
|
||||
},
|
||||
r"src/autofit/afshaper.c": {
|
||||
# link against harfbuzz.lib once it becomes available
|
||||
|
|