Repository: python-pillow/Pillow

Problem statement:
Stroke Text Tittles Not Working Correctly
### What did you do?

When trying to create a piece of text with a stroke fill the tittles on `i` and `j` do not look correct. 

### What did you expect to happen?

I would expect that the stroke would fill all the way to the edge of the inner fill for the tittles.

### What actually happened?

The tittles on the `i` and `j` look like they have a thin outline around them rather than filling to the tittle.

### What are your OS, Python and Pillow versions?

* OS: Windows
* Python 3.11: 
* Pillow:  10.3.0

```text
Pillow 10.3.0
Python 3.11.5 | packaged by Anaconda, Inc. | (main, Sep 11 2023, 13:26:23) [MSC v.1916 64 bit (AMD64)]
Python executable is C:\Users\bradl\anaconda3\python.exe
System Python files loaded from C:\Users\bradl\anaconda3
--------------------------------------------------------------------
Python Pillow modules loaded from C:\Users\USER\anaconda3\Lib\site-packages\PIL
Binary Pillow modules loaded from C:\Users\USER\anaconda3\Lib\site-packages\PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 10.3.0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.3.2
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 3.0.2
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1
--- LIBTIFF support ok, loaded 4.6.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
*** XCB (X protocol) support not installed
```

```python


from PIL import Image, ImageDraw, ImageFont

# Create an image with a white background
width, height = 2000, 400
image = Image.new("RGB", (width, height), "white")

# Load a font
font_size = 100
font = ImageFont.load_default(font_size)

# Create a drawing context
draw = ImageDraw.Draw(image)

# Define text and positions
text = 'i j h o w'
text_position = (100, 100)  # Position of the text
stroke_width = 14  # Width of the stroke
stroke_fill = "black"  # Color of the stroke
text_fill = "blue"  # Color of the text

# Draw text with stroke
draw.text(
    text_position,
    text,
    font=font,
    fill=text_fill,
    stroke_width=stroke_width,
    stroke_fill=stroke_fill,
)

image.save("testing.png", dpi=(300, 300))
```

![Image](https://github.com/user-attachments/assets/75b14f62-199b-43fa-a9b6-f8eea2fb5fb4)




Hints:
Taking a look, the gap between the regular text and the stroked text is generated by FreeType. It's not unique to our default font - the same issue occurs with Times New Roman.ttf on my macOS machine.

I've contacted FreeType support to see if this is considered a bug.

Patch:
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -643,6 +643,7 @@ def draw_text(ink: int, stroke_width: float = 0) -> None:
                     features=features,
                     language=language,
                     stroke_width=stroke_width,
+                    stroke_filled=True,
                     anchor=anchor,
                     ink=ink,
                     start=start,
@@ -692,7 +693,8 @@ def draw_text(ink: int, stroke_width: float = 0) -> None:
                 draw_text(stroke_ink, stroke_width)
 
                 # Draw normal text
-                draw_text(ink, 0)
+                if ink != stroke_ink:
+                    draw_text(ink)
             else:
                 # Only draw normal text
                 draw_text(ink)
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -644,6 +644,7 @@ def fill(width: int, height: int) -> Image.core.ImagingCore:
             features,
             language,
             stroke_width,
+            kwargs.get("stroke_filled", False),
             anchor,
             ink,
             start[0],


Test patch:
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1396,6 +1396,28 @@ def test_stroke_descender() -> None:
     assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
 
 
+@skip_unless_feature("freetype2")
+def test_stroke_inside_gap() -> None:
+    # Arrange
+    im = Image.new("RGB", (120, 130))
+    draw = ImageDraw.Draw(im)
+    font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
+
+    # Act
+    draw.text((12, 12), "i", "#f00", font, stroke_width=20)
+
+    # Assert
+    for y in range(im.height):
+        glyph = ""
+        for x in range(im.width):
+            if im.getpixel((x, y)) == (0, 0, 0):
+                if glyph == "started":
+                    glyph = "ended"
+            else:
+                assert glyph != "ended", "Gap inside stroked glyph"
+                glyph = "started"
+
+
 @skip_unless_feature("freetype2")
 def test_split_word() -> None:
     # Arrange
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -461,6 +461,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
     assert mask.size == (108, 13)
 
 
+def test_stroke_mask() -> None:
+    # Arrange
+    text = "i"
+
+    # Act
+    font = ImageFont.truetype(FONT_PATH, 128)
+    mask = font.getmask(text, stroke_width=2)
+
+    # Assert
+    assert mask.getpixel((34, 5)) == 255
+    assert mask.getpixel((38, 5)) == 0
+    assert mask.getpixel((42, 5)) == 255
+
+
 def test_load_when_image_not_found() -> None:
     with tempfile.NamedTemporaryFile(delete=False) as tmp:
         pass