Skip to content

Commit

Permalink
Merge pull request #7497 from ZachNagengast/fix-alpha-for-overlapping…
Browse files Browse the repository at this point in the history
…-glyphs

Fix incorrect color blending for overlapping glyphs in BGRA mode
  • Loading branch information
radarhere committed Dec 24, 2023
2 parents b8826ef + a6a612c commit 6768d3a
Show file tree
Hide file tree
Showing 19 changed files with 66 additions and 31 deletions.
Binary file added Tests/fonts/CBDTTestFont.ttf
Binary file not shown.
Binary file added Tests/fonts/EBDTTestFont.ttf
Binary file not shown.
3 changes: 2 additions & 1 deletion Tests/fonts/LICENSE.txt
Expand Up @@ -2,7 +2,6 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
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
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
Expand All @@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base

"Public domain font. Share and enjoy."

CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.
Binary file removed Tests/fonts/NotoColorEmoji.ttf
Binary file not shown.
Binary file added Tests/images/bitmap_font_blend.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/bitmap_font_stroke_basic.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/bitmap_font_stroke_raqm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/cbdt.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/cbdt_mask.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Tests/images/cbdt_notocoloremoji.png
Binary file not shown.
Binary file removed Tests/images/cbdt_notocoloremoji_mask.png
Binary file not shown.
Binary file modified Tests/images/default_font_freetype.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/test_combine_caron_below_ttb.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/test_combine_caron_below_ttb_lb.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/test_combine_caron_ttb.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Tests/images/test_combine_caron_ttb_lt.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 21 additions & 10 deletions Tests/test_imagefont.py
Expand Up @@ -859,6 +859,19 @@ def test_bitmap_font_stroke(layout_engine):
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):
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
Expand Down Expand Up @@ -897,15 +910,15 @@ def test_float_coord(layout_engine, fontmode):
def test_cbdt(layout_engine):
try:
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.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
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
Expand All @@ -914,17 +927,15 @@ def test_cbdt(layout_engine):
def test_cbdt_mask(layout_engine):
try:
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.text((10, 10), "\U0001f469", "black", font=font)
d.text((16, 16), "AB", "green", font=font)

assert_image_similar_tofile(
im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
)
assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png")
except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
Expand Down
4 changes: 3 additions & 1 deletion src/PIL/ImageDraw.py
Expand Up @@ -33,6 +33,7 @@

import math
import numbers
import struct

from . import Image, ImageColor

Expand Down Expand Up @@ -543,7 +544,8 @@ def draw_text(ink, stroke_width=0, stroke_offset=None):
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha
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
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
Expand Down
59 changes: 40 additions & 19 deletions src/_imagingft.c
Expand Up @@ -1049,8 +1049,8 @@ font_render(FontObject *self, PyObject *args) {
if (yy >= 0 && yy < im->ysize) {
/* blend this glyph into the buffer */
int k;
unsigned char v;
unsigned char *target;
unsigned int tmp;
if (color) {
/* target[RGB] returns the color, target[A] returns the mask */
/* target bands get split again in ImageDraw.text */
Expand All @@ -1061,34 +1061,55 @@ font_render(FontObject *self, PyObject *args) {
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
/* paste color glyph */
for (k = x0; k < x1; k++) {
if (target[k * 4 + 3] < source[k * 4 + 3]) {
/* unpremultiply BGRa to RGBA */
target[k * 4 + 0] = CLIP8(
(255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]);
target[k * 4 + 1] = CLIP8(
(255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]);
target[k * 4 + 2] = CLIP8(
(255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]);
target[k * 4 + 3] = source[k * 4 + 3];
unsigned int src_alpha = source[k * 4 + 3];

/* paste only if source has data */
if (src_alpha > 0) {
/* unpremultiply BGRa */
int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);

/* 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) {
if (color) {
unsigned char *ink = (unsigned char *)&foreground_ink;
for (k = x0; k < x1; k++) {
v = source[k] * convert_scale;
if (target[k * 4 + 3] < v) {
target[k * 4 + 0] = ink[0];
target[k * 4 + 1] = ink[1];
target[k * 4 + 2] = ink[2];
target[k * 4 + 3] = v;
unsigned int src_alpha = source[k] * convert_scale;
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 + 1] = ink[1];
target[k * 4 + 2] = ink[2];
target[k * 4 + 3] = src_alpha;
}
}
}
} else {
for (k = x0; k < x1; k++) {
v = source[k] * convert_scale;
if (target[k] < v) {
target[k] = v;
unsigned int src_alpha = source[k] * convert_scale;
if (src_alpha > 0) {
target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha;
}
}
}
Expand Down

0 comments on commit 6768d3a

Please sign in to comment.