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 */