diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 18f9c0c00..2afe93714 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -256,7 +256,20 @@ class ImageDraw(object): ## # Draw text. + def _multiline_check(self, text): + split_character = "\n" if isinstance(text, type("")) else b"\n" + + return split_character in text + + def _multiline_split(self, text): + split_character = "\n" if isinstance(text, type("")) else b"\n" + + return text.split(split_character) + def text(self, xy, text, fill=None, font=None, anchor=None): + if self._multiline_check(text): + return self.multiline_text(xy, text, fill, font, anchor) + ink, fill = self._getink(fill) if font is None: font = self.getfont() @@ -273,14 +286,51 @@ class ImageDraw(object): mask = font.getmask(text) self.draw.draw_bitmap(xy, mask, ink) + def multiline_text(self, xy, text, fill=None, font=None, anchor=None, + spacing=0, align="left"): + widths, heights = [], [] + max_width = 0 + lines = self._multiline_split(text) + for line in lines: + line_width, line_height = self.textsize(line, font) + widths.append(line_width) + max_width = max(max_width, line_width) + heights.append(line_height) + left, top = xy + for idx, line in enumerate(lines): + if align == "left": + pass # left = x + elif align == "center": + left += (max_width - widths[idx]) / 2.0 + elif align == "right": + left += (max_width - widths[idx]) + else: + assert False, 'align must be "left", "center" or "right"' + self.text((left, top), line, fill, font, anchor) + top += heights[idx] + spacing + left = xy[0] + ## # Get the size of a given string, in pixels. def textsize(self, text, font=None): + if self._multiline_check(text): + return self.multiline_textsize(text, font) + if font is None: font = self.getfont() return font.getsize(text) + def multiline_textsize(self, text, font=None, spacing=0): + max_width = 0 + height = 0 + lines = self._multiline_split(text) + for line in lines: + line_width, line_height = self.textsize(line, font) + height += line_height + spacing + max_width = max(max_width, line_width) + return max_width, height + ## # A simple 2D drawing interface for PIL images. diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png new file mode 100644 index 000000000..f44d0783a Binary files /dev/null and b/Tests/images/multiline_text_center.png differ diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png new file mode 100644 index 000000000..1b32d9167 Binary files /dev/null and b/Tests/images/multiline_text_right.png differ diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png new file mode 100644 index 000000000..869070407 Binary files /dev/null and b/Tests/images/multiline_text_spacing.png differ diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 88858c717..8414584f9 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -10,6 +10,8 @@ import copy FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 +TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" + try: from PIL import ImageFont @@ -95,7 +97,7 @@ try: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE) ttf.getsize(txt) - + img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) d.text((10, 10), txt, font=ttf, fill='black') @@ -134,7 +136,7 @@ try: draw = ImageDraw.Draw(im) ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) line_spacing = draw.textsize('A', font=ttf)[1] + 4 - lines = ['hey you', 'you are awesome', 'this looks awkward'] + lines = TEST_TEXT.split("\n") y = 0 for line in lines: draw.text((0, y), line, font=ttf) @@ -148,6 +150,69 @@ try: # at epsilon = ~38. self.assert_image_similar(im, target_img, .5) + def test_render_multiline_text(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=ttf) + + target = 'Tests/images/multiline_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + # Test align center and right + for align, ext in {"center": "_center", + "right": "_right"}.items(): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) + + target = 'Tests/images/multiline_text'+ext+'.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_unknown_align(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act/Assert + self.assertRaises(AssertionError, lambda: draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown")) + + def test_multiline_size(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textsize() correctly connects to multiline_textsize() + self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), + draw.multiline_textsize(TEST_TEXT, font=ttf)) + + def test_multiline_width(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + self.assertEqual(draw.textsize("longest line", font=ttf)[0], + draw.multiline_textsize("longest line\nline", font=ttf)[0]) + + def test_multiline_spacing(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) + + target = 'Tests/images/multiline_text_spacing.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index e6d5c36ee..e030147e9 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -232,17 +232,39 @@ Methods Draws the string at the given position. :param xy: Top left corner of the text. - :param text: Text to be drawn. + :param text: Text to be drawn. If it contains any newline characters, + the text is passed on to mulitiline_text() :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param fill: Color to use for the text. +.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") + + Draws the string at the given position. + + :param xy: Top left corner of the text. + :param text: Text to be drawn. If it contains any newline characters, + the text is split and passed on to mulitiline_text() + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: The number of pixels between lines. + :param align: "left", "center" or "right". + .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None) Return the size of the given string, in pixels. - :param text: Text to be measured. + :param text: Text to be measured. If it contains any newline characters, + the text is passed on to mulitiline_textsize() :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. +.. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=0) + + Return the size of the given string, in pixels. + + :param text: Text to be measured. If it contains any newline characters, + the text is split and passed on to mulitiline_textsize() + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: The number of pixels between lines. + Legacy API ----------