Merge pull request #7206 from radarhere/text_layout

This commit is contained in:
Hugo van Kemenade 2023-06-13 20:31:24 +03:00 committed by GitHub
commit 8f3ccff8f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 105 deletions

View File

@ -463,6 +463,11 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png") assert_image_equal_tofile(im, "Tests/images/default_font.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
def test_getbbox(font, mode):
assert (0, 4, 12, 16) == font.getbbox("A", mode)
def test_getbbox_empty(font): def test_getbbox_empty(font):
# issue #2614, should not crash. # issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("") assert (0, 0, 0, 0) == font.getbbox("")

View File

@ -26,7 +26,6 @@
# #
import base64 import base64
import math
import os import os
import sys import sys
import warnings import warnings
@ -547,28 +546,23 @@ class FreeTypeFont:
:py:mod:`PIL.Image.core` interface module, and the text offset, the :py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking gap between the starting coordinate and the first marking
""" """
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
if start is None: if start is None:
start = (0, 0) start = (0, 0)
size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) im, size, offset = self.font.render(
offset = offset[0] - stroke_width, offset[1] - stroke_width text,
Image.core.fill,
mode,
direction,
features,
language,
stroke_width,
anchor,
ink,
start[0],
start[1],
Image.MAX_IMAGE_PIXELS,
)
Image._decompression_bomb_check(size) Image._decompression_bomb_check(size)
im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0)
if min(size):
self.font.render(
text,
im.id,
mode,
direction,
features,
language,
stroke_width,
ink,
start[0],
start[1],
)
return im, offset return im, offset
def font_variant( def font_variant(

View File

@ -549,73 +549,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length); return PyLong_FromLong(length);
} }
static PyObject * static int
font_getsize(FontObject *self, PyObject *args) { bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) {
int position; /* pen position along primary axis, in 26.6 precision */ int position; /* pen position along primary axis, in 26.6 precision */
int advanced; /* pen position along primary axis, in pixels */ int advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */ int px, py; /* position of current glyph, in pixels */
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 load_flags; /* FreeType load_flags parameter */
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 */
GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i; /* glyph_info index */
size_t i, count; /* glyph_info index and length */
int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
/* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(
args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
return NULL;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
if (anchor == NULL) {
anchor = horizontal_dir ? "la" : "lt";
}
if (strlen(anchor) != 2) {
goto bad_anchor;
}
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) {
return NULL;
}
load_flags = FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
/* /*
* text bounds are given by: * text bounds are given by:
* - bounding boxes of individual glyphs * - bounding boxes of individual glyphs
* - 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 = self->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);
@ -638,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); error = FT_Load_Glyph(face, glyph_info[i].index, load_flags);
if (error) { if (error) {
return geterror(error); geterror(error);
return 1;
} }
error = FT_Get_Glyph(face->glyph, &glyph); error = FT_Get_Glyph(face->glyph, &glyph);
if (error) { if (error) {
return geterror(error); geterror(error);
return 1;
} }
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox);
@ -667,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) {
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }
if (glyph_info) { if (anchor == NULL) {
PyMem_Free(glyph_info); anchor = horizontal_dir ? "la" : "lt";
glyph_info = NULL; }
if (strlen(anchor) != 2) {
goto bad_anchor;
} }
x_anchor = y_anchor = 0; x_anchor = y_anchor = 0;
if (face) { if (count) {
if (horizontal_dir) { if (horizontal_dir) {
switch (anchor[0]) { switch (anchor[0]) {
case 'l': // left case 'l': // left
@ -691,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) {
} }
switch (anchor[1]) { switch (anchor[1]) {
case 'a': // ascender case 'a': // ascender
y_anchor = PIXEL(self->face->size->metrics.ascender); y_anchor = PIXEL(face->size->metrics.ascender);
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
y_anchor = PIXEL( y_anchor = PIXEL(
(self->face->size->metrics.ascender + (face->size->metrics.ascender +
self->face->size->metrics.descender) / face->size->metrics.descender) /
2); 2);
break; break;
case 's': // horizontal baseline case 's': // horizontal baseline
@ -709,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) {
y_anchor = y_min; y_anchor = y_min;
break; break;
case 'd': // descender case 'd': // descender
y_anchor = PIXEL(self->face->size->metrics.descender); y_anchor = PIXEL(face->size->metrics.descender);
break; break;
default: default:
goto bad_anchor; goto bad_anchor;
@ -749,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) {
} }
} }
} }
*width = x_max - x_min;
return Py_BuildValue( *height = y_max - y_min;
"(ii)(ii)", *x_offset = -x_anchor + x_min;
(x_max - x_min), *y_offset = -(-y_anchor + y_max);
(y_max - y_min), return 0;
(-x_anchor + x_min),
-(-y_anchor + y_max));
bad_anchor: bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
return NULL; return 1;
}
static PyObject *
font_getsize(FontObject *self, PyObject *args) {
int width, height, x_offset, y_offset;
int load_flags; /* FreeType load_flags parameter */
int error;
GlyphInfo *glyph_info = NULL; /* computed text layout */
size_t count; /* glyph_info length */
int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
/* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(
args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
return NULL;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) {
return NULL;
}
load_flags = FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
if (glyph_info) {
PyMem_Free(glyph_info);
glyph_info = NULL;
}
if (error) {
return NULL;
}
return Py_BuildValue(
"(ii)(ii)",
width,
height,
x_offset,
y_offset);
} }
static PyObject * static PyObject *
@ -783,6 +796,7 @@ font_render(FontObject *self, PyObject *args) {
unsigned int bitmap_y; /* glyph bitmap y index */ unsigned int bitmap_y; /* glyph bitmap y index */
unsigned char *source; /* glyph bitmap source buffer */ unsigned char *source; /* glyph bitmap source buffer */
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
PyObject *image;
Imaging im; Imaging im;
Py_ssize_t id; Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
@ -793,27 +807,34 @@ font_render(FontObject *self, PyObject *args) {
const char *mode = NULL; const char *mode = NULL;
const char *dir = NULL; const char *dir = NULL;
const char *lang = NULL; const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None; PyObject *features = Py_None;
PyObject *string; PyObject *string;
PyObject *fill;
float x_start = 0; float x_start = 0;
float y_start = 0; float y_start = 0;
int width, height, x_offset, y_offset;
int horizontal_dir; /* is primary axis horizontal? */
PyObject *max_image_pixels = Py_None;
/* render string into given buffer (the buffer *must* have /* render string into given buffer (the buffer *must* have
the right size, or this will crash) */ the right size, or this will crash) */
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"On|zzOziLff:render", "OO|zzOzizLffO:render",
&string, &string,
&id, &fill,
&mode, &mode,
&dir, &dir,
&features, &features,
&lang, &lang,
&stroke_width, &stroke_width,
&anchor,
&foreground_ink_long, &foreground_ink_long,
&x_start, &x_start,
&y_start)) { &y_start,
&max_image_pixels)) {
return NULL; return NULL;
} }
@ -839,8 +860,41 @@ font_render(FontObject *self, PyObject *args) {
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
return NULL; return NULL;
} }
if (count == 0) {
Py_RETURN_NONE; load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
if (error) {
PyMem_Del(glyph_info);
return NULL;
}
width += stroke_width * 2 + ceil(x_start);
height += stroke_width * 2 + ceil(y_start);
if (max_image_pixels != Py_None) {
if (width * height > PyLong_AsLong(max_image_pixels) * 2) {
PyMem_Del(glyph_info);
return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0);
}
}
image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
im = (Imaging)id;
x_offset -= stroke_width;
y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info);
return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
} }
if (stroke_width) { if (stroke_width) {
@ -857,15 +911,6 @@ font_render(FontObject *self, PyObject *args) {
0); 0);
} }
im = (Imaging)id;
load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
/* /*
* calculate x_min and y_max * calculate x_min and y_max
* must match font_getsize or there may be clipping! * must match font_getsize or there may be clipping!
@ -1062,7 +1107,7 @@ font_render(FontObject *self, PyObject *args) {
} }
FT_Stroker_Done(stroker); FT_Stroker_Done(stroker);
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
Py_RETURN_NONE; return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
glyph_error: glyph_error:
if (stroker != NULL) { if (stroker != NULL) {