add support for fonts with COLR data

This commit is contained in:
nulano 2020-03-28 05:58:35 +01:00
parent 877831be13
commit 82a28d12e2
4 changed files with 160 additions and 36 deletions

View File

@ -291,7 +291,7 @@ Methods
Draw a shape. Draw a shape.
.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) .. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
Draws the string at the given position. Draws the string at the given position.
@ -352,7 +352,12 @@ Methods
.. versionadded:: 6.2.0 .. versionadded:: 6.2.0
.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None) :param embedded_color: Whether to use embedded color info in COLR and CPAL tables.
.. versionadded:: 8.0.0
.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
Draws the string at the given position. Draws the string at the given position.
@ -399,6 +404,19 @@ Methods
.. versionadded:: 6.0.0 .. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:param stroke_fill: Color to use for the text stroke. If not given, will default to
the ``fill`` parameter.
.. versionadded:: 6.2.0
:param embedded_color: Whether to use embedded color info in COLR and CPAL tables.
.. versionadded:: 8.0.0
.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
Return the size of the given string, in pixels. Return the size of the given string, in pixels.

View File

@ -282,6 +282,7 @@ class ImageDraw:
language=None, language=None,
stroke_width=0, stroke_width=0,
stroke_fill=None, stroke_fill=None,
embedded_color=False,
*args, *args,
**kwargs, **kwargs,
): ):
@ -299,8 +300,12 @@ class ImageDraw:
language, language,
stroke_width, stroke_width,
stroke_fill, stroke_fill,
embedded_color,
) )
if embedded_color and self.mode not in ("RGB", "RGBA"):
raise ValueError("Embedded color supported only in RGB and RGBA modes")
if font is None: if font is None:
font = self.getfont() font = self.getfont()
@ -311,16 +316,20 @@ class ImageDraw:
return ink return ink
def draw_text(ink, stroke_width=0, stroke_offset=None): def draw_text(ink, stroke_width=0, stroke_offset=None):
mode = self.fontmode
if stroke_width == 0 and embedded_color:
mode = "RGBA"
coord = xy coord = xy
try: try:
mask, offset = font.getmask2( mask, offset = font.getmask2(
text, text,
self.fontmode, mode,
direction=direction, direction=direction,
features=features, features=features,
language=language, language=language,
stroke_width=stroke_width, stroke_width=stroke_width,
anchor=anchor, anchor=anchor,
ink=ink,
*args, *args,
**kwargs, **kwargs,
) )
@ -329,12 +338,13 @@ class ImageDraw:
try: try:
mask = font.getmask( mask = font.getmask(
text, text,
self.fontmode, mode,
direction, direction,
features, features,
language, language,
stroke_width, stroke_width,
anchor, anchor,
ink,
*args, *args,
**kwargs, **kwargs,
) )
@ -342,7 +352,15 @@ class ImageDraw:
mask = font.getmask(text) mask = font.getmask(text)
if stroke_offset: if stroke_offset:
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
self.draw.draw_bitmap(coord, mask, ink) if mode == "RGBA":
# 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)
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
self.im.paste(color, coord + coord2, mask)
else:
self.draw.draw_bitmap(coord, mask, ink)
ink = getink(fill) ink = getink(fill)
if ink is not None: if ink is not None:
@ -374,6 +392,7 @@ class ImageDraw:
language=None, language=None,
stroke_width=0, stroke_width=0,
stroke_fill=None, stroke_fill=None,
embedded_color=False,
): ):
if direction == "ttb": if direction == "ttb":
raise ValueError("ttb direction is unsupported for multiline text") raise ValueError("ttb direction is unsupported for multiline text")
@ -440,6 +459,7 @@ class ImageDraw:
language=language, language=language,
stroke_width=stroke_width, stroke_width=stroke_width,
stroke_fill=stroke_fill, stroke_fill=stroke_fill,
embedded_color=embedded_color,
) )
top += line_spacing top += line_spacing

View File

@ -261,7 +261,7 @@ class FreeTypeFont:
""" """
# vertical offset is added for historical reasons # vertical offset is added for historical reasons
# see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929
size, offset = self.font.getsize(text, False, direction, features, language) size, offset = self.font.getsize(text, "L", direction, features, language)
return ( return (
size[0] + stroke_width * 2, size[0] + stroke_width * 2,
size[1] + stroke_width * 2 + offset[1], size[1] + stroke_width * 2 + offset[1],
@ -348,12 +348,14 @@ class FreeTypeFont:
language=None, language=None,
stroke_width=0, stroke_width=0,
anchor=None, anchor=None,
ink=0,
): ):
""" """
Create a bitmap for the text. Create a bitmap for the text.
If the font uses antialiasing, the bitmap should have mode ``L`` and use a If the font uses antialiasing, the bitmap should have mode ``L`` and use a
maximum value of 255. Otherwise, it should have mode ``1``. maximum value of 255. If the font has embedded color data, the bitmap
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
:param text: Text to render. :param text: Text to render.
:param mode: Used by some graphics drivers to indicate what mode the :param mode: Used by some graphics drivers to indicate what mode the
@ -402,6 +404,10 @@ class FreeTypeFont:
.. versionadded:: 8.0.0 .. versionadded:: 8.0.0
:param ink: Foreground ink for rendering in RGBA mode.
.. versionadded:: 8.0.0
:return: An internal PIL storage memory instance as defined by the :return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module. :py:mod:`PIL.Image.core` interface module.
""" """
@ -413,6 +419,7 @@ class FreeTypeFont:
language=language, language=language,
stroke_width=stroke_width, stroke_width=stroke_width,
anchor=anchor, anchor=anchor,
ink=ink,
)[0] )[0]
def getmask2( def getmask2(
@ -425,6 +432,7 @@ class FreeTypeFont:
language=None, language=None,
stroke_width=0, stroke_width=0,
anchor=None, anchor=None,
ink=0,
*args, *args,
**kwargs, **kwargs,
): ):
@ -432,7 +440,8 @@ class FreeTypeFont:
Create a bitmap for the text. Create a bitmap for the text.
If the font uses antialiasing, the bitmap should have mode ``L`` and use a If the font uses antialiasing, the bitmap should have mode ``L`` and use a
maximum value of 255. Otherwise, it should have mode ``1``. maximum value of 255. If the font has embedded color data, the bitmap
should have mode ``RGBA``. Otherwise, it should have mode ``1``.
:param text: Text to render. :param text: Text to render.
:param mode: Used by some graphics drivers to indicate what mode the :param mode: Used by some graphics drivers to indicate what mode the
@ -481,18 +490,22 @@ class FreeTypeFont:
.. versionadded:: 8.0.0 .. versionadded:: 8.0.0
:param ink: Foreground ink for rendering in RGBA mode.
.. versionadded:: 8.0.0
:return: A tuple of an internal PIL storage memory instance as defined by the :return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the :py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking gap between the starting coordinate and the first marking
""" """
size, offset = self.font.getsize( size, offset = self.font.getsize(
text, mode == "1", direction, features, language, anchor text, mode, direction, features, language, anchor
) )
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
offset = offset[0] - stroke_width, offset[1] - stroke_width offset = offset[0] - stroke_width, offset[1] - stroke_width
im = fill("L", size, 0) im = fill("RGBA" if mode == "RGBA" else "L", size, 0)
self.font.render( self.font.render(
text, im.id, mode == "1", direction, features, language, stroke_width text, im.id, mode, direction, features, language, stroke_width, ink
) )
return im, offset return im, offset

View File

@ -28,6 +28,9 @@
#include FT_STROKER_H #include FT_STROKER_H
#include FT_MULTIPLE_MASTERS_H #include FT_MULTIPLE_MASTERS_H
#include FT_SFNT_NAMES_H #include FT_SFNT_NAMES_H
#ifdef FT_COLOR_H
#include FT_COLOR_H
#endif
#define KEEP_PY_UNICODE #define KEEP_PY_UNICODE
@ -350,7 +353,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out)
static size_t static size_t
text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask) const char* lang, GlyphInfo **glyph_info, int mask, int color)
{ {
size_t i = 0, count = 0, start = 0; size_t i = 0, count = 0, start = 0;
raqm_t *rq; raqm_t *rq;
@ -529,7 +532,7 @@ failed:
static size_t static size_t
text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask) const char* lang, GlyphInfo **glyph_info, int mask, int color)
{ {
int error, load_flags; int error, load_flags;
FT_ULong ch; FT_ULong ch;
@ -565,6 +568,11 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje
if (mask) { if (mask) {
load_flags |= FT_LOAD_TARGET_MONO; load_flags |= FT_LOAD_TARGET_MONO;
} }
#ifdef FT_LOAD_COLOR
if (color) {
load_flags |= FT_LOAD_COLOR;
}
#endif
for (i = 0; font_getchar(string, i, &ch); i++) { for (i = 0; font_getchar(string, i, &ch); i++) {
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch);
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags);
@ -595,14 +603,14 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje
static size_t static size_t
text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features,
const char* lang, GlyphInfo **glyph_info, int mask) const char* lang, GlyphInfo **glyph_info, int mask, int color)
{ {
size_t count; size_t count;
if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask); count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask, color);
} else { } else {
count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask, color);
} }
return count; return count;
} }
@ -624,6 +632,8 @@ font_getsize(FontObject* self, PyObject* args)
size_t i, count; /* glyph_info index and length */ size_t i, count; /* glyph_info index and length */
int horizontal_dir; /* is primary axis horizontal? */ int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
const char *mode = NULL;
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
const char *anchor = NULL; const char *anchor = NULL;
@ -632,12 +642,15 @@ font_getsize(FontObject* self, PyObject* args)
/* calculate size and bearing for a given string */ /* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(args, "O|izOzz:getsize", &string, &mask, &dir, &features, &lang, &anchor)) { if (!PyArg_ParseTuple(args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
return NULL; return NULL;
} }
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
if (anchor == NULL) { if (anchor == NULL) {
anchor = horizontal_dir ? "la" : "lt"; anchor = horizontal_dir ? "la" : "lt";
} }
@ -645,7 +658,7 @@ font_getsize(FontObject* self, PyObject* args)
goto bad_anchor; goto bad_anchor;
} }
count = text_layout(string, self, dir, features, lang, &glyph_info, mask); count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -657,6 +670,11 @@ font_getsize(FontObject* self, PyObject* args)
if (mask) { if (mask) {
load_flags |= FT_LOAD_TARGET_MONO; load_flags |= FT_LOAD_TARGET_MONO;
} }
#ifdef FT_LOAD_COLOR
if (color) {
load_flags |= FT_LOAD_COLOR;
}
#endif
/* /*
* text bounds are given by: * text bounds are given by:
@ -834,7 +852,10 @@ font_render(FontObject* self, PyObject* args)
Py_ssize_t id; Py_ssize_t id;
int horizontal_dir; /* is primary axis horizontal? */ int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
int stroke_width = 0; int stroke_width = 0;
PY_LONG_LONG foreground_ink = 0;
const char *mode = NULL;
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
PyObject *features = Py_None; PyObject *features = Py_None;
@ -843,14 +864,28 @@ font_render(FontObject* self, PyObject* args)
/* render string into given buffer (the buffer *must* have /* render string into given buffer (the buffer *must* have
the right size, or this will crash) */ the right size, or this will crash) */
if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, if (!PyArg_ParseTuple(args, "On|zzOziL:render", &string, &id, &mode, &dir, &features, &lang,
&stroke_width)) { &stroke_width, &foreground_ink)) {
return NULL; return NULL;
} }
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
count = text_layout(string, self, dir, features, lang, &glyph_info, mask); mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
#ifdef FT_COLOR_H
if (color) {
FT_Color foreground_color;
foreground_color.red = (FT_Byte) (foreground_ink);
foreground_color.green = (FT_Byte) (foreground_ink >> 8);
foreground_color.blue = (FT_Byte) (foreground_ink >> 16);
foreground_color.alpha = (FT_Byte) 255; /* ink alpha is handled in ImageDraw.text */
FT_Palette_Set_Foreground_Color(self->face, foreground_color);
}
#endif
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -874,6 +909,11 @@ font_render(FontObject* self, PyObject* args)
if (mask) { if (mask) {
load_flags |= FT_LOAD_TARGET_MONO; load_flags |= FT_LOAD_TARGET_MONO;
} }
#ifdef FT_LOAD_COLOR
if (color) {
load_flags |= FT_LOAD_COLOR;
}
#endif
/* /*
* calculate x_min and y_max * calculate x_min and y_max
@ -960,25 +1000,58 @@ font_render(FontObject* self, PyObject* args)
/* clip glyph bitmap height to target image bounds */ /* clip glyph bitmap height to target image bounds */
if (yy >= 0 && yy < im->ysize) { if (yy >= 0 && yy < im->ysize) {
// blend this glyph into the buffer // blend this glyph into the buffer
unsigned char *target = im->image8[yy] + xx; if (color) {
if (mask) { /* target[RGB] returns the color, target[A] returns the mask */
// use monochrome mask (on palette images, etc) /* target bands get split again in ImageDraw.text */
int j, k, m = 128; unsigned char *target = im->image[yy] + xx * 4;
for (j = k = 0; j < x1; j++) { #ifdef FT_LOAD_COLOR
if (j >= x0 && (source[k] & m)) { if (bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
target[j] = 255; // paste color glyph
int k;
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];
}
} }
if (!(m >>= 1)) { } else
m = 128; #endif
k++; { // pixel_mode should be FT_PIXEL_MODE_GRAY
// fill with ink
int k;
for (k = x0; k < x1; k++) {
if (target[k * 4 + 3] < source[k]) {
target[k * 4 + 0] = (unsigned char) (foreground_ink);
target[k * 4 + 1] = (unsigned char) (foreground_ink >> 8);
target[k * 4 + 2] = (unsigned char) (foreground_ink >> 16);
target[k * 4 + 3] = source[k];
}
} }
} }
} else { } else {
// use antialiased rendering unsigned char *target = im->image8[yy] + xx;
int k; if (mask) {
for (k = x0; k < x1; k++) { // use monochrome mask (on palette images, etc)
if (target[k] < source[k]) { int j, k, m = 128;
target[k] = source[k]; for (j = k = 0; j < x1; j++) {
if (j >= x0 && (source[k] & m)) {
target[j] = 255;
}
if (!(m >>= 1)) {
m = 128;
k++;
}
}
} else {
// use antialiased rendering
int k;
for (k = x0; k < x1; k++) {
if (target[k] < source[k]) {
target[k] = source[k];
}
} }
} }
} }