From a22eedad073193f67f8801d58299b15f3becfba5 Mon Sep 17 00:00:00 2001 From: Paddez Date: Tue, 15 Nov 2016 10:48:31 +0000 Subject: [PATCH 1/2] Add outline feature to ImageDraw.text() This change adds an additional method argument to the draw() method which draws the text in black, before drawing it in the color selected in the fill parameter. Additionally due to the positional requirements enforced on the method by using **kwargs, move to using an explicit signature with default values. * Helps improve readability * Allows for easier modification later. --- PIL/ImageDraw.py | 20 +++++++++++++------- Tests/images/stroked_text_image.png | Bin 0 -> 4776 bytes Tests/test_imagefont.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 Tests/images/stroked_text_image.png diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 72040392001..9408dc8f509 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -217,13 +217,13 @@ def _multiline_split(self, text): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, - *args, **kwargs): + def text(self, xy, text, fill=None, font=None, anchor=None, spacing=4, + align="left", outline=None): if self._multiline_check(text): return self.multiline_text(xy, text, fill, font, anchor, - *args, **kwargs) - + spacing, align, outline) ink, fill = self._getink(fill) + print("Outline: %s" % outline) if font is None: font = self.getfont() if ink is None: @@ -237,10 +237,17 @@ def text(self, xy, text, fill=None, font=None, anchor=None, mask = font.getmask(text, self.fontmode) except TypeError: mask = font.getmask(text) + if outline is not None: + color, _ = self._getink(outline) + for offset in range(0, 2): + tmp_xy = list(xy) + for difference in [-1, 1]: + tmp_xy[offset] = xy[offset] + difference + self.draw.draw_bitmap(tmp_xy, mask, color) self.draw.draw_bitmap(xy, mask, ink) def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left"): + spacing=4, align="left", outline=None): widths = [] max_width = 0 lines = self._multiline_split(text) @@ -259,10 +266,9 @@ def multiline_text(self, xy, text, fill=None, font=None, anchor=None, left += (max_width - widths[idx]) else: assert False, 'align must be "left", "center" or "right"' - self.text((left, top), line, fill, font, anchor) + self.text((left, top), line, fill, font, anchor, outline=outline) top += line_spacing left = xy[0] - def textsize(self, text, font=None, *args, **kwargs): """Get the size of a given string, in pixels.""" if self._multiline_check(text): diff --git a/Tests/images/stroked_text_image.png b/Tests/images/stroked_text_image.png new file mode 100644 index 0000000000000000000000000000000000000000..90c3282d286abc04bea159513914dda4338ecc39 GIT binary patch literal 4776 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 K)T&f$BmM*3_d~$| literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index de89ac92974..a1910983c29 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -127,6 +127,18 @@ def test_textsize_equal(self): target_img = Image.open(target) self.assert_image_similar(im, target_img, .5) + def test_stroke_text(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + draw.text((5, 5), TEST_TEXT, 'black', font=ttf, + outline='white') + + target = 'Tests/images/stroked_text_image.png' + target_img = Image.open(target) + self.assert_image_similar(im, target_img, .5) + def test_render_multiline(self): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) From 61baf1f0886cef6261f973774b8be3f08bc44722 Mon Sep 17 00:00:00 2001 From: Paddez Date: Tue, 15 Nov 2016 10:53:30 +0000 Subject: [PATCH 2/2] Adding Documentation for outline [ci-skip] --- PIL/ImageDraw.py | 1 - docs/reference/ImageDraw.rst | 2 ++ docs/releasenotes/3.5.0.rst | 10 ++++++++++ docs/releasenotes/index.rst | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/3.5.0.rst diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 9408dc8f509..efe84048632 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -223,7 +223,6 @@ def text(self, xy, text, fill=None, font=None, anchor=None, spacing=4, return self.multiline_text(xy, text, fill, font, anchor, spacing, align, outline) ink, fill = self._getink(fill) - print("Outline: %s" % outline) if font is None: font = self.getfont() if ink is None: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 842407c9092..53c83ee7da4 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -240,6 +240,7 @@ Methods the number of pixels between lines. :param align: If the text is passed on to multiline_text(), "left", "center" or "right". + :param outline: Color to use to draw an outline around the text. .. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") @@ -252,6 +253,7 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". + :param outline: Color to use to draw an outline around the text. .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) diff --git a/docs/releasenotes/3.5.0.rst b/docs/releasenotes/3.5.0.rst new file mode 100644 index 00000000000..eb6d665acc2 --- /dev/null +++ b/docs/releasenotes/3.5.0.rst @@ -0,0 +1,10 @@ +3.5.0 +----- + +New text-outlining feature +========================== + +Both ``ImageDraw.text()`` and ``ImageDraw.multiline_text()`` now supports +text-stroking. +Passing in a color value to the methods using the ``outline`` keyword argument +will now outline the text in that color. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 8c484af4473..920dc8834b0 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 3.5.0 3.4.0 3.3.2 3.3.0