diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png index 2b75a5e9c7a..ca77cea7323 100644 Binary files a/Tests/images/rectangle_surrounding_text.png and b/Tests/images/rectangle_surrounding_text.png differ diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 5786764a64d..10a172b4675 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -33,9 +33,9 @@ def fuzz_font(data): # different font objects. return - font.getsize_multiline("ABC\nAaaa") + font.getbbox("ABC") font.getmask("test text") with Image.new(mode="RGBA", size=(200, 200)) as im: draw = ImageDraw.Draw(im) - draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) draw.text((10, 10), "Test Text", font=font, fill="#000") diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 92ed60362fd..c217378fb74 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -76,12 +76,19 @@ def test_textsize(request, tmp_path): tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): - (dx, dy) = font.getsize(chr(i)) + (ox, oy, dx, dy) = font.getbbox(chr(i)) + assert ox == 0 + assert oy == 0 assert dy == 20 assert dx in (0, 10) + assert font.getlength(chr(i)) == dx + with pytest.warns(DeprecationWarning) as log: + assert font.getsize(chr(i)) == (dx, dy) + assert len(log) == 1 for i in range(len(message)): msg = message[: i + 1] - assert font.getsize(msg) == (len(msg) * 10, 20) + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) def _test_high_characters(request, tmp_path, message): diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index a1036fd28e6..4477ee29d55 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -101,13 +101,17 @@ def _test_textsize(request, tmp_path, encoding): tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): - (dx, dy) = font.getsize(bytearray([i])) + (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + assert ox == 0 + assert oy == 0 assert dy == 20 assert dx in (0, 10) + assert font.getlength(bytearray([i])) == dx message = charsets[encoding]["message"].encode(encoding) for i in range(len(message)): msg = message[: i + 1] - assert font.getsize(msg) == (len(msg) * 10, 20) + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) def test_textsize_iso8859_1(request, tmp_path): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69d1ac9fad3..23bc756bb14 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1232,21 +1232,39 @@ def test_textsize_empty_string(): # Act # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("") - draw.textsize("\n") - draw.textsize("test\n") + draw.textbbox((0, 0), "") + draw.textbbox((0, 0), "\n") + draw.textbbox((0, 0), "test\n") + draw.textlength("") @skip_unless_feature("freetype2") -def test_textsize_stroke(): +def test_textbbox_stroke(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) # Act / Assert - assert draw.textsize("A", font, stroke_width=2) == (16, 20) - assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44) + assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) + assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) + + +def test_textsize_deprecation(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello\nWorld") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.multiline_textsize("Hello\nWorld") + assert len(log) == 1 @skip_unless_feature("freetype2") diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 3a70176cee5..e4e8a38cb59 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,5 +1,7 @@ import os.path +import pytest + from PIL import Image, ImageDraw, ImageDraw2 from .helper import ( @@ -205,7 +207,9 @@ def test_textsize(): font = ImageDraw2.Font("white", FONT_PATH) # Act - size = draw.textsize("ImageDraw2", font) + with pytest.warns(DeprecationWarning) as log: + size = draw.textsize("ImageDraw2", font) + assert len(log) == 1 # Assert assert size[1] == 12 @@ -221,9 +225,10 @@ def test_textsize_empty_string(): # Act # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("", font) - draw.textsize("\n", font) - draw.textsize("test\n", font) + draw.textbbox((0, 0), "", font) + draw.textbbox((0, 0), "\n", font) + draw.textbbox((0, 0), "test\n", font) + draw.textlength("", font) @skip_unless_feature("freetype2") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0c50303f902..16da87d469a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -94,7 +94,7 @@ def test_non_ascii_path(self, tmp_path): def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) - ttf.getsize(txt) + ttf.getbbox(txt) img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) @@ -135,15 +135,15 @@ def test_I16(self): target = "Tests/images/transparent_background_text_L.png" assert_image_similar_tofile(im.convert("L"), target, 0.01) - def test_textsize_equal(self): + def test_textbbox_equal(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) ttf = self.get_font() txt = "Hello World!" - size = draw.textsize(txt, ttf) + bbox = draw.textbbox((10, 10), txt, ttf) draw.text((10, 10), txt, font=ttf) - draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + draw.rectangle(bbox) assert_image_similar_tofile( im, "Tests/images/rectangle_surrounding_text.png", 2.5 @@ -184,7 +184,7 @@ def test_render_multiline(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) ttf = self.get_font() - line_spacing = draw.textsize("A", font=ttf)[1] + 4 + line_spacing = ttf.getbbox("A")[3] + 4 lines = TEST_TEXT.split("\n") y = 0 for line in lines: @@ -245,19 +245,39 @@ def test_multiline_size(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) - # Test that textsize() correctly connects to multiline_textsize() - assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( - TEST_TEXT, font=ttf + with pytest.warns(DeprecationWarning) as log: + # Test that textsize() correctly connects to multiline_textsize() + assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( + TEST_TEXT, font=ttf + ) + + # Test that multiline_textsize corresponds to ImageFont.textsize() + # for single line text + assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) + + # Test that textsize() can pass on additional arguments + # to multiline_textsize() + draw.textsize(TEST_TEXT, font=ttf, spacing=4) + draw.textsize(TEST_TEXT, ttf, 4) + assert len(log) == 6 + + def test_multiline_bbox(self): + ttf = self.get_font() + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textbbox() correctly connects to multiline_textbbox() + assert draw.textbbox((0, 0), TEST_TEXT, font=ttf) == draw.multiline_textbbox( + (0, 0), TEST_TEXT, font=ttf ) - # Test that multiline_textsize corresponds to ImageFont.textsize() + # Test that multiline_textbbox corresponds to ImageFont.textbbox() # for single line text - assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) + assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf) - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=ttf, spacing=4) - draw.textsize(TEST_TEXT, ttf, 4) + # Test that textbbox() can pass on additional arguments + # to multiline_textbbox() + draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4) def test_multiline_width(self): ttf = self.get_font() @@ -265,9 +285,15 @@ def test_multiline_width(self): draw = ImageDraw.Draw(im) assert ( - draw.textsize("longest line", font=ttf)[0] - == draw.multiline_textsize("longest line\nline", font=ttf)[0] + draw.textbbox((0, 0), "longest line", font=ttf)[2] + == draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2] ) + with pytest.warns(DeprecationWarning) as log: + assert ( + draw.textsize("longest line", font=ttf)[0] + == draw.multiline_textsize("longest line\nline", font=ttf)[0] + ) + assert len(log) == 2 def test_multiline_spacing(self): ttf = self.get_font() @@ -289,16 +315,33 @@ def test_rotated_transposed_font(self): # Original font draw.font = font - box_size_a = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert box_size_a == font.getsize(word) + assert len(log) == 2 + bbox_a = draw.textbbox((10, 10), word) # Rotated font draw.font = transposed_font - box_size_b = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert box_size_b == transposed_font.getsize(word) + assert len(log) == 2 + bbox_b = draw.textbbox((20, 20), word) # Check (w,h) of box a is (h,w) of box b assert box_size_a[0] == box_size_b[1] assert box_size_a[1] == box_size_b[0] + # Check bbox b is (20, 20, 20 + h, 20 + w) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] + assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + + # text length is undefined for vertical text + pytest.raises(ValueError, draw.textlength, word) + def test_unrotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) @@ -310,15 +353,31 @@ def test_unrotated_transposed_font(self): # Original font draw.font = font - box_size_a = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) # Rotated font draw.font = transposed_font - box_size_b = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) # Check boxes a and b are same size assert box_size_a == box_size_b + # Check bbox b is (20, 20, 20 + w, 20 + h) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] + assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + + assert length_a == length_b + def test_rotated_transposed_font_get_mask(self): # Arrange text = "mask this" @@ -373,9 +432,11 @@ def test_free_type_font_get_offset(self): text = "offset this" # Act - offset = font.getoffset(text) + with pytest.warns(DeprecationWarning) as log: + offset = font.getoffset(text) # Assert + assert len(log) == 1 assert offset == (0, 3) def test_free_type_font_get_mask(self): @@ -417,11 +478,11 @@ def test_default_font(self): # Assert assert_image_equal_tofile(im, "Tests/images/default_font.png") - def test_getsize_empty(self): + def test_getbbox_empty(self): # issue #2614 font = self.get_font() # should not crash. - assert (0, 0) == font.getsize("") + assert (0, 0, 0, 0) == font.getbbox("") def test_render_empty(self): # issue 2666 @@ -438,7 +499,7 @@ def test_unicode_pilfont(self): # issue #2826 font = ImageFont.load_default() with pytest.raises(UnicodeEncodeError): - font.getsize("’") + font.getbbox("’") def test_unicode_extended(self): # issue #3777 @@ -563,17 +624,29 @@ def test_imagefont_getters(self): assert t.font.x_ppem == 20 assert t.font.y_ppem == 20 assert t.font.glyphs == 4177 - assert t.getsize("A") == (12, 16) - assert t.getsize("AB") == (24, 16) - assert t.getsize("M") == (12, 16) - assert t.getsize("y") == (12, 20) - assert t.getsize("a") == (12, 16) - assert t.getsize_multiline("A") == (12, 16) - assert t.getsize_multiline("AB") == (24, 16) - assert t.getsize_multiline("a") == (12, 16) - assert t.getsize_multiline("ABC\n") == (36, 36) - assert t.getsize_multiline("ABC\nA") == (36, 36) - assert t.getsize_multiline("ABC\nAaaa") == (48, 36) + assert t.getbbox("A") == (0, 4, 12, 16) + assert t.getbbox("AB") == (0, 4, 24, 16) + assert t.getbbox("M") == (0, 4, 12, 16) + assert t.getbbox("y") == (0, 7, 12, 20) + assert t.getbbox("a") == (0, 7, 12, 16) + assert t.getlength("A") == 12 + assert t.getlength("AB") == 24 + assert t.getlength("M") == 12 + assert t.getlength("y") == 12 + assert t.getlength("a") == 12 + with pytest.warns(DeprecationWarning) as log: + assert t.getsize("A") == (12, 16) + assert t.getsize("AB") == (24, 16) + assert t.getsize("M") == (12, 16) + assert t.getsize("y") == (12, 20) + assert t.getsize("a") == (12, 16) + assert t.getsize_multiline("A") == (12, 16) + assert t.getsize_multiline("AB") == (24, 16) + assert t.getsize_multiline("a") == (12, 16) + assert t.getsize_multiline("ABC\n") == (36, 36) + assert t.getsize_multiline("ABC\nA") == (36, 36) + assert t.getsize_multiline("ABC\nAaaa") == (48, 36) + assert len(log) == 11 def test_getsize_stroke(self): # Arrange @@ -581,14 +654,22 @@ def test_getsize_stroke(self): # Act / Assert for stroke_width in [0, 2]: - assert t.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, + assert t.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, ) + with pytest.warns(DeprecationWarning) as log: + assert t.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, + ) + assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, + ) + assert len(log) == 2 def test_complex_font_settings(self): # Arrange @@ -720,8 +801,11 @@ def test_textbbox_non_freetypefont(self): im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() - with pytest.raises(ValueError): - d.textbbox((0, 0), "test", font=default_font) + with pytest.warns(DeprecationWarning) as log: + width, height = d.textsize("test", font=default_font) + assert len(log) == 1 + assert d.textlength("test", font=default_font) == width + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) @pytest.mark.parametrize( "anchor, left, top", @@ -868,7 +952,7 @@ def test_bitmap_font_stroke(self): def test_standard_embedded_color(self): txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) - ttf.getsize(txt) + ttf.getbbox(txt) im = Image.new("RGB", (300, 64), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index ffb70cf1799..cf039e86e61 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -140,8 +140,8 @@ def test_ligature_features(): target = "Tests/images/test_ligature_features.png" assert_image_similar_tofile(im, target, 0.5) - liga_size = ttf.getsize("fi", features=["-liga"]) - assert liga_size == (13, 19) + liga_bbox = ttf.getbbox("fi", features=["-liga"]) + assert liga_bbox == (0, 4, 13, 19) def test_kerning_features(): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8c5b8a748d7..9be92770ab9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -178,6 +178,25 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). +Font size and offset methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=========================================================================== ============================================================================================================= + Removed features ---------------- diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index b95d8d591a7..c2d72c804c1 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -436,12 +436,14 @@ Methods .. 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. + .. deprecated:: 9.2.0 Use :py:meth:`textlength()` to measure the offset of following text with 1/64 pixel precision. Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. + Return the size of the given string, in pixels. + .. note:: For historical reasons this function measures text height from the ascender line instead of the top, see :ref:`text-anchors`. If you wish to measure text height from the top, it is recommended @@ -484,6 +486,10 @@ Methods .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) + .. deprecated:: 9.2.0 + + Use :py:meth:`.multiline_textbbox` instead. + Return the size of the given string, in pixels. Use :py:meth:`textlength()` to measure the offset of following text with diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8efef7cfd5c..516fa63a783 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -56,6 +56,7 @@ Methods .. autoclass:: PIL.ImageFont.TransposedFont :members: + :undoc-members: Constants --------- diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index ca52f6ab952..9c102f1776a 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -40,6 +40,25 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). +Font size and offset methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` +=========================================================================== ============================================================================================================= + API Additions ============= diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 3824626bd6a..8970471d3b2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,8 +32,10 @@ import math import numbers +import warnings from . import Image, ImageColor +from ._deprecate import deprecate """ A simple 2D drawing interface for PIL images. @@ -372,6 +374,19 @@ def _multiline_split(self, text): return text.split(split_character) + def _multiline_spacing(self, font, spacing, stroke_width): + # this can be replaced with self.textbbox(...)[3] when textsize is removed + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return ( + self.textsize( + "A", + font=font, + stroke_width=stroke_width, + )[1] + + spacing + ) + def text( self, xy, @@ -511,9 +526,7 @@ def multiline_text( widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( line, font, direction=direction, features=features, language=language @@ -573,14 +586,31 @@ def textsize( stroke_width=0, ): """Get the size of a given string, in pixels.""" + deprecate("textsize", 10, "textbbox or textlength") if self._multiline_check(text): - return self.multiline_textsize( - text, font, spacing, direction, features, language, stroke_width - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.multiline_textsize( + text, + font, + spacing, + direction, + features, + language, + stroke_width, + ) if font is None: font = self.getfont() - return font.getsize(text, direction, features, language, stroke_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return font.getsize( + text, + direction, + features, + language, + stroke_width, + ) def multiline_textsize( self, @@ -592,16 +622,23 @@ def multiline_textsize( language=None, stroke_width=0, ): + deprecate("multiline_textsize", 10, "multiline_textbbox") max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) - for line in lines: - line_width, line_height = self.textsize( - line, font, spacing, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + for line in lines: + line_width, line_height = self.textsize( + line, + font, + spacing, + direction, + features, + language, + stroke_width, + ) + max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing def textlength( @@ -625,9 +662,16 @@ def textlength( try: return font.getlength(text, mode, direction, features, language) except AttributeError: - size = self.textsize( - text, font, direction=direction, features=features, language=language - ) + deprecate("textlength support for fonts without getlength", 10) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + size = self.textsize( + text, + font, + direction=direction, + features=features, + language=language, + ) if direction == "ttb": return size[1] return size[0] @@ -667,10 +711,6 @@ def textbbox( if font is None: font = self.getfont() - from . import ImageFont - - if not isinstance(font, ImageFont.FreeTypeFont): - raise ValueError("Only supported for TrueType fonts") mode = "RGBA" if embedded_color else self.fontmode bbox = font.getbbox( text, mode, direction, features, language, stroke_width, anchor @@ -704,9 +744,7 @@ def multiline_textbbox( widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( line, diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 1f63110fd26..2667b77dd43 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,7 +24,10 @@ """ +import warnings + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._deprecate import deprecate class Pen: @@ -172,8 +175,35 @@ def text(self, xy, text, font): def textsize(self, text, font): """ + .. deprecated:: 9.2.0 + Return the size of the given string, in pixels. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` """ - return self.draw.textsize(text, font=font.font) + deprecate("textsize", 10, "textbbox or textlength") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.draw.textsize(text, font=font.font) + + def textbbox(self, xy, text, font): + """ + Returns bounding box (in pixels) of given text. + + :return: ``(left, top, right, bottom)`` bounding box + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` + """ + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + return self.draw.textbbox(xy, text, font=font.font) + + def textlength(self, text, font): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` + """ + return self.draw.textlength(text, font=font.font) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 681b75d448b..a3b711c6077 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -137,12 +137,17 @@ def _load_pilfont_data(self, file, image): def getsize(self, text, *args, **kwargs): """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + Returns width and height (in pixels) of given text. :param text: Text to measure. :return: (width, height) """ + deprecate("getsize", 10, "getbbox or getlength") return self.font.getsize(text) def getmask(self, text, mode="", *args, **kwargs): @@ -165,6 +170,33 @@ def getmask(self, text, mode="", *args, **kwargs): """ return self.font.getmask(text, mode) + def getbbox(self, text, *args, **kwargs): + """ + Returns bounding box (in pixels) of given text. + + .. versionadded:: 9.2.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :return: ``(left, top, right, bottom)`` bounding box + """ + width, height = self.font.getsize(text) + return 0, 0, width, height + + def getlength(self, text, *args, **kwargs): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. versionadded:: 9.2.0 + """ + width, height = self.font.getsize(text) + return width + ## # Wrapper for FreeType fonts. Application code should use the @@ -386,16 +418,23 @@ def getbbox( return left, top, left + width, top + height def getsize( - self, text, direction=None, features=None, language=None, stroke_width=0 + self, + text, + direction=None, + features=None, + language=None, + stroke_width=0, ): """ - Returns width and height (in pixels) of given text if rendered in font with - provided direction, features, and language. + .. deprecated:: 9.2.0 Use :py:meth:`getlength()` to measure the offset of following text with 1/64 pixel precision. Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + Returns width and height (in pixels) of given text if rendered in font with + provided direction, features, and language. + .. note:: For historical reasons this function measures text height from the ascender line instead of the top, see :ref:`text-anchors`. If you wish to measure text height from the top, it is recommended @@ -438,6 +477,7 @@ def getsize( :return: (width, height) """ + deprecate("getsize", 10, "getbbox or getlength") # vertical offset is added for historical reasons # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 size, offset = self.font.getsize(text, "L", direction, features, language) @@ -456,6 +496,10 @@ def getsize_multiline( stroke_width=0, ): """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.ImageDraw.multiline_textbbox` instead. + Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language, while respecting newline characters. @@ -495,19 +539,26 @@ def getsize_multiline( :return: (width, height) """ + deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") max_width = 0 lines = self._multiline_split(text) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing + for line in lines: + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) + max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing def getoffset(self, text): """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` instead. + Returns the offset of given text. This is the gap between the starting coordinate and the first marking. Note that this gap is included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. @@ -516,6 +567,7 @@ def getoffset(self, text): :return: A tuple of the x and y offset """ + deprecate("getoffset", 10, "getbbox") return self.font.getsize(text)[1] def getmask( @@ -796,7 +848,15 @@ def __init__(self, font, orientation=None): self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text, *args, **kwargs): - w, h = self.font.getsize(text) + """ + .. deprecated:: 9.2.0 + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + """ + deprecate("getsize", 10, "getbbox or getlength") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + w, h = self.font.getsize(text) if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): return h, w return w, h @@ -807,6 +867,23 @@ def getmask(self, text, mode="", *args, **kwargs): return im.transpose(self.orientation) return im + def getbbox(self, text, *args, **kwargs): + # TransposedFont doesn't support getmask2, move top-left point to (0, 0) + # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont + left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) + width = right - left + height = bottom - top + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + return 0, 0, height, width + return 0, 0, width, height + + def getlength(self, text, *args, **kwargs): + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + raise ValueError( + "text length is undefined for text rotated by 90 or 270 degrees" + ) + return self.font.getlength(text, *args, **kwargs) + def load(filename): """