Merge pull request #7497 from ZachNagengast/fix-alpha-for-overlapping-glyphs
Fix incorrect color blending for overlapping glyphs in BGRA mode
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/CBDTTestFont.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/EBDTTestFont.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -2,7 +2,6 @@
 | 
				
			||||||
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
 | 
					NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
 | 
				
			||||||
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
 | 
					NotoSans-Regular.ttf, from https://www.google.com/get/noto/
 | 
				
			||||||
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
 | 
					NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
 | 
				
			||||||
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
 | 
					 | 
				
			||||||
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
 | 
					AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
 | 
				
			||||||
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
 | 
					TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
 | 
				
			||||||
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
 | 
					ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
 | 
				
			||||||
| 
						 | 
					@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
 | 
				
			||||||
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
 | 
					10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"Public domain font.  Share and enjoy."
 | 
					"Public domain font.  Share and enjoy."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_blend.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 387 B  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cbdt.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 348 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cbdt_mask.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 367 B  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 3.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB  | 
| 
						 | 
					@ -859,6 +859,19 @@ def test_bitmap_font_stroke(layout_engine):
 | 
				
			||||||
    assert_image_similar_tofile(im, target, 0.03)
 | 
					    assert_image_similar_tofile(im, target, 0.03)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize("embedded_color", (False, True))
 | 
				
			||||||
 | 
					def test_bitmap_blend(layout_engine, embedded_color):
 | 
				
			||||||
 | 
					    font = ImageFont.truetype(
 | 
				
			||||||
 | 
					        "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = Image.new("RGBA", (128, 96), "white")
 | 
				
			||||||
 | 
					    d = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					    d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_standard_embedded_color(layout_engine):
 | 
					def test_standard_embedded_color(layout_engine):
 | 
				
			||||||
    txt = "Hello World!"
 | 
					    txt = "Hello World!"
 | 
				
			||||||
    ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
 | 
					    ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
 | 
				
			||||||
| 
						 | 
					@ -897,15 +910,15 @@ def test_float_coord(layout_engine, fontmode):
 | 
				
			||||||
def test_cbdt(layout_engine):
 | 
					def test_cbdt(layout_engine):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        font = ImageFont.truetype(
 | 
					        font = ImageFont.truetype(
 | 
				
			||||||
            "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
 | 
					            "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        im = Image.new("RGB", (150, 150), "white")
 | 
					        im = Image.new("RGB", (128, 96), "white")
 | 
				
			||||||
        d = ImageDraw.Draw(im)
 | 
					        d = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
 | 
					        d.text((16, 16), "AB", font=font, embedded_color=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
 | 
					        assert_image_equal_tofile(im, "Tests/images/cbdt.png")
 | 
				
			||||||
    except OSError as e:  # pragma: no cover
 | 
					    except OSError as e:  # pragma: no cover
 | 
				
			||||||
        assert str(e) in ("unimplemented feature", "unknown file format")
 | 
					        assert str(e) in ("unimplemented feature", "unknown file format")
 | 
				
			||||||
        pytest.skip("freetype compiled without libpng or CBDT support")
 | 
					        pytest.skip("freetype compiled without libpng or CBDT support")
 | 
				
			||||||
| 
						 | 
					@ -914,17 +927,15 @@ def test_cbdt(layout_engine):
 | 
				
			||||||
def test_cbdt_mask(layout_engine):
 | 
					def test_cbdt_mask(layout_engine):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        font = ImageFont.truetype(
 | 
					        font = ImageFont.truetype(
 | 
				
			||||||
            "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
 | 
					            "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        im = Image.new("RGB", (150, 150), "white")
 | 
					        im = Image.new("RGB", (128, 96), "white")
 | 
				
			||||||
        d = ImageDraw.Draw(im)
 | 
					        d = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        d.text((10, 10), "\U0001f469", "black", font=font)
 | 
					        d.text((16, 16), "AB", "green", font=font)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar_tofile(
 | 
					        assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png")
 | 
				
			||||||
            im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    except OSError as e:  # pragma: no cover
 | 
					    except OSError as e:  # pragma: no cover
 | 
				
			||||||
        assert str(e) in ("unimplemented feature", "unknown file format")
 | 
					        assert str(e) in ("unimplemented feature", "unknown file format")
 | 
				
			||||||
        pytest.skip("freetype compiled without libpng or CBDT support")
 | 
					        pytest.skip("freetype compiled without libpng or CBDT support")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,7 @@ from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
import numbers
 | 
					import numbers
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import Image, ImageColor
 | 
					from . import Image, ImageColor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -543,7 +544,8 @@ class ImageDraw:
 | 
				
			||||||
                # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
 | 
					                # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
 | 
				
			||||||
                # extract mask and set text alpha
 | 
					                # extract mask and set text alpha
 | 
				
			||||||
                color, mask = mask, mask.getband(3)
 | 
					                color, mask = mask, mask.getband(3)
 | 
				
			||||||
                color.fillband(3, (ink >> 24) & 0xFF)
 | 
					                ink_alpha = struct.pack("i", ink)[3]
 | 
				
			||||||
 | 
					                color.fillband(3, ink_alpha)
 | 
				
			||||||
                x, y = coord
 | 
					                x, y = coord
 | 
				
			||||||
                self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
 | 
					                self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1049,8 +1049,8 @@ font_render(FontObject *self, PyObject *args) {
 | 
				
			||||||
            if (yy >= 0 && yy < im->ysize) {
 | 
					            if (yy >= 0 && yy < im->ysize) {
 | 
				
			||||||
                /* blend this glyph into the buffer */
 | 
					                /* blend this glyph into the buffer */
 | 
				
			||||||
                int k;
 | 
					                int k;
 | 
				
			||||||
                unsigned char v;
 | 
					 | 
				
			||||||
                unsigned char *target;
 | 
					                unsigned char *target;
 | 
				
			||||||
 | 
					                unsigned int tmp;
 | 
				
			||||||
                if (color) {
 | 
					                if (color) {
 | 
				
			||||||
                    /* target[RGB] returns the color, target[A] returns the mask */
 | 
					                    /* target[RGB] returns the color, target[A] returns the mask */
 | 
				
			||||||
                    /* target bands get split again in ImageDraw.text */
 | 
					                    /* target bands get split again in ImageDraw.text */
 | 
				
			||||||
| 
						 | 
					@ -1061,34 +1061,55 @@ font_render(FontObject *self, PyObject *args) {
 | 
				
			||||||
                if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
 | 
					                if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
 | 
				
			||||||
                    /* paste color glyph */
 | 
					                    /* paste color glyph */
 | 
				
			||||||
                    for (k = x0; k < x1; k++) {
 | 
					                    for (k = x0; k < x1; k++) {
 | 
				
			||||||
                        if (target[k * 4 + 3] < source[k * 4 + 3]) {
 | 
					                        unsigned int src_alpha = source[k * 4 + 3];
 | 
				
			||||||
                            /* unpremultiply BGRa to RGBA */
 | 
					
 | 
				
			||||||
                            target[k * 4 + 0] = CLIP8(
 | 
					                        /* paste only if source has data */
 | 
				
			||||||
                                (255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]);
 | 
					                        if (src_alpha > 0) {
 | 
				
			||||||
                            target[k * 4 + 1] = CLIP8(
 | 
					                            /* unpremultiply BGRa */
 | 
				
			||||||
                                (255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]);
 | 
					                            int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
 | 
				
			||||||
                            target[k * 4 + 2] = CLIP8(
 | 
					                            int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
 | 
				
			||||||
                                (255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]);
 | 
					                            int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);
 | 
				
			||||||
                            target[k * 4 + 3] = source[k * 4 + 3];
 | 
					
 | 
				
			||||||
 | 
					                            /* blend required if target has data */
 | 
				
			||||||
 | 
					                            if (target[k * 4 + 3] > 0) {
 | 
				
			||||||
 | 
					                                /* blend RGBA colors */
 | 
				
			||||||
 | 
					                                target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], src_red, tmp);
 | 
				
			||||||
 | 
					                                target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], src_green, tmp);
 | 
				
			||||||
 | 
					                                target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp);
 | 
				
			||||||
 | 
					                                target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                /* paste unpremultiplied RGBA values */
 | 
				
			||||||
 | 
					                                target[k * 4 + 0] = src_red;
 | 
				
			||||||
 | 
					                                target[k * 4 + 1] = src_green;
 | 
				
			||||||
 | 
					                                target[k * 4 + 2] = src_blue;
 | 
				
			||||||
 | 
					                                target[k * 4 + 3] = src_alpha;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
 | 
					                } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
 | 
				
			||||||
                    if (color) {
 | 
					                    if (color) {
 | 
				
			||||||
                        unsigned char *ink = (unsigned char *)&foreground_ink;
 | 
					                        unsigned char *ink = (unsigned char *)&foreground_ink;
 | 
				
			||||||
                        for (k = x0; k < x1; k++) {
 | 
					                        for (k = x0; k < x1; k++) {
 | 
				
			||||||
                            v = source[k] * convert_scale;
 | 
					                            unsigned int src_alpha = source[k] * convert_scale;
 | 
				
			||||||
                            if (target[k * 4 + 3] < v) {
 | 
					                            if (src_alpha > 0) {
 | 
				
			||||||
                                target[k * 4 + 0] = ink[0];
 | 
					                                if (target[k * 4 + 3] > 0) {
 | 
				
			||||||
                                target[k * 4 + 1] = ink[1];
 | 
					                                    target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp);
 | 
				
			||||||
                                target[k * 4 + 2] = ink[2];
 | 
					                                    target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp);
 | 
				
			||||||
                                target[k * 4 + 3] = v;
 | 
					                                    target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], ink[2], tmp);
 | 
				
			||||||
 | 
					                                    target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    target[k * 4 + 0] = ink[0];
 | 
				
			||||||
 | 
					                                    target[k * 4 + 1] = ink[1];
 | 
				
			||||||
 | 
					                                    target[k * 4 + 2] = ink[2];
 | 
				
			||||||
 | 
					                                    target[k * 4 + 3] = src_alpha;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        for (k = x0; k < x1; k++) {
 | 
					                        for (k = x0; k < x1; k++) {
 | 
				
			||||||
                            v = source[k] * convert_scale;
 | 
					                            unsigned int src_alpha = source[k] * convert_scale;
 | 
				
			||||||
                            if (target[k] < v) {
 | 
					                            if (src_alpha > 0) {
 | 
				
			||||||
                                target[k] = v;
 | 
					                                target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||