From 6002b0a49ec0204a9b2d23ff61a1f2127b5679f1 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 1 Feb 2023 17:05:41 +0000 Subject: [PATCH 01/10] (WIP) support font-family style fallback with basic text layout --- src/PIL/ImageFont.py | 282 ++++++++++++++++++++++++++ src/_imagingft.c | 459 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 714 insertions(+), 27 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfef..6625a9e02 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -720,6 +720,288 @@ class FreeTypeFont: raise NotImplementedError(msg) from e +class FreeTypeFontFamily: + """FreeType font family""" + + # example: + # from PIL import Image, ImageDraw, ImageFont + # f1 = ImageFont.truetype("segoeui.ttf", 24) + # f2 = ImageFont.truetype("seguisym.ttf", 24) + # ff = ImageFont.FreeTypeFontFamily(f1, f2) + # s = "a↦ľ" + # im = Image.new("RGBA", (100, 100), "white") + # d = ImageDraw.Draw(im) + # d.text((10, 10), s, "black", f1) + # d.text((10, 40), s, "black", f2) + # d.text((10, 70), s, "black", ff) + # im.show() + + def __init__(self, *fonts): + fonts_list = [] + for font in fonts: + try: + fonts_list.append( + ("", font.size, font.index, font.encoding, font.font_bytes) + ) + except AttributeError: + fonts_list.append((font.path, font.size, font.index, font.encoding)) + + self.fonts = tuple(fonts_list) + self.font = core.getfamily(self.fonts, layout_engine=Layout.BASIC) + + def getlength(self, text, mode="", direction=None, features=None, language=None): + """ + Returns length (in pixels with 1/64 precision) of given text when rendered + in font with provided direction, features, and language. + + This is the amount by which following text should be offset. + Text bounding box may extend past the length in some fonts, + e.g. when using italics or accents. + + The result is returned as a float; it is a whole number if using basic layout. + + Note that the sum of two lengths may not equal the length of a concatenated + string due to kerning. If you need to adjust for kerning, include the following + character and subtract its length. + + For example, instead of + + .. code-block:: python + + hello = font.getlength("Hello") + world = font.getlength("World") + hello_world = hello + world # not adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # may fail + + use + + .. code-block:: python + + hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning + world = font.getlength("World") + hello_world = hello + world # adjusted for kerning + assert hello_world == font.getlength("HelloWorld") # True + + or disable kerning with (requires libraqm) + + .. code-block:: python + + hello = draw.textlength("Hello", font, features=["-kern"]) + world = draw.textlength("World", font, features=["-kern"]) + hello_world = hello + world # kerning is disabled, no need to adjust + assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"]) + + .. versionadded:: 8.0.0 + + :param text: Text to measure. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :return: Width for horizontal, height for vertical text. + """ + return self.font.getlength(text, mode, direction, features, language) / 64 + + def getbbox( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ): + """ + Returns bounding box (in pixels) of given text relative to given anchor + when rendered in font with provided direction, features, and language. + + Use :py:meth:`getlength()` to get the offset of following text with + 1/64 pixel precision. The bounding box includes extra margins for + some fonts, e.g. italics or accents. + + .. versionadded:: 8.0.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + :param stroke_width: The width of the text stroke. + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + :return: ``(left, top, right, bottom)`` bounding box + """ + _string_length_check(text) + size, offset = self.font.getsize( + text, mode, direction, features, language, anchor + ) + left, top = offset[0] - stroke_width, offset[1] - stroke_width + width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width + return left, top, left + width, top + height + + def getmask2( + self, + text, + mode="", + *, + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + start=None, + **kwargs, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + 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 + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param fill: Optional fill function. By default, an internal Pillow function + will be used. + + Deprecated. This parameter will be removed in Pillow 10 + (2023-07-01). + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left. + See :ref:`text-anchors` for valid values. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.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 + """ + _string_length_check(text) + if start is None: + start = (0, 0) + + def fill(width, height): + size = (width, height) + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) + + return self.font.render( + text, + fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + ) + + class TransposedFont: """Wrapper for writing rotated or mirrored text""" diff --git a/src/_imagingft.c b/src/_imagingft.c index f8143e0cc..521e01567 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -67,6 +67,12 @@ static int have_raqm = 0; #define LAYOUT_RAQM 1 typedef struct { + FT_Face face; + unsigned char *font_bytes; +} FontFamilyFont; + +typedef struct { + FT_Face face; int index, x_offset, x_advance, y_offset, y_advance; unsigned int cluster; } GlyphInfo; @@ -89,7 +95,14 @@ typedef struct { int layout_engine; } FontObject; +typedef struct { + PyObject_HEAD int font_count; + FontFamilyFont *fonts; + int layout_engine; +} FontFamilyObject; + static PyTypeObject Font_Type; +static PyTypeObject FontFamily_Type; /* round a 26.6 pixel coordinate to the nearest integer */ #define PIXEL(x) ((((x) + 32) & -64) >> 6) @@ -238,6 +251,133 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return (PyObject *)self; } +static PyObject * +getfamily(PyObject *self_, PyObject *args, PyObject *kw) { + /* create a font family object from a list of file names and a sizes (in pixels) */ + + FontFamilyObject *self; + int error = 0; + + PyTupleObject *fonts_tuple = NULL; + Py_ssize_t layout_engine = 0; + static char *kwlist[] = {"fonts", "layout_engine", NULL}; + + if (!library) { + PyErr_SetString(PyExc_OSError, "failed to initialize FreeType library"); + return NULL; + } + + if (!PyArg_ParseTupleAndKeywords( + args, kw, "O!|n", kwlist, &PyTuple_Type, &fonts_tuple, &layout_engine)) { + return NULL; + } + + if (PyTuple_GET_SIZE(fonts_tuple) == 0) { + PyErr_BadArgument(); + return NULL; + } + + self = PyObject_New(FontFamilyObject, &FontFamily_Type); + if (!self) { + return NULL; + } + + self->font_count = PyTuple_GET_SIZE(fonts_tuple); + self->layout_engine = layout_engine; + self->fonts = PyMem_New(FontFamilyFont, self->font_count); + if (!self->fonts) { + PyObject_Del(self); + return NULL; + } + + FontFamilyFont *font = self->fonts; + for (int i = 0; i < self->font_count; ++i, ++font) { + char *filename; + Py_ssize_t size; + Py_ssize_t index; + unsigned char *encoding; + unsigned char *font_bytes = NULL; + Py_ssize_t font_bytes_size = 0; + + if (!PyArg_ParseTuple( + PyTuple_GET_ITEM(fonts_tuple, i), + "etnns|y#", + Py_FileSystemDefaultEncoding, // TODO PyConfig.filesystem_encoding + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size)) { + goto err; + } + + font->face = NULL; + + if (filename && font_bytes_size <= 0) { + font->font_bytes = NULL; + error = FT_New_Face(library, filename, index, &font->face); + } else { + /* need to have allocated storage for font_bytes for the life of the + * object.*/ + /* Don't free this before FT_Done_Face */ + font->font_bytes = PyMem_Malloc(font_bytes_size); + if (!font->font_bytes) { + error = FT_Err_Out_Of_Memory; + } + if (!error) { + memcpy(font->font_bytes, font_bytes, (size_t)font_bytes_size); + error = FT_New_Memory_Face( + library, + (FT_Byte *)font->font_bytes, + font_bytes_size, + index, + &font->face); + } + } + + if (!error) { + error = FT_Set_Pixel_Sizes(font->face, 0, size); + } + + if (!error && encoding && strlen((char *)encoding) == 4) { + FT_Encoding encoding_tag = + FT_MAKE_TAG(encoding[0], encoding[1], encoding[2], encoding[3]); + error = FT_Select_Charmap(font->face, encoding_tag); + } + + if (filename) { + PyMem_Free(filename); + } + + if (error) { + if (font->font_bytes) { + PyMem_Free(font->font_bytes); + font->font_bytes = NULL; + } + geterror(error); + goto err; + } + } + + return (PyObject *)self; + +err: + for (FontFamilyFont *f = self->fonts; f != font; ++f) { + if (f->font_bytes) { + PyMem_Free(f->font_bytes); + f->font_bytes = NULL; + } + if (f->face) { + FT_Done_Face(f->face); + f->face = NULL; + } + } + + PyObject_Del(self); + return NULL; +} + #ifdef HAVE_RAQM static size_t @@ -381,6 +521,7 @@ text_layout_raqm( } for (i = 0; i < count; i++) { + (*glyph_info)[i].face = self->face; (*glyph_info)[i].index = glyphs[i].index; (*glyph_info)[i].x_offset = glyphs[i].x_offset; (*glyph_info)[i].x_advance = glyphs[i].x_advance; @@ -451,6 +592,7 @@ text_layout_fallback( } else { ch = PyUnicode_READ_CHAR(string, i); } + (*glyph_info)[i].face = self->face; (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); if (error) { @@ -483,6 +625,107 @@ text_layout_fallback( return count; } +static size_t +text_layout_family( + PyObject *string, + FontFamilyObject *self, + const char *dir, + PyObject *features, + const char *lang, + GlyphInfo **glyph_info, + int mask, + int color) { + int error, load_flags; + FT_ULong ch; + Py_ssize_t count; + FT_GlyphSlot glyph; + FT_UInt last_index = 0; + int i; + + if (features != Py_None || dir != NULL || lang != NULL) { + PyErr_SetString( + PyExc_KeyError, + "setting text direction, language or font features is not supported " + "without libraqm"); + } + if (!PyUnicode_Check(string)) { + PyErr_SetString(PyExc_TypeError, "expected string"); + return 0; + } + + count = 0; + if (PyUnicode_Check(string)) { + count = PyUnicode_GET_LENGTH(string); + } else { + PyBytes_AsStringAndSize(string, &buffer, &count); + } + if (count == 0) { + return 0; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + return 0; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } +#ifdef FT_LOAD_COLOR + if (color) { + load_flags |= FT_LOAD_COLOR; + } +#endif + for (i = 0; i < count; i++) { + if (buffer) { + ch = buffer[i]; + } else { + ch = PyUnicode_READ_CHAR(string, i); + } + FontFamilyFont *font = self->fonts; + int found = 0; + for (int j = 0; !found && j < self->font_count; j++, font++) { + (*glyph_info)[i].index = FT_Get_Char_Index(font->face, ch); + if ((*glyph_info)[i].index != 0) { + found = 1; + } + if (j == 0 || found) { /* use first font's missing glyph */ + (*glyph_info)[i].face = font->face; + error = FT_Load_Glyph(font->face, (*glyph_info)[i].index, load_flags); + if (error) { + geterror(error); + return 0; + } + glyph = font->face->glyph; + (*glyph_info)[i].x_offset = 0; + (*glyph_info)[i].y_offset = 0; + if (FT_HAS_KERNING(font->face) && last_index && + (*glyph_info)[i].index) { + FT_Vector delta; + if (FT_Get_Kerning( + font->face, + last_index, + (*glyph_info)[i].index, + ft_kerning_default, + &delta) == 0) { + (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); + (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); + } + } + + (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + // y_advance is only used in ttb, which is not supported by basic layout + (*glyph_info)[i].y_advance = 0; + last_index = (*glyph_info)[i].index; + (*glyph_info)[i].cluster = ch; + } + } + } + return count; +} + static size_t text_layout( PyObject *string, @@ -509,7 +752,7 @@ text_layout( } static PyObject * -font_getlength(FontObject *self, PyObject *args) { +text_getlength(void *self, int is_font_family, PyObject *args) { int length; /* length along primary axis, in 26.6 precision */ GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i, count; /* glyph_info index and length */ @@ -535,7 +778,20 @@ font_getlength(FontObject *self, PyObject *args) { mask = mode && strcmp(mode, "1") == 0; color = mode && strcmp(mode, "RGBA") == 0; - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (is_font_family) { + count = text_layout_family( + string, + (FontFamilyObject *)self, + dir, + features, + lang, + &glyph_info, + mask, + color); + } else { + count = text_layout( + string, (FontObject *)self, dir, features, lang, &glyph_info, mask, color); + } if (PyErr_Occurred()) { return NULL; } @@ -557,9 +813,20 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } +static PyObject * +font_getlength(FontObject *self, PyObject *args) { + return text_getlength(self, 0, args); +} + +static PyObject * +family_getlength(FontFamilyObject *self, PyObject *args) { + return text_getlength(self, 1, args); +} + static int bounding_box_and_anchors( - FT_Face face, + void *self, + int is_font_family, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, @@ -576,17 +843,30 @@ bounding_box_and_anchors( int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ int error; + FT_Face face; + FT_Face primaryFace; FT_Glyph glyph; FT_BBox bbox; /* glyph bounding box */ size_t i; /* glyph_info index */ + + if (is_font_family) { + FontFamilyObject *family = (FontFamilyObject *)self; + primaryFace = family->fonts->face; + } else { + FontObject *font = (FontObject *)self; + primaryFace = font->face; + } /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ + face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { + face = glyph_info[i].face; + if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -666,14 +946,14 @@ bounding_box_and_anchors( } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(face->size->metrics.ascender); + y_anchor = PIXEL(primaryFace->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (face->size->metrics.ascender + face->size->metrics.descender) / + (primaryFace->size->metrics.ascender + primaryFace->size->metrics.descender) / 2 ); break; @@ -684,7 +964,7 @@ bounding_box_and_anchors( y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(face->size->metrics.descender); + y_anchor = PIXEL(primaryFace->size->metrics.descender); break; default: goto bad_anchor; @@ -736,7 +1016,7 @@ bad_anchor: } static PyObject * -font_getsize(FontObject *self, PyObject *args) { +text_getsize(void *self, int is_font_family, PyObject *args) { int width, height, x_offset, y_offset; int load_flags; /* FreeType load_flags parameter */ int error; @@ -765,7 +1045,15 @@ font_getsize(FontObject *self, PyObject *args) { mask = mode && strcmp(mode, "1") == 0; color = mode && strcmp(mode, "RGBA") == 0; - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (is_font_family) { + FontFamilyObject *family = (FontFamilyObject *)self; + count = text_layout_family( + string, family, dir, features, lang, &glyph_info, mask, color); + } else { + FontObject *font = (FontObject *) self; + count = text_layout( + string, font, dir, features, lang, &glyph_info, mask, color); + } if (PyErr_Occurred()) { return NULL; } @@ -779,7 +1067,8 @@ font_getsize(FontObject *self, PyObject *args) { } error = bounding_box_and_anchors( - self->face, + self, + is_font_family, anchor, horizontal_dir, glyph_info, @@ -802,12 +1091,23 @@ font_getsize(FontObject *self, PyObject *args) { } static PyObject * -font_render(FontObject *self, PyObject *args) { +font_getsize(FontObject *self, PyObject *args) { + return text_getsize(self, 0, args); +} + +static PyObject * +family_getsize(FontFamilyObject *self, PyObject *args) { + return text_getsize(self, 1, args); +} + +static PyObject * +text_render(void *self, int is_font_family, PyObject *args) { int x, y; /* pen position, in 26.6 precision */ int px, py; /* position of current glyph, in pixels */ int x_min, y_max; /* text offset in 26.6 precision */ int load_flags; /* FreeType load_flags parameter */ int error; + FT_Face face; FT_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; @@ -868,20 +1168,45 @@ font_render(FontObject *self, PyObject *args) { foreground_ink = foreground_ink_long; + if (is_font_family) { + FontFamilyObject *family = (FontFamilyObject *)self; + #ifdef FT_COLOR_H - if (color) { - FT_Color foreground_color; - FT_Byte *ink = (FT_Byte *)&foreground_ink; - foreground_color.red = ink[0]; - foreground_color.green = ink[1]; - foreground_color.blue = ink[2]; - foreground_color.alpha = - (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ - FT_Palette_Set_Foreground_Color(self->face, foreground_color); - } + for (int i = 0; i < family->font_count; i++) { + if (color) { + FT_Color foreground_color; + FT_Byte *ink = (FT_Byte *)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = + (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(family->fonts[i].face, foreground_color); + } + } #endif - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + count = + text_layout_family(string, family, dir, features, lang, &glyph_info, mask, color); + } else { + FontObject *font = (FontObject *)self; + +#ifdef FT_COLOR_H + if (color) { + FT_Color foreground_color; + FT_Byte *ink = (FT_Byte *)&foreground_ink; + foreground_color.red = ink[0]; + foreground_color.green = ink[1]; + foreground_color.blue = ink[2]; + foreground_color.alpha = + (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ + FT_Palette_Set_Foreground_Color(font->face, foreground_color); + } +#endif + + count = + text_layout(string, font, dir, features, lang, &glyph_info, mask, color); + } if (PyErr_Occurred()) { return NULL; } @@ -897,7 +1222,8 @@ font_render(FontObject *self, PyObject *args) { horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; error = bounding_box_and_anchors( - self->face, + self, + is_font_family, anchor, horizontal_dir, glyph_info, @@ -960,14 +1286,15 @@ font_render(FontObject *self, PyObject *args) { px = PIXEL(x + glyph_info[i].x_offset); py = PIXEL(y + glyph_info[i].y_offset); + face = glyph_info[i].face; error = - FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); + FT_Load_Glyph(face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { geterror(error); goto glyph_error; } - glyph_slot = self->face->glyph; + glyph_slot = face->glyph; bitmap = glyph_slot->bitmap; if (glyph_slot->bitmap_top + py > y_max) { @@ -993,13 +1320,14 @@ font_render(FontObject *self, PyObject *args) { px = PIXEL(x + glyph_info[i].x_offset); py = PIXEL(y + glyph_info[i].y_offset); - error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); + face = glyph_info[i].face; + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { geterror(error); goto glyph_error; } - glyph_slot = self->face->glyph; + glyph_slot = face->glyph; if (stroker != NULL) { error = FT_Get_Glyph(glyph_slot, &glyph); if (!error) { @@ -1214,6 +1542,16 @@ glyph_error: return NULL; } +static PyObject * +font_render(FontObject *self, PyObject *args) { + return text_render(self, 0, args); +} + +static PyObject * +family_render(FontFamilyObject *self, PyObject *args) { + return text_render(self, 1, args); +} + #if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject * @@ -1545,8 +1883,74 @@ static PyTypeObject Font_Type = { font_getsetters, /*tp_getset*/ }; +static void +family_dealloc(FontFamilyObject *self) { + FontFamilyFont *font = self->fonts; + for (int i = 0; i < self->font_count; ++i, ++font) { + if (font->face) { + FT_Done_Face(font->face); + } + if (font->font_bytes) { + PyMem_Free(font->font_bytes); + } + } + PyMem_Free(self->fonts); + PyObject_Del(self); +} + +static PyMethodDef family_methods[] = { + {"render", (PyCFunction)family_render, METH_VARARGS}, + {"getsize", (PyCFunction)family_getsize, METH_VARARGS}, + {"getlength", (PyCFunction)family_getlength, METH_VARARGS}, +/* TODO +#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, + {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, + {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, +#endif +*/ + {NULL, NULL}}; + + +static PyTypeObject FontFamily_Type = { + PyVarObject_HEAD_INIT(NULL, 0) "FontFamily", + sizeof(FontObject), + 0, + /* methods */ + (destructor)family_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + family_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*TODO tp_getset*/ +}; + static PyMethodDef _functions[] = { - {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, {NULL, NULL} + {"getfont", (PyCFunction)getfont, METH_VARARGS | METH_KEYWORDS}, + {"getfamily", (PyCFunction)getfamily, METH_VARARGS | METH_KEYWORDS}, + {NULL, NULL} }; static int @@ -1559,6 +1963,7 @@ setup_module(PyObject *m) { /* Ready object type */ PyType_Ready(&Font_Type); + PyType_Ready(&FontFamily_Type); if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ From 3710f2d2998aeddb9e243a0d0039d0249f5930c2 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 00:23:33 +0000 Subject: [PATCH 02/10] merge single font and font family code paths --- src/_imagingft.c | 353 ++++++++++++++++------------------------------- 1 file changed, 119 insertions(+), 234 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 521e01567..d06a7a51d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -67,9 +67,10 @@ static int have_raqm = 0; #define LAYOUT_RAQM 1 typedef struct { - FT_Face face; - unsigned char *font_bytes; -} FontFamilyFont; + FT_Face *faces; + int font_count; + int layout_engine; +} FontFamily; typedef struct { FT_Face face; @@ -96,9 +97,8 @@ typedef struct { } FontObject; typedef struct { - PyObject_HEAD int font_count; - FontFamilyFont *fonts; - int layout_engine; + PyObject_HEAD FontFamily data; + unsigned char **font_bytes; } FontFamilyObject; static PyTypeObject Font_Type; @@ -256,6 +256,7 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { /* create a font family object from a list of file names and a sizes (in pixels) */ FontFamilyObject *self; + FontFamily *family; int error = 0; PyTupleObject *fonts_tuple = NULL; @@ -282,16 +283,23 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } - self->font_count = PyTuple_GET_SIZE(fonts_tuple); - self->layout_engine = layout_engine; - self->fonts = PyMem_New(FontFamilyFont, self->font_count); - if (!self->fonts) { + family = &self->data; + + family->font_count = PyTuple_GET_SIZE(fonts_tuple); + family->layout_engine = layout_engine; + family->faces = PyMem_New(FT_Face, family->font_count); + if (!family->faces) { + PyObject_Del(self); + return NULL; + } + self->font_bytes = PyMem_New(unsigned char *, family->font_count); + if (!self->font_bytes) { + PyMem_Free(family->faces); PyObject_Del(self); return NULL; } - FontFamilyFont *font = self->fonts; - for (int i = 0; i < self->font_count; ++i, ++font) { + for (int i = 0; i < family->font_count; i++) { char *filename; Py_ssize_t size; Py_ssize_t index; @@ -312,38 +320,38 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { goto err; } - font->face = NULL; + family->faces[i] = NULL; if (filename && font_bytes_size <= 0) { - font->font_bytes = NULL; - error = FT_New_Face(library, filename, index, &font->face); + self->font_bytes[i] = NULL; + error = FT_New_Face(library, filename, index, &family->faces[i]); } else { /* need to have allocated storage for font_bytes for the life of the * object.*/ /* Don't free this before FT_Done_Face */ - font->font_bytes = PyMem_Malloc(font_bytes_size); - if (!font->font_bytes) { + self->font_bytes[i] = PyMem_Malloc(font_bytes_size); + if (!self->font_bytes[i]) { error = FT_Err_Out_Of_Memory; } if (!error) { - memcpy(font->font_bytes, font_bytes, (size_t)font_bytes_size); + memcpy(self->font_bytes[i], font_bytes, (size_t)font_bytes_size); error = FT_New_Memory_Face( library, - (FT_Byte *)font->font_bytes, + (FT_Byte *)self->font_bytes[i], font_bytes_size, index, - &font->face); + &family->faces[i]); } } if (!error) { - error = FT_Set_Pixel_Sizes(font->face, 0, size); + error = FT_Set_Pixel_Sizes(family->faces[i], 0, size); } if (!error && encoding && strlen((char *)encoding) == 4) { FT_Encoding encoding_tag = FT_MAKE_TAG(encoding[0], encoding[1], encoding[2], encoding[3]); - error = FT_Select_Charmap(font->face, encoding_tag); + error = FT_Select_Charmap(family->faces[i], encoding_tag); } if (filename) { @@ -351,31 +359,33 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { } if (error) { - if (font->font_bytes) { - PyMem_Free(font->font_bytes); - font->font_bytes = NULL; + if (self->font_bytes[i]) { + PyMem_Free(self->font_bytes[i]); + self->font_bytes[i] = NULL; + } + if (family->faces[i]) { + FT_Done_Face(family->faces[i]); } geterror(error); goto err; } + + continue; + + err: + for (int j = 0; j < i; j++) { + if (family->faces[j]) { + FT_Done_Face(family->faces[j]); + } + if (self->font_bytes[j]) { + PyMem_Free(self->font_bytes[j]); + } + } + PyObject_Del(self); + return NULL; } return (PyObject *)self; - -err: - for (FontFamilyFont *f = self->fonts; f != font; ++f) { - if (f->font_bytes) { - PyMem_Free(f->font_bytes); - f->font_bytes = NULL; - } - if (f->face) { - FT_Done_Face(f->face); - f->face = NULL; - } - } - - PyObject_Del(self); - return NULL; } #ifdef HAVE_RAQM @@ -383,7 +393,7 @@ err: static size_t text_layout_raqm( PyObject *string, - FontObject *self, + FontFamily *family, const char *dir, PyObject *features, const char *lang, @@ -496,7 +506,7 @@ text_layout_raqm( Py_DECREF(seq); } - if (!raqm_set_freetype_face(rq, self->face)) { + if (!raqm_set_freetype_face(rq, family->faces[0])) { PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); goto failed; } @@ -521,7 +531,7 @@ text_layout_raqm( } for (i = 0; i < count; i++) { - (*glyph_info)[i].face = self->face; + (*glyph_info)[i].face = family->faces[0]; (*glyph_info)[i].index = glyphs[i].index; (*glyph_info)[i].x_offset = glyphs[i].x_offset; (*glyph_info)[i].x_advance = glyphs[i].x_advance; @@ -540,7 +550,7 @@ failed: static size_t text_layout_fallback( PyObject *string, - FontObject *self, + FontFamily *family, const char *dir, PyObject *features, const char *lang, @@ -553,7 +563,6 @@ text_layout_fallback( FT_ULong ch; Py_ssize_t count; FT_GlyphSlot glyph; - FT_Bool kerning = FT_HAS_KERNING(self->face); FT_UInt last_index = 0; if (features != Py_None || dir != NULL || lang != NULL) { @@ -592,116 +601,27 @@ text_layout_fallback( } else { ch = PyUnicode_READ_CHAR(string, i); } - (*glyph_info)[i].face = self->face; - (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); - error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); - if (error) { - geterror(error); - return 0; - } - glyph = self->face->glyph; - (*glyph_info)[i].x_offset = 0; - (*glyph_info)[i].y_offset = 0; - if (kerning && last_index && (*glyph_info)[i].index) { - FT_Vector delta; - if (FT_Get_Kerning( - self->face, - last_index, - (*glyph_info)[i].index, - ft_kerning_default, - &delta - ) == 0) { - (*glyph_info)[i - 1].x_advance += PIXEL(delta.x); - (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); - } - } - - (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - // y_advance is only used in ttb, which is not supported by basic layout - (*glyph_info)[i].y_advance = 0; - last_index = (*glyph_info)[i].index; - (*glyph_info)[i].cluster = ch; - } - return count; -} - -static size_t -text_layout_family( - PyObject *string, - FontFamilyObject *self, - const char *dir, - PyObject *features, - const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { - int error, load_flags; - FT_ULong ch; - Py_ssize_t count; - FT_GlyphSlot glyph; - FT_UInt last_index = 0; - int i; - - if (features != Py_None || dir != NULL || lang != NULL) { - PyErr_SetString( - PyExc_KeyError, - "setting text direction, language or font features is not supported " - "without libraqm"); - } - if (!PyUnicode_Check(string)) { - PyErr_SetString(PyExc_TypeError, "expected string"); - return 0; - } - - count = 0; - if (PyUnicode_Check(string)) { - count = PyUnicode_GET_LENGTH(string); - } else { - PyBytes_AsStringAndSize(string, &buffer, &count); - } - if (count == 0) { - return 0; - } - - (*glyph_info) = PyMem_New(GlyphInfo, count); - if ((*glyph_info) == NULL) { - PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); - return 0; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } -#ifdef FT_LOAD_COLOR - if (color) { - load_flags |= FT_LOAD_COLOR; - } -#endif - for (i = 0; i < count; i++) { - if (buffer) { - ch = buffer[i]; - } else { - ch = PyUnicode_READ_CHAR(string, i); - } - FontFamilyFont *font = self->fonts; int found = 0; - for (int j = 0; !found && j < self->font_count; j++, font++) { - (*glyph_info)[i].index = FT_Get_Char_Index(font->face, ch); + for (int j = 0; !found && j < family->font_count; j++) { + FT_Face face = family->faces[j]; + (*glyph_info)[i].index = FT_Get_Char_Index(face, ch); if ((*glyph_info)[i].index != 0) { found = 1; } - if (j == 0 || found) { /* use first font's missing glyph */ - (*glyph_info)[i].face = font->face; - error = FT_Load_Glyph(font->face, (*glyph_info)[i].index, load_flags); + /* prefer first font's missing glyph if no font support this codepoint */ + if (j == 0 || found) { + (*glyph_info)[i].face = face; + error = FT_Load_Glyph(face, (*glyph_info)[i].index, load_flags); if (error) { geterror(error); return 0; } - glyph = font->face->glyph; + glyph = face->glyph; (*glyph_info)[i].x_offset = 0; (*glyph_info)[i].y_offset = 0; - if (FT_HAS_KERNING(font->face) && last_index && + + /* This has been broken and had no effect for many years now... + if (FT_HAS_KERNING(face) && last_index && (*glyph_info)[i].index) { FT_Vector delta; if (FT_Get_Kerning( @@ -714,9 +634,10 @@ text_layout_family( (*glyph_info)[i - 1].y_advance += PIXEL(delta.y); } } + */ (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - // y_advance is only used in ttb, which is not supported by basic layout + /* y_advance is only used in ttb, which is not supported by basic layout */ (*glyph_info)[i].y_advance = 0; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; @@ -729,7 +650,7 @@ text_layout_family( static size_t text_layout( PyObject *string, - FontObject *self, + FontFamily *family, const char *dir, PyObject *features, const char *lang, @@ -739,20 +660,20 @@ text_layout( ) { size_t count; #ifdef HAVE_RAQM - if (have_raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, lang, glyph_info); + if (have_raqm && family->layout_engine == LAYOUT_RAQM) { + count = text_layout_raqm(string, family, dir, features, lang, glyph_info); } else #endif { count = text_layout_fallback( - string, self, dir, features, lang, glyph_info, mask, color + string, family, dir, features, lang, glyph_info, mask, color ); } return count; } static PyObject * -text_getlength(void *self, int is_font_family, PyObject *args) { +text_getlength(FontFamily *family, PyObject *args) { int length; /* length along primary axis, in 26.6 precision */ GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i, count; /* glyph_info index and length */ @@ -778,20 +699,8 @@ text_getlength(void *self, int is_font_family, PyObject *args) { mask = mode && strcmp(mode, "1") == 0; color = mode && strcmp(mode, "RGBA") == 0; - if (is_font_family) { - count = text_layout_family( - string, - (FontFamilyObject *)self, - dir, - features, - lang, - &glyph_info, - mask, - color); - } else { - count = text_layout( - string, (FontObject *)self, dir, features, lang, &glyph_info, mask, color); - } + count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { return NULL; } @@ -815,18 +724,23 @@ text_getlength(void *self, int is_font_family, PyObject *args) { static PyObject * font_getlength(FontObject *self, PyObject *args) { - return text_getlength(self, 0, args); + FontFamily family; + + family.faces = &self->face; + family.font_count = 1; + family.layout_engine = self->layout_engine; + + return text_getlength(&family, args); } static PyObject * family_getlength(FontFamilyObject *self, PyObject *args) { - return text_getlength(self, 1, args); + return text_getlength(&self->data, args); } static int bounding_box_and_anchors( - void *self, - int is_font_family, + FontFamily *family, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, @@ -844,18 +758,9 @@ bounding_box_and_anchors( int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ int error; FT_Face face; - FT_Face primaryFace; FT_Glyph glyph; FT_BBox bbox; /* glyph bounding box */ size_t i; /* glyph_info index */ - - if (is_font_family) { - FontFamilyObject *family = (FontFamilyObject *)self; - primaryFace = family->fonts->face; - } else { - FontObject *font = (FontObject *)self; - primaryFace = font->face; - } /* * text bounds are given by: * - bounding boxes of individual glyphs @@ -946,14 +851,14 @@ bounding_box_and_anchors( } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(primaryFace->size->metrics.ascender); + y_anchor = PIXEL(family->faces[0]->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (primaryFace->size->metrics.ascender + primaryFace->size->metrics.descender) / + (family->faces[0]->size->metrics.ascender + family->faces[0]->size->metrics.descender) / 2 ); break; @@ -964,7 +869,7 @@ bounding_box_and_anchors( y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(primaryFace->size->metrics.descender); + y_anchor = PIXEL(family->faces[0]->size->metrics.descender); break; default: goto bad_anchor; @@ -1016,7 +921,7 @@ bad_anchor: } static PyObject * -text_getsize(void *self, int is_font_family, PyObject *args) { +text_getsize(FontFamily *family, PyObject *args) { int width, height, x_offset, y_offset; int load_flags; /* FreeType load_flags parameter */ int error; @@ -1045,15 +950,7 @@ text_getsize(void *self, int is_font_family, PyObject *args) { mask = mode && strcmp(mode, "1") == 0; color = mode && strcmp(mode, "RGBA") == 0; - if (is_font_family) { - FontFamilyObject *family = (FontFamilyObject *)self; - count = text_layout_family( - string, family, dir, features, lang, &glyph_info, mask, color); - } else { - FontObject *font = (FontObject *) self; - count = text_layout( - string, font, dir, features, lang, &glyph_info, mask, color); - } + count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color); if (PyErr_Occurred()) { return NULL; } @@ -1067,8 +964,7 @@ text_getsize(void *self, int is_font_family, PyObject *args) { } error = bounding_box_and_anchors( - self, - is_font_family, + family, anchor, horizontal_dir, glyph_info, @@ -1092,16 +988,22 @@ text_getsize(void *self, int is_font_family, PyObject *args) { static PyObject * font_getsize(FontObject *self, PyObject *args) { - return text_getsize(self, 0, args); + FontFamily family; + + family.faces = &self->face; + family.font_count = 1; + family.layout_engine = self->layout_engine; + + return text_getsize(&family, args); } static PyObject * family_getsize(FontFamilyObject *self, PyObject *args) { - return text_getsize(self, 1, args); + return text_getsize(&self->data, args); } static PyObject * -text_render(void *self, int is_font_family, PyObject *args) { +text_render(FontFamily *family, PyObject *args) { int x, y; /* pen position, in 26.6 precision */ int px, py; /* position of current glyph, in pixels */ int x_min, y_max; /* text offset in 26.6 precision */ @@ -1168,30 +1070,8 @@ text_render(void *self, int is_font_family, PyObject *args) { foreground_ink = foreground_ink_long; - if (is_font_family) { - FontFamilyObject *family = (FontFamilyObject *)self; - -#ifdef FT_COLOR_H - for (int i = 0; i < family->font_count; i++) { - if (color) { - FT_Color foreground_color; - FT_Byte *ink = (FT_Byte *)&foreground_ink; - foreground_color.red = ink[0]; - foreground_color.green = ink[1]; - foreground_color.blue = ink[2]; - foreground_color.alpha = - (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ - FT_Palette_Set_Foreground_Color(family->fonts[i].face, foreground_color); - } - } -#endif - - count = - text_layout_family(string, family, dir, features, lang, &glyph_info, mask, color); - } else { - FontObject *font = (FontObject *)self; - #ifdef FT_COLOR_H + for (int i = 0; i < family->font_count; i++) { if (color) { FT_Color foreground_color; FT_Byte *ink = (FT_Byte *)&foreground_ink; @@ -1200,13 +1080,13 @@ text_render(void *self, int is_font_family, PyObject *args) { foreground_color.blue = ink[2]; foreground_color.alpha = (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ - FT_Palette_Set_Foreground_Color(font->face, foreground_color); + FT_Palette_Set_Foreground_Color(family->faces[i], foreground_color); } + } #endif - count = - text_layout(string, font, dir, features, lang, &glyph_info, mask, color); - } + count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { return NULL; } @@ -1222,8 +1102,7 @@ text_render(void *self, int is_font_family, PyObject *args) { horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; error = bounding_box_and_anchors( - self, - is_font_family, + family, anchor, horizontal_dir, glyph_info, @@ -1544,12 +1423,18 @@ glyph_error: static PyObject * font_render(FontObject *self, PyObject *args) { - return text_render(self, 0, args); + FontFamily family; + + family.faces = &self->face; + family.font_count = 1; + family.layout_engine = self->layout_engine; + + return text_render(&family, args); } static PyObject * family_render(FontFamilyObject *self, PyObject *args) { - return text_render(self, 1, args); + return text_render(&self->data, args); } #if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ @@ -1885,16 +1770,16 @@ static PyTypeObject Font_Type = { static void family_dealloc(FontFamilyObject *self) { - FontFamilyFont *font = self->fonts; - for (int i = 0; i < self->font_count; ++i, ++font) { - if (font->face) { - FT_Done_Face(font->face); + for (int i = 0; i < self->data.font_count; i++) { + if (self->data.faces[i]) { + FT_Done_Face(self->data.faces[i]); } - if (font->font_bytes) { - PyMem_Free(font->font_bytes); + if (self->font_bytes[i]) { + PyMem_Free(self->font_bytes[i]); } } - PyMem_Free(self->fonts); + PyMem_Free(self->data.faces); + PyMem_Free(self->font_bytes); PyObject_Del(self); } From 910511606bcc025f6aad6197c414190d278f28ce Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 00:24:03 +0000 Subject: [PATCH 03/10] font fallback for Raqm layout --- src/PIL/ImageFont.py | 40 ++++++--- src/_imagingft.c | 203 ++++++++++++++++++++++++++++++------------- 2 files changed, 171 insertions(+), 72 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6625a9e02..e7f9829e5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -725,18 +725,20 @@ class FreeTypeFontFamily: # example: # from PIL import Image, ImageDraw, ImageFont - # f1 = ImageFont.truetype("segoeui.ttf", 24) - # f2 = ImageFont.truetype("seguisym.ttf", 24) - # ff = ImageFont.FreeTypeFontFamily(f1, f2) - # s = "a↦ľ" - # im = Image.new("RGBA", (100, 100), "white") - # d = ImageDraw.Draw(im) - # d.text((10, 10), s, "black", f1) - # d.text((10, 40), s, "black", f2) - # d.text((10, 70), s, "black", ff) - # im.show() + # le = ImageFont.Layout.RAQM + # f1 = ImageFont.truetype(r"C:\Users\Nulano\AppData\Local\Microsoft\Windows\Fonts\SCRIPTIN.ttf", 24) + # f2 = ImageFont.truetype("segoeui.ttf", 24) + # f3 = ImageFont.truetype("seguisym.ttf", 24) + # ff = ImageFont.FreeTypeFontFamily(f1, f2, f3, layout_engine=le) + # for s in ("testčingšsšccčcč", "ية↦α,abc", "a↦ľ"): + # im = Image.new("RGBA", (300, 300), "white") + # d = ImageDraw.Draw(im) + # d.text((10, 60), s, "black", f1, direction="ltr", anchor="ls") + # d.text((10, 160), s, "black", f2, direction="ltr", anchor="ls") + # d.text((10, 260), s, "black", ff, direction="ltr", anchor="ls") + # im.show() - def __init__(self, *fonts): + def __init__(self, *fonts, layout_engine=None): fonts_list = [] for font in fonts: try: @@ -745,9 +747,21 @@ class FreeTypeFontFamily: ) except AttributeError: fonts_list.append((font.path, font.size, font.index, font.encoding)) - self.fonts = tuple(fonts_list) - self.font = core.getfamily(self.fonts, layout_engine=Layout.BASIC) + + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC + if core.HAVE_RAQM: + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC + self.layout_engine = layout_engine + + self.font = core.getfamily(self.fonts, layout_engine=self.layout_engine) def getlength(self, text, mode="", direction=None, features=None, language=None): """ diff --git a/src/_imagingft.c b/src/_imagingft.c index d06a7a51d..efaa9bf1e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -404,40 +404,35 @@ text_layout_raqm( raqm_glyph_t *glyphs = NULL; raqm_direction_t direction; + char *buffer = NULL; + Py_UCS4 *text = NULL; + Py_ssize_t size; + int *fallback = NULL; + rq = raqm_create(); if (rq == NULL) { PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); goto failed; } - Py_ssize_t size; - int set_text; if (PyUnicode_Check(string)) { - Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); + text = PyUnicode_AsUCS4Copy(string); size = PyUnicode_GET_LENGTH(string); if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - set_text = raqm_set_text(rq, text, size); - PyMem_Free(text); } else { - char *buffer; PyBytes_AsStringAndSize(string, &buffer, &size); if (!buffer || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ goto failed; } - set_text = raqm_set_text_utf8(rq, buffer, size); } - if (!set_text) { - PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); - goto failed; - } - if (lang && !raqm_set_language(rq, lang, start, size)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + if (!buffer && !text) { + PyErr_SetString(PyExc_ValueError, "expected string"); goto failed; } @@ -464,63 +459,142 @@ text_layout_raqm( } } - if (!raqm_set_par_direction(rq, direction)) { - PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); - goto failed; + if (family->font_count > 1) { + fallback = PyMem_New(int, size); + if (!fallback) { + PyErr_SetString(PyExc_ValueError, "failed to allocate fallback buffer."); + goto failed; + } + fallback[0] = -1; + for (size_t j = 1; j < size; j++) { + fallback[j] = -2; + } } - if (features != Py_None) { - int j, len; - PyObject *seq = PySequence_Fast(features, "expected a sequence"); - if (!seq) { + for (int face = 0; face < family->font_count; face++) { + raqm_clear_contents(rq); + + int set_text = text != NULL ? raqm_set_text(rq, text, size) : raqm_set_text_utf8(rq, buffer, size); + if (!set_text) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + if (lang) { + if (!raqm_set_language(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } + + if (!raqm_set_par_direction(rq, direction)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); goto failed; } - len = PySequence_Fast_GET_SIZE(seq); - for (j = 0; j < len; j++) { - PyObject *item = PySequence_Fast_GET_ITEM(seq, j); - char *feature = NULL; - Py_ssize_t size = 0; - PyObject *bytes; + if (features != Py_None) { + int j, len; + PyObject *seq = PySequence_Fast(features, "expected a sequence"); + if (!seq) { + goto failed; + } - if (!PyUnicode_Check(item)) { - Py_DECREF(seq); - PyErr_SetString(PyExc_TypeError, "expected a string"); - goto failed; - } - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { - Py_DECREF(seq); - goto failed; - } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); - if (!raqm_add_font_feature(rq, feature, size)) { - Py_DECREF(seq); + len = PySequence_Fast_GET_SIZE(seq); + for (j = 0; j < len; j++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, j); + char *feature = NULL; + Py_ssize_t size = 0; + PyObject *bytes; + + if (!PyUnicode_Check(item)) { + Py_DECREF(seq); + PyErr_SetString(PyExc_TypeError, "expected a string"); + goto failed; + } + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) { + Py_DECREF(seq); + goto failed; + } + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); + if (!raqm_add_font_feature(rq, feature, size)) { + Py_DECREF(seq); + Py_DECREF(bytes); + PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + goto failed; + } Py_DECREF(bytes); - PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + } + Py_DECREF(seq); + } + + if (face == 0) { + if (!raqm_set_freetype_face(rq, family->faces[0])) { + PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); goto failed; } - Py_DECREF(bytes); + } else { + start = 0; + for (size_t j = 0; j <= size; j++) { + if (j < size) { + if (fallback[j] == -2) { + /* not a cluster boundary */ + continue; + } + if (fallback[start] == fallback[j]) { + /* use same font face for this cluster */ + continue; + } + } + if (fallback[start] == -1) { + raqm_set_freetype_face_range( + rq, family->faces[face], start, j - start); + } else { + raqm_set_freetype_face_range( + rq, family->faces[fallback[start]], start, j - start); + } + start = j; + } } - Py_DECREF(seq); - } - if (!raqm_set_freetype_face(rq, family->faces[0])) { - PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); - goto failed; - } + if (!raqm_layout(rq)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); + goto failed; + } - if (!raqm_layout(rq)) { - PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); - goto failed; - } + glyphs = raqm_get_glyphs(rq, &count); + if (glyphs == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + count = 0; + goto failed; + } - glyphs = raqm_get_glyphs(rq, &count); - if (glyphs == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); - count = 0; - goto failed; + if (i + 1 == family->font_count) { + break; + } + + for (size_t j = 1; j < size; j++) { + if (fallback[j] == -1) { + fallback[j] = -2; + } + } + + int missing = 0; + for (size_t j = 0; j < count; j++) { + int cluster = glyphs[j].cluster; + if (glyphs[j].index == 0) { + /* cluster contains missing glyph */ + fallback[cluster] = -1; + missing = 1; + } else if (fallback[cluster] < 0) { + /* use current font face for this cluster */ + fallback[cluster] = face; + } + } + + if (!missing) { + break; + } } (*glyph_info) = PyMem_New(GlyphInfo, count); @@ -531,16 +605,27 @@ text_layout_raqm( } for (i = 0; i < count; i++) { - (*glyph_info)[i].face = family->faces[0]; (*glyph_info)[i].index = glyphs[i].index; (*glyph_info)[i].x_offset = glyphs[i].x_offset; (*glyph_info)[i].x_advance = glyphs[i].x_advance; (*glyph_info)[i].y_offset = glyphs[i].y_offset; (*glyph_info)[i].y_advance = glyphs[i].y_advance; - (*glyph_info)[i].cluster = glyphs[i].cluster; + + uint32_t cluster = glyphs[i].cluster; + (*glyph_info)[i].cluster = cluster; + if (fallback && fallback[cluster] >= 0) { + (*glyph_info)[i].face = family->faces[fallback[cluster]]; + } else { + /* FIXME use first font's missing glyph, not last font's */ + (*glyph_info)[i].face = family->faces[family->font_count - 1]; + } } failed: + PyMem_Free(text); + if (fallback) { + PyMem_Free(fallback); + } raqm_destroy(rq); return count; } From 3b3cac84a81479ce6fb2a03bb37f2881ea1073a2 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 00:56:51 +0000 Subject: [PATCH 04/10] C89 / Lint / Raqm<0.9.0 fixes --- src/PIL/ImageFont.py | 3 +- src/_imagingft.c | 85 ++++++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index e7f9829e5..135f38cae 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -726,7 +726,8 @@ class FreeTypeFontFamily: # example: # from PIL import Image, ImageDraw, ImageFont # le = ImageFont.Layout.RAQM - # f1 = ImageFont.truetype(r"C:\Users\Nulano\AppData\Local\Microsoft\Windows\Fonts\SCRIPTIN.ttf", 24) + # f1 = ImageFont.truetype( + # r"C:\Users\Nulano\AppData\Local\Microsoft\Windows\Fonts\SCRIPTIN.ttf", 24) # f2 = ImageFont.truetype("segoeui.ttf", 24) # f3 = ImageFont.truetype("seguisym.ttf", 24) # ff = ImageFont.FreeTypeFontFamily(f1, f2, f3, layout_engine=le) diff --git a/src/_imagingft.c b/src/_imagingft.c index efaa9bf1e..81a5c0099 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -254,7 +254,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { static PyObject * getfamily(PyObject *self_, PyObject *args, PyObject *kw) { /* create a font family object from a list of file names and a sizes (in pixels) */ - + int i, j; FontFamilyObject *self; FontFamily *family; int error = 0; @@ -299,7 +299,7 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } - for (int i = 0; i < family->font_count; i++) { + for (i = 0; i < family->font_count; i++) { char *filename; Py_ssize_t size; Py_ssize_t index; @@ -369,23 +369,21 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { geterror(error); goto err; } - - continue; - - err: - for (int j = 0; j < i; j++) { - if (family->faces[j]) { - FT_Done_Face(family->faces[j]); - } - if (self->font_bytes[j]) { - PyMem_Free(self->font_bytes[j]); - } - } - PyObject_Del(self); - return NULL; } return (PyObject *)self; + +err: + for (j = 0; j < i; j++) { + if (family->faces[j]) { + FT_Done_Face(family->faces[j]); + } + if (self->font_bytes[j]) { + PyMem_Free(self->font_bytes[j]); + } + } + PyObject_Del(self); + return NULL; } #ifdef HAVE_RAQM @@ -399,8 +397,9 @@ text_layout_raqm( const char *lang, GlyphInfo **glyph_info ) { - size_t i = 0, count = 0, start = 0; - raqm_t *rq; + int face = 0; + size_t i = 0, j = 0, count = 0, start = 0; + raqm_t *rq = NULL; raqm_glyph_t *glyphs = NULL; raqm_direction_t direction; @@ -409,12 +408,6 @@ text_layout_raqm( Py_ssize_t size; int *fallback = NULL; - rq = raqm_create(); - if (rq == NULL) { - PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); - goto failed; - } - if (PyUnicode_Check(string)) { text = PyUnicode_AsUCS4Copy(string); size = PyUnicode_GET_LENGTH(string); @@ -466,13 +459,29 @@ text_layout_raqm( goto failed; } fallback[0] = -1; - for (size_t j = 1; j < size; j++) { + for (j = 1; j < size; j++) { fallback[j] = -2; } } - for (int face = 0; face < family->font_count; face++) { - raqm_clear_contents(rq); + for (face = 0; face < family->font_count; face++) { +#ifdef RAQM_VERSION_ATLEAST +#if RAQM_VERSION_ATLEAST(0, 9, 0) + if (face >= 1) { + raqm_clear_contents(rq); + } else +#endif +#endif + { + if (rq != NULL) { + raqm_destroy(rq); + } + rq = raqm_create(); + if (rq == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); + goto failed; + } + } int set_text = text != NULL ? raqm_set_text(rq, text, size) : raqm_set_text_utf8(rq, buffer, size); if (!set_text) { @@ -480,6 +489,7 @@ text_layout_raqm( goto failed; } if (lang) { + start = 0; if (!raqm_set_language(rq, lang, start, size)) { PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); goto failed; @@ -535,7 +545,7 @@ text_layout_raqm( } } else { start = 0; - for (size_t j = 0; j <= size; j++) { + for (j = 0; j <= size; j++) { if (j < size) { if (fallback[j] == -2) { /* not a cluster boundary */ @@ -573,14 +583,14 @@ text_layout_raqm( break; } - for (size_t j = 1; j < size; j++) { + for (j = 1; j < size; j++) { if (fallback[j] == -1) { fallback[j] = -2; } } int missing = 0; - for (size_t j = 0; j < count; j++) { + for (j = 0; j < count; j++) { int cluster = glyphs[j].cluster; if (glyphs[j].index == 0) { /* cluster contains missing glyph */ @@ -622,11 +632,15 @@ text_layout_raqm( } failed: - PyMem_Free(text); + if (text) { + PyMem_Free(text); + } if (fallback) { PyMem_Free(fallback); } - raqm_destroy(rq); + if (rq != NULL) { + raqm_destroy(rq); + } return count; } @@ -643,7 +657,7 @@ text_layout_fallback( int mask, int color ) { - int error, load_flags, i; + int error, load_flags, i, j; char *buffer = NULL; FT_ULong ch; Py_ssize_t count; @@ -687,7 +701,7 @@ text_layout_fallback( ch = PyUnicode_READ_CHAR(string, i); } int found = 0; - for (int j = 0; !found && j < family->font_count; j++) { + for (j = 0; !found && j < family->font_count; j++) { FT_Face face = family->faces[j]; (*glyph_info)[i].index = FT_Get_Char_Index(face, ch); if ((*glyph_info)[i].index != 0) { @@ -1855,7 +1869,8 @@ static PyTypeObject Font_Type = { static void family_dealloc(FontFamilyObject *self) { - for (int i = 0; i < self->data.font_count; i++) { + int i; + for (i = 0; i < self->data.font_count; i++) { if (self->data.faces[i]) { FT_Done_Face(self->data.faces[i]); } From c244169a8ea6acc779b1a5c3f5846488649ee3e7 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 15:41:29 +0000 Subject: [PATCH 05/10] fix Raqm fallback algorithm when a missing glyph is followed by a non-missing glyph in a single cluster --- src/PIL/ImageFont.py | 2 +- src/_imagingft.c | 42 ++++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 135f38cae..ac7cf3c07 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -731,7 +731,7 @@ class FreeTypeFontFamily: # f2 = ImageFont.truetype("segoeui.ttf", 24) # f3 = ImageFont.truetype("seguisym.ttf", 24) # ff = ImageFont.FreeTypeFontFamily(f1, f2, f3, layout_engine=le) - # for s in ("testčingšsšccčcč", "ية↦α,abc", "a↦ľ"): + # for s in ("testčingšsšccčcč", "ية↦α,abc", "a↦ľ", "ῶ,ω̃,ώ,ώ, ́,á"): # im = Image.new("RGBA", (300, 300), "white") # d = ImageDraw.Draw(im) # d.text((10, 60), s, "black", f1, direction="ltr", anchor="ls") diff --git a/src/_imagingft.c b/src/_imagingft.c index 81a5c0099..4a270d446 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -398,7 +398,7 @@ text_layout_raqm( GlyphInfo **glyph_info ) { int face = 0; - size_t i = 0, j = 0, count = 0, start = 0; + size_t i = 0, count = 0, start = 0; raqm_t *rq = NULL; raqm_glyph_t *glyphs = NULL; raqm_direction_t direction; @@ -458,9 +458,8 @@ text_layout_raqm( PyErr_SetString(PyExc_ValueError, "failed to allocate fallback buffer."); goto failed; } - fallback[0] = -1; - for (j = 1; j < size; j++) { - fallback[j] = -2; + for (i = 0; i < size; i++) { + fallback[i] = -2; } } @@ -545,25 +544,25 @@ text_layout_raqm( } } else { start = 0; - for (j = 0; j <= size; j++) { - if (j < size) { - if (fallback[j] == -2) { + for (i = 0; i <= size; i++) { + if (i < size) { + if (fallback[i] == -2) { /* not a cluster boundary */ continue; } - if (fallback[start] == fallback[j]) { + if (fallback[start] == fallback[i]) { /* use same font face for this cluster */ continue; } } - if (fallback[start] == -1) { + if (fallback[start] < 0) { raqm_set_freetype_face_range( - rq, family->faces[face], start, j - start); + rq, family->faces[face], start, i - start); } else { raqm_set_freetype_face_range( - rq, family->faces[fallback[start]], start, j - start); + rq, family->faces[fallback[start]], start, i - start); } - start = j; + start = i; } } @@ -579,24 +578,27 @@ text_layout_raqm( goto failed; } - if (i + 1 == family->font_count) { + //if (face + 1 == family->font_count) { + // break; + //} + if (family->font_count == 1) { break; } - for (j = 1; j < size; j++) { - if (fallback[j] == -1) { - fallback[j] = -2; + for (i = 1; i < size; i++) { + if (fallback[i] == -1) { + fallback[i] = -2; } } int missing = 0; - for (j = 0; j < count; j++) { - int cluster = glyphs[j].cluster; - if (glyphs[j].index == 0) { + for (i = 0; i < count; i++) { + int cluster = glyphs[i].cluster; + if (glyphs[i].index == 0) { /* cluster contains missing glyph */ fallback[cluster] = -1; missing = 1; - } else if (fallback[cluster] < 0) { + } else if (fallback[cluster] == -2) { /* use current font face for this cluster */ fallback[cluster] = face; } From cbbd9bd27dd664f11803191141e1e947ee84057b Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 15:55:42 +0000 Subject: [PATCH 06/10] fix Raqm fallback for first codepoint, use first font's missing glyph instead of last font's missing glyph --- src/_imagingft.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 4a270d446..d7b42dd7e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -463,7 +463,7 @@ text_layout_raqm( } } - for (face = 0; face < family->font_count; face++) { + for (face = 0; ; face++) { #ifdef RAQM_VERSION_ATLEAST #if RAQM_VERSION_ATLEAST(0, 9, 0) if (face >= 1) { @@ -544,7 +544,9 @@ text_layout_raqm( } } else { start = 0; - for (i = 0; i <= size; i++) { + /* use first font's missing glyph */ + int f = face < family->font_count ? face : 0; + for (i = 1; i <= size; i++) { if (i < size) { if (fallback[i] == -2) { /* not a cluster boundary */ @@ -557,7 +559,7 @@ text_layout_raqm( } if (fallback[start] < 0) { raqm_set_freetype_face_range( - rq, family->faces[face], start, i - start); + rq, family->faces[f], start, i - start); } else { raqm_set_freetype_face_range( rq, family->faces[fallback[start]], start, i - start); @@ -578,14 +580,11 @@ text_layout_raqm( goto failed; } - //if (face + 1 == family->font_count) { - // break; - //} - if (family->font_count == 1) { + if (family->font_count == 1 || face == family->font_count) { break; } - for (i = 1; i < size; i++) { + for (i = 0; i < size; i++) { if (fallback[i] == -1) { fallback[i] = -2; } @@ -603,7 +602,6 @@ text_layout_raqm( fallback[cluster] = face; } } - if (!missing) { break; } From 5aab9065a65051431df7e0820d306a341257bfa7 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 2 Feb 2023 16:57:31 +0000 Subject: [PATCH 07/10] fix face reference for missing glyphs with Raqm layout --- src/_imagingft.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d7b42dd7e..6082af0d3 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -626,8 +626,7 @@ text_layout_raqm( if (fallback && fallback[cluster] >= 0) { (*glyph_info)[i].face = family->faces[fallback[cluster]]; } else { - /* FIXME use first font's missing glyph, not last font's */ - (*glyph_info)[i].face = family->faces[family->font_count - 1]; + (*glyph_info)[i].face = family->faces[0]; } } From 90816952cf5b2f073c76b64cfbe230faaa5113b9 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 8 Feb 2023 02:40:46 +0000 Subject: [PATCH 08/10] consider all fonts when computing font family metrics --- src/PIL/ImageFont.py | 8 +++++ src/_imagingft.c | 76 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ac7cf3c07..a4d35f63b 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1016,6 +1016,14 @@ class FreeTypeFontFamily: start[1], ) + def getmetrics(self): + """ + :return: A tuple of the maximum font ascent (the distance from the baseline to + the highest outline point) and maximum descent (the distance from the + baseline to the lowest outline point, a negative value) + """ + return self.font.ascent, self.font.descent + class TransposedFont: """Wrapper for writing rotated or mirrored text""" diff --git a/src/_imagingft.c b/src/_imagingft.c index 6082af0d3..ffe7a5165 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -386,6 +386,42 @@ err: return NULL; } +static FT_Pos +family_getascender(FontFamily *family) { + int i; + FT_Pos ascender = 0; + for (i = 0; i < family->font_count; i++) { + ascender = (ascender > family->faces[i]->size->metrics.ascender) + ? ascender + : family->faces[i]->size->metrics.ascender; + } + return ascender; +} + +static FT_Pos +family_getdescender(FontFamily *family) { + int i; + FT_Pos descender = 0; + for (i = 0; i < family->font_count; i++) { + descender = (descender < family->faces[i]->size->metrics.descender) + ? descender + : family->faces[i]->size->metrics.descender; + } + return descender; +} + +static FT_Pos +family_getheight(FontFamily *family) { + int i; + FT_Pos height = 0; + for (i = 0; i < family->font_count; i++) { + height = (height > family->faces[i]->size->metrics.height) + ? height + : family->faces[i]->size->metrics.height; + } + return height; +} + #ifdef HAVE_RAQM static size_t @@ -949,16 +985,16 @@ bounding_box_and_anchors( } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(family->faces[0]->size->metrics.ascender); + // this should be consistent with getmetrics() + y_anchor = PIXEL(family_getascender(family)); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 + // this should be consistent with getmetrics() y_anchor = PIXEL( - (family->faces[0]->size->metrics.ascender + family->faces[0]->size->metrics.descender) / - 2 - ); + (family_getascender(family) + family_getdescender(family)) / 2); break; case 's': // horizontal baseline y_anchor = 0; @@ -967,7 +1003,8 @@ bounding_box_and_anchors( y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(family->faces[0]->size->metrics.descender); + // this should be consistent with getmetrics() + y_anchor = PIXEL(family_getdescender(family)); break; default: goto bad_anchor; @@ -1897,6 +1934,33 @@ static PyMethodDef family_methods[] = { */ {NULL, NULL}}; +static PyObject * +family_getattr_ascent(FontFamilyObject *self, void *closure) { + return PyLong_FromLong(PIXEL(family_getascender(&self->data))); +} + +static PyObject * +family_getattr_descent(FontFamilyObject *self, void *closure) { + return PyLong_FromLong(-PIXEL(family_getdescender(&self->data))); +} + +static PyObject * +family_getattr_height(FontFamilyObject *self, void *closure) { + return PyLong_FromLong(PIXEL(family_getheight(&self->data))); +} + + +static struct PyGetSetDef family_getsetters[] = { + //{"family", (getter)font_getattr_family}, + //{"style", (getter)font_getattr_style}, + {"ascent", (getter)family_getattr_ascent}, + {"descent", (getter)family_getattr_descent}, + {"height", (getter)family_getattr_height}, + //{"x_ppem", (getter)font_getattr_x_ppem}, + //{"y_ppem", (getter)font_getattr_y_ppem}, + //{"glyphs", (getter)font_getattr_glyphs}, + {NULL}}; + static PyTypeObject FontFamily_Type = { PyVarObject_HEAD_INIT(NULL, 0) "FontFamily", @@ -1928,7 +1992,7 @@ static PyTypeObject FontFamily_Type = { 0, /*tp_iternext*/ family_methods, /*tp_methods*/ 0, /*tp_members*/ - 0, /*TODO tp_getset*/ + family_getsetters, /* tp_getset*/ }; static PyMethodDef _functions[] = { From 4f957e158b60c0f1957b846883622278ec5a42f8 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 21 Apr 2024 20:28:38 +0200 Subject: [PATCH 09/10] FreeTypeFontFamily: add getmask() method --- src/PIL/ImageFont.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a4d35f63b..cbbd3f2b1 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -906,6 +906,97 @@ class FreeTypeFontFamily: width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width return left, top, left + width, top + height + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + anchor=None, + ink=0, + start=None, + ): + """ + Create a bitmap for the text. + + If the font uses antialiasing, the bitmap should have mode ``L`` and use a + 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 + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP 47 language code + `_ + Requires libraqm. + + .. versionadded:: 6.0.0 + + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param anchor: The text anchor alignment. Determines the relative location of + the anchor to the text. The default alignment is top left, + specifically ``la`` for horizontal text and ``lt`` for + vertical text. See :ref:`text-anchors` for details. + + .. versionadded:: 8.0.0 + + :param ink: Foreground ink for rendering in RGBA mode. + + .. versionadded:: 8.0.0 + + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + + :return: An internal PIL storage memory instance as defined by the + :py:mod:`PIL.Image.core` interface module. + """ + return self.getmask2( + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + anchor=anchor, + ink=ink, + start=start, + )[0] + def getmask2( self, text, From 9ccec98cb12f27077d51482db4df22ea480d307d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 13:37:53 +0000 Subject: [PATCH 10/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_imagingft.c | 55 +++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index ffe7a5165..d8cb2dbcd 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -269,7 +269,8 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { } if (!PyArg_ParseTupleAndKeywords( - args, kw, "O!|n", kwlist, &PyTuple_Type, &fonts_tuple, &layout_engine)) { + args, kw, "O!|n", kwlist, &PyTuple_Type, &fonts_tuple, &layout_engine + )) { return NULL; } @@ -316,7 +317,8 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { &index, &encoding, &font_bytes, - &font_bytes_size)) { + &font_bytes_size + )) { goto err; } @@ -340,7 +342,8 @@ getfamily(PyObject *self_, PyObject *args, PyObject *kw) { (FT_Byte *)self->font_bytes[i], font_bytes_size, index, - &family->faces[i]); + &family->faces[i] + ); } } @@ -499,7 +502,7 @@ text_layout_raqm( } } - for (face = 0; ; face++) { + for (face = 0;; face++) { #ifdef RAQM_VERSION_ATLEAST #if RAQM_VERSION_ATLEAST(0, 9, 0) if (face >= 1) { @@ -518,7 +521,8 @@ text_layout_raqm( } } - int set_text = text != NULL ? raqm_set_text(rq, text, size) : raqm_set_text_utf8(rq, buffer, size); + int set_text = text != NULL ? raqm_set_text(rq, text, size) + : raqm_set_text_utf8(rq, buffer, size); if (!set_text) { PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; @@ -595,10 +599,12 @@ text_layout_raqm( } if (fallback[start] < 0) { raqm_set_freetype_face_range( - rq, family->faces[f], start, i - start); + rq, family->faces[f], start, i - start + ); } else { raqm_set_freetype_face_range( - rq, family->faces[fallback[start]], start, i - start); + rq, family->faces[fallback[start]], start, i - start + ); } start = i; } @@ -771,7 +777,8 @@ text_layout_fallback( */ (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - /* y_advance is only used in ttb, which is not supported by basic layout */ + /* y_advance is only used in ttb, which is not supported by basic layout + */ (*glyph_info)[i].y_advance = 0; last_index = (*glyph_info)[i].index; (*glyph_info)[i].cluster = ch; @@ -994,7 +1001,8 @@ bounding_box_and_anchors( case 'm': // middle (ascender + descender) / 2 // this should be consistent with getmetrics() y_anchor = PIXEL( - (family_getascender(family) + family_getdescender(family)) / 2); + (family_getascender(family) + family_getdescender(family)) / 2 + ); break; case 's': // horizontal baseline y_anchor = 0; @@ -1301,8 +1309,7 @@ text_render(FontFamily *family, PyObject *args) { py = PIXEL(y + glyph_info[i].y_offset); face = glyph_info[i].face; - error = - FT_Load_Glyph(face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { geterror(error); goto glyph_error; @@ -1923,16 +1930,17 @@ static PyMethodDef family_methods[] = { {"render", (PyCFunction)family_render, METH_VARARGS}, {"getsize", (PyCFunction)family_getsize, METH_VARARGS}, {"getlength", (PyCFunction)family_getlength, METH_VARARGS}, -/* TODO -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) - {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, - {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, - {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, - {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, -#endif -*/ - {NULL, NULL}}; + /* TODO + #if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ + (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) + {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, + {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, + {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, + {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, + #endif + */ + {NULL, NULL} +}; static PyObject * family_getattr_ascent(FontFamilyObject *self, void *closure) { @@ -1949,7 +1957,6 @@ family_getattr_height(FontFamilyObject *self, void *closure) { return PyLong_FromLong(PIXEL(family_getheight(&self->data))); } - static struct PyGetSetDef family_getsetters[] = { //{"family", (getter)font_getattr_family}, //{"style", (getter)font_getattr_style}, @@ -1959,8 +1966,8 @@ static struct PyGetSetDef family_getsetters[] = { //{"x_ppem", (getter)font_getattr_x_ppem}, //{"y_ppem", (getter)font_getattr_y_ppem}, //{"glyphs", (getter)font_getattr_glyphs}, - {NULL}}; - + {NULL} +}; static PyTypeObject FontFamily_Type = { PyVarObject_HEAD_INIT(NULL, 0) "FontFamily",