This commit is contained in:
Ondrej Baranovič 2024-09-04 13:57:20 +03:00 committed by GitHub
commit 5d55759c17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 983 additions and 127 deletions

View File

@ -720,6 +720,402 @@ class FreeTypeFont:
raise NotImplementedError(msg) from e raise NotImplementedError(msg) from e
class FreeTypeFontFamily:
"""FreeType font family"""
# 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)
# 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, layout_engine=None):
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)
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):
"""
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
<https://www.w3.org/International/articles/language-tags/>`_
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
<https://www.w3.org/International/articles/language-tags/>`_
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 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
<https://www.w3.org/International/articles/language-tags/>`_
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,
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
<https://www.w3.org/International/articles/language-tags/>`_
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],
)
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: class TransposedFont:
"""Wrapper for writing rotated or mirrored text""" """Wrapper for writing rotated or mirrored text"""

View File

@ -67,6 +67,13 @@ static int have_raqm = 0;
#define LAYOUT_RAQM 1 #define LAYOUT_RAQM 1
typedef struct { typedef struct {
FT_Face *faces;
int font_count;
int layout_engine;
} FontFamily;
typedef struct {
FT_Face face;
int index, x_offset, x_advance, y_offset, y_advance; int index, x_offset, x_advance, y_offset, y_advance;
unsigned int cluster; unsigned int cluster;
} GlyphInfo; } GlyphInfo;
@ -89,7 +96,13 @@ typedef struct {
int layout_engine; int layout_engine;
} FontObject; } FontObject;
typedef struct {
PyObject_HEAD FontFamily data;
unsigned char **font_bytes;
} FontFamilyObject;
static PyTypeObject Font_Type; static PyTypeObject Font_Type;
static PyTypeObject FontFamily_Type;
/* round a 26.6 pixel coordinate to the nearest integer */ /* round a 26.6 pixel coordinate to the nearest integer */
#define PIXEL(x) ((((x) + 32) & -64) >> 6) #define PIXEL(x) ((((x) + 32) & -64) >> 6)
@ -238,56 +251,220 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return (PyObject *)self; 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) */
int i, j;
FontFamilyObject *self;
FontFamily *family;
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;
}
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;
}
for (i = 0; i < family->font_count; i++) {
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;
}
family->faces[i] = NULL;
if (filename && font_bytes_size <= 0) {
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 */
self->font_bytes[i] = PyMem_Malloc(font_bytes_size);
if (!self->font_bytes[i]) {
error = FT_Err_Out_Of_Memory;
}
if (!error) {
memcpy(self->font_bytes[i], font_bytes, (size_t)font_bytes_size);
error = FT_New_Memory_Face(
library,
(FT_Byte *)self->font_bytes[i],
font_bytes_size,
index,
&family->faces[i]
);
}
}
if (!error) {
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(family->faces[i], encoding_tag);
}
if (filename) {
PyMem_Free(filename);
}
if (error) {
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;
}
}
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;
}
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 #ifdef HAVE_RAQM
static size_t static size_t
text_layout_raqm( text_layout_raqm(
PyObject *string, PyObject *string,
FontObject *self, FontFamily *family,
const char *dir, const char *dir,
PyObject *features, PyObject *features,
const char *lang, const char *lang,
GlyphInfo **glyph_info GlyphInfo **glyph_info
) { ) {
int face = 0;
size_t i = 0, count = 0, start = 0; size_t i = 0, count = 0, start = 0;
raqm_t *rq; raqm_t *rq = NULL;
raqm_glyph_t *glyphs = NULL; raqm_glyph_t *glyphs = NULL;
raqm_direction_t direction; raqm_direction_t direction;
rq = raqm_create(); char *buffer = NULL;
if (rq == NULL) { Py_UCS4 *text = NULL;
PyErr_SetString(PyExc_ValueError, "raqm_create() failed.");
goto failed;
}
Py_ssize_t size; Py_ssize_t size;
int set_text; int *fallback = NULL;
if (PyUnicode_Check(string)) { if (PyUnicode_Check(string)) {
Py_UCS4 *text = PyUnicode_AsUCS4Copy(string); text = PyUnicode_AsUCS4Copy(string);
size = PyUnicode_GET_LENGTH(string); size = PyUnicode_GET_LENGTH(string);
if (!text || !size) { if (!text || !size) {
/* return 0 and clean up, no glyphs==no size, /* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */ and raqm fails with empty strings */
goto failed; goto failed;
} }
set_text = raqm_set_text(rq, text, size);
PyMem_Free(text);
} else { } else {
char *buffer;
PyBytes_AsStringAndSize(string, &buffer, &size); PyBytes_AsStringAndSize(string, &buffer, &size);
if (!buffer || !size) { if (!buffer || !size) {
/* return 0 and clean up, no glyphs==no size, /* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */ and raqm fails with empty strings */
goto failed; goto failed;
} }
set_text = raqm_set_text_utf8(rq, buffer, size);
} }
if (!set_text) { if (!buffer && !text) {
PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); PyErr_SetString(PyExc_ValueError, "expected string");
goto failed;
}
if (lang && !raqm_set_language(rq, lang, start, size)) {
PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed");
goto failed; goto failed;
} }
@ -314,63 +491,162 @@ text_layout_raqm(
} }
} }
if (!raqm_set_par_direction(rq, direction)) { if (family->font_count > 1) {
PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); fallback = PyMem_New(int, size);
goto failed; if (!fallback) {
PyErr_SetString(PyExc_ValueError, "failed to allocate fallback buffer.");
goto failed;
}
for (i = 0; i < size; i++) {
fallback[i] = -2;
}
} }
if (features != Py_None) { for (face = 0;; face++) {
int j, len; #ifdef RAQM_VERSION_ATLEAST
PyObject *seq = PySequence_Fast(features, "expected a sequence"); #if RAQM_VERSION_ATLEAST(0, 9, 0)
if (!seq) { 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) {
PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed");
goto failed;
}
if (lang) {
start = 0;
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; goto failed;
} }
len = PySequence_Fast_GET_SIZE(seq); if (features != Py_None) {
for (j = 0; j < len; j++) { int j, len;
PyObject *item = PySequence_Fast_GET_ITEM(seq, j); PyObject *seq = PySequence_Fast(features, "expected a sequence");
char *feature = NULL; if (!seq) {
Py_ssize_t size = 0; goto failed;
PyObject *bytes; }
if (!PyUnicode_Check(item)) { len = PySequence_Fast_GET_SIZE(seq);
Py_DECREF(seq); for (j = 0; j < len; j++) {
PyErr_SetString(PyExc_TypeError, "expected a string"); PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
goto failed; char *feature = NULL;
} Py_ssize_t size = 0;
bytes = PyUnicode_AsUTF8String(item); PyObject *bytes;
if (bytes == NULL) {
Py_DECREF(seq); if (!PyUnicode_Check(item)) {
goto failed; Py_DECREF(seq);
} PyErr_SetString(PyExc_TypeError, "expected a string");
feature = PyBytes_AS_STRING(bytes); goto failed;
size = PyBytes_GET_SIZE(bytes); }
if (!raqm_add_font_feature(rq, feature, size)) { bytes = PyUnicode_AsUTF8String(item);
Py_DECREF(seq); 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); 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; goto failed;
} }
Py_DECREF(bytes); } else {
start = 0;
/* 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 */
continue;
}
if (fallback[start] == fallback[i]) {
/* use same font face for this cluster */
continue;
}
}
if (fallback[start] < 0) {
raqm_set_freetype_face_range(
rq, family->faces[f], start, i - start
);
} else {
raqm_set_freetype_face_range(
rq, family->faces[fallback[start]], start, i - start
);
}
start = i;
}
} }
Py_DECREF(seq);
}
if (!raqm_set_freetype_face(rq, self->face)) { if (!raqm_layout(rq)) {
PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed.");
goto failed; goto failed;
} }
if (!raqm_layout(rq)) { glyphs = raqm_get_glyphs(rq, &count);
PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); if (glyphs == NULL) {
goto failed; PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed.");
} count = 0;
goto failed;
}
glyphs = raqm_get_glyphs(rq, &count); if (family->font_count == 1 || face == family->font_count) {
if (glyphs == NULL) { break;
PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); }
count = 0;
goto failed; for (i = 0; i < size; i++) {
if (fallback[i] == -1) {
fallback[i] = -2;
}
}
int missing = 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] == -2) {
/* use current font face for this cluster */
fallback[cluster] = face;
}
}
if (!missing) {
break;
}
} }
(*glyph_info) = PyMem_New(GlyphInfo, count); (*glyph_info) = PyMem_New(GlyphInfo, count);
@ -386,11 +662,26 @@ text_layout_raqm(
(*glyph_info)[i].x_advance = glyphs[i].x_advance; (*glyph_info)[i].x_advance = glyphs[i].x_advance;
(*glyph_info)[i].y_offset = glyphs[i].y_offset; (*glyph_info)[i].y_offset = glyphs[i].y_offset;
(*glyph_info)[i].y_advance = glyphs[i].y_advance; (*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 {
(*glyph_info)[i].face = family->faces[0];
}
} }
failed: failed:
raqm_destroy(rq); if (text) {
PyMem_Free(text);
}
if (fallback) {
PyMem_Free(fallback);
}
if (rq != NULL) {
raqm_destroy(rq);
}
return count; return count;
} }
@ -399,7 +690,7 @@ failed:
static size_t static size_t
text_layout_fallback( text_layout_fallback(
PyObject *string, PyObject *string,
FontObject *self, FontFamily *family,
const char *dir, const char *dir,
PyObject *features, PyObject *features,
const char *lang, const char *lang,
@ -407,12 +698,11 @@ text_layout_fallback(
int mask, int mask,
int color int color
) { ) {
int error, load_flags, i; int error, load_flags, i, j;
char *buffer = NULL; char *buffer = NULL;
FT_ULong ch; FT_ULong ch;
Py_ssize_t count; Py_ssize_t count;
FT_GlyphSlot glyph; FT_GlyphSlot glyph;
FT_Bool kerning = FT_HAS_KERNING(self->face);
FT_UInt last_index = 0; FT_UInt last_index = 0;
if (features != Py_None || dir != NULL || lang != NULL) { if (features != Py_None || dir != NULL || lang != NULL) {
@ -451,34 +741,49 @@ text_layout_fallback(
} else { } else {
ch = PyUnicode_READ_CHAR(string, i); ch = PyUnicode_READ_CHAR(string, i);
} }
(*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); int found = 0;
error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); for (j = 0; !found && j < family->font_count; j++) {
if (error) { FT_Face face = family->faces[j];
geterror(error); (*glyph_info)[i].index = FT_Get_Char_Index(face, ch);
return 0; if ((*glyph_info)[i].index != 0) {
} found = 1;
glyph = self->face->glyph; }
(*glyph_info)[i].x_offset = 0; /* prefer first font's missing glyph if no font support this codepoint */
(*glyph_info)[i].y_offset = 0; if (j == 0 || found) {
if (kerning && last_index && (*glyph_info)[i].index) { (*glyph_info)[i].face = face;
FT_Vector delta; error = FT_Load_Glyph(face, (*glyph_info)[i].index, load_flags);
if (FT_Get_Kerning( if (error) {
self->face, geterror(error);
last_index, return 0;
(*glyph_info)[i].index, }
ft_kerning_default, glyph = face->glyph;
&delta (*glyph_info)[i].x_offset = 0;
) == 0) { (*glyph_info)[i].y_offset = 0;
(*glyph_info)[i - 1].x_advance += PIXEL(delta.x);
(*glyph_info)[i - 1].y_advance += PIXEL(delta.y); /* 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(
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;
} }
} }
(*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; return count;
} }
@ -486,7 +791,7 @@ text_layout_fallback(
static size_t static size_t
text_layout( text_layout(
PyObject *string, PyObject *string,
FontObject *self, FontFamily *family,
const char *dir, const char *dir,
PyObject *features, PyObject *features,
const char *lang, const char *lang,
@ -496,20 +801,20 @@ text_layout(
) { ) {
size_t count; size_t count;
#ifdef HAVE_RAQM #ifdef HAVE_RAQM
if (have_raqm && self->layout_engine == LAYOUT_RAQM) { if (have_raqm && family->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(string, self, dir, features, lang, glyph_info); count = text_layout_raqm(string, family, dir, features, lang, glyph_info);
} else } else
#endif #endif
{ {
count = text_layout_fallback( count = text_layout_fallback(
string, self, dir, features, lang, glyph_info, mask, color string, family, dir, features, lang, glyph_info, mask, color
); );
} }
return count; return count;
} }
static PyObject * static PyObject *
font_getlength(FontObject *self, PyObject *args) { text_getlength(FontFamily *family, PyObject *args) {
int length; /* length along primary axis, in 26.6 precision */ int length; /* length along primary axis, in 26.6 precision */
GlyphInfo *glyph_info = NULL; /* computed text layout */ GlyphInfo *glyph_info = NULL; /* computed text layout */
size_t i, count; /* glyph_info index and length */ size_t i, count; /* glyph_info index and length */
@ -535,7 +840,8 @@ font_getlength(FontObject *self, PyObject *args) {
mask = mode && strcmp(mode, "1") == 0; mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0; color = mode && strcmp(mode, "RGBA") == 0;
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -557,9 +863,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length); return PyLong_FromLong(length);
} }
static PyObject *
font_getlength(FontObject *self, PyObject *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->data, args);
}
static int static int
bounding_box_and_anchors( bounding_box_and_anchors(
FT_Face face, FontFamily *family,
const char *anchor, const char *anchor,
int horizontal_dir, int horizontal_dir,
GlyphInfo *glyph_info, GlyphInfo *glyph_info,
@ -576,6 +898,7 @@ bounding_box_and_anchors(
int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ 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 x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
int error; int error;
FT_Face face;
FT_Glyph glyph; FT_Glyph glyph;
FT_BBox bbox; /* glyph bounding box */ FT_BBox bbox; /* glyph bounding box */
size_t i; /* glyph_info index */ size_t i; /* glyph_info index */
@ -585,8 +908,11 @@ bounding_box_and_anchors(
* - pen line, i.e. 0 to `advanced` along primary axis * - pen line, i.e. 0 to `advanced` along primary axis
* this means point (0, 0) is part of the text bounding box * this means point (0, 0) is part of the text bounding box
*/ */
face = NULL;
position = x_min = x_max = y_min = y_max = 0; position = x_min = x_max = y_min = y_max = 0;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
face = glyph_info[i].face;
if (horizontal_dir) { if (horizontal_dir) {
px = PIXEL(position + glyph_info[i].x_offset); px = PIXEL(position + glyph_info[i].x_offset);
py = PIXEL(glyph_info[i].y_offset); py = PIXEL(glyph_info[i].y_offset);
@ -666,15 +992,16 @@ bounding_box_and_anchors(
} }
switch (anchor[1]) { switch (anchor[1]) {
case 'a': // ascender case 'a': // ascender
y_anchor = PIXEL(face->size->metrics.ascender); // this should be consistent with getmetrics()
y_anchor = PIXEL(family_getascender(family));
break; break;
case 't': // top case 't': // top
y_anchor = y_max; y_anchor = y_max;
break; break;
case 'm': // middle (ascender + descender) / 2 case 'm': // middle (ascender + descender) / 2
// this should be consistent with getmetrics()
y_anchor = PIXEL( y_anchor = PIXEL(
(face->size->metrics.ascender + face->size->metrics.descender) / (family_getascender(family) + family_getdescender(family)) / 2
2
); );
break; break;
case 's': // horizontal baseline case 's': // horizontal baseline
@ -684,7 +1011,8 @@ bounding_box_and_anchors(
y_anchor = y_min; y_anchor = y_min;
break; break;
case 'd': // descender case 'd': // descender
y_anchor = PIXEL(face->size->metrics.descender); // this should be consistent with getmetrics()
y_anchor = PIXEL(family_getdescender(family));
break; break;
default: default:
goto bad_anchor; goto bad_anchor;
@ -736,7 +1064,7 @@ bad_anchor:
} }
static PyObject * static PyObject *
font_getsize(FontObject *self, PyObject *args) { text_getsize(FontFamily *family, PyObject *args) {
int width, height, x_offset, y_offset; int width, height, x_offset, y_offset;
int load_flags; /* FreeType load_flags parameter */ int load_flags; /* FreeType load_flags parameter */
int error; int error;
@ -765,7 +1093,7 @@ font_getsize(FontObject *self, PyObject *args) {
mask = mode && strcmp(mode, "1") == 0; mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0; color = mode && strcmp(mode, "RGBA") == 0;
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -779,7 +1107,7 @@ font_getsize(FontObject *self, PyObject *args) {
} }
error = bounding_box_and_anchors( error = bounding_box_and_anchors(
self->face, family,
anchor, anchor,
horizontal_dir, horizontal_dir,
glyph_info, glyph_info,
@ -802,12 +1130,29 @@ font_getsize(FontObject *self, PyObject *args) {
} }
static PyObject * static PyObject *
font_render(FontObject *self, PyObject *args) { font_getsize(FontObject *self, PyObject *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->data, args);
}
static PyObject *
text_render(FontFamily *family, PyObject *args) {
int x, y; /* pen position, in 26.6 precision */ int x, y; /* pen position, in 26.6 precision */
int px, py; /* position of current glyph, in pixels */ int px, py; /* position of current glyph, in pixels */
int x_min, y_max; /* text offset in 26.6 precision */ int x_min, y_max; /* text offset in 26.6 precision */
int load_flags; /* FreeType load_flags parameter */ int load_flags; /* FreeType load_flags parameter */
int error; int error;
FT_Face face;
FT_Glyph glyph; FT_Glyph glyph;
FT_GlyphSlot glyph_slot; FT_GlyphSlot glyph_slot;
FT_Bitmap bitmap; FT_Bitmap bitmap;
@ -869,19 +1214,22 @@ font_render(FontObject *self, PyObject *args) {
foreground_ink = foreground_ink_long; foreground_ink = foreground_ink_long;
#ifdef FT_COLOR_H #ifdef FT_COLOR_H
if (color) { for (int i = 0; i < family->font_count; i++) {
FT_Color foreground_color; if (color) {
FT_Byte *ink = (FT_Byte *)&foreground_ink; FT_Color foreground_color;
foreground_color.red = ink[0]; FT_Byte *ink = (FT_Byte *)&foreground_ink;
foreground_color.green = ink[1]; foreground_color.red = ink[0];
foreground_color.blue = ink[2]; foreground_color.green = ink[1];
foreground_color.alpha = foreground_color.blue = ink[2];
(FT_Byte)255; /* ink alpha is handled in ImageDraw.text */ foreground_color.alpha =
FT_Palette_Set_Foreground_Color(self->face, foreground_color); (FT_Byte)255; /* ink alpha is handled in ImageDraw.text */
FT_Palette_Set_Foreground_Color(family->faces[i], foreground_color);
}
} }
#endif #endif
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); count = text_layout(string, family, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
@ -897,7 +1245,7 @@ font_render(FontObject *self, PyObject *args) {
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
error = bounding_box_and_anchors( error = bounding_box_and_anchors(
self->face, family,
anchor, anchor,
horizontal_dir, horizontal_dir,
glyph_info, glyph_info,
@ -960,14 +1308,14 @@ font_render(FontObject *self, PyObject *args) {
px = PIXEL(x + glyph_info[i].x_offset); px = PIXEL(x + glyph_info[i].x_offset);
py = PIXEL(y + glyph_info[i].y_offset); py = PIXEL(y + glyph_info[i].y_offset);
error = face = glyph_info[i].face;
FT_Load_Glyph(self->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) { if (error) {
geterror(error); geterror(error);
goto glyph_error; goto glyph_error;
} }
glyph_slot = self->face->glyph; glyph_slot = face->glyph;
bitmap = glyph_slot->bitmap; bitmap = glyph_slot->bitmap;
if (glyph_slot->bitmap_top + py > y_max) { if (glyph_slot->bitmap_top + py > y_max) {
@ -993,13 +1341,14 @@ font_render(FontObject *self, PyObject *args) {
px = PIXEL(x + glyph_info[i].x_offset); px = PIXEL(x + glyph_info[i].x_offset);
py = PIXEL(y + glyph_info[i].y_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) { if (error) {
geterror(error); geterror(error);
goto glyph_error; goto glyph_error;
} }
glyph_slot = self->face->glyph; glyph_slot = face->glyph;
if (stroker != NULL) { if (stroker != NULL) {
error = FT_Get_Glyph(glyph_slot, &glyph); error = FT_Get_Glyph(glyph_slot, &glyph);
if (!error) { if (!error) {
@ -1214,6 +1563,22 @@ glyph_error:
return NULL; return NULL;
} }
static PyObject *
font_render(FontObject *self, PyObject *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->data, args);
}
#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ #if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \
(FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1)
static PyObject * static PyObject *
@ -1545,8 +1910,102 @@ static PyTypeObject Font_Type = {
font_getsetters, /*tp_getset*/ font_getsetters, /*tp_getset*/
}; };
static void
family_dealloc(FontFamilyObject *self) {
int i;
for (i = 0; i < self->data.font_count; i++) {
if (self->data.faces[i]) {
FT_Done_Face(self->data.faces[i]);
}
if (self->font_bytes[i]) {
PyMem_Free(self->font_bytes[i]);
}
}
PyMem_Free(self->data.faces);
PyMem_Free(self->font_bytes);
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 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",
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*/
family_getsetters, /* tp_getset*/
};
static PyMethodDef _functions[] = { 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 static int
@ -1559,6 +2018,7 @@ setup_module(PyObject *m) {
/* Ready object type */ /* Ready object type */
PyType_Ready(&Font_Type); PyType_Ready(&Font_Type);
PyType_Ready(&FontFamily_Type);
if (FT_Init_FreeType(&library)) { if (FT_Init_FreeType(&library)) {
return 0; /* leave it uninitialized */ return 0; /* leave it uninitialized */