Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import subprocess
import sys
import tempfile
from enum import IntEnum
from functools import lru_cache
from io import BytesIO

Expand All @@ -33,6 +34,21 @@
uploader = "github_actions"


class SnapshotsMode(IntEnum):
NONE = 0 # no creation
CREATE = 1 # create if doesn't exist
UPDATE = 2 # create or update ("this is the new expected result")
OVERWRITE = 3 # always overwrite ("this is the new expected result")


try:
snapshots_mode = SnapshotsMode[
os.environ.get("PILLOW_TEST_SNAPSHOTS", "NONE").upper()
]
except ValueError as ve:
pytest.fail(f"Invalid value for PILLOW_TEST_SNAPSHOTS: {ve}")


def upload(a: Image.Image, b: Image.Image) -> str | None:
if uploader == "show":
# local img.show for errors.
Expand Down Expand Up @@ -95,7 +111,7 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
except Exception:
pass

pytest.fail(msg or "got different content")
raise AssertionError(msg or "got different content")


def assert_image_equal_tofile(
Expand All @@ -104,9 +120,19 @@ def assert_image_equal_tofile(
msg: str | None = None,
mode: str | None = None,
) -> None:
if snapshots_mode == SnapshotsMode.OVERWRITE or (
snapshots_mode and not os.path.isfile(filename)
):
a.save(filename)
with Image.open(filename) as im:
converted_im = im.convert(mode) if mode else im
assert_image_equal(a, converted_im, msg)
try:
assert_image_equal(a, converted_im, msg)
except AssertionError:
if snapshots_mode == SnapshotsMode.UPDATE:
a.save(filename)
else:
raise


def assert_image_similar(
Expand Down Expand Up @@ -146,8 +172,18 @@ def assert_image_similar_tofile(
epsilon: float,
msg: str | None = None,
) -> None:
if snapshots_mode == SnapshotsMode.OVERWRITE or (
snapshots_mode and not os.path.isfile(filename)
):
a.save(filename)
with Image.open(filename) as img:
assert_image_similar(a, img, epsilon, msg)
try:
assert_image_similar(a, img, epsilon, msg)
except AssertionError:
if snapshots_mode == SnapshotsMode.UPDATE:
a.save(filename)
else:
raise


def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
Expand Down
Binary file modified Tests/images/bitmap_font_stroke_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 modified Tests/images/default_font_freetype.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 modified Tests/images/imagedraw_default_font_size.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 modified Tests/images/test_arabictext_features.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 modified Tests/images/test_combine_caron.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 modified Tests/images/test_combine_caron_below.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 modified Tests/images/test_combine_caron_below_lb.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 modified Tests/images/test_combine_caron_below_ld.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 modified Tests/images/test_combine_caron_below_ls.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 modified Tests/images/test_combine_caron_la.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 modified Tests/images/test_combine_caron_ls.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 modified Tests/images/test_combine_caron_lt.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 modified Tests/images/test_combine_double_breve_below.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 modified Tests/images/test_combine_double_breve_below_ma.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 modified Tests/images/test_combine_double_breve_below_ra.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 modified Tests/images/test_combine_multiline_lm_center.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 modified Tests/images/test_combine_multiline_lm_left.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 modified Tests/images/test_combine_multiline_lm_right.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 modified Tests/images/test_combine_multiline_mm_center.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 modified Tests/images/test_combine_multiline_mm_left.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 modified Tests/images/test_combine_multiline_mm_right.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 modified Tests/images/test_combine_multiline_rm_center.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 modified Tests/images/test_combine_multiline_rm_left.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 modified Tests/images/test_combine_multiline_rm_right.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 modified Tests/images/test_complex_unicode_text.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 modified Tests/images/test_direction_ltr.png
Binary file modified Tests/images/test_direction_rtl.png
Binary file removed Tests/images/text_float_coord.png
Diff not rendered.
Binary file added Tests/images/text_float_coord_1_BASIC.png
Binary file added Tests/images/text_float_coord_1_RAQM.png
Binary file removed Tests/images/text_float_coord_1_alt.png
Diff not rendered.
Binary file added Tests/images/text_float_coord_L_BASIC.png
Binary file added Tests/images/text_float_coord_L_RAQM.png
Binary file added Tests/images/text_float_coord_RGBA_BASIC.png
Binary file added Tests/images/text_float_coord_RGBA_RAQM.png
Binary file added Tests/images/text_size_ladder_basic.png
Binary file added Tests/images/text_size_ladder_raqm.png
4 changes: 2 additions & 2 deletions Tests/test_font_pcf_charsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ def test_textsize(
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
ox, oy, dx, dy = font.getbbox(bytearray([i]))
ox, oy, dx, dy = font.getbbox(bytes([i]))
assert ox == 0
assert oy == 0
assert dy == 20
assert dx in (0, 10)
assert font.getlength(bytearray([i])) == dx
assert font.getlength(bytes([i])) == dx
message = charsets[encoding]["message"].encode(encoding)
for i in range(len(message)):
msg = message[: i + 1]
Expand Down
6 changes: 3 additions & 3 deletions Tests/test_image_resize.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def test_reducing_gap_1(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
)

with pytest.raises(pytest.fail.Exception):
with pytest.raises(AssertionError):
assert_image_equal(ref, im)

assert_image_similar(ref, im, epsilon)
Expand All @@ -242,7 +242,7 @@ def test_reducing_gap_2(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
)

with pytest.raises(pytest.fail.Exception):
with pytest.raises(AssertionError):
assert_image_equal(ref, im)

assert_image_similar(ref, im, epsilon)
Expand All @@ -262,7 +262,7 @@ def test_reducing_gap_3(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
)

with pytest.raises(pytest.fail.Exception):
with pytest.raises(AssertionError):
assert_image_equal(ref, im)

assert_image_similar(ref, im, epsilon)
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image_thumbnail.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def test_reducing_gap_values() -> None:

ref = hopper()
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
with pytest.raises(pytest.fail.Exception):
with pytest.raises(AssertionError):
assert_image_equal(ref, im)

assert_image_similar(ref, im, 3.5)
Expand Down
6 changes: 3 additions & 3 deletions Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1534,12 +1534,12 @@ def draw_text() -> None:
check(draw_text)

def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216
assert draw.textlength(text, font_size=16) == pytest.approx(205, rel=0.5)

check(draw_textlength)

def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 205, 19)

check(draw_textbbox)

Expand All @@ -1555,7 +1555,7 @@ def draw_multiline_text() -> None:
check(draw_multiline_text)

def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 205, 19)

check(draw_multiline_textbbox)

Expand Down
5 changes: 2 additions & 3 deletions Tests/test_imagedraw2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from PIL import Image, ImageDraw, ImageDraw2, features
from PIL import Image, ImageDraw, ImageDraw2

from .helper import (
assert_image_equal,
Expand Down Expand Up @@ -244,8 +244,7 @@ def test_textbbox() -> None:
bbox = draw.textbbox((0, 0), "ImageDraw2", font)

# Assert
right = 72 if features.check_feature("raqm") else 70
assert bbox == (0, 2, right, 12)
assert bbox == (0, 2, 72, 12)


@skip_unless_feature("freetype2")
Expand Down
96 changes: 75 additions & 21 deletions Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import pytest

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

from .helper import (
assert_image_equal,
Expand Down Expand Up @@ -181,12 +181,12 @@ def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None:
# basic test
("text", "L", "FreeMono.ttf", 15, 36, 36),
("text", "1", "FreeMono.ttf", 15, 36, 36),
# issue 4177
("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875),
# issue 4177; mode "1" is placed on whole pixels, so it still differs
("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 22.21875, 22.21875),
("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875),
# test 'l' not including extra margin
# using exact value 2047 / 64 for raqm, checked with debugger
("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 31.984375, 31.984375),
("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
),
)
Expand All @@ -196,7 +196,7 @@ def test_getlength(
fontname: str,
size: int,
layout_engine: ImageFont.Layout,
length_basic: int,
length_basic: float,
length_raqm: float,
) -> None:
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
Expand All @@ -213,6 +213,24 @@ def test_getlength(
assert length == length_raqm


def test_size_ladder(layout_engine: ImageFont.Layout) -> None:
# See https://github.com/python-pillow/Pillow/issues/9898
image = Image.new("RGBA", (220, 140), "#fff")
draw = ImageDraw.Draw(image)

y = 5
for size in (12, 13, 14, 15, 16, 20, 24):
font = ImageFont.truetype(
"Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine
)
text = ImageText.Text(f"Power bolt ({size}px)", font)
draw.text((5, y), text, anchor="la", fill="#000")
y += size

name = "basic" if layout_engine == ImageFont.Layout.BASIC else "raqm"
assert_image_similar_tofile(image, f"Tests/images/text_size_ladder_{name}.png", 4)


def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = []
for size in (48, 48.5, 49):
Expand Down Expand Up @@ -571,6 +589,24 @@ def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
assert (0, 0, 0, 0) == font.getbbox("")


def test_bytearray_not_supported(font: ImageFont.FreeTypeFont) -> None:
# The C text layout only accepts str and bytes, so bytearray is rejected
# even though these methods used to be annotated as taking one.
for op in (font.getlength, font.getbbox, font.getmask):
assert op(b"A") is not None
with pytest.raises(TypeError, match="expected string or bytes"):
op(bytearray(b"A")) # type: ignore[arg-type]


def test_core_font_arguments_are_optional(font: ImageFont.FreeTypeFont) -> None:
# Every argument after the leading string is optional at the C level,
# and the mode argument additionally accepts None.
assert font.font.getlength("A") == font.font.getlength("A", "", None, None, None)
assert font.font.getlength("A", None) == font.font.getlength("A", "")
assert font.font.getsize("A") == font.font.getsize("A", "", None, None, None, None)
assert font.font.getsize("A", None) == font.font.getsize("A", "")


def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
# issue 2666
im = Image.new(mode="RGB", size=(300, 100))
Expand Down Expand Up @@ -794,6 +830,17 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
_check_text(font, "Tests/images/variation_tiny_name.png", 40)


def test_truetype_bytes() -> None:
"""Ensure bytestrings with the high bit set do not cause sign extension."""
font = ImageFont.truetype(
"Tests/fonts/FreeMono.ttf",
20,
# Have to use BASIC as RAQM would parse `b"\xe9"` as invalid Unicode
layout_engine=ImageFont.Layout.BASIC,
)
assert font.getbbox(b"\xe9") == font.getbbox("\u00e9")


def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
with pytest.raises(OSError):
font.set_variation_by_axes([500, 50])
Expand All @@ -807,6 +854,17 @@ def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)


def test_variation_set_by_fractional_axes() -> None:
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 200)

def mask(weight: float) -> bytes:
font.set_variation_by_axes([weight, 0])
return bytes(font.getmask("Hg", mode="L"))

# a fractional value must not be truncated or rounded
assert mask(600) != mask(600.5) != mask(601)


@pytest.mark.parametrize(
"anchor, left, top",
(
Expand All @@ -829,10 +887,7 @@ def test_anchor(
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"

if layout_engine == ImageFont.Layout.RAQM:
width, height = (129, 44)
else:
width, height = (128, 44)
width, height = (129, 44)

bbox_expected = (left, top, left + width, top + height)

Expand Down Expand Up @@ -887,7 +942,9 @@ def test_anchor_multiline(
d.line(((300, 0), (300, 400)), "gray")
d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align)

assert_image_similar_tofile(im, target, 4)
# the reference is shared between the layout engines, and they no longer
# differ by more than the GPOS kerning that basic layout cannot apply
assert_image_similar_tofile(im, target, 6)


def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:
Expand Down Expand Up @@ -985,17 +1042,14 @@ def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
if fontmode == "1":
d.fontmode = "1"

embedded_color = fontmode == "RGBA"
d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color)
try:
assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9)
except AssertionError:
if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC:
assert_image_similar_tofile(
im, "Tests/images/text_float_coord_1_alt.png", 1
)
else:
raise
d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=(fontmode == "RGBA"))
assert_image_similar_tofile(
im,
f"Tests/images/text_float_coord_{fontmode}_{layout_engine.name}.png",
epsilon=(
1 if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC else 3.9
),
)


def test_cbdt(layout_engine: ImageFont.Layout) -> None:
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_imagefontctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def test_x_max_and_y_offset() -> None:
draw.text((0, 0), "لح", font=ttf, fill=500)

target = "Tests/images/test_x_max_and_y_offset.png"
assert_image_similar_tofile(im, target, 3.8)
assert_image_similar_tofile(im, target, 4.2)


def test_language() -> None:
Expand Down
6 changes: 3 additions & 3 deletions Tests/test_imagetext.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,15 @@ def test_wrap_shrink() -> None:
assert text.font.size == 10

with pytest.raises(ValueError, match="Text could not be scaled"):
text.wrap(50, 15, ("shrink", 9))
text.wrap(50, 15, ("shrink", 10))

assert text.wrap(50, 15, "shrink") is None
assert text.font.size == 8
assert text.font.size == 9

text = ImageText.Text("Hello World!")
assert text.wrap(50, 15, ("shrink", 7)) is None
assert isinstance(text.font, ImageFont.FreeTypeFont)
assert text.font.size == 8
assert text.font.size == 9


@skip_unless_feature("freetype2")
Expand Down
Loading
Loading