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.
.. 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.
@ -352,7 +352,12 @@ Methods
.. 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.
@ -399,6 +404,19 @@ Methods
.. 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)
Return the size of the given string, in pixels.

View File

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

View File

@ -261,7 +261,7 @@ class FreeTypeFont:
"""
# vertical offset is added for historical reasons
# 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 (
size[0] + stroke_width * 2,
size[1] + stroke_width * 2 + offset[1],
@ -348,12 +348,14 @@ class FreeTypeFont:
language=None,
stroke_width=0,
anchor=None,
ink=0,
):
"""
Create a bitmap for the text.
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 mode: Used by some graphics drivers to indicate what mode the
@ -402,6 +404,10 @@ class FreeTypeFont:
.. 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
:py:mod:`PIL.Image.core` interface module.
"""
@ -413,6 +419,7 @@ class FreeTypeFont:
language=language,
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
)[0]
def getmask2(
@ -425,6 +432,7 @@ class FreeTypeFont:
language=None,
stroke_width=0,
anchor=None,
ink=0,
*args,
**kwargs,
):
@ -432,7 +440,8 @@ class FreeTypeFont:
Create a bitmap for the text.
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 mode: Used by some graphics drivers to indicate what mode the
@ -481,18 +490,22 @@ class FreeTypeFont:
.. 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
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
"""
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
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(
text, im.id, mode == "1", direction, features, language, stroke_width
text, im.id, mode, direction, features, language, stroke_width, ink
)
return im, offset

View File

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