diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index 9bfa5a931..a03845ace 100644 Binary files a/Tests/images/test_arabictext_features.png and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index f1a6f7ec6..61174d75f 100644 Binary files a/Tests/images/test_complex_unicode_text.png and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png index 543b174c0..0526233c0 100644 Binary files a/Tests/images/test_complex_unicode_text2.png and b/Tests/images/test_complex_unicode_text2.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png index 42239334d..b30fcd5d8 100644 Binary files a/Tests/images/test_direction_ltr.png and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 966b67d6b..282eed883 100644 Binary files a/Tests/images/test_direction_rtl.png and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png index 825f3213e..52dbf5723 100644 Binary files a/Tests/images/test_direction_ttb.png and b/Tests/images/test_direction_ttb.png differ diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png index 3fa844e9a..4b689c38e 100644 Binary files a/Tests/images/test_direction_ttb_stroke.png and b/Tests/images/test_direction_ttb_stroke.png differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index ca895735c..78bcd951b 100644 Binary files a/Tests/images/test_kerning_features.png and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png index 8daf007b0..c77215318 100644 Binary files a/Tests/images/test_language.png and b/Tests/images/test_language.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png index 664e9929d..89ea648bf 100644 Binary files a/Tests/images/test_ligature_features.png and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png index c156399cd..5888e52e5 100644 Binary files a/Tests/images/test_text.png and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_x_max_and_y_offset.png b/Tests/images/test_x_max_and_y_offset.png index f8bec3e95..21401813f 100644 Binary files a/Tests/images/test_x_max_and_y_offset.png and b/Tests/images/test_x_max_and_y_offset.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png index 2d57890cb..bda2490df 100644 Binary files a/Tests/images/test_y_offset.png and b/Tests/images/test_y_offset.png differ diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png index 3ac326f51..e9cfafb48 100644 Binary files a/Tests/images/variation_adobe.png and b/Tests/images/variation_adobe.png differ diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png index 93cad98ff..ad3a3a960 100644 Binary files a/Tests/images/variation_adobe_axes.png and b/Tests/images/variation_adobe_axes.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index acb0df63c..11ceaf6e6 100644 Binary files a/Tests/images/variation_adobe_name.png and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz.png b/Tests/images/variation_adobe_older_harfbuzz.png index 71b879bc5..5abc907ca 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz.png and b/Tests/images/variation_adobe_older_harfbuzz.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_axes.png b/Tests/images/variation_adobe_older_harfbuzz_axes.png index 9376c1d7b..b39d460f9 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz_axes.png and b/Tests/images/variation_adobe_older_harfbuzz_axes.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png index 9e5fe70e5..2adb517a7 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz_name.png and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ diff --git a/Tests/images/variation_tiny_axes.png b/Tests/images/variation_tiny_axes.png index d06ac7a60..8cb6d1f62 100644 Binary files a/Tests/images/variation_tiny_axes.png and b/Tests/images/variation_tiny_axes.png differ diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png index a0c6ffe3f..69f1550db 100644 Binary files a/Tests/images/variation_tiny_name.png and b/Tests/images/variation_tiny_name.png differ diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 62b6230ae..3e9a7f1cf 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -35,9 +35,24 @@ class TestImageFont: # Freetype has different metrics depending on the version. # (and, other things, but first things first) METRICS = { - (">=2.3", "<2.4"): {"multiline": 30, "textsize": 12, "getters": (13, 16)}, - (">=2.7",): {"multiline": 6.2, "textsize": 2.5, "getters": (12, 16)}, - "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, + (">=2.3", "<2.4"): { + "multiline": 30, + "textsize": 12, + "getters": (13, 16), + "mask": (107, 13), + }, + (">=2.7",): { + "multiline": 6.2, + "textsize": 2.5, + "getters": (12, 16), + "mask": (108, 13), + }, + "Default": { + "multiline": 0.5, + "textsize": 0.5, + "getters": (12, 16), + "mask": (108, 13), + }, } @classmethod @@ -343,7 +358,7 @@ class TestImageFont: mask = transposed_font.getmask(text) # Assert - assert mask.size == (13, 108) + assert mask.size == self.metrics["mask"][::-1] def test_unrotated_transposed_font_get_mask(self): # Arrange @@ -356,7 +371,7 @@ class TestImageFont: mask = transposed_font.getmask(text) # Assert - assert mask.size == (108, 13) + assert mask.size == self.metrics["mask"] def test_free_type_font_get_name(self): # Arrange @@ -400,7 +415,7 @@ class TestImageFont: mask = font.getmask(text) # Assert - assert mask.size == (108, 13) + assert mask.size == self.metrics["mask"] def test_load_path_not_found(self): # Arrange @@ -471,7 +486,8 @@ class TestImageFont: d = ImageDraw.Draw(img) d.text((10, 10), text, font=ttf) - assert_image_similar_tofile(img, target, self.metrics["multiline"]) + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original @@ -728,7 +744,7 @@ class TestImageFont: font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) font.set_variation_by_axes([500, 50]) - self._check_text(font, "Tests/images/variation_adobe_axes.png", 5.1) + self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) font.set_variation_by_axes([100]) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 63f819151..5528108da 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -259,9 +259,11 @@ class FreeTypeFont: :return: (width, height) """ + # vertical offset is added for historical reasons + # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 size, offset = self.font.getsize(text, False, direction, features, language) return ( - size[0] + stroke_width * 2 + offset[0], + size[0] + stroke_width * 2, size[1] + stroke_width * 2 + offset[1], ) diff --git a/src/_imagingft.c b/src/_imagingft.c index e00be58f7..70817002e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -131,8 +131,8 @@ typedef struct { static p_raqm_func p_raqm; -/* round a 26.6 pixel coordinate to the nearest larger integer */ -#define PIXEL(x) ((((x)+63) & -64)>>6) +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x)+32) & -64)>>6) static PyObject* geterror(int code) @@ -585,7 +585,8 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje } (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; - (*glyph_info)[i].y_advance = glyph->metrics.vertAdvance; + // 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; } @@ -609,116 +610,103 @@ text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *featu static PyObject* font_getsize(FontObject* self, PyObject* args) { - int x_position, x_max, x_min, y_max, y_min; + int position; /* pen position along primary axis, in 26.6 precision */ + int advanced; /* pen position along primary axis, 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_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ + int load_flags; /* FreeType load_flags parameter */ + int error; FT_Face face; - int xoffset, yoffset; - int horizontal_dir; - int mask = 0; - int load_flags; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + GlyphInfo *glyph_info = NULL; /* computed text layout */ + 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? */ const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; + PyObject *string; /* calculate size and bearing for a given string */ - PyObject* string; if (!PyArg_ParseTuple(args, "O|izOz:getsize", &string, &mask, &dir, &features, &lang)) { return NULL; } + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + count = text_layout(string, self, dir, features, lang, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; } - face = NULL; - xoffset = yoffset = 0; - x_position = x_max = x_min = y_max = y_min = 0; + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 + * Yifu Yu, 2014-10-15 + */ + load_flags = FT_LOAD_NO_BITMAP; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + /* + * 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++) { - int index, error, offset, x_advanced; - FT_BBox bbox; - FT_Glyph glyph; face = self->face; - index = glyph_info[i].index; - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ - load_flags = FT_LOAD_NO_BITMAP; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; + + if (horizontal_dir) { + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; + } } - error = FT_Load_Glyph(face, index, load_flags); + + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } - if (i == 0) { - if (horizontal_dir) { - if (face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x_position -= xoffset; - } - } else { - if (face->glyph->metrics.vertBearingY < 0) { - yoffset = face->glyph->metrics.vertBearingY; - y_max -= yoffset; - } - } + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + return geterror(error); } - FT_Get_Glyph(face->glyph, &glyph); - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - if (horizontal_dir) { - x_position += glyph_info[i].x_advance; - - x_advanced = x_position; - offset = glyph_info[i].x_advance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) { - x_advanced -= offset; - } - if (x_advanced > x_max) { - x_max = x_advanced; - } - - bbox.yMax += glyph_info[i].y_offset; - bbox.yMin += glyph_info[i].y_offset; - if (bbox.yMax > y_max) { - y_max = bbox.yMax; - } - if (bbox.yMin < y_min) { - y_min = bbox.yMin; - } - - // find max distance of baseline from top - if (face->glyph->metrics.horiBearingY > yoffset) { - yoffset = face->glyph->metrics.horiBearingY; - } - } else { - y_max -= glyph_info[i].y_advance; - - if (i == count - 1) { - // trim end gap from final glyph - int offset; - offset = -glyph_info[i].y_advance - - face->glyph->metrics.height - - face->glyph->metrics.vertBearingY; - if (offset < 0) { - y_max -= offset; - } - } - - if (bbox.xMax > x_max) { - x_max = bbox.xMax; - } - if (i == 0 || bbox.xMin < x_min) { - x_min = bbox.xMin; - } + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); + bbox.xMax += px; + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + bbox.xMin += px; + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + bbox.yMax += py; + if (bbox.yMax > y_max) { + y_max = bbox.yMax; + } + bbox.yMin += py; + if (bbox.yMin < y_min) { + y_min = bbox.yMin; } FT_Done_Glyph(glyph); @@ -729,72 +717,63 @@ font_getsize(FontObject* self, PyObject* args) glyph_info = NULL; } + x_anchor = y_anchor = 0; if (face) { if (horizontal_dir) { - // left bearing - if (xoffset < 0) { - x_max -= xoffset; - } else { - xoffset = 0; - } - - /* difference between the font ascender and the distance of - * the baseline from the top */ - yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); + x_anchor = 0; + y_anchor = PIXEL(self->face->size->metrics.ascender); } else { - // top bearing - if (yoffset < 0) { - y_max -= yoffset; - } else { - yoffset = 0; - } + x_anchor = x_min; + y_anchor = 0; } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x_max - x_min), PIXEL(y_max - y_min), - PIXEL(xoffset), yoffset - ); + (x_max - x_min), (y_max - y_min), + (-x_anchor + x_min), -(-y_anchor + y_max) + ); } static PyObject* font_render(FontObject* self, PyObject* args) { - int x; - unsigned int y; - Imaging im; - int index, error, ascender, horizontal_dir; - int load_flags; - unsigned char *source; + 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_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; - int stroke_width = 0; FT_Stroker stroker = NULL; - FT_Int left; - /* render string into given buffer (the buffer *must* have - the right size, or this will crash) */ - PyObject* string; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t i, count; /* glyph_info index and length */ + int xx, yy; /* pixel offset of current glyph bitmap */ + int x0, x1; /* horizontal bounds of glyph bitmap to copy */ + unsigned int bitmap_y; /* glyph bitmap y index */ + unsigned char *source; /* glyph bitmap source buffer */ + Imaging im; Py_ssize_t id; - int mask = 0; - int temp; - int xx, x0, x1; - int yy; - unsigned int bitmap_y; + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int stroke_width = 0; const char *dir = NULL; const char *lang = NULL; - size_t i, count; - GlyphInfo *glyph_info; - PyObject *features = NULL; + PyObject *features = Py_None; + PyObject* string; + + /* render string into given buffer (the buffer *must* have + the right size, or this will crash) */ if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, &stroke_width)) { return NULL; } - glyph_info = NULL; + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + count = text_layout(string, self, dir, features, lang, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; @@ -813,16 +792,23 @@ font_render(FontObject* self, PyObject* args) } im = (Imaging) id; + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ load_flags = FT_LOAD_NO_BITMAP; if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } - ascender = 0; + /* + * calculate x_min and y_max + * must match font_getsize or there may be clipping! + */ + x = y = x_min = y_max = 0; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); + 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 | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -830,22 +816,30 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; - temp = bitmap.rows - glyph_slot->bitmap_top; - temp -= PIXEL(glyph_info[i].y_offset); - if (temp > ascender) { - ascender = temp; + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; } + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; + } + + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; } + /* set pen position to text origin */ + x = (-x_min + stroke_width) << 6; + y = (-y_max + (-stroke_width)) << 6; + if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; } - x = y = 0; - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; for (i = 0; i < count; i++) { - index = glyph_info[i].index; - error = FT_Load_Glyph(self->face, index, load_flags); + 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); if (error) { return geterror(error); } @@ -867,25 +861,15 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; - left = bitmap_glyph->left; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); } else { bitmap = glyph_slot->bitmap; - left = glyph_slot->bitmap_left; - } - - if (horizontal_dir) { - if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { - x = -glyph_slot->metrics.horiBearingX; - } - xx = PIXEL(x) + left; - xx += PIXEL(glyph_info[i].x_offset) + stroke_width; - } else { - if (glyph_slot->metrics.vertBearingX < 0) { - x = -glyph_slot->metrics.vertBearingX; - } - xx = im->xsize / 2 - bitmap.width / 2; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); } + /* clip glyph bitmap width to target image bounds */ x0 = 0; x1 = bitmap.width; if (xx < 0) { @@ -896,14 +880,8 @@ font_render(FontObject* self, PyObject* args) } source = (unsigned char*) bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { - if (horizontal_dir) { - yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; - } else { - yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; - yy += PIXEL(glyph_info[i].y_offset); - } + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { + /* clip glyph bitmap height to target image bounds */ if (yy >= 0 && yy < im->ysize) { // blend this glyph into the buffer unsigned char *target = im->image8[yy] + xx; @@ -932,7 +910,7 @@ font_render(FontObject* self, PyObject* args) source += bitmap.pitch; } x += glyph_info[i].x_advance; - y -= glyph_info[i].y_advance; + y += glyph_info[i].y_advance; if (stroker != NULL) { FT_Done_Glyph(glyph); }