Merge pull request #7497 from ZachNagengast/fix-alpha-for-overlapping-glyphs

Fix incorrect color blending for overlapping glyphs in BGRA mode
This commit is contained in:
Andrew Murray 2023-12-24 19:03:28 +11:00 committed by GitHub
commit 6768d3aa91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 66 additions and 31 deletions

Binary file not shown.

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
Tests/images/cbdt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
Tests/images/cbdt_mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -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")

View File

@ -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:

View File

@ -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) {
if (target[k * 4 + 3] > 0) {
target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp);
target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp);
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 + 0] = ink[0];
target[k * 4 + 1] = ink[1]; target[k * 4 + 1] = ink[1];
target[k * 4 + 2] = ink[2]; target[k * 4 + 2] = ink[2];
target[k * 4 + 3] = v; 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;
} }
} }
} }