From e8dcbff64e5588afa17e95b89884d161cd9ee97d Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 14 Apr 2020 01:23:53 +0200 Subject: [PATCH 01/10] horizontal text - cleanup and bugfixes --- src/_imagingft.c | 68 +++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index e00be58f783..fea767fa15c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -658,9 +658,9 @@ font_getsize(FontObject* self, PyObject* args) if (i == 0) { if (horizontal_dir) { - if (face->glyph->metrics.horiBearingX < 0) { - xoffset = face->glyph->metrics.horiBearingX; - x_position -= xoffset; + if (face->glyph->metrics.horiBearingX + glyph_info[i].x_offset < 0) { + x_min = x_position = face->glyph->metrics.horiBearingX + + glyph_info[i].x_offset; } } else { if (face->glyph->metrics.vertBearingY < 0) { @@ -677,6 +677,7 @@ font_getsize(FontObject* self, PyObject* args) x_advanced = x_position; offset = glyph_info[i].x_advance - + glyph_info[i].x_offset - face->glyph->metrics.width - face->glyph->metrics.horiBearingX; if (offset < 0) { @@ -694,11 +695,6 @@ font_getsize(FontObject* self, PyObject* args) 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; @@ -731,16 +727,8 @@ font_getsize(FontObject* self, PyObject* args) 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); + xoffset = x_min; + yoffset = self->face->size->metrics.ascender - y_max; } else { // top bearing if (yoffset < 0) { @@ -754,7 +742,7 @@ font_getsize(FontObject* self, PyObject* args) return Py_BuildValue( "(ii)(ii)", PIXEL(x_max - x_min), PIXEL(y_max - y_min), - PIXEL(xoffset), yoffset + PIXEL(xoffset), PIXEL(yoffset) ); } @@ -764,7 +752,7 @@ font_render(FontObject* self, PyObject* args) int x; unsigned int y; Imaging im; - int index, error, ascender, horizontal_dir; + int index, error, baseline, horizontal_dir; int load_flags; unsigned char *source; FT_Glyph glyph; @@ -819,7 +807,8 @@ font_render(FontObject* self, PyObject* args) load_flags |= FT_LOAD_TARGET_MONO; } - ascender = 0; + x = y = baseline = 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 | FT_LOAD_RENDER); @@ -830,10 +819,25 @@ 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 (horizontal_dir) { + temp = glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset; + if (temp > baseline) { + baseline = temp; + } + + if (i == 0) { + temp = glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset; + if (temp < 0) { + x = -temp; + } + } + } else { + // TODO + // baseline was called ascender and measured the descender + temp = bitmap.rows - glyph_slot->bitmap_top; + temp -= PIXEL(glyph_info[i].y_offset); + if (temp > baseline) + baseline = temp; } } @@ -841,8 +845,6 @@ font_render(FontObject* self, PyObject* args) 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); @@ -874,11 +876,7 @@ font_render(FontObject* self, PyObject* args) } 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; + xx = PIXEL(x + glyph_info[i].x_offset) + left + stroke_width; } else { if (glyph_slot->metrics.vertBearingX < 0) { x = -glyph_slot->metrics.vertBearingX; @@ -898,10 +896,10 @@ 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; + yy = PIXEL(baseline - glyph_slot->metrics.horiBearingY - glyph_info[i].y_offset) + + stroke_width + bitmap_y; } else { - yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; + yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + baseline; yy += PIXEL(glyph_info[i].y_offset); } if (yy >= 0 && yy < im->ysize) { From cee61d762218bcca1f30a7fd4c6f2abc6bd45fb6 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 14 Apr 2020 03:16:32 +0200 Subject: [PATCH 02/10] vertical text - cleanup and bugfixes --- src/_imagingft.c | 108 +++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index fea767fa15c..a8bb018879d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -609,7 +609,8 @@ 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, advanced; + int x_max, x_min, y_max, y_min; FT_Face face; int xoffset, yoffset; int horizontal_dir; @@ -634,12 +635,10 @@ font_getsize(FontObject* self, PyObject* args) } face = NULL; - xoffset = yoffset = 0; - x_position = x_max = x_min = y_max = y_min = 0; - + position = x_max = x_min = y_max = y_min = 0; horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; for (i = 0; i < count; i++) { - int index, error, offset, x_advanced; + int index, error, offset; FT_BBox bbox; FT_Glyph glyph; face = self->face; @@ -657,15 +656,16 @@ font_getsize(FontObject* self, PyObject* args) } if (i == 0) { + // adjust start position if glyph bearing extends before origin if (horizontal_dir) { if (face->glyph->metrics.horiBearingX + glyph_info[i].x_offset < 0) { - x_min = x_position = face->glyph->metrics.horiBearingX + - glyph_info[i].x_offset; + x_min = position = face->glyph->metrics.horiBearingX + + glyph_info[i].x_offset; } } else { - if (face->glyph->metrics.vertBearingY < 0) { - yoffset = face->glyph->metrics.vertBearingY; - y_max -= yoffset; + if (face->glyph->metrics.vertBearingY + glyph_info[i].y_offset > 0) { + y_max = position = face->glyph->metrics.vertBearingY + + glyph_info[i].y_offset; } } } @@ -673,18 +673,19 @@ font_getsize(FontObject* self, PyObject* args) 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; + position += glyph_info[i].x_advance; - x_advanced = x_position; + advanced = position; + // adjust glyph end position if bearing extends past advanced point offset = glyph_info[i].x_advance - glyph_info[i].x_offset - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; + face->glyph->metrics.horiBearingX - + face->glyph->metrics.width; if (offset < 0) { - x_advanced -= offset; + advanced -= offset; } - if (x_advanced > x_max) { - x_max = x_advanced; + if (advanced > x_max) { + x_max = advanced; } bbox.yMax += glyph_info[i].y_offset; @@ -696,23 +697,27 @@ font_getsize(FontObject* self, PyObject* args) y_min = bbox.yMin; } } 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; - } + position += glyph_info[i].y_advance; + + advanced = position; + // adjust glyph end position if bearing extends past advanced point + offset = glyph_info[i].y_advance - + glyph_info[i].y_offset - + face->glyph->metrics.vertBearingY + + face->glyph->metrics.height; + if (offset > 0) { + advanced -= offset; + } + if (advanced < y_min) { + y_min = advanced; } + bbox.xMax += glyph_info[i].x_offset; + bbox.xMin += glyph_info[i].x_offset; if (bbox.xMax > x_max) { x_max = bbox.xMax; } - if (i == 0 || bbox.xMin < x_min) { + if (bbox.xMin < x_min) { x_min = bbox.xMin; } } @@ -730,12 +735,8 @@ font_getsize(FontObject* self, PyObject* args) xoffset = x_min; yoffset = self->face->size->metrics.ascender - y_max; } else { - // top bearing - if (yoffset < 0) { - y_max -= yoffset; - } else { - yoffset = 0; - } + xoffset = 0; + yoffset = -y_max; } } @@ -749,8 +750,7 @@ font_getsize(FontObject* self, PyObject* args) static PyObject* font_render(FontObject* self, PyObject* args) { - int x; - unsigned int y; + int x, y; Imaging im; int index, error, baseline, horizontal_dir; int load_flags; @@ -761,15 +761,13 @@ font_render(FontObject* self, PyObject* args) 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; Py_ssize_t id; int mask = 0; int temp; - int xx, x0, x1; - int yy; + int xx, x0, x1, yy; unsigned int bitmap_y; const char *dir = NULL; const char *lang = NULL; @@ -832,12 +830,17 @@ font_render(FontObject* self, PyObject* args) } } } else { - // TODO - // baseline was called ascender and measured the descender - temp = bitmap.rows - glyph_slot->bitmap_top; - temp -= PIXEL(glyph_info[i].y_offset); - if (temp > baseline) + temp = -(glyph_slot->metrics.vertBearingX + glyph_info[i].x_offset); + if (temp > baseline) { baseline = temp; + } + + if (i == 0) { + temp = glyph_slot->metrics.vertBearingY + glyph_info[i].y_offset; + if (temp > 0) { + y = -temp; + } + } } } @@ -869,19 +872,14 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; - left = bitmap_glyph->left; } else { bitmap = glyph_slot->bitmap; - left = glyph_slot->bitmap_left; } if (horizontal_dir) { - xx = PIXEL(x + glyph_info[i].x_offset) + left + stroke_width; + xx = PIXEL(x + glyph_slot->metrics.horiBearingX + 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 = PIXEL(baseline + glyph_slot->metrics.vertBearingX + glyph_info[i].x_offset) + stroke_width; } x0 = 0; @@ -899,8 +897,8 @@ font_render(FontObject* self, PyObject* args) yy = PIXEL(baseline - glyph_slot->metrics.horiBearingY - glyph_info[i].y_offset) + stroke_width + bitmap_y; } else { - yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + baseline; - yy += PIXEL(glyph_info[i].y_offset); + yy = PIXEL(-(y + glyph_slot->metrics.vertBearingY + glyph_info[i].y_offset)) + + stroke_width + bitmap_y; } if (yy >= 0 && yy < im->ysize) { // blend this glyph into the buffer @@ -930,7 +928,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); } From e3450d1f6eedf89e418b70a11bb16456d1867c2c Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 14 Apr 2020 10:07:28 +0200 Subject: [PATCH 03/10] text bugfixes --- src/_imagingft.c | 102 ++++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index a8bb018879d..3baf0a939f7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -655,27 +655,18 @@ font_getsize(FontObject* self, PyObject* args) return geterror(error); } - if (i == 0) { - // adjust start position if glyph bearing extends before origin - if (horizontal_dir) { - if (face->glyph->metrics.horiBearingX + glyph_info[i].x_offset < 0) { - x_min = position = face->glyph->metrics.horiBearingX + - glyph_info[i].x_offset; - } - } else { - if (face->glyph->metrics.vertBearingY + glyph_info[i].y_offset > 0) { - y_max = position = face->glyph->metrics.vertBearingY + - glyph_info[i].y_offset; - } - } - } - FT_Get_Glyph(face->glyph, &glyph); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); if (horizontal_dir) { + // adjust start position if glyph bearing extends before origin + offset = position + face->glyph->metrics.horiBearingX + glyph_info[i].x_offset; + if (offset < x_min) { + x_min = offset; + } + position += glyph_info[i].x_advance; - advanced = position; + // adjust glyph end position if bearing extends past advanced point offset = glyph_info[i].x_advance - glyph_info[i].x_offset - @@ -697,13 +688,25 @@ font_getsize(FontObject* self, PyObject* args) y_min = bbox.yMin; } } else { - position += glyph_info[i].y_advance; + // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal + // bearings even for vertical text and adjusts offsets to compensate as follows: + // hb-ft.cc: hb_ft_get_glyph_v_origin(): + // x_offset += horiBearingX - vertBearingX; + // y_offset += horiBearingY - (-vertBearingY); + // adjust start position if glyph bearing extends before origin + offset = position + face->glyph->metrics.horiBearingY + glyph_info[i].y_offset; + if (offset > y_max) { + y_max = offset; + } + + position += glyph_info[i].y_advance; advanced = position; + // adjust glyph end position if bearing extends past advanced point offset = glyph_info[i].y_advance - - glyph_info[i].y_offset - - face->glyph->metrics.vertBearingY + + glyph_info[i].y_offset - + face->glyph->metrics.horiBearingY + face->glyph->metrics.height; if (offset > 0) { advanced -= offset; @@ -732,7 +735,7 @@ font_getsize(FontObject* self, PyObject* args) if (face) { if (horizontal_dir) { - xoffset = x_min; + xoffset = 0; yoffset = self->face->size->metrics.ascender - y_max; } else { xoffset = 0; @@ -750,9 +753,9 @@ font_getsize(FontObject* self, PyObject* args) static PyObject* font_render(FontObject* self, PyObject* args) { - int x, y; + int x, y, baseline, offset; Imaging im; - int index, error, baseline, horizontal_dir; + int index, error, horizontal_dir; int load_flags; unsigned char *source; FT_Glyph glyph; @@ -805,7 +808,7 @@ font_render(FontObject* self, PyObject* args) load_flags |= FT_LOAD_TARGET_MONO; } - x = y = baseline = 0; + x = y = baseline = offset = 0; horizontal_dir = (dir && strcmp(dir, "ttb") == 0) ? 0 : 1; for (i = 0; i < count; i++) { index = glyph_info[i].index; @@ -817,33 +820,44 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; + // compute baseline and adjust start position if glyph bearing extends before origin if (horizontal_dir) { temp = glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset; if (temp > baseline) { baseline = temp; } - if (i == 0) { - temp = glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset; - if (temp < 0) { - x = -temp; - } + temp = x + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset; + if (temp < offset) { + offset = temp; } + x += glyph_info[i].x_advance; } else { - temp = -(glyph_slot->metrics.vertBearingX + glyph_info[i].x_offset); + // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal + // bearings even for vertical text and adjusts offsets to compensate as follows: + // hb-ft.cc: hb_ft_get_glyph_v_origin(): + // x_offset += horiBearingX - vertBearingX; + // y_offset += horiBearingY - (-vertBearingY); + + temp = -(glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); if (temp > baseline) { baseline = temp; } - if (i == 0) { - temp = glyph_slot->metrics.vertBearingY + glyph_info[i].y_offset; - if (temp > 0) { - y = -temp; - } + temp = y + glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset; + if (temp > offset) { + offset = temp; } + y += glyph_info[i].y_advance; } } + if (horizontal_dir) { + x = -offset; + } else { + y = -offset; + } + if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; } @@ -876,10 +890,15 @@ font_render(FontObject* self, PyObject* args) bitmap = glyph_slot->bitmap; } + // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal + // bearings even for vertical text and adjusts offsets to compensate as follows: + // hb-ft.cc: hb_ft_get_glyph_v_origin(): + // x_offset += horiBearingX - vertBearingX; + // y_offset += horiBearingY - (-vertBearingY); if (horizontal_dir) { - xx = PIXEL(x + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset) + stroke_width; + xx = PIXEL(x + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); } else { - xx = PIXEL(baseline + glyph_slot->metrics.vertBearingX + glyph_info[i].x_offset) + stroke_width; + xx = PIXEL(baseline + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); } x0 = 0; @@ -893,12 +912,15 @@ font_render(FontObject* self, PyObject* args) source = (unsigned char*) bitmap.buffer; for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { + // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal + // bearings even for vertical text and adjusts offsets to compensate as follows: + // hb-ft.cc: hb_ft_get_glyph_v_origin(): + // x_offset += horiBearingX - vertBearingX; + // y_offset += horiBearingY - (-vertBearingY); if (horizontal_dir) { - yy = PIXEL(baseline - glyph_slot->metrics.horiBearingY - glyph_info[i].y_offset) + - stroke_width + bitmap_y; + yy = PIXEL(baseline - glyph_slot->metrics.horiBearingY - glyph_info[i].y_offset) + bitmap_y; } else { - yy = PIXEL(-(y + glyph_slot->metrics.vertBearingY + glyph_info[i].y_offset)) + - stroke_width + bitmap_y; + yy = PIXEL(-(y + glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset)) + bitmap_y; } if (yy >= 0 && yy < im->ysize) { // blend this glyph into the buffer From 5d57261b9c5085e060a29dfc6f54c70ac92ae559 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Apr 2020 22:44:28 +0200 Subject: [PATCH 04/10] fix text clipping due to rounding (cherry picked from commit 35500aad08fb18a904f326df2ce8c59ae6413801) --- src/_imagingft.c | 199 +++++++++++++++++++---------------------------- 1 file changed, 80 insertions(+), 119 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 3baf0a939f7..27af444545f 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -612,7 +612,7 @@ font_getsize(FontObject* self, PyObject* args) int position, advanced; int x_max, x_min, y_max, y_min; FT_Face face; - int xoffset, yoffset; + int x_anchor, y_anchor; int horizontal_dir; int mask = 0; int load_flags; @@ -621,6 +621,7 @@ font_getsize(FontObject* self, PyObject* args) size_t i, count; GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; + FT_Vector pen; /* calculate size and bearing for a given string */ @@ -643,6 +644,29 @@ font_getsize(FontObject* self, PyObject* args) FT_Glyph glyph; face = self->face; index = glyph_info[i].index; + + if (horizontal_dir) { + pen.x = position + glyph_info[i].x_offset; + pen.y = glyph_info[i].y_offset; + + position += glyph_info[i].x_advance; + advanced = PIXEL(position); + if (advanced > x_max) { + x_max = advanced; + } + } else { + pen.x = glyph_info[i].x_offset; + pen.y = position + glyph_info[i].y_offset; + + position += glyph_info[i].y_advance; + advanced = PIXEL(position); + if (advanced < y_min) { + y_min = advanced; + } + } + + FT_Set_Transform(face, NULL, &pen); + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 * Yifu Yu, 2014-10-15 */ @@ -655,74 +679,24 @@ font_getsize(FontObject* self, PyObject* args) return geterror(error); } - FT_Get_Glyph(face->glyph, &glyph); - FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); - if (horizontal_dir) { - // adjust start position if glyph bearing extends before origin - offset = position + face->glyph->metrics.horiBearingX + glyph_info[i].x_offset; - if (offset < x_min) { - x_min = offset; - } - - position += glyph_info[i].x_advance; - advanced = position; - - // adjust glyph end position if bearing extends past advanced point - offset = glyph_info[i].x_advance - - glyph_info[i].x_offset - - face->glyph->metrics.horiBearingX - - face->glyph->metrics.width; - if (offset < 0) { - advanced -= offset; - } - if (advanced > x_max) { - x_max = 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; - } - } else { - // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal - // bearings even for vertical text and adjusts offsets to compensate as follows: - // hb-ft.cc: hb_ft_get_glyph_v_origin(): - // x_offset += horiBearingX - vertBearingX; - // y_offset += horiBearingY - (-vertBearingY); - - // adjust start position if glyph bearing extends before origin - offset = position + face->glyph->metrics.horiBearingY + glyph_info[i].y_offset; - if (offset > y_max) { - y_max = offset; - } + error = FT_Get_Glyph(face->glyph, &glyph); + if (error) { + return geterror(error); + } - position += glyph_info[i].y_advance; - advanced = position; - - // adjust glyph end position if bearing extends past advanced point - offset = glyph_info[i].y_advance - - glyph_info[i].y_offset - - face->glyph->metrics.horiBearingY + - face->glyph->metrics.height; - if (offset > 0) { - advanced -= offset; - } - if (advanced < y_min) { - y_min = advanced; - } + FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); - bbox.xMax += glyph_info[i].x_offset; - bbox.xMin += glyph_info[i].x_offset; - if (bbox.xMax > x_max) { - x_max = bbox.xMax; - } - if (bbox.xMin < x_min) { - x_min = bbox.xMin; - } + if (bbox.xMax > x_max) { + x_max = bbox.xMax; + } + if (bbox.xMin < x_min) { + x_min = bbox.xMin; + } + if (bbox.yMax > y_max) { + y_max = bbox.yMax; + } + if (bbox.yMin < y_min) { + y_min = bbox.yMin; } FT_Done_Glyph(glyph); @@ -733,21 +707,23 @@ font_getsize(FontObject* self, PyObject* args) glyph_info = NULL; } + x_anchor = y_anchor = 0; if (face) { + FT_Set_Transform(face, NULL, NULL); if (horizontal_dir) { - xoffset = 0; - yoffset = self->face->size->metrics.ascender - y_max; + x_anchor = 0; + y_anchor = PIXEL(self->face->size->metrics.ascender); } else { - xoffset = 0; - yoffset = -y_max; + x_anchor = x_min; + y_anchor = 0; } } return Py_BuildValue( "(ii)(ii)", - PIXEL(x_max - x_min), PIXEL(y_max - y_min), - PIXEL(xoffset), PIXEL(yoffset) - ); + (x_max - x_min), (y_max - y_min), + (-x_anchor + x_min), -(-y_anchor + y_max) + ); } static PyObject* @@ -777,6 +753,7 @@ font_render(FontObject* self, PyObject* args) size_t i, count; GlyphInfo *glyph_info; PyObject *features = NULL; + FT_Vector pen; if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, &stroke_width)) { @@ -808,10 +785,15 @@ font_render(FontObject* self, PyObject* args) load_flags |= FT_LOAD_TARGET_MONO; } - x = y = baseline = offset = 0; + pen.x = pen.y = x = y = baseline = offset = 0; horizontal_dir = (dir && strcmp(dir, "ttb") == 0) ? 0 : 1; for (i = 0; i < count; i++) { index = glyph_info[i].index; + + pen.x = x + glyph_info[i].x_offset; + pen.y = y + glyph_info[i].y_offset; + FT_Set_Transform(self->face, NULL, &pen); + error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); @@ -822,40 +804,30 @@ font_render(FontObject* self, PyObject* args) // compute baseline and adjust start position if glyph bearing extends before origin if (horizontal_dir) { - temp = glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset; - if (temp > baseline) { - baseline = temp; + if (glyph_slot->bitmap_top > baseline) { + baseline = glyph_slot->bitmap_top; } - - temp = x + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset; - if (temp < offset) { - offset = temp; + if (glyph_slot->bitmap_left < offset) { + offset = glyph_slot->bitmap_left; } x += glyph_info[i].x_advance; } else { - // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal - // bearings even for vertical text and adjusts offsets to compensate as follows: - // hb-ft.cc: hb_ft_get_glyph_v_origin(): - // x_offset += horiBearingX - vertBearingX; - // y_offset += horiBearingY - (-vertBearingY); - - temp = -(glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); - if (temp > baseline) { - baseline = temp; + if (glyph_slot->bitmap_top > offset) { + offset = glyph_slot->bitmap_top; } - - temp = y + glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset; - if (temp > offset) { - offset = temp; + if (glyph_slot->bitmap_left < baseline) { + baseline = glyph_slot->bitmap_left; } y += glyph_info[i].y_advance; } } if (horizontal_dir) { - x = -offset; + x = (-offset + stroke_width) * 64; + y = (-baseline + (-stroke_width)) * 64; } else { - y = -offset; + x = (-baseline + stroke_width) * 64; + y = (-offset + (-stroke_width)) * 64; } if (stroker == NULL) { @@ -864,6 +836,11 @@ font_render(FontObject* self, PyObject* args) for (i = 0; i < count; i++) { index = glyph_info[i].index; + + pen.x = x + glyph_info[i].x_offset; + pen.y = y + glyph_info[i].y_offset; + FT_Set_Transform(self->face, NULL, &pen); + error = FT_Load_Glyph(self->face, index, load_flags); if (error) { return geterror(error); @@ -886,19 +863,12 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; + xx = bitmap_glyph->left; + yy = -bitmap_glyph->top; } else { bitmap = glyph_slot->bitmap; - } - - // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal - // bearings even for vertical text and adjusts offsets to compensate as follows: - // hb-ft.cc: hb_ft_get_glyph_v_origin(): - // x_offset += horiBearingX - vertBearingX; - // y_offset += horiBearingY - (-vertBearingY); - if (horizontal_dir) { - xx = PIXEL(x + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); - } else { - xx = PIXEL(baseline + glyph_slot->metrics.horiBearingX + glyph_info[i].x_offset); + xx = glyph_slot->bitmap_left; + yy = -glyph_slot->bitmap_top; } x0 = 0; @@ -911,17 +881,7 @@ font_render(FontObject* self, PyObject* args) } source = (unsigned char*) bitmap.buffer; - for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { - // NOTE: harfbuzz (called from raqm) assumes that we are using horizontal - // bearings even for vertical text and adjusts offsets to compensate as follows: - // hb-ft.cc: hb_ft_get_glyph_v_origin(): - // x_offset += horiBearingX - vertBearingX; - // y_offset += horiBearingY - (-vertBearingY); - if (horizontal_dir) { - yy = PIXEL(baseline - glyph_slot->metrics.horiBearingY - glyph_info[i].y_offset) + bitmap_y; - } else { - yy = PIXEL(-(y + glyph_slot->metrics.horiBearingY + glyph_info[i].y_offset)) + bitmap_y; - } + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { if (yy >= 0 && yy < im->ysize) { // blend this glyph into the buffer unsigned char *target = im->image8[yy] + xx; @@ -956,6 +916,7 @@ font_render(FontObject* self, PyObject* args) } } + FT_Set_Transform(self->face, NULL, NULL); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; From ea582a19e30fc92d5140a987a0f8d9980e8bb2b6 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 22 Apr 2020 00:30:28 +0200 Subject: [PATCH 05/10] fix basic layout (cherry picked from commit 132cb7e5a6f892bba99d179d419afec754004e09) --- src/_imagingft.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 27af444545f..ac2b5dabdc6 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -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; } From 54e067779b77d7005cb1110ea8007f07a9c5c673 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 1 Sep 2020 23:29:06 +0200 Subject: [PATCH 06/10] fix and add tests (cherry picked from commit 0b711f10d0490863976699c051f2027b6799d501) (+1 squashed commits) Squashed commits: [9d4e6c17] fix tests --- Tests/images/test_arabictext_features.png | Bin 1435 -> 1912 bytes Tests/images/test_complex_unicode_text.png | Bin 1442 -> 1458 bytes Tests/images/test_complex_unicode_text2.png | Bin 1303 -> 1538 bytes Tests/images/test_direction_ltr.png | Bin 1834 -> 1966 bytes Tests/images/test_direction_rtl.png | Bin 1199 -> 1368 bytes Tests/images/test_direction_ttb.png | Bin 1723 -> 1901 bytes Tests/images/test_direction_ttb_stroke.png | Bin 3749 -> 3747 bytes Tests/images/test_kerning_features.png | Bin 963 -> 1098 bytes Tests/images/test_language.png | Bin 777 -> 804 bytes Tests/images/test_ligature_features.png | Bin 605 -> 720 bytes Tests/images/test_text.png | Bin 1088 -> 1119 bytes Tests/images/test_x_max_and_y_offset.png | Bin 810 -> 800 bytes Tests/images/test_y_offset.png | Bin 1624 -> 1634 bytes Tests/test_imagedraw.py | 2 +- Tests/test_imagefont.py | 15 ++++++++------- src/PIL/ImageFont.py | 3 ++- 16 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index 9bfa5a931cf0abab932dba401a7d5a7c5ed23aaa..cb9a07443b199d9b4490b9fc314516f060ea1c47 100644 GIT binary patch literal 1912 zcmd6o`#;l*1BVyG+_z?OWXujRxy@82w=px9=8{};o0UdX=2f92*V&MiX;iK?l(}?t zSZ9*UXQbPlh@_+mEv4;raqIBy{1@NnhxhCG?fv7!4xo5xs_CnNKp;(TGBF4Q+G6?3 z4?`4wZO5UjTo6db)tg8NPP_hj>U`;eIOLFgnI08RRb}Oc;TYMwFo>{Rjy~fsIFHX~ z_bC-nD=u9LhhuyUG>;9w3^F0vS5!6FVhf%dUj`c+H?#!sXSC~x<0)A4Snj#C*vdxN zC1>IC(zNjGm)8Afu^`KQ>}0cM_I9ducbm*O{7c!VJRA@zI{VIr;kiuWL}4?c>{)ct z8EmV}in1-0O-QcMyLQX{eAw?LEIN=XV)YjszVB~`jMBZMk+lVH7Mhb{p06B_A;u&5 zx-nV*%7TGJ*SmLg@U&m>CRK^yAV zW_)3r76HYe;=J^B#1XIVh=^1z3Ic-mH-7F+xb&c9giuvctlym8qJaES|W1gM}F%oWWOs-|CD~Ljc!V zHFoya*LSH=5khi(v%)bo2Xco3x{3_pXA| zTsLUf;mN;9V5|%8{iy6J1kIYI-Dp}m`P|OR0yg#|#F>l0Jw~xsL7d(jXqMOW)Nsg< zq*PzD47jIMReSR*hD|qs!peQo*im1^mNb(!@93_w^`-Xdt=X~Up7uxhcVR^{# zX$QM+-Uai6hi6YNp~Ebvhg}||pcDj_8x|F4CPAIgYGO4NrrAF>fAu#NH2th4r9U6j zICrdxorbtQ^Hbm+Q?bM6ox^Jw){#aaBD<@nyY2w2D;lzkSU3pJ@n{XJ>QKn%PXvUun_PdRrL(aOpZ-=WfSq=aBO)t1mOo1>^e%gl z%WA_j_Kwb6Z%RjR&a@1LX%L*OnI1}fUBXYN9{oWNX!9TR<5QzwNqx#NV$n-0Lzp(} zg@hsB&OlS9OMuGg-7!qmafMnN1Na3=4ka~x|0l5yUG=A@ebE+%x3%=cF(?49*B}j8 zz<{rEYoomVvhUC%ty0?+3@4yJCRYE3%Md%%v8$qtNDfNTf2sX*{6@lT#)Mt^8;M;yV7vyDP^eISD7`EZrz z2~cszv~!%=H@;l%8x)S@urdhn{>DjPz})vJnjQCQkHjA+Y$T955PCT;C z|2DY_L|vI*ZMB* zAHnLJ28{m=>_&C+iccBsy-@n>tC6$Vbk+GN9}bU=Cf*m|C960cAOs890Dp(M=H8f0 zq~wWNkJ5OHjQFfZbmv0n`#+%#y&70uX78#jG!3zvNI!J$UgiG-a|@fh68f-;j~1lA PiVE`fq!3%(sX6}xlM#rv literal 1435 zcmcIk=~L1P00mPN6BxxKL8(zm+b-NvF(KA#67s;jNIWo2#?@8wWYy6u)U7-wH#3jb zG1JCcfz~rkz#mbW3|4rqY5l5ITNPBijD)pNNS7Wj!EZyAlpG{XQW;X{Pz0Mh~Ua37cr|6%P8~Ie&QNvlMK^-nz zS%01bt#j(|`pITBDs>)g7WG@wCaNEj*+0N3&$SsaT! z*7KY+v>xt+IsLYZsc?Fe~ac z`Qp;N95GrMv+#*LF4o<$%ERIe8ut2(@X5zCKH+ik0$#gIuo-is^^u8!r<3327Rp?R zImJbY^u_nN$@@w(Rn?7NuWXLF*hlEu18*9u;EIpxRgQPl7F2UacGSWlR@$XbVMJeG z6<{|g*4gol>m9e`GiAvI7QmK};`><`zrpH(;R;I4roNjoq!^_vh`-zi%Y?n%z?@`7 zj#v)@Q#DE3rEAS;!$$ZD^Tw68o$Ayc3dw>tVH}$N`}by121A%y3#na+i!%jQEN#Kn zY}wopil!UOC97?h^RkF0D#O~CFro>K+uEHlf!!4KB@T~ZTW8c5e76T23t=GNpzyiv z@3C6C1K29mQxIq8Q67V@T_*>+J>h$kvgY}8z}qipOroM=%KO1JjvuP+#h#~>g*2Tumm7Im>51$8Y8v-I z-D}l6f)4okisB3?G+At3$GWSoomuZ^Z z#$aCXlD7Q1R4S8bSxBvQ&C_$hjjKp;BAt&(}a5U(_wc1iTq~}4_tb-m(0^YJ@ ze9qaPZpU!(?y!MoweN)fFc8Rq-vv(WVXx8tu}R`{)9d5z?jnp%)EOOQ1gJ@W{ zBn1G+*m;g5j0kgNsSDP3@P_;=lbxao>(xG2Dd<3P*<`-Cl>xqPs-wNitye(~O1bl= zH1TnrZo?%vV1M|`qGgpkd{bH~M2-zq3gI|I0na4Q_~!7o{5=O(B4uVP+MzT}hH_ud zA*)5_*^_X*mXwD04T8NDIq_&2ftW;oi!3w{c%Ronc1EG&aREiO31=&-1IoA8&+4EF z)-6nNZ%_VK#>jZ-Ks_Cx01Ba7`~Cg+T>CWTQ11vfK>|8e_+LIMA%pcCriM5@PN}UV zxNg716Oi2!5giEb>FSge(t!fb;lsA6mJxc<$q^2rl2A?1Eo8BDSvn1B^HSINYd)8a z1jf#3?wf>MzGzMx!tMOjns1kcT22mIR1(Z+MALeMK49j_U+=(J)!0qs2XV!2(*y#T zQ8%A+FA(UuItrqNT`jA<4Mn4B5-IQjddoR~Foa_rVXEL&a!jL&`pr-hF#C_sOT-d8 h>dycC&uH!6hPvF7xqAE}Uf(%g2-Vx)i|@g@^e?ZOrF{SZ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index f1a6f7ec61d7d9d096f2a39bf84364e7547b4438..3501b2a347499de2252d35fdbe6e331010d2f548 100644 GIT binary patch delta 1349 zcmV-L1-km83$hE4B!78HL_t(|obB6xtXFj%$MNS2?kzynVk#w9jS>^LDDQGIYs<>k zKUzu~KSJ89^9Tg#~2Ot7v{@=YQ^DwsNkAl!Ru zt!1Y_xF^2f`@LT7_j|u^Wj!Ce`)=oRKA-n_pS$mSpL5RlbALdRBuP4xoi*a#DbNcS zou*udaVhk|#4*R|hF+Li*K#5B!qhQGmfANL)`s6?q&~e+gzt{n=akwUig50j}zYzte$eAw6Vq}p{gYza4wcN$Vlx?1Wz6FwDghI8Q` zunxXoQad%7wSQqd^uY44u^jJIq)}^OCcGAY7d``rVR^}{lBKVt3IzTQ_mo@#?;Y<< zq|s}k2yY9M;ntG3maKwfV?K7^EiezR8*`}t`2iI0+3~vtm0Hz85xyFLC!r(Pde^|t zur1sdmUgaY`r%%<4leIlNa@sSp$NZS0;~-ejQOZ@V1HAX1}kAv$(K8N3%9!DrMk(` zNl~wGEfnD=sIaH^`&?5W@><@dxQ(-|<@BhF@Oa2TWhR+Qb?v79kZyLpcU)9ty%2hR2 z!aV`l2s_{^Sl?7FeegJZ94>|)0JNFl2itt0QI!uP=;UZWQ z<~0o%fW;**!5!iKO?xkZnVkG$^)Z+Uw^W;R;eYHjU}^6=9K`;hAWKSupcJCy71=4 zU6boRkDJug0^DBm9IOg`VQ1J9u7_WYBHeS~4M24%Tf#vA&Ti_t8U6y7SI=_iHtu~r z0DqTNo3jBO$h88(64~~Q*;bs^J zeRX}_!{DJG4ijNwwZ62dmb`tE!ObOg0e_b<^weBCs!m!6yQ`A84c-UesJ@r|p%xwp zkA_FX_An5Nuo%8q4e1LTa-u?tuq&+1V9eux2gBCzSomwz@@%*>LnoY{o$^# zCcIDusfBmeb=w%$hM$Kb9Ih^>*M%bN3HOE_Rq;PleeS9XvIs}RFT;_#w0DM;p?_BW zE3T+JmPM$Jxe^A#(eRVTLV70rI2;Hsgr}>>?|LMlfgVmEB>9pyha|^o`hKpfe$=AZxa(s$VI^(qVJ{{r67FSC^ zr}{ddG-?xo15oS4t4XKSF{r`*@lHdNQ3W}ZU@900000NkvXX Hu0mjf7{ixs delta 1333 zcmV-51RmRO+33#4{U5DgfO zF;R=Av{70Ns3Z^(YhtWyAd$a)p;irAqkuqSEnBS#fz%K}jWN^$Hkc+ZgaR$3ElA7u zAMRi^1jiTMX=neiyZ7!bWzF}K`{tZE=b2}c^URrhGZ!RDl7FO0u4B}rozMlxzfHLe zH3Pcf5Ztwb8 zNe5invUt*w)j$#Mf>kgR?r259d`LG!_H8r;#?h22F--M^aPpVDtkl({Aup|JdHnkNl4XeUQ zO{FI_WPfN8UIE*~mhg1AbfgI{!DS_Hz^}pwM;fpxtQp_BAHL6Yj%yy*!DX-=?uVUl zH7p%G;0?H=WG-9?a{-t#c)-V?H+-|!(-V5*)~BOjP!TSKUxaD!3UtCf8F-=_;t9B| zqyyH38N-gAF}_;b0B6DFRr8~;Iy^eQ>iueX6MsHiaz)8!N`QsKCf)I!a&&7A@Cm96N+$u?E&@Bg?}6>c;6jWD=mV}RXudWr{Vk6a{p)O4OhYX z41AsoMfgXUQ!VtD!-lY_Dykyv3md~@VQ1A-gw5gJupw-#dR_{*Wnf<#(>y4`*3b=q ztc86boKlUsA@qd%!tPLn6`>6-g(B<<>uV`(4Mo@!o(TQno4~AW}^|v*w2|L2>(9PlgUE!e&{NQmK+!bD|cBiMp5?B_Vua()V z@I`n$>oPJ{Dd_C&Ij8q^L0clYN3cpd&QfpFh}^>93V0PdW4c8x3Hg+|8r zL0_$T05-NbBWVn6wDiTUTf#-~xzO!HnlwJ`V-IP>2?t*RExE3>ai#xXZ4JN<=!Msg rb}D000000NkvXXu0mjf+k%6t diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png index 543b174c0a7b5cfd9c65a6089b4a4bdaf1dc5e52..c295321dbf1c8d7f3506856b7af6ed86242d3f1a 100644 GIT binary patch delta 1447 zcmV;Y1z7r*3W5xfBYy?SNkl-iF2nK2+ zHDYQTOrx=0f-$A3RpW_mh={eejhARJhNLYZCH;c6MFsJK+^iOf7gE$>EmaJH z)}0h9!uVnLb!T>$dZ4rCEb#y3eAx5M^UnM1WS}+tIRn zcCabUihW4sweK0kpb-EVb z3VssoUkJ9s;$USk37S)I5KIqV42}a}ANXzXRB%YG$3v>Gk+3nXTf9*I4{>&o1-}YL z!VqYOCxa|FzmU@ezX`HnebABi^JXwL_;R_U;jkHl^Y9o!Lo7v=@m zrM3%#+kaB3WisA=%i}Q4Auts z1t*l_BOwb`mFLD|APb&J!F}Pm;Emwt!NG;JBjFE07TjBC-Z_ij6#xgq1#~qD7*E%k z=O9=Vyd2yS9GV&qg=bUnD}`RpfwjTH;D?2C(SN(r@E!`Sa2||Gv)|X?h+R2CQvW*@ zvS3|#y#0?sxTGAs00QhmvycC(2vo<&#+F)}(rx zV9{R-kq_WafYODBN_tt9cSp9v>Z*H@s((_~LD+fLxxF0RR@l^ky=6Sy4rXHM#()M%{yOC_Ps=f!ASb&h@+D403<4wAZ~j(g4&w2tS6C`pWoQ zZ40OQ=ciPl_FiT{8=TRPwMf~--!sU>`uL~dX_uwUHf#+dPp}|Etx5L@+YOpDIG}r~{IHiWn(Xcky(AP7KJ+#7&!E?bo!G>T}aBKPLL}}ot>uqa&T3mDH znRGpv`yXh9Z97;=0%io?gv)Xk!ZtV=9tg$+6YH~vEA5QN&xEz>{DUzqXMZEC3jR`l zUa@Eb%!NO~N3a!E!h<>E{0d+VNR6Kfch*6{e7GuS6g(C@-B(UCOo5|M#fF#|sG19_n3A?J zv|{3g;!QBVpf|D?il{9@LBtB76$=*3Yas6pE=1rqX)}(5T z$3|@JcriQfIoa9ltl5{@f1ZnZ_Wz%A<}65(q<<68#n2s!P=vR_oiKU4 zOOl$(co#5Q{t@QEa@bPiNAT+~3vR0M!FU%Yoh%N7!{I`h*?3(8Z-&|9U6^#Tc&V`$ zJ?i!J3A^@dqB>zel2c$l{ID9V!8`E&xD+HwlB7X&!R~N7oK}qoZiP+Z!|+Mi6E=lg zn{8pcU{}}?o_`BB!w&|}H4`?6wduQIFc;nq%Tk&1D)=a@2|pZhSeg(T5hmVCH18^4XDP2dorcva_rQp2AgtOq^rGNMpEoYJ1$iv~ua5bzCORI17 zgrd5_Oot+DEj8hdusea)`vr(2nWLa>V$8HFTw)2t5eNRd^yShIQegQuZgpqv0xeJp8$_(6y39Y9o(^p0Fm&9q?BD6MU5VLie2w zVPE)bxPPEDW;(10Mc7n5{gY6HE&%4hlE&4`4)_gp0PtAY9@c~lODAp!+r#Q`Q7JeH zUI{&6MfmA3vqmK>D`({7n7WEe)jiT4WzOb+wTptdGjj3GOp9FWn z8=*IR9`=X7hpuLiRvgbA#d4Oz$IR07^ykB;VRM)jYf9MU};ejyq_)3*dSgkK*^+y)#O*rkCr*8}&gnz{_ z1t!A+crU!*f+;oCdJ`65N0TN;PGu&~B2;NXCfe+z>^S83)w@cQx9DU*=}Ss5g01bqvIgj*z7U1~M} O0000b1locTuY9Ptq{LlH7_gwO`x4H-3MJUZ)Mr6;_-Q_y7B_D zc+J-?{F;;j6}OF1^oo3-;#(h5X$Q&giZ(F)Y1>jV)u=+8L^YpT`|Ht|!8g4ziu6L9 zHA|VsG}EJ-52}sR{rk54J>P5#xw!#D4JBCsOh6(X%))sLt@X~g-O=OmZD14 zQHv_B_arxx=TV zKg%pfZ3H*1G^zp*nO<~S%*Mmx{#a%1sFfvg)1_(%<3s$lq63vOT>k5cdTB9KY)$pd zg_5fLK(2HUQ4dHp$;;km5MZf`nNBCf@zt<9VpFu7EW;|1_GhL}Ls=o;`x9cM1|Up5 zOV(}NlKjcL168DR1hrSl8(~m!(U;|`sBYl%#z!Kh96ZXfL*EXQlt8_aCIa+^DV0IE z8yD*)B{@#2N8mIv!ALzuP8-F2ZXqd?{e^hd;=w6WL5&nfNORk{sZ#z%X%2q81)cBK z?Y3OcPNtNH2h-$4xVgBXLs#*TmnJsZ-yDcn5Q#c#IR}?32&UD@Q>%tKH;#9?H4s+D z-vi%`DatD_3vl~8cue@GjBY@ycGM1H&DHtI%DJUhzOvUa@gfFrztEyZF|QX>opu^M z_g6D(c53%=O1R%>Zr<$<`+LrF?m^A}fDQN0bmt3OsuUA?!eb#$Tf|Kq)y%Q_^F)I; z?xooUX`M(tt~+q)lC2PzjIa-3tlT|qI=|I=5&|$DO7v}!{~?7{-n4G)fb@$l0<7Xb zl%X8XK$BHh82YfIZV%hyU0+!F1U5sTnn0dw1gQ+z$8TxRrPTz!kwtYL7w?Nv6=qzX z>J04|LP)hD1Al19N0D-$*N(8ibetz42bDGxq284Oo(ph=mDEMpr z=*fS$5Dil;;Lk@ftfJS?2PQ?qU38{9@4;l&p>)VjomT&)g;=Z~(h?WS>GQ?PVc+tu zG{p^10}j){m2VJKxH!qQq9yKOAd}`feEg4DLf9t0S~-i!T= z0DkHclzJ@&%=QGsvUhV$Qr<8@f_?s)jb59*PMB!L!g9O*G#BH{!1!K8`mvwKufbAA zs)mHg7@$CzJYQYmF>A1RwylC!ry=`nNtU1X6yf5SxB$GwLb)q>8)s(Mx7`9Z1eDqV zUaAHZY459zA++$u}M9Fw&{($Gmx9(*1Fj+a*FIpFahB~3m`i7rX0O7#k?>W za^4xsLJdeY(E!^H4-R#@Q_*|)9DMQw+_uXS4U>z}V)?#J>SB+r7ek93`yNbpVq?!l zBSl=1(dlEqaz?R~Y$vNsBa((7O9K8IxtQt2ycLmm`gKqJBxMd0aWgtH8s&A*e@^AU zk%9j@rjyWPL9QKDi<-b4?9rNXVg(4CK~wpo*uPKAIy!&ncnVMn_fO1{boYdkut5do zOuebc2M~*Y)E6GcJ+OYhOBMc1@J z98-JDbtmGxVH(^;H>q-Gsn<1)pj;w}Z7|q+_%JV?YOa(8rkdsDoD%=%E1fB+?ZhJ& zGKZ=ZUyyS~&6Ki&vtHDASp9o-`{Q1O@)1()Ml~94u;e00`GtFFf#F6jSC5)kkV}kH z1EiD2b72MW0Onb*Z^Xr;<`Zy>toUL5$ewVjgc|xd$9u__L#tGUh{3UXJMYMxbGYjx zOdh(3#e*1;LRc1$kem4Zn6!i%k;Rk2beWjLXHZcZvLlwJa^+Q}uOfB}KA23Qn66fj z?ada3;@IK8EM6Dd;_FDwWySM?LGy%fSLD0zLLF~Z4Bk->|3ATDJ-Fu9URzDW5%%v7 N1Ki0JlF)^I{eK(Ycv=7e literal 1834 zcmd6o`#al*0><^G4n;dk-Qtu@ogfiZTspQjnprJcmn7;kN`xkzphOy%S?W@)=Cs5$ zHMDBxo*>jc?U;O18C91^Nt-4{i3$s;;yC}sK0mzA`#kUOpZE496jEE$SW`_+P22lh zgrAz)0o(l^rEzdS*^y<%YHI3h-iWUQQkW~k*oF|C(a{g{FTbg3^HYAuL(Sx|E*Mz0 zUe3(Dnx|o{uI*M~$6o&%sMI>4Ms6c0HspB%6kN{9$c!1{@aK~eZtSjlW)ExIpgvdsjpoMn z^njuGBaSBiCgn~ZGo8$MW@ukKi+ZZQMs)Aqc=WMCwCitEbB|&enGV+}zrfM%tD8R9 zWo}jK&50Z)H;n(T+@>kmW&nme2q)p1doc-?dM-~QT?h=ckzn!Mr7&A^5pBSjIVfUP zih^~_gu=cMKM+ImVr8G>Pe%%}D70vB0LTke(Q?t=sjrAJPA2SfN<}6&C?3i|+pR!{k^IQqF1Lb8(W{j<&J|18@||=4{poNXkG~j zJZ`owayC`4}UX&uLoz;M%~!riGq zVV@fLZ#~kvf*_g}&}aE@Q^L0!CL?5pU1voToI;Adh~ICL(qQB9=i27m-9fU)oAX_D zrq2V;Tg|11rqxMS;rHk3&>itx*6`xh4dITJF6%(K&MZ$U!Yai_tQ22DmcV!2OXRQr z{+QPFROGRI>Rgs*VVX-jta0u5Y?74tvj2gv_>;nbZQ^Kxhoj<{69We<^LGlS%ktgE zlcNY$9dKk1Dee$*({oJRG<)G19)T}>`&lx9I8h=h5!xm`TJtc@DdoRw_b~Du?B<-0 z`>8!gmfG9!dlzxGWDNWKO=?WcXlee!7OtzFZ^tPT#J_KO=$_ZObwP49OaiiEUqo^b z4<+ees6FJ$>G@+fpWyb^QFDlluu|Sc(rBZ~l^5-HK}1o9Sr9$jYsWn!HWHTefe}Wf zM;XK=EIJtSC&m1sM?E^x5JJIPpK;FjdC9Fw!G=BKbE)?+D(tYxW#qv;3qlZ6Kv6xX zHxp9sxH|E}jf>e-NoNFfP+O}57uPp*jC8Gcds;=wYs^N=Djj+6O(#3e@8W-Gg4Sl9 z5iUIBriY`43(schY3YO`Nr&~^M*IHNt(f1nG^~&{sHcZQY^f|Ay&0MouUT`H$TycC zI>o>bjNsCqka9uqt6RY~%utkUV8|@A`;2|FPx#BB*%dJb=iCgV|4y8#IFkW1=6i=+ zy7;6FuhMdyZrE|*{1@o_b#2+&jq?SZaa~w0P`CxJ+v>s^Bij>U^fvtBiy4ic=X&}D zB|~oFcI5<|j+x0GSy3*mlaq-@f5C&}K4`eJb5+ay3@lwY5hWS6?9EzTf3_6A`q3E= zPj_~j2A7C>IAU+6W80F9^tjdOq|J-?Hr{er_q|sV!`hKg)#t{-vP`Tqz+`d*&>PSM zrWY9kc2b4{Tk78@YPvOMGF*{b6lx@JYJzI<{P%~iTr(4SmZ6?sak7;QV~Xws(*oB# zDo{NRUIzlD_VRrLjgEr_jQPSk=BqHYne`92xC$bNg0o9IIx9f!TNMf|++%D5DqL+? zDb22bN}Pgvx67*J+u>V=u;qD-Xf1=H3^L!9rX$N@Ro=ACxnUxxb*YGS2i16}IY(ox z+TmwdnC8E$a&bxEMn!@uv8ycvEsxz3Oq{SZH#-45Gwke@82nfn;{T<=Ig6{RD<5OS p&v%HggFwlPP>X*GXz1S2Xu`Xs$B^S|`yZ(0?S(=#eG~rk{{YR=ddUC) diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 966b67d6b6436564ced292ce598f0561824d3b43..7ab6bdc19b6f0df68cb74b7b94af091461ff549d 100644 GIT binary patch delta 1262 zcmVxRpzaQqG_kITEncw@qI}4H|Ns=U;n#Myq3cnUaqpbK`kbe#aX4UKpme+Z$DjG31 zot9GN4nS(^nwL9(I++4flCrIScKMc8Q{aq;rUPvqfh9Z`-0^`1w0?D4R!}V z24nKNRlw81s$f^JA{Ym)>u`iS>P*-I9)fRzUxF>cl3)U~%^TGkUI{kDf5S&L_&w@$ zbyByZ9bslL0Dpi#VeSdt<~4`;LH|s#8`K7=gx51QDR?*Q#-$5P3N9*rp*MqJS;gMa zJ7@=wXT6;YSP=Bi6nntT;84Q5r7tI)FlWJvU|%pJ7{QTjIkscmjy{ytZC{WM)&)bM zEt~^2nVSFI1?gaOFdW)KM;H%#^DbJCeLLC~_6I)))qhY0t>6-<2_|GZS7icT8C(xl z&>mh4(!o-%OjTa@Lppf4#8D+BVZYq5OM-N8Zl<^}NC(xqeI}Kjgy~>z@pL+zgnNSv zvMN)sI@ljn)agyDX*qdk!Ycyc?Rt%!0+q?puj#pccEGaSn$9zsZW)=T|H4so$+G=VN>Y0QCVP$Y%k=9G$4rpJfsYPzF3M%Ut zL7ts?H3tD`20%b1{F>KiYv~K!o-h?|hA)C`!J=SH-r4%t!ARMTlZ0o&)L>VzA-FE9 zz7je^yCS1H!Hi%?Zsn2$;Hlt@OrMv6bwQVU#eZF@1k#1R7dn(aRT`d2=$X(xVRXV8 zs10U%-cx<0fJ@+=;I=}|lY@I8+Sm@o_4K^1%lqmrWA(dq#B6m~!l=ml+HDbyrfm~eNA z8F-@n4Ywq`3_rm^*a%N0yqW;)sq-Y?!c7V9zy|mOw!=3tCSk0iXup>Q6 zUi~$&IY}3(6>xYxk96oj;Mt3&;xF)^M5%^33g_Epce<%Hg1d!(ix@& z-v>K_Ey2QIc)3H8a&&}ra2w1GzN*`^Y+|rHcrSPYdNp!*lVJollW+tllW+tF7}OyD Y0+x(JV0H33p#T5?07*qoM6N<$g2_cdQvd(} delta 1089 zcmV-H1it&&3a<%}BYy;TNklR+v?jW@? zsnZ2WC%Udq7tpTGP8DaGCXSO(>aY$pVc-H-30IHnwnZqyWLOX1hQ9D+xTQX4Dm)Ut z2}i@eupTB?7q+wch?VeeI1u{7{;(}v)oj)1#Ymm81omb4Oz&*(R;ZmO8A*Gwy7|b@ zHPwW`&W5L3(tl+C(IbyGY|<)!lwzdLSPXsPaM%!TfJN=CYI@>Zo%=&!1x$yz@OCJ| z8`ZSipa{Q)yJ05GgthQn`6m1^6yfP`7Muj9!FBLv$zr6=*b$C}3rnsl)r7a!L>562 zeypbL4n=sNe)IKXwOnK5r%;4tqh4x_S&Y;f2f}CNPk&*lx1*=kM5aIy2I`wYsq#&>bFz+2xCo#=%$N)z(MqTDZO8+UG#{x?#zjv0kWD6OIx*7K(6keZsWz zP1ptN;M1Cc+#9YaS&Y=KDezo48uo`}^^cB)y{(VbGoc9Q*9Tt?MOa<^^)lEIzAt$* zSt_kIcz<}DCgF$~a3j1Eim<1Re49`$od>UkRm~n%dg+UBB=m-Js_`i>7(Qryr0#$s z+*2Q%3;RP6KF{zS=|I@>Pc>oXYUKXU5q_}=bv!nOB3#t$(TAZ3m%~yR z412;<09HT|wzfV}m4u!!qb4{9o(zMb2z}v&Fn_;|^2VzPZ)fv&Vak%*YdZc z-C-5XhAx-|8`9*)#U@lauZ1F<-t5sW@O$VD3t?j@LU;J8+DJ`5p{`h0J#)W=z2TE^ zsCwKFho{1sZI?IluUF$wV_MU-6@gt1J8TU_=r7ZRO)Gh%_3MrvlmIq2+^5%iJQQIK zbbrBf;ma@>dcu99?)hFQ!b{;uD8gVk7`BI1a7xMY#;XZ|li{(@6Z*ow@DMD3A{;2w zgqOhP@J;9s2gAGJrWQ4~6O;fhhJnx@Hik=K9?XZuupFMx@FYBE%w1}%h9Z3YUpR1; z&k0}uFO;hPx#jRfi)qsx7B^e+09?`#oPPxO!tY@qEScz{%X=$a53^w!EPy9MUns)O zoi0o#vcE>aG*|`~h0|d&48V6od;~o$Ru*By5Ssv)1oPl(SQ8e)A8_9g+y8;`OKSRV za2Krhn@alksLZ!3VQt$>-VgoNu{jX7hjS;czT>kLwuOD+X!tq2AMW(qTXau diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png index 825f3213ec89e0447754f0aa2feb596fcd19c921..a112eb4c677478705856d652e33d603fde2d07db 100644 GIT binary patch literal 1901 zcmbtVc{tmN7X6`0L(-@UwI9ge7>3d@2og&%omxUIWoWBW zQ9ctzsn8*oVp@Z#5wUa$l~N;9%H#EW@0{3|CDXF%A6b< zg5*kVY@VFc{n}INy2r0yEI9ii+y0z_oFmw-UIq)+w!@&bOf7AY9;riYsfN`kJ8^UefSsa@IknS2s^ee``$?4=}PSFiirGI8gCSB58shreF54*so?mrc)I8LBk$ zuexAzrA(6ZdDm?g#CH!Rm}SM5NYAPI=(xDfY89qxH8F}4)BSmmJjpm+O|$?kAYX5L zsWj!)W6@cv?<{B)A|0?rz2J8@!fKyIbEw?xtA7b~QOF7NZAe$)E-@{y$!RM?pC@Yf^2xtu@e2_MydmIZ-d$(oT z@N5J$piu`--&zbir~deSVX6FZJ;ah6Fa!q<(sgW@j8C2My13~(LLLdxx2Kmq+-0o% zT(N4r_VJ?X5ogvHK719rxI8;kn7_p(lvlc{A5nN9I~@84g&?ZSEoTMijw(FT$U!%EB5R!=cs%zFT>35^+Bs(xDi^ z7Q9Vtkjl*|jdOzX3kV;v34m`0x2lM4d=-_=R)?kzEDSPPNWNp|sa?~(CZSHCT>OcG zi}+#sxyl3^3*OkZu-2IUv*z4N#O@+0%Lp)JlYQQ9uIX`JxmxMJ|LfiH(=E7-%wGpx zSjNj=0ToKc5!s?Z-;3pe6w-OcYYh@oYQYtD?X$C2Ge}5S1584

hRHiUSb8*!d2bmc z!=^@f#{F+?{~Wqc%I9V>jLYeC`pb^xr%QD5*MSJ)Q)i~xl~nD{&5e0Moq|ljAhFfG zfW~igEAb*BpgUMnfW~!Y#q&3AAzNi(-vYvb1oVu;)u#B{ic=yI1L-XHZZzM>q*f{o zo-^pw>JeC)Sgpu}PDc`gDk|F`UP|U6HXr|fk)ZjVpharj(BA@}7`1V7OW)e5OwVSE9gY0s zv--Z~i;%_L?WxkoNFg^OvZ4_SRJ3t!srB$9VxM-aVqw39pN0XY(sJDolZ2}0|1>E} zlJAR=p-CAZMId_Gm>l|%m8T)N0LWi9^A6G)Ns)7t|4J2vNW(I#UHKv%RzI7JcR~K^ z|3e|S3)?Yh*{=Fn5;exJ{7VFmVsM1EgF7(4#Svm-q}efE1tWCXYe{(Ui;jdU{_ZD{ za*=iBX@kr&f^`?sRVNLm?QJm~?v03!%w~dWyFJBz5iQKjX=8daHs zP{^W?75?OyN2pq!P04@nhy&rvS`y2PyYq+;<^26>k&(smH*Jwl*7Be096sj%G&weP zGOqzAF;dnkC{~d9L54rDO*WCvzd`6yIE5xw85x%LAqizOz6tX4mWOG~u%Ce!rhXQM zvGIc~16PF&!dzgEdr#~A`Q40ITIk{NSrG)t(6&E# z64{VAKm8%~khIg)CnGcGd9}$twfTf9pT|a{o}Zf0jH!ut*}AncU4#(}l=@dmuUzl3 zmb~$IcAS=eUBJy-^#ds)W0${2g#g*PxhM2$I^R+q7TMMFBA}(M(Fl&*>qRKH<1xMy zv{Y0OLyw;bnmJD5r#a(==mJ=kd-L-BVjotW;*XmT0O@a6b$;mu`kFrNYr@jSn40_V zx^Kq)kQ2(RGrzOh1gvF06t2BGNekdDVP%N%Ji@SfEN3pGyyW8`S6VbPIhkChnV2Ze*@6YcX$8* literal 1723 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 delta 3627 zcmV+`4%G3Z9i<(RB!6s4L_t(|obB9scvZ#S!13?B*>}i>KmtTI0TGZbB8V786eZNE zXp#1zU_k+WY^(M~tPuC^4laa6Kp+9bo{$BykPwpGdH=X` zIQJ$uB#?7*gU|2toClJ*GjnDp=gyh4%zzL=2qAISX-dj0b=kJ1@SD=4(dppF?dpmu$ayFRC5DuoW?P~Zfto2Wc zTCk@;@%XcpIrR3}bD-cke;4e${B&fs0*Ed#1J+&-z`91HZ4dpS9jt_(tS{Pm_^Gkl z03_z=-)aKb)*paxynmxPpbd00wjMA5E;BwYgR7dgz3geMDewdUhu|3GK_L`D8Jq!N zJV4uHI(l|D&V&N+BE&<2@oyXi)XuxrN&04zpUAG>Fbd||QUjlCXspZNAK(Na`Fs)2 zHP)VUSR#Nr@%zT+$y&=NVl7BbEp2l+0b0^Ejax|H| z%g9fpD3+70DT|^q1mz&Gn!sw34(k+Q<@|0Q@dYI06IVcdKJod)6%bcIV0As^uC}ZA z=V8O%Dge#~lX8HR1C2^C@(V_OabY|8xinTeKt;I4xt@;TXfj#d$nHjVSJsRGntQ}= zLt~u=D2?LVRDUAN2rDPNoUjUlD+#J1paz$d>Oho(kV>7RGK8WyPR3FcM^PMwaTLW- z6iZPY`H37!=14N9TG!dp4*1ae;y1hOI+rqaw!4>@BhrY{hU1&te3%AlK2DT&v*$}c z2A+ma5CIVo2B8oFL1qnO3-u3`A)Jl`U^f(cZ!ZDd`+p9bhLV$FWS#)?Y$2`?wE0OerFdNXk8cmRgM5ZGvcIqUF8W2Jyc zpH+}RVt*ckHtQ|fO`p`(4%D4dQHXh3 zY;Zdi&r?qkeF_wuf;;?OBfIo~t-7VIUBi*s3uvQS+u=Cw{HHOJc-Y_dvfflE2St~? z{A-Ml`k#nkSvn8R=8k2guBSr|!Iga5TYs9}j(;CI*G@8LIt~{oxZmHkvy21g>Z4%N z0!~EKuJiJnxOoMk6}1PS!@oA;Jn%Q1HKcz)MyglZxetJX3b?}G)wDnm{1X(R6})?w z@9)%ubBzXC4>Mt${&neC9pP9juOa@kk9#Gb_&O-q;P0ARU=b){PV(h2uNwYss`>Nu zIe(zwc_YbqP;@##Nq~;<{G*^?3p@?gpqTNBnLICwc1J+L2!B?z$({j4WCR;BC>GwQKRp#h@19o&$>3vEuaXj z;^SM)T5ah`m%aMibSPd2&YB4dUIgH2P=5^CM5$AM`qp?*u+t;wiTXrwDoDpU+M2*B zsDM!a)U|1%;V>v({Daw2`;usX*z9_CUarGuKLiT?7kWZD4wu=B=ZDiF2Ne9qBXcKY zg5sUK&CCaF0tIRIR?BHit}j9o*Q{sSTpdsv%^fQ^ngn0NUVySF{ZWMb!Z;C@L4O#N zUSpu~jOVY9ai}AF5AS$n?t=FKw)Zu|2Dp>-vZXe5N`^`TtJv0E|Jh3dxc)0p@B_ra zdQfJU|F1FT;nwp9^22rb?oNc2g9}pY?BaG%-1doiWIg?a_gTUoHXK=xK``T%&}E+v zc;i7~2hZ z9f03MuCKkM0(8kT1N42K6PDGZS-k3X7qra5mD@qVO&)Q60g9Vfm~lS6+59;_+(@#x zo?QgNaZqgRSLbYaQ$0yqs8yg?ev_`l!30oH$N7zOQ~Jyaft&)&I2laeXQ{ zm*iM00;=H*SpEFXW

rOMg#YjdcS+!5;58>3Y0+ZV-QNd*mhKE_;u9@*fCgp!oL< zW-2{!&9J?4Rx9)xop0<13U)vsw1)|L+<>Cr_vA%+wUoZ@sq{D%)RGECpqM=0OjYWn z*A7rHxmhb}g+pGwNq#tS`L*x8Blz4uE{#tWqp&>tql$GV3?(PO7qKjj{v4fqBW zEcfSI*G*H`Dt5Ojes4}**unJxQm&Lof^c}9};`yN_=iC}b z`T26k(pS4fAc>$VRt=#li0UA!f>@r;f*H`W;nel!d4C&>htUuW3jP3pwX)RyQv!

s!Gj>BhgEsWp$_tOO zFPW9s^T4YR1dAZJ>1Fl1U0x4a%N&gP5ET6KqH;S7iZ>^CJ>1vt4nAUW!Tn(yC^q*o ztCzMC6n~7osN8-7is)19?qp{8$tdIYRx3+O-Uzn5`RDS;Rj)yC=AtrM1Rqio!?ZWd zup3u$!rvcd~&B5HpM;j zcUlv&y>M1H)>(kF9*t$e0u0sgGQb-i>xqp1jHJWR3$AZM&KJn`#tN!(|JSr^m%>Rl zTxTxC5>UgW*8rH_w45)5)s0mSQ10>4o;B|gSLnAU-<)EGO?-opN=S#Kre)q}RyS5D zK!2G>(?q!!$%^tB`$8-Lc<9`pqajjwlvm>HfBKEqd)^ov|WI&0Wv&}Bfap}CD~Xx00&ddfQ~ut|LRZD3SNMJ z=*2M_@D)JDjb=da?X*1x9hz`6)^7r#ihyc!X^Y%wV;(?#&qfv-_vv#R(Ow}?fq%1x zqpi*B>bsXfvn*I(!-e!u)C7=oxekadrNcppfG)n`$G|-VRx@lhw1NLXg0E=jRKj{( zPSy=K!@9d?aM*vZ*@pXBSpeA{FOK%v3P4|9kw-xgL)P(rI(;*t3#@2pjmYl-e)O2M zdMP*7Zh#$q&42;l0x;ZHj%HYa_R z1Q?S>`gq5j3W~uS%xdRF5#gRv(Z2rt6G0J|@AWRTel320y*0DpYghz7P=6jyrcrdK zi*ZW;?-Q0oFHkgNt{L{hI8H~vS~yyN{wJWA!gzCiNtcs$?)Yu3sX_Ca4T|Y=%)k7W zo;Yiu+~bfL2Z|0kUQ@gVZv+MRHI)B%pvXvNUlLFL*;xCb20GbUDZBIqMM9p}46a`- z0tKre1d{cm=6RFN2pOryGk>lIX9Fw(MfcrCSLL3;b6Yc1%qH7wKa%;Wjd2LWz8Utwk9{4uZD8p+s1N`Z`-vt)g>qfKB(ti(=#yhQa|zAp$E)`@1O{Z xVH-RTo&8-ZA%qY@2$L2Lc9T#J1{nNY{Xgy0+j`F@Ze{=g002ovPDHLkV1huc^Ire} diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index ca895735c4dc81a2c55a76514ce7e9ec8425b08f..bded50557de1b8d6b343ece4fe96d463d0a46d68 100644 GIT binary patch delta 976 zcmV;>126o;2g(SLBYy-DNklbW;~m=0y+*UPX`)5*2lk)kTX0d10kRU>_(Ei;C<;q}HaG%bwkLD{q=E zPDf^T&N-glP0vdIAH#up&O7hC1LryKJI@SAk|arz{Ng??UXp`UMKA|reO zQZN!MD#kB{$ix}P3M!J8AB`trIV1-pX5ps&>1Es%n}4bHMBNWtpK4naD%rus}+ zod(~@*aO$VO@GC2_reia21`l@UJEPX7<_$R#wktM_>yoDYz+C=uZTF)f1wEw> zt0{eZF0u+zuzS*Xq*5C+O2P*r1<#h^8zBX2O7SL1crZx8ZN=c0AO+iU?dL-!I1$XM zMec_b?0+crxdBq}b}s%#kb)=coLrjn8YSUtK?;V0N-!J@2P45qZ6*11sePj)oLgHx zj&VJt;G10h8A!pqxkwkB3@X7@#bf8d$=vh6P8bMI1(&vZ(Na6LT9~=4)_OWjgJ}?O zCP8Mle7n)$J+RuVgd?5hCwcTZ~6sX+?p4rv{r5Y))J%d_}wEDT-s;DjyttR2V z4B+vY?1RN2I0_3Gdr$ai0Q^|nEvH%D5?Ef0dxLqQ5|24DvK9HjVnc{#>=K?+_i^}IPq zL0^!9E!IVK;cD`na0MJ~@MiY7lCT4I*Xv|+t??R2!GQ*|?g~i3kzhtKenakM^rhpv zW~r^lvAWz3D>Jr2Kl}y5Z~)$d)!F#okSsn8cV}$QI1Hy@6uyHE886lD;;L)WZ5e+z y7{>Q-Iowr@ABGcgL#$ delta 844 zcmV-S1GD_f2*U@EBYy*mNklL0pD>B3NPD^^8{;>MN5 zLP3jKB^xogup&j!BIu$bsE8}UjZ#!ZThN8NtGH-%VTl2Y3qcK9l?n|>X>ymvxR{Pk z^5(sC64OlZ`ymA8zH`sqSzOM{dG~=NNs=V#f2(Bqzbl`r(|?Ij!k69Vo(d)GtC_eD zO890)Zz^3e1M9tjWsJa`0PKdHm8%~u034{M)crA4+(> za&-sHg_&?|<9a5ngRs9caX)NtK=;aoTw9*1h|?NGv07=)=%!X1^EQ7GX=W%3uH zgwdvtBK4)_P545$Udu$-+GDDt-SWI$34T44@LB*~4JEu$i5Y_FFduH{7#xB5Fdc@P zKdsb<8g|Y-P{NPle%J2-^(5gc5cE za0`_1YuH>#_--iS*^ZItpoI7Ai)tfk*o4PI33n|X+zBO|?qw5x-mS~>VkqIWj`(|_ zgvpMPiBQ6xO2Ss;w;hp_HIdQ=(69+xb?HL55az;MyJ=a%`Cc~R@1bPr{e~N%gfkuS z)%x~KO@Dn`QZsL@ubEOG>Y+P&Jph|v6AZvWM=Ry3Uf#0R$oX^eVC&^ScN&IZFD$_E zj+o={7u?6vUwRkeW1!tiZTQ6mX!czc@t-9NB8h`_f2OkZ;FupN$sVVHu6)h<(7 zCk>VGZZ7=@d^7BVtuP2T!DDnk*jbnAp>}ulNf?Gydp$*G4hFdD5%N;NUq9yL2rqgn!nc9nMv~acR7$0~W#cWapWh zSUQ*rKyO*WYk;A1yOM>jAI|?}kJ7kS0~`$M;R3A6Y0Rn5nVfT=xafTVU}o9;Spd8r z{tKqTJ~-?CQyNAMumFG~Ih~NfHvl#jWAi8nSW%Xx6#zUe4lc-bLQSFO{2+pzdDO^ ze|_%hxB2wDzw^5wNs=V}Blf_%ASt^fjaluVN0-3mU=TV|-haZ--0>?YhtEM0)RW6}S~=jiF0M}td zN>fThN?pp)baeCw0JF*-F&p@8&DO%LV9oeUSek4Kz+wPSro4cp^jDZ|WqY<)0HeFq z1kKP7UowN&pf05@r7>k?N*5Ghx0^tzk^=AnfKxHNAzTiZgKcHen<>Dtpe8eTBB+5I zRi2*0P-m&t?g^!;*1`Auk=?hTGf0AtOf2AbkOX&w9k2z?1xfHGXe?!_odFI6OY<4u zj!HS3;X%+B^agi>jj)%|B?P9y`QU4&U3Y>7nZT$LehOX&ok43^#u7?JW2r&|EQ4C; pfxdB{k(1E^JCo1?FBoZZ`3bMRxD-!05xM{X002ovPDHLkV1gd4FvS1> diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png index 664e9929d05990b58673d318b9518a63255675cd..9d0882124a788f2805ddadb9c18d9efc2384c4e5 100644 GIT binary patch delta 637 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 Date: Wed, 2 Sep 2020 03:13:00 +0200 Subject: [PATCH 07/10] simplify code, organize variable declarations, add comments --- src/_imagingft.c | 151 +++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index ac2b5dabdc6..4cdde9a64de 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -610,41 +610,56 @@ text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *featu static PyObject* font_getsize(FontObject* self, PyObject* args) { - int position, advanced; - int 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 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 x_anchor, y_anchor; - int horizontal_dir; - int mask = 0; - int load_flags; + FT_Glyph glyph; + FT_BBox bbox; /* glyph bounding box */ + FT_Vector pen; + 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; - FT_Vector pen; + 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; } + /* 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; + } + + /* + * 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_max = x_min = y_max = y_min = 0; - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - int index, error, offset; - FT_BBox bbox; - FT_Glyph glyph; face = self->face; - index = glyph_info[i].index; if (horizontal_dir) { pen.x = position + glyph_info[i].x_offset; @@ -665,17 +680,9 @@ font_getsize(FontObject* self, PyObject* args) y_min = advanced; } } - FT_Set_Transform(face, NULL, &pen); - /* 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; - } - error = FT_Load_Glyph(face, index, load_flags); + error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } @@ -686,7 +693,6 @@ font_getsize(FontObject* self, PyObject* args) } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); - if (bbox.xMax > x_max) { x_max = bbox.xMax; } @@ -730,38 +736,42 @@ font_getsize(FontObject* self, PyObject* args) static PyObject* font_render(FontObject* self, PyObject* args) { - int x, y, baseline, offset; - Imaging im; - int index, error, horizontal_dir; - int load_flags; - unsigned char *source; + int x, y; /* pen position, in 26.6 precision */ + 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_Vector pen; FT_Stroker stroker = NULL; - /* 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, 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; - FT_Vector pen; + 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; @@ -780,22 +790,24 @@ 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; } - pen.x = pen.y = x = y = baseline = offset = 0; - horizontal_dir = (dir && strcmp(dir, "ttb") == 0) ? 0 : 1; + /* + * 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; - pen.x = x + glyph_info[i].x_offset; pen.y = y + glyph_info[i].y_offset; FT_Set_Transform(self->face, NULL, &pen); - error = FT_Load_Glyph(self->face, index, load_flags | FT_LOAD_RENDER); + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { return geterror(error); } @@ -803,46 +815,31 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; - // compute baseline and adjust start position if glyph bearing extends before origin - if (horizontal_dir) { - if (glyph_slot->bitmap_top > baseline) { - baseline = glyph_slot->bitmap_top; - } - if (glyph_slot->bitmap_left < offset) { - offset = glyph_slot->bitmap_left; - } - x += glyph_info[i].x_advance; - } else { - if (glyph_slot->bitmap_top > offset) { - offset = glyph_slot->bitmap_top; - } - if (glyph_slot->bitmap_left < baseline) { - baseline = glyph_slot->bitmap_left; - } - y += glyph_info[i].y_advance; + if (glyph_slot->bitmap_top > y_max) { + y_max = glyph_slot->bitmap_top; + } + if (glyph_slot->bitmap_left < x_min) { + x_min = glyph_slot->bitmap_left; } - } - if (horizontal_dir) { - x = (-offset + stroke_width) * 64; - y = (-baseline + (-stroke_width)) * 64; - } else { - x = (-baseline + stroke_width) * 64; - y = (-offset + (-stroke_width)) * 64; + x += glyph_info[i].x_advance; + y += glyph_info[i].y_advance; } + /* set pen position to text origin */ + x = (-x_min + stroke_width) * 64; + y = (-y_max + (-stroke_width)) * 64; + if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; } for (i = 0; i < count; i++) { - index = glyph_info[i].index; - pen.x = x + glyph_info[i].x_offset; pen.y = y + glyph_info[i].y_offset; FT_Set_Transform(self->face, NULL, &pen); - error = FT_Load_Glyph(self->face, index, load_flags); + error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { return geterror(error); } @@ -872,6 +869,7 @@ font_render(FontObject* self, PyObject* args) yy = -glyph_slot->bitmap_top; } + /* clip glyph bitmap width to target image bounds */ x0 = 0; x1 = bitmap.width; if (xx < 0) { @@ -883,6 +881,7 @@ font_render(FontObject* self, PyObject* args) source = (unsigned char*) bitmap.buffer; 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; From ee1cc6ad7c2793eebc899b89bfe00fd294635ec1 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 5 Sep 2020 07:21:40 +0200 Subject: [PATCH 08/10] remove use of FT_Set_Transform --- Tests/images/test_arabictext_features.png | Bin 1912 -> 1472 bytes Tests/images/test_complex_unicode_text.png | Bin 1458 -> 1439 bytes Tests/images/test_complex_unicode_text2.png | Bin 1538 -> 1313 bytes Tests/images/test_direction_ltr.png | Bin 1966 -> 1949 bytes Tests/images/test_direction_rtl.png | Bin 1368 -> 1324 bytes Tests/images/test_direction_ttb.png | Bin 1901 -> 1749 bytes Tests/images/test_direction_ttb_stroke.png | Bin 3747 -> 3761 bytes Tests/images/test_kerning_features.png | Bin 1098 -> 992 bytes Tests/images/test_language.png | Bin 804 -> 785 bytes Tests/images/test_ligature_features.png | Bin 720 -> 714 bytes Tests/images/test_text.png | Bin 1119 -> 1101 bytes Tests/images/test_x_max_and_y_offset.png | Bin 800 -> 802 bytes Tests/images/test_y_offset.png | Bin 1634 -> 1600 bytes Tests/images/variation_adobe.png | Bin 1482 -> 1487 bytes Tests/images/variation_adobe_axes.png | Bin 1439 -> 1442 bytes Tests/images/variation_adobe_name.png | Bin 1461 -> 1431 bytes .../images/variation_adobe_older_harfbuzz.png | Bin 1479 -> 1486 bytes .../variation_adobe_older_harfbuzz_axes.png | Bin 1448 -> 1451 bytes .../variation_adobe_older_harfbuzz_name.png | Bin 1454 -> 1432 bytes Tests/images/variation_tiny_axes.png | Bin 937 -> 918 bytes Tests/images/variation_tiny_name.png | Bin 1144 -> 1154 bytes Tests/test_imagefont.py | 6 +- src/_imagingft.c | 53 +++++++++--------- 23 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png index cb9a07443b199d9b4490b9fc314516f060ea1c47..a03845acef389fc70a309a7e9bfe67721de8a490 100644 GIT binary patch literal 1472 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 PiVE`fq!3%(sX6}xlM#rv diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png index 3501b2a347499de2252d35fdbe6e331010d2f548..61174d75f6816e34e26646d93251353d03278ddd 100644 GIT binary patch delta 1330 zcmV-21^0iL7VLqrK1vA9$Z) z8O3zygA>O+PUA$np$}$`dt|9i$HF4`bNF7yYWCAmgueGHsz%pB5f+VmoW_aFfFk^M z%;`&qG^vd~r@<}Zxv(P4ftSjTN7be&ur~a_tk8tgS`vO1eisJ9QrHi-l*}$!b(lhH z-2vDJS9^=734f)9BwQZWhIz0a7MGk|@*~)N_(E&T-6er*-3&ExG@FDc!4lXD7nEF7 zvKHPM_qhY-z(V-kxQBYzbFdCBhvPdprgT^}n}ppk8U6*&cj&~QhQEi$!*9aX9o@L@ zD(Qi*cXCMSFlsglcfl%{0rzwwVa|m|!ijJPEH1gZqkoTf{t5%|&CmsaccwbZweX+t zx{^(>0B#ufS(k;$@THPlOV+}(t&h+LUxCy9&(XU}X21dfKG;@-=fiwBwynNWOGZh; zZuszMP4~m%lGowJ@a6HH^*q=No7(Ez1^0%>!u{cy@RMrCt9uY$gCzktrLC=SX;>9b zXe&agC4c{$gjc}!uq8YjE@^1}GF)2nHvB4lY_#Dvg*DAB`o>MBx2b-72QG!}@DS{T zZ@|)qHgCf{C3E2dmm&__zPy$>x+WP(@0KKp}98;@51}4?k@j(ZR zQ$|+aRI(F(5jKW_urADlbxnlZ3n#bD$S)3qzp@d&1B1-s&I2ouLRt7zz)Eo5CML5mt_Nr8pA?LJ__jo(g-z*|n&i z4n=q)!&!PI!y~T_fe*p%P=tr78`O6vQh!f)-yKyaErQKeN-Tp5;rrE!|7REo>#N`M zp$M;rIn^qEC2R>D5$+0sOP~n5!uncNTSF0chbO~OcqRNT6yd3`r8fTdT9^Kj`F~~- zs$=HDP}mn93A<|RZ);c+c2o=c;BQxWG{a9jPKA5JOV#f5OjrWT!wa=ETNN&bC&Hfa zL>LM$hW@aq#qXPb9ExyNRh$NMt9!`0J8C&!|8im}?5Gay3@gJaaC3%t`#s?@_yq4= zAo{~+;L$4mc7&B-+Q@U6gzA{(p??V11JDB(!8P#JhJSZQ)-OX5<{zqA;0&16^pln8 zFsJrO=uCJmE8)df+7H5Dt+*dH zb~qyG5W48li(TDtYdBCcJ6s4Wj+&1QrK4Qq+a_%~0LL9nJkw8V#Ux+{3<|)TM>>?T oO^6^RlTigplW+wZ7#-FA3%FQFX~rit9smFU07*qoM6N<$f~O9HbN~PV delta 1349 zcmV-L1-km53$hE4B!78HL_t(|obB6xtXFj%$MNS2?kzynVk#w9jS>^LDDQGIYs<>k zKUzu~KSJ89^9Tg#~2Ot7v{@=YQ^DwsNkAl!Ru zt!1Y_xF^2f`@LT7_j|u^Wj!Ce`)=oRKA-n_pS$mSpL5RlbALdRBuP4xoi*a#DbNcS zou*udaVhk|#4*R|hF+Li*K#5B!qhQGmfANL)`s6?q&~e+gzt{n=akwUig50j}zYzte$eAw6Vq}p{gYza4wcN$Vlx?1Wz6FwDghI8Q` zunxXoQad%7wSQqd^uY44u^jJIq)}^OCcGAY7d``rVR^}{lBKVt3IzTQ_mo@#?;Y<< zq|s}k2yY9M;ntG3maKwfV?K7^EiezR8*`}t`2iI0+3~vtm0Hz85xyFLC!r(Pde^|t zur1sdmUgaY`r%%<4leIlNa@sSp$NZS0;~-ejQOZ@V1HAX1}kAv$(K8N3%9!DrMk(` zNl~wGEfnD=sIaH^`&?5W@><@dxQ(-|<@BhF@Oa2TWhR+Qb?v79kZyLpcU)9ty%2hR2 z!aV`l2s_{^Sl?7FeegJZ94>|)0JNFl2itt0QI!uP=;UZWQ z<~0o%fW;**!5!iKO?xkZnVkG$^)Z+Uw^W;R;eYHjU}^6=9K`;hAWKSupcJCy71=4 zU6boRkDJug0^DBm9IOg`VQ1J9u7_WYBHeS~4M24%Tf#vA&Ti_t8U6y7SI=_iHtu~r z0DqTNo3jBO$h88(64~~Q*;bs^J zeRX}_!{DJG4ijNwwZ62dmb`tE!ObOg0e_b<^weBCs!m!6yQ`A84c-UesJ@r|p%xwp zkA_FX_An5Nuo%8q4e1LTa-u?tuq&+1V9eux2gBCzSomwz@@%*>LnoY{o$^# zCcIDusfBmeb=w%$hM$Kb9Ih^>*M%bN3HOE_Rq;PleeS9XvIs}RFT;_#w0DM;p?_BW zE3T+JmPM$Jxe^A#(eRVTLV70rI2;Hsgr}>>?|LMlfgVmEB>9pyha|^o`hKpfe$=AZxa(s$VI^(qVJ{{r67FSC^ zr}{ddG-?xo15oS4t4XKSF{r`*@lHdNF$GqWU@900000NkvXX Hu0mjf1!$K_ diff --git a/Tests/images/test_complex_unicode_text2.png b/Tests/images/test_complex_unicode_text2.png index c295321dbf1c8d7f3506856b7af6ed86242d3f1a..0526233c0f5282687e1efb57dd7ce6d8f20929fc 100644 GIT binary patch delta 1214 zcmV;v1VQ_P4512;BYyEL3syxCq*ft_SWs$?1qH?0TBAg7M9~EgC z!>~K_hHFMTgnwNL?}i=WiEtJCphcniusJ-C!5an(;mxor!xy{={u4HYpO%6vU{AO| zoLo)58uo;H!@N;ekvhdsV1MJfMPK!MdPgY2<6!}G!D;Z%P=wp7X@3eu*b_c!tn)!w z7ka9N7Q)U(!p3ku>}qU29DdwLI}vu5E~Q-3X!3(naDP!Fxd;ARivPClDpCizH~c+Z z3>(9W>RbJxDD7c@c~FF{r6Ifm_J$<@90!-e#&94MVP9BW`s|zG)35}tYfRMIa1Lw^ zMR=gG=Bn^mSO$-T`@%5*tP78Z3*nJ)PpSH&;h}I5{4M-Br0ptF2YD#;hYew2(_8f~ z@i6rr-G8?>h4;f>!8(}%z4BJ8x?mmJl z&GFTt2ycZO4p&9$AQLl$?}ws#;JS#xJ=OQF(j2c>Iir+tZi^~XCpcyzi_L{Yu(cF< z4S*BJp~|*WgHDyQswKBv!sqNG=za!BW3i5%O|XzF_PB*Fd%iB zp?@LlhUZ2)Yr3q)s;PE1)eQsT*2!)|YRAwJegVH2DPt>iPgpx+Y2m;tB4UosdUL-i`_Ll1Pp>3^_#G*xE8hVa6ewj_2fwpQ^9oOfkHY3~%HgU=0=O9Vg=gBH zkanhBnnEV}K4G&%(9O`(N#AXK__&RUoeOt{Eny%GguZZRn0v&nO4C;RyR!Nti+}YY zJgN1^pAPSZ3t<+_geCA!c*?z{)Jpq9ScDznBv=;?gj-sC{6L1^+q?}vb_msxrrz^C zhhTM>1HY`XDZ_tKt?pR(11tzv)&?X0^GCSVsV4uNa8wRL56pneYxKji8p{CqtSPe( zem=p?N#9FH>lc5DklfVTK c7)>wV0OdwoJutTJHvj+t07*qoM6N<$f^YR$p8x;= delta 1447 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 literal 1966 zcmd6o`9Bj31Hfly8b1locTuY9Ptq{LlH7_gwO`x4H-3MJUZ)Mr6;_-Q_y7B_D zc+J-?{F;;j6}OF1^oo3-;#(h5X$Q&giZ(F)Y1>jV)u=+8L^YpT`|Ht|!8g4ziu6L9 zHA|VsG}EJ-52}sR{rk54J>P5#xw!#D4JBCsOh6(X%))sLt@X~g-O=OmZD14 zQHv_B_arxx=TV zKg%pfZ3H*1G^zp*nO<~S%*Mmx{#a%1sFfvg)1_(%<3s$lq63vOT>k5cdTB9KY)$pd zg_5fLK(2HUQ4dHp$;;km5MZf`nNBCf@zt<9VpFu7EW;|1_GhL}Ls=o;`x9cM1|Up5 zOV(}NlKjcL168DR1hrSl8(~m!(U;|`sBYl%#z!Kh96ZXfL*EXQlt8_aCIa+^DV0IE z8yD*)B{@#2N8mIv!ALzuP8-F2ZXqd?{e^hd;=w6WL5&nfNORk{sZ#z%X%2q81)cBK z?Y3OcPNtNH2h-$4xVgBXLs#*TmnJsZ-yDcn5Q#c#IR}?32&UD@Q>%tKH;#9?H4s+D z-vi%`DatD_3vl~8cue@GjBY@ycGM1H&DHtI%DJUhzOvUa@gfFrztEyZF|QX>opu^M z_g6D(c53%=O1R%>Zr<$<`+LrF?m^A}fDQN0bmt3OsuUA?!eb#$Tf|Kq)y%Q_^F)I; z?xooUX`M(tt~+q)lC2PzjIa-3tlT|qI=|I=5&|$DO7v}!{~?7{-n4G)fb@$l0<7Xb zl%X8XK$BHh82YfIZV%hyU0+!F1U5sTnn0dw1gQ+z$8TxRrPTz!kwtYL7w?Nv6=qzX z>J04|LP)hD1Al19N0D-$*N(8ibetz42bDGxq284Oo(ph=mDEMpr z=*fS$5Dil;;Lk@ftfJS?2PQ?qU38{9@4;l&p>)VjomT&)g;=Z~(h?WS>GQ?PVc+tu zG{p^10}j){m2VJKxH!qQq9yKOAd}`feEg4DLf9t0S~-i!T= z0DkHclzJ@&%=QGsvUhV$Qr<8@f_?s)jb59*PMB!L!g9O*G#BH{!1!K8`mvwKufbAA zs)mHg7@$CzJYQYmF>A1RwylC!ry=`nNtU1X6yf5SxB$GwLb)q>8)s(Mx7`9Z1eDqV zUaAHZY459zA++$u}M9Fw&{($Gmx9(*1Fj+a*FIpFahB~3m`i7rX0O7#k?>W za^4xsLJdeY(E!^H4-R#@Q_*|)9DMQw+_uXS4U>z}V)?#J>SB+r7ek93`yNbpVq?!l zBSl=1(dlEqaz?R~Y$vNsBa((7O9K8IxtQt2ycLmm`gKqJBxMd0aWgtH8s&A*e@^AU zk%9j@rjyWPL9QKDi<-b4?9rNXVg(4CK~wpo*uPKAIy!&ncnVMn_fO1{boYdkut5do zOuebc2M~*Y)E6GcJ+OYhOBMc1@J z98-JDbtmGxVH(^;H>q-Gsn<1)pj;w}Z7|q+_%JV?YOa(8rkdsDoD%=%E1fB+?ZhJ& zGKZ=ZUyyS~&6Ki&vtHDASp9o-`{Q1O@)1()Ml~94u;e00`GtFFf#F6jSC5)kkV}kH z1EiD2b72MW0Onb*Z^Xr;<`Zy>toUL5$ewVjgc|xd$9u__L#tGUh{3UXJMYMxbGYjx zOdh(3#e*1;LRc1$kem4Zn6!i%k;Rk2beWjLXHZcZvLlwJa^+Q}uOfB}KA23Qn66fj z?ada3;@IK8EM6Dd;_FDwWySM?LGy%fSLD0zLLF~Z4Bk->|3ATDJ-Fu9URzDW5%%v7 N1Ki0JlF)^I{eK(Ycv=7e diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png index 7ab6bdc19b6f0df68cb74b7b94af091461ff549d..282eed883933c0c0a9741113120902bc3c459df1 100644 GIT binary patch delta 1214 zcmV;v1VQ`Q3ako{BYy<*NklF`80KGn+2vjchsvg|&KcW}4kO z$2n&l=VbH$abEV?Ykm7WaMpZ#t-S{%Ns=T<`foY{a#8d%@_!&)u^;4uWl{LBO(P)} zEGfCaR8{E&q^_=er4uMgBlJy`x%Br?U7<^>i?9+(71r64FkmEH4Ci-T%$8-?iSR_w z5bO%x2`1(B><05;U9daY5ZoCY4h_Nj;!C)KYr=`JI@lcS3_b}S35MkjY%>|D0&1YX zc~9kq(zY)4ZGX)Cu;0%6CgF?0#_YCDV=yk;b8+6*&I`e}*-6;u#?0k~_KND8JCvS` zv`^<%!cp)_a4g&hmn9aKv(V~U=y$(9uR865$%zva!xJl^3g)-!a}HbyzrupVpv1t$ zyhIHgTjm#;6##c7h9r6=MkKCJe3tFgW-?L*EDe4RMt_$%YCn%C)@O^)_d)Mg1CN58 z!B?$*_m2g+U~V@4b^Loiw^<3_3U&p>CRB;ZNG&?A5}pYgVO_h%HbQm!_h)#vmVN{~ zTOIxi4cRJpIshxOk)JO=nU#qnU}N=g+d>m*K%^Z zFP~>uUVqCU(5=Vri=pE8q?TC_0<4}%AT-NEKy za`x0I#i!7OU`cRlw()%!3^SV#$HLO!old+%`vtm_xl06k!ja{Ns)KVAeG?NCOA}Ya z+TfTD$x>ZU6>u6X3ob2m`kr7GY=Iu|Ofa~){eLj1DqchC6TtXvgV*pr?xIEDVo{Xkj z%Bc{rwMft$0P}+5S~dO&b&y~<^n<5ibYe~?zKl|r0PYB`h2C&HTm^TRUy9!d7Qo2n zDt~$=Ob_tA2ensspg#cL6gvHI;_=|6;G9G)+zhLO-ZYiOXNydsz5on>^}!bS3r?bG zLhga5U~ytwIa%&|M3HZHVp+*S*1~fz18RfX=Em1yEbJ{OnV~Q)FPX35wQ_<;ExH3c z05}hp=bc^wL*X_U2MZI`!FU)94e)4UZGVxV(*j^6On?!PU?;4CRf$#QZ-jK9xrq&M zO>i>ofaOq|*cyfJ0*bjHaYb+*oB|2HgO}ma)Kvea_80?=!B4?G!C5d62Eiz}2x@~p z!H!m|!8Xl=T=3o@@=WYX$OR7_BH8Ny>>}7&8 z%npXb?=UY>*E#8z>Hc6&UgNWHga53g|A=b11%|;im{Ho0H-n#Ay`x+g3_Z9x9*`5@ z-r(b4d$2utJDBaRrYbG780rUK1Q)Xt(}Mm`Q$l@sDd+>6;G@K=@a@6MQS@ c6&DWJ-${;MHK=irVE_OC07*qoM6N<$g4OmvoB#j- delta 1254 zcmVxRpzaQqG_kITEncw@qI}4H|Ns=U;n#Myq3cnUaqpbK`kbe#aX4UKpme+Z$DjG31 zot9GN4nS(^nwL9(I++4flCrIScKMc8Q{aq;rUPvqfh9Z`-0^`1w0?D4R!}V z24nKNRlw81s$f^JA{Ym)>u`iS>P*-I9)fRzUxF>cl3)U~%^TGkUI{kDf5S&L_&w@$ zbyByZ9bslL0Dpi#VeSdt<~4`;LH|s#8`K7=gx51QDR?*Q#-$5P3N9*rp*MqJS;gMa zJ7@=wXT6;YSP=Bi6nntT;84Q5r7tI)FlWJvU|%pJ7{QTjIkscmjy{ytZC{WM)&)bM zEt~^2nVSFI1?gaOFdW)KM;H%#^DbJCeLLC~_6I)))qhY0t>6-<2_|GZS7icT8C(xl z&>mh4(!o-%OjTa@Lppf4#8D+BVZYq5OM-N8Zl<^}NC(xqeI}Kjgy~>z@pL+zgnNSv zvMN)sI@ljn)agyDX*qdk!Ycyc?Rt%!0+q?puj#pccEGaSn$9zsZW)=T|H4so$+G=VN>Y0QCVP$Y%k=9G$4rpJfsYPzF3M%Ut zL7ts?H3tD`20%b1{F>KiYv~K!o-h?|hA)C`!J=SH-r4%t!ARMTlZ0o&)L>VzA-FE9 zz7je^yCS1H!Hi%?Zsn2$;Hlt@OrMv6bwQVU#eZF@1k#1R7dn(aRT`d2=$X(xVRXV8 zs10U%-cx<0fJ@+=;I=}|lY@I8+Sm@o_4K^1%lqmrWA(dq#B6m~!l=ml+HDbyrfm~eNA z8F-@n4Ywq`3_rm^*a%N0yqW;)sq-Y?!c7V9zy|mOw!=3tCSk0iXup>Q6 zUi~$&IY}3(6>xYxk96oj;Mt3&;xF)^M5%^33g_Epce<%Hg1d!(ix@& z-v>K_Ey2QIc)3H8a&&}ra2w1GzN*`^Y+|rHcrSPYdNp!*ld%M37}OyD0+x(JV0H33 Qp#T5?07*qoM6N<$g5WehlK=n! diff --git a/Tests/images/test_direction_ttb.png b/Tests/images/test_direction_ttb.png index a112eb4c677478705856d652e33d603fde2d07db..52dbf57234011d23293ce150aa084dd595b51ade 100644 GIT binary patch literal 1749 zcmbW2eLNG0AIImZF(xm=gjLc;S(q_26*p6%Da7Xad8hNR#SJG9TfgmW)bE_kI1eSs z)5)Z`4vlS*M8u3!X6NA!F-7fFF6Umq*X!>0`}g<$=ll8l`}ux8U-o&zX)wqH1ONcQ zXU}-}?ao*LKn1C(x~m>-(;or=dn(R)xCdlZym|FiVZegQlZ_qpbx*e%YC}iOC__`6 z%y<>vrRUx-?RTxCvD>QL(=_R2yJLd~wg45o&AzX_k$ETCuA-;K?&RZ%yharEslJRzas4O*eBulWGTAHA&uI|_6 z=3jpGHaR10z^h4{Jr--v_vGUaHydEx&{UF2XkcD>SO9`1jCt^tYtcKuB6hjBwIA#x zB9vI=nqn5S0|BLmf;bHdNyYP28bC7d>PM{k*F>pfGhA`IE6*-|7s+y+J0eH(*ST~# zc%y}Qh`@m%VOvWqw0Ks$&>7wQUl} zu9}XzW1W4XiJ-;hgj4VFfFIbeqAQiCPy=kpWgC{ycHJe5M;BvjEP`w1b*j#c=n397 zQi)+MZdtu-eGE~;GK=(^*g>c%Ja1)|i!zAEFdUMd?l98{-{E|i*6IcMDl{)iUWq=Nc6Nq|2Xlt9NGoxGE2ANUaQo7x$g8yNGdqh=kK|exvxtw&Q-#?Fje|(rqSPh_I|wa5 zNgj?mLE|APsxuZ33w&J7d-h{}Av{2(qDN{@D1gcy{4Qk{xO>>U8Ti=tzAq|={zh|o_S`3Y#)WowrwU>wJt=G064iI-nQbeq zBqGvOVA~3oL8W^Xw{U-_B9vS9-%R`fm=umnQRPA>a|k-7|BU}WXAN3!veFo#+Ny=< zQ+Ec{3DWp`b0lwl6S#Cd&T}fc-lSUsOzGns!rk!_INb#6vg||QJvvTHn*v=WfF{1q zYdMu4ckzdb1O*ud53i~n|96cGgI-2{@&z{ZV2}0z=>8@x%_?XL8(ib7X3>ovh4;X) zA;ARZ5+3o{yk~E5>M*i#AGxJ`)9_9=DA=ifEPy(8iVDv0vWS@Oh#m zwb`!+CnAkNk@lM$Z|jvCQMWVd#XymEV1CP>(dOw5hB45#AbDa4|NaJXYC5iDgr&># z&%BYKU7@{~^VAM|_v6j%qs<(Mz2Sncvc8dd%mpImQ<8=sHEzZCq7+^Q)f?M^n`~WY zlNM$W9+7k5=V1fs;qVDrZPKwzdsZ9Ge7D&BVhr^zK;3`NX4C@1M#r2!*Vmx1?kuM# zd}DL<2nE>%w;#3M@BcjD!m*~&$zObL?&bCl_sMx>Q{+WwCH#stt#Hu}W=|a%nB@cs z{7^lDGf$7k0j@1ByjHtE$CaF9t_3aU_T9N^1{LBeVEDDqV*29*dMd` z?dnLN3|K-N{uV=Ym>*dAsiJyOtyt92PR3Vvf(Wdg{O9skY0=o0q14Z@b8b+Y>G$EH pvgtRYchQ!pzpC*61gm!iG(Q>x4AZaF*nL-kvv`6>%NLQ1KLMq4EFu5^ literal 1901 zcmbtVc{tmN7X6`0L(-@UwI9ge7>3d@2og&%omxUIWoWBW zQ9ctzsn8*oVp@Z#5wUa$l~N;9%H#EW@0{3|CDXF%A6b< zg5*kVY@VFc{n}INy2r0yEI9ii+y0z_oFmw-UIq)+w!@&bOf7AY9;riYsfN`kJ8^UefSsa@IknS2s^ee``$?4=}PSFiirGI8gCSB58shreF54*so?mrc)I8LBk$ zuexAzrA(6ZdDm?g#CH!Rm}SM5NYAPI=(xDfY89qxH8F}4)BSmmJjpm+O|$?kAYX5L zsWj!)W6@cv?<{B)A|0?rz2J8@!fKyIbEw?xtA7b~QOF7NZAe$)E-@{y$!RM?pC@Yf^2xtu@e2_MydmIZ-d$(oT z@N5J$piu`--&zbir~deSVX6FZJ;ah6Fa!q<(sgW@j8C2My13~(LLLdxx2Kmq+-0o% zT(N4r_VJ?X5ogvHK719rxI8;kn7_p(lvlc{A5nN9I~@84g&?ZSEoTMijw(FT$U!%EB5R!=cs%zFT>35^+Bs(xDi^ z7Q9Vtkjl*|jdOzX3kV;v34m`0x2lM4d=-_=R)?kzEDSPPNWNp|sa?~(CZSHCT>OcG zi}+#sxyl3^3*OkZu-2IUv*z4N#O@+0%Lp)JlYQQ9uIX`JxmxMJ|LfiH(=E7-%wGpx zSjNj=0ToKc5!s?Z-;3pe6w-OcYYh@oYQYtD?X$C2Ge}5S1584

hRHiUSb8*!d2bmc z!=^@f#{F+?{~Wqc%I9V>jLYeC`pb^xr%QD5*MSJ)Q)i~xl~nD{&5e0Moq|ljAhFfG zfW~igEAb*BpgUMnfW~!Y#q&3AAzNi(-vYvb1oVu;)u#B{ic=yI1L-XHZZzM>q*f{o zo-^pw>JeC)Sgpu}PDc`gDk|F`UP|U6HXr|fk)ZjVpharj(BA@}7`1V7OW)e5OwVSE9gY0s zv--Z~i;%_L?WxkoNFg^OvZ4_SRJ3t!srB$9VxM-aVqw39pN0XY(sJDolZ2}0|1>E} zlJAR=p-CAZMId_Gm>l|%m8T)N0LWi9^A6G)Ns)7t|4J2vNW(I#UHKv%RzI7JcR~K^ z|3e|S3)?Yh*{=Fn5;exJ{7VFmVsM1EgF7(4#Svm-q}efE1tWCXYe{(Ui;jdU{_ZD{ za*=iBX@kr&f^`?sRVNLm?QJm~?v03!%w~dWyFJBz5iQKjX=8daHs zP{^W?75?OyN2pq!P04@nhy&rvS`y2PyYq+;<^26>k&(smH*Jwl*7Be096sj%G&weP zGOqzAF;dnkC{~d9L54rDO*WCvzd`6yIE5xw85x%LAqizOz6tX4mWOG~u%Ce!rhXQM zvGIc~16PF&!dzgEdr#~A`Q40ITIk{NSrG)t(6&E# z64{VAKm8%~khIg)CnGcGd9}$twfTf9pT|a{o}Zf0jH!ut*}AncU4#(}l=@dmuUzl3 zmb~$IcAS=eUBJy-^#ds)W0${2g#g*PxhM2$I^R+q7TMMFBA}(M(Fl&*>qRKH<1xMy zv{Y0OLyw;bnmJD5r#a(==mJ=kd-L-BVjotW;*XmT0O@a6b$;mu`kFrNYr@jSn40_V zx^Kq)kQ2(RGrzOh1gvF06t2BGNekdDVP%N%Ji@SfEN3pGyyW8`S6VbPIhkChnV2Ze*@6YcX$8* diff --git a/Tests/images/test_direction_ttb_stroke.png b/Tests/images/test_direction_ttb_stroke.png index 288792cc43175808cd5fff2d57602a03fd3cb083..4b689c38ec7039877ba61f11510abf58b15a41c5 100644 GIT binary patch delta 3636 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 diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png index bded50557de1b8d6b343ece4fe96d463d0a46d68..78bcd951bbaa47bf002af19cff49d00390c33da1 100644 GIT binary patch delta 873 zcmV-v1D5>C2;c{hBYy*@NklT&VRzw7ixw_+L4#nrd|&v z+~0ieJE4TT2mey(KQnf=FW}TR=kGr@W#-h5lfB0)OW}4n4!h6PrAcSNx1ofkLlbHx zl&~ip3*Uqp={61r8j2+F5R~v-LyAg6V%RHTRR&)>$pl;mtGZ|Zgq;9v?3HO%g{-Qj zG&1^^ubbh`aDOng!gt}Nh7vTJQ0rkL90|XMyJGvNvaTtd&7^7>q zV9p?4S*6)ewed*+7PSwnzUZ!w-sbPH8^&N`?cAybTJ=cJg*&0ZyMtVnG%T9R*QcR` zOHW6r{*`bc(C&FQed;~Ws*lG;q766kmirm_YughMdZ zYj%b!;im49Kj3Yc0~_2P)TnDJUsuENa3VY#R=@&S3^%|w*g0T#bX7-pbQvsyb?`$d zVH?o#9L&02VP8q_=U`U0Hw;hodbUOy8GSbQHbbk|&EdWQlMr~iSDZcVeZnp|S>p@bjPV{`xWcBt*9phj2!$6T(1m&5+>TR0Rx3fsfw|0>iy@L@O_j)i?;8=TXg zs@{!wxcMSKXm~fG&-)gW@dH7V@B=9rq_gEOeUA}%7Iwx000000NkvXXu0mjfwF0j+ delta 980 zcmV;_11tRC2g(SLBYy-DNklbW;~m=0y+*UPX`)5*2lk)kTX0d10kRU>_(Ei;C<;q}HaG%bwkLD{q=E zPDf^T&N-glP0vdIAH#up&O7hC1LryKJI@SAk|arz{Ng??UXp`UMKA|reO zQZN!MD#kB{$ix}P3M!J8AB`trIV1-pX5ps&>1Es%n}4bHMBNWtpK4naD%rus}+ zod(~@*aO$VO@GC2_reia21`l@UJEPX7<_$R#wktM_>yoDYz+C=uZTF)f1wEw> zt0{eZF0u+zuzS*Xq*5C+O2P*r1<#h^8zBX2O7SL1crZx8ZN=c0AO+iU?dL-!I1$XM zMec_b?0+crxdBq}b}s%#kb)=coLrjn8YSUtK?;V0N-!J@2P45qZ6*11sePj)oLgHx zj&VJt;G10h8A!pqxkwkB3@X7@#bf8d$=vh6P8bMI1(&vZ(Na6LT9~=4)_OWjgJ}?O zCP8Mle7n)$J+RuVgd?5hCwcTZ~6sX+?p4rv{r5Y))J%d_}wEDT-s;DjyttR2V z4B+vY?1RN2I0_3Gdr$ai0Q^|nEvH%D5?Ef0dxLqQ5|24DvK9HjVnc{#>=K?+_i^}IPq zL0^!9E!IVK;cD`na0MJ~@MiY7lCT4I*Xv|+t??R2!GQ*|?g~i3kzhtKenakM^rhpv zW~r^lvAWz3D>Jr2Kl}y5Z~)$d)!F#okSsn8cV}$QI1Hy@6uyHE886lD;;L)WZ5e+z z7{>Q-Iowr@ABGBuSDqY~qj7bubm0(_s&Wc1DtF*#a+utm=~VH-GAXGdBymfP(OYM~eP@E!fH8wh?x~yC4f1Niv;QPWq(- zkOk|C+pZuB+S5!M^aW39l<=!tL3dCg$yh>Zz_%lcHaHNhgvlgp-IU_T0s!u&gI_`q zOoegqvFg>0un}5ePwtV1R8u`L2X3aHPX>%CevAX)O@CG3HMFKB+y%Qz3*BAl8?xC- zBTxm{A2h*vSf108)0ESYvoB9_p8=RqHGU%S)0!=XOTm&Mo3J#}RDhWP9LjkJS@Ca} zwduC)F~Hz1wLmNM!28m}bI_2}kkg#AFy{hPV3V6bsfG&h3V_4$Z9_O8P6un#+{GMV ze^6I?cz-acgNrp@okM?7Y4?QE&n$ya>8ahvpfkvVjv^CqImm*m!FpH?$Ac_*8Z;M~ zT4#X0!R$2S*HLM(Rd6Hd3A%%;!3x+)QbJ%1oCrRY+I1zERtgL%;oIO*&>76B%2+~a zz#q4v3FbjPJcpitF(XNmBuSDaNs=Tydp$*G4hFdD5%N;NUq9yL2rqgn!nc9nMv~acR7$0~W#cWapWh zSUQ*rKyO*WYk;A1yOM>jAI|?}kJ7kS0~`$M;R3A6Y0Rn5nVfT=xafTVU}o9;Spd8r z{tKqTJ~-?CQyNAMumFG~Ih~NfHvl#jWAi8nSW%Xx6#zUe4lc-Bo!xlk)Nh?ZM7E|?@KDHnDnTK=?*8o#-it*?u1+1W0(Y3KY-oyX(m z_xrqmpU?06zIpZee!hJR004kBZq!rt_2|H8HhR3SuG#4Q@qgc(Qz~bi0Gy5W$37ma zSA_@W;dlt|8&zATKCXXG{I)oq+kgFh?n{833)>!bPh0&CL3%cI%o=2};?(QNd~ zsQ-1qJ7vi&H5<1>>!y(avPLXCKeafuw~p50)Z)}t=k;87cO9tB zQyZslocgd9YWqnEyQ{X=p?YTO`l;)to}Rj+EhV-qGzR zO?Yrr?U>qiqVjgFpL*R^$l9>5+!y$=;|V>)g(ziNGLSX#WaE}Bm}u+ruq!f&g3U}GU3WBwuhq~5KI>y6P5qaR0ajy|u0b!6VX!3vmv2&-*V zFHC)3hfnLLdaa(Ax~%?~cVn=2ZLQg8r<+n(8PCcg@{{kV9cTr3h R|GfYJ002ovPDHLkV1hUbR-ga? delta 635 zcmV->0)+j_1<(bMB!8DlL_t(|obB4*%b#N$!14FBEm_+Vp=fDHaUh2d9T6u|vIC9M z(#Ra(V47A+Bsp+GA+>`=loJVY;17_5HMK(r%QHW-uM@Uz2Ya48Pu6DH^E_+M>(yP~ z`?~JW^}RnQpT3{#{@w)u0KghIYIl7(`e`&9?W#*^Hu`Y<6MwUJG#lNuXw{L?w~GRH zG`|-3?>3zL*ikQz(?h=1xHq}iFSMMKH&rI#FA8W4Otmi!7VSmk7_98r9pDdj@RMn*m!&|C) zZ)wcC%bOION0z+^f3H86&YT>sUl?wwKkC@hn4@)kxif?F!%DQHr+2J2)t04`SFX$> zBRCJNei43C)qP7Rx1S;qE=;Rmgl|t(&yOCg%j=5TS^NA&!J4u9P2b1$e%)KIEh@YZ z>W)P(&{e?&Z1sz<+BtQw9vE${AL^ZYYwGam*VRr8)~<(ZHhNi83M*spXjk1(8*5uV zUUQ?_=+@Ov4AzQQ$NanFi;q|Q48n@Js$Lp>F`66AjXobeRB8~DaRNaa001s5e*^a; Vc~CbaEr9?4002ovPDHLkV1ftxL2Lj3 diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png index 2841da66f433dfdcf5ac3709543fe513de948291..5888e52e53c4f6aed88a5602f7117b5e0d0b3189 100644 GIT binary patch delta 991 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 JLI{kIxrX@dQs5TOmEjFRdS1YsS5 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 diff --git a/Tests/images/variation_adobe.png b/Tests/images/variation_adobe.png index 3ac326f51966bc36b869ab74b5c487844515d102..e9cfafb48b5a6cfe7d4e344edb316b2f5174e7e7 100644 GIT binary patch delta 1372 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 LNkvXXu0mjfNcqfb diff --git a/Tests/images/variation_adobe_axes.png b/Tests/images/variation_adobe_axes.png index 93cad98ff0c0c230206f864e5dd9d15505715775..ad3a3a9608837ad32d824e32d5cb610af67f7b45 100644 GIT binary patch literal 1442 zcmb7^|5MU;9LG_l{8GvAYhG4ril?ZX){#qbGBvSgYGm3}%z>JjrnmfppiNA$rcE6> z!*5{>m601!*ow$NCUKUeh&l?tq(%v5&Q8HE``rG3-S)%#{eIo+p6`1-?{$xRR>edg z-D&A$2?Bw3Vj{!hfEN#(W9Fv7Z9%*{2?9aRVZuVOMa{Yey^{Yinz6*7fG4 zrKQ2!!Q`E`Qw}=PMF~ihnH=D zLGD(4N~LmQVnWck(ZutB!*vD5f68s!wwZMqwGMZgOlEiY)sPy8DUCx`R+g8S*O1<5 zG!6~L#Ky*EW_AJ=j(c3Is_No$U#ivXy}aR^GKZ%F1LoG&TayK^#>Q5a#`=JP#lC&= zCyB|)bouNKF)fU_u#nA-XEZg*xRD27nDgg3FLVA_7Tmihl}g_mFc{1>ib>NU#-La% zX0cdFSoUz!V0r^#%dmYtp*n&hg?o6=o;PlQd4N7BICytutqgBvWtBk=&#J7F{2|RwoyacZ`aKjgO#vy3Z>=Q3ytP_z9|^&=jVsR;p{BK zv$GdwX58{}@#db#I7(*4{riEXra31O_9CG$u*DM&-_D}b>38}30X$DA3_eYC`%+Se zdN*x7rc$Zij6+=-<%8(~0&ZLv8jXJcW_)~nU|?WlBiO09xHzw{aQ*~eB--cfT#c(F z3Q@gL0)fEyV?^a;o+RzFsKY3f$6_r3u(+h< zpW$8;*1Al}LvnL-`Q*I3si-c6B9;VvC@d%#rI!QNy%C5V7Z&w;m}-1tB95bMX=wp? zQk%%ZplM-o@yO0DtXCKIDiZ)gX?v~T+uIutNEa+qy0FYWthjS`##aNa zt*sLi6Cn`DdQ3^l5%H%&p>Q=7gR$t|ZDX_i;lq;Ngqb=>Uz<(KX`LwYB?gZTq^%M@A%VNMGMw?vwAP=I1k3 zt2V8UHz>1`aX6&o4l^uII4nu~EsDiUK-2^W&QPGOkC!$#Z^v_-VXyPorl(!skev6I zslQ9RGd?#zk3=H5@f<4kXhK2)k@&kvB)S-olbwD1#0iaBy=pLc*Bc1+JmOa)KaXT2L%a1C6>;+-Mqr+~aQ_KGbqc6N& literal 1439 zcmb7^`#Tc~7{_Nb*W;2#;j~zfwMQbCHXOGxMb<(?o4NEfR60#%@h~|l-IgS|9hQ@o zrCO71;mq8Qa&k@X!_wrEHJOl^=Ik#x=Q%&T@B2L8-@ecLc|JFTaR&|bEc5^XfI(mY zCRBaN>Iu>Ts<&<$e-Z!y#|L82_|!64Nl<`O;`a9T`*V^C-9{0lFWV|W|M{GuKY7bu zV8nz~6vp1p)l^o2(5W!77HOl6qpSk1R6NoTA30EEXKOq6ARZ+sNpLLt-f&R1bh|rc zP4a{N=KBw2m$P@foRK$sz8=v!yr^ulT+6qr0T>eiH|ziyGXP8eSSU%*;&`zx_c~Rn_2N*sTg8 zkq86=8(B}J&c($TnB2W5mOHvw#pCfD%x3Ql4B$CJMQa3yU~X<6vkCAyZeLAgA7IMI zI#pPLv$HdWLirHAn$TeZh0Z)$V8slGM8twa({6XR$s6olTwDf*h9tXitP9D>yoLr3 zcX#)F`|ehrdUL#sByG{dUAHw7& z;+9073I>A@J$7<%$R*s@(ah@U>8Y(nLBt}_?`36eQWlFPO?H1Ycm|o9lM@ye=E$|k zc|&%pC%tG=X=HMIANox7_xC@~9bQQ%AI9Odw6sDY{{72KOHLN$B_-&@yw%l6*+@hnICKMNmeUj`u$xgc2zHsFJ{nN;?!NJ#9g+jsRa0K19 z5Ir8frpAf(l*`SpK=6sOAWE=nQ2~R|O|Ic`Jrj(Kj6$~5lNOMPeBxiNt--Nm@>O$G zX}3zHGBq{zTGh&qByhQ01hem?2gvX`2n^e`%M;}1=Z7$cLN8IND@#kTynuW6?m?kY z+UFebct@yh0fJjsH)r1b>ylz)csQaLd;GX{2Ri%=kto}k!G){ls#64VH? z)L_C=asvb7j&_~tB-cE7f}pK#Y|v=5jfDln91l-Vg;FUn9UU7B!eZNc(d_U@JpR)O z%9&WUxxFeu1fA;cPB*D?q@nm{J!u6;;HhXV9Njx?d857fc zyN+a^?JHkfTQk_36`O&R_=|rTl+MowS5{VDdmVk2LfL9!a{j^v25xp@VuxyL-}v~K zAa%j;!f4Jx^M}Ey$;r#ne5q6_6ngfaE5X7cdVTkCEI+V`7F#+x4&j6C%)Gt5)6Lp;W|lJ;FWo#SN%qp^ zWk^%p+paDWfzSfdx?=6-MnFIozJ8sW`u6P`@bVwk)$iDo)6>(On5N*CSFe!04EXNd zOhDuIOKE9YJLj63$)mE>l@%%jhjMkj(HZ(|kjNf7>5|5ecPf!gP048_5-B7kF+)crXJrr>{aI7Z#^ad8B@LYYi9 zKYzLkuV;D{l>eIM;58{^C0-OnJr4c2M_0?M+#&z1p`oFwKOf~)+zR>$$2w|XBmiSz zc`dPk9URLql$2wWNv?&3g-8@ib}`+@#|MkW;&6s4#(FdoiJY04dBNeRS+p?;Jbn79 u!{+5s$Vz0~kI#3TzH0n$J^hEwnm>HJ!+c221$3kdYbVP5#gX8i+nlcZ?? diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index acb0df63c57628b321eb635ef0f588c374d838dd..11ceaf6e65b60a9a9fafacf1d72f8bc8227d8e1c 100644 GIT binary patch delta 1305 zcmV+!1?Kv-3zrL!ByC%$z3uDFCzCe=jZ=eU5h>F%*)G5o8t(f)YMcwR9|0DjW;(p+pW(9b#-+%8~Y|q z_biD-;_dCtFgY|dlwl@EEG{l`QI@GJ(E9(aES*lbySvLpWu~%1LPD4q1ttGDOMgpC z7HcXyW#jgODh6?}bt@9*#NiQC)T4i0~}RE0ueHk;|FR)4D%i^V)1 zZ+3Q;U1KcZTvl~;HJ#Sb(13@`W^+J503Ge@?6jI&RaJ$brN6(Q8aEn^adB}7p~l8W z){U`%TUlDIRv-}2X{V>BRtZT-Npv(iI@&7w`1t7K<3lHiM53dkBa6jCJw;__XXDoz zs4;eMD+}LuJUuW|p`oFVkB_VyV+Xgg zs4ojmEJk>ExQmMm9u)`#Hj89sWuf;-rBaPX^PR@n!L2M$Pfx^{pr9a|Mb6I7L?RKM zIXF1@&BoZl?fb0T+gtirp-_C?idL(&S;XJpzqPd$AEAB$Iqlpg+<(e)a&n@NudlCJ z*Kv1u$3HGFFAon7vu}$n+{*Iv^P`VXPEPcCJ&Pi0wYsvh(rQqtRGy!of2%n-+{(i5 zo?l;IshfBZPft&|xw&s|Z#z3XwY9bMxL&U>EiL_-E}6r6x5JBkwvR3>EZ}JY0Reiw z{$rZSWKt@XdwY8yLx0qJ&bGETi^cN%{2UY%gclqc8DZBLJGhl)Hk+fPqw&mygan;V zhv(ei-{<7yAcTsFimav&4GkfL;^X6uMk6({yu6H8lSm{N7Z+Bu@Mr1B$Vk=;{0Qfd zJ6f$)B9Y+9LZPsrprE6pqo=2*w6v6pdU<)>-QD36cXoE1oqwH0BGKXDAwFJGQi4}Y zOG~plV?;y*9d&ecq%K&K$uu!BVK5k2uh#;VrB$AH0 z($mue0|OC4bbk&zV6u99d%svSh0TfA*Vo0x#rT~Bg+L%EDk{3Uxp~*q=Vh6k+GH|S zR#x(OJUry)<~B4m1o!g}p4|?95QD+6zP^5NaG=-gg+ifRE>|cN_~YRZa(a5Yva)h@ zbtM*y<#KsSO3K&o1)1=xvi^6--}eBBEC56n03r(jk&}A`B$HYNA{O8eJGYWOXH>{J P00000NkvXXu0mjfocw-0 delta 1329 zcmV-11Kq4-~!7a5K_EP%q}yF4lzz6@efjLz`bow9E#!85-Np|Kh;; zza95+p0gbzcz;)C@AJIR`n$dR%VlTZa9e@HJ&lmXq{ z-NK*3@gW9fmzS3_$M4_2=NS4c`2GH=sVRhz(P+GW{d#taQt4nazkKL{W zTUuJmdReZrN=r+*7X>8?e}UKQr3Co#IQ<5H`}S>wP+eUe_oAmypLRGLkx1mig$u0E z>gwvdckhD1pj0Z=>2wy0B|kqO)G-$(CML386Cn;8d@}GNX*bb$JpSs{E0f8@T3sv_ z*Vor?ZEd9}p0MGBA(zW_=FAzHOeVznQWOQdb>PR|-rmidH$5H?e?my5QaKzBlgTtc zKR+}y6pcnb9?$&zyw~f!c<~}L<>kwl>+9=XU0rw`yWMWJTIpk#%eB0`+}qnrf4qA2 zs@-mn#bRo;TCG;6xUIV z;lqg&|L>|6izOb9e>2fQAfQkvL?Y4j^fbTTpa?FjqoadKyMO;a9*W1~dcB^BK701; z==ApXc6_11!9jXF8jW7MbO|B!;K2jly+ILHRv-|NNF+?!=H@0Vp{lBiiJHykqZNMs z{HfFFm>juWzP7eTQ55|kWwY7vYYo&J4}_J4Z#r77mK6;Ke}ihZnn}=TGzp8$%*^0x zRaaNPefw4{7MGQk?d|RH?u`e+%BrZSz|+bx&YnG+o12SAb8>Q$t!1;>(4VMOs+EqV2n3SVDK0L4 z^ym>jGCMmRbN@+A2{G-q38yy|Zq#zH3l~q(!#2jyIY=pyM zes%nQe_LA{YtZR*e*XNKe{*nzm4)9rA08gkH}O#R_xG<|yY~C{@5RN%&dyF|JRA<+ zx^*iHU2=!_Zig31wu#Qo&EaW!y*?ZcA5DwJVos;?f8)oG35(G0IUhfMOi|RYU%yI9 zO7NP`pFii<8xMq)6_3ZwW;337`SRspFo@@bLZOC+282*!W8={ZBO@aSp^A!%Xf#TX z_R- zx0?=4OiVbPPF4k#O4ZZT^ZNB`I%i;DfOYj*f2~%7!GI8&oSfw2iJ~Z|tp5J~6z5En zaN^F+PE%78ekVaAkw_XF8^3@5&TcDtWiG47VzIWiHjzk#hw}3BMn*<}e*Qp|cKIw4 zi9{9_7FJeP!r`z?CNmn17K;UcJj^beo11Umy!ra|t3shL8jV-3TuJ?2FfGR_>tBcb n0(=jVuoRO}1u>DZ3>N+a;s=s6-Ne0j00000NkvXXu0mjf!5Xl2 diff --git a/Tests/images/variation_adobe_older_harfbuzz.png b/Tests/images/variation_adobe_older_harfbuzz.png index 71b879bc5a214d09e4b04898327324ce2432e9c4..5abc907caab61ae026894516e33814ddedefaa02 100644 GIT binary patch literal 1486 zcmb7E`BxGM5Jp|!nr4!jn!9;tYM{LU1CI=HRW=`4c>w0IiRRH_<$>rbZ<9hi@qpDe z??PLS4#QH?v@FXKBg5l`2WTp(OR1Pg>f(RU{xHW+^UZuS^U?h=KKi<5x&Qz`AMNXP zMjZ)iJE^0o_B+x9;{bp*6YYfzNWC#%9_nipZq%VH!$f8t$KT`;+2z-#awD9wb!Ay) zy0YNcIq0=I1M9ZcVkA4^>bOa^f=}nn5SfuN5+~@v zGSETi&Pkbvf`@Wnba$Ve_rb7OtV!@o+e21X8UauzC(Ieh5gQ>_kB>v6(M%?@rKP2& z4uL?pRDc~E6o&bs#=?Ef`ZKp5KbB_3*d~;#i1sF-p`j9qL?{$$XlN9gd&uF$l$3#p!=^j0n>dF- zj~)$gOJhchdV6_1UaSCrnICrN31zE&rQsD}ZF3_%J$+p+x3;>;W>38;IY3SLwf+75)t;2-=;(JpAb}#DWf)Hqry3eE?ORkR6qLOfbY(F}@%-QjScNgH{Q{rr55sYh9L^R2S$uI@TieXcjQR&GJTtSiT~c!L7x%4C5)Nd7OeRwqRaNHGo$4pa&ep--t*w=B9B7sdt#SZ+#?Z-7_eZ zv1jRCl^9ZLs=b{v2vo{qz4H$Y3{;=$!om^#jP>Q;laka&AtASRMy#l*Uc927*k~;7 zd3X1Ps3>(;Bdy8tk&#*t;&+#EySltcA-C=t?X$3;EYA(A7eKC>CLgo5MsXyq?H51$ zSGWC1r_^Q{-w5tsfZ QMy=BT^e-4MJ}TnsfBVnPVE_OC literal 1479 zcma)62~!dX6lPpq@zx|AMKV0lL@WdVQEGH*j zsPhV&{MTb_MLP2Rxur*+v++qVONuCB)6ZU{thsH=;$xC!1&l*{Epp^(ev zR@E3-dbPb-R5@g)7ewwB#f^;z$_mk3D?(&sBp`TeXPZu^iwQhtQ4vVphcn`lwW(C} zmAs#$pLrD(&Xe8^cfev>EX@a^lKuui|27maqNTW`r02;K3Wefc97Q6zm@^M^?u$es zu^4mcP-&9W!Gp^S3o}$Sxw$(oATZEqH#$0cOF0{sle7E!wK=r5y1KfymSI>!;7L=F zD?7hhLk7xwwHJ~JbUNlv=*g3bt@4@4l@+qTzXl{{ZK3SVpWEBp`T0QK0B`TExw*Sh z)b#X&C=|hv(c9}!L@SvT!HXowfw%Nqa8ROto*!q#&{suk>+7Dzx}aABEIIAQ9_i`nX=t$b1@@7bKRmvGzLS-erBpaZ zr}_Bt<7%-e(PI(B(JB(FT>E(^mb+Cvh`Tr1e?t+DA3A?4huWC z`YG59li({zy01348*HdvzO}X(H2~k)+6aqwL!&n*C%vEP6GLW#Q_|8LJ-@CkvZkz5 zw~?Kl-Jbpd#;|}uM#R{a1Yj58B&j z6&PqOpFb4!dTOdRS=m=&IWREr@#Du+otZ|Or#deB@Knzd6CM3^>EE6_gEYEGZMt)* zBXi^WrwJjs}g#DF$ICBG#Zo192y?} zkDX1S{G;%!iEY7RFvd3`A|h0K#{1D|G!BOY27}$&gu+A=OZ8td^jPddD&CN}C<;%>jjaV>QBd1gIyW#6Qp>9jV2Nj7}ZC$re P0W=`cACGJHjl23kUTwM} diff --git a/Tests/images/variation_adobe_older_harfbuzz_axes.png b/Tests/images/variation_adobe_older_harfbuzz_axes.png index 9376c1d7b8fc6b11907267aec1a32ad73a0c45fd..b39d460f977052d35a601499d806b248f01e370f 100644 GIT binary patch delta 1327 zcmV+~18H&gv+Jvhn zNEeZd3T{QaC<=mTK#2B15JUq6X;73j(7aI^sW6IYGHtR^@8aD1-uJ4Xp67h$^W%&6 zw>rM>|2*$=e(Ld@BhO~DQ3Pgq{q6?-B#0~kL>2%d3jmP?fRlU$7L!c{5`Tf)1dqpK zUHzLq;CC+zQCC;z@9&RZ_V)IoZG}P+7#K)zzrMZ>4i0kE9tX_K%xr9IP!!eL+Uo1; z`%|@S?x155iNxtR&Hep7K9!c1#;~{tNKH+3+9V5@p0hGCGMK*spztrnXf$qbZ*x`c zo6DM;o3mQ2Tvhw#vPMToxqmF@_KR9uTU%dW?>LSZaXcL~Ha6DQ);jL>_4PGAK3-m4 z9u*bk=jSI9i4qeNo0^*T_V!%VciOXysodP$n3xzBr}MV9Hn#`vG95Rc2{@D{KYkGRx&(DvdD3M6Cy1I(S=jP_n z6-h@&huvOUtrlJAiA18y%S(Lr^Yb$$B}E_*Xti2SnskkaWa)G|p-@PNXJ%&XX1lw) z=_xNSubrJ8yIF-ofqxb%D=WjNy1Ke3iW(Rg;G{{nct}=ub~c^N&dzq6Jv%!?rQ~wC z-K^1Q3=IuMfw8eMdR(W|`S|!$R8(-)q+2{8YkqznB~>bwW58mupg=@KgkxZ2WCR7m z!ou$E?mj*~BoawdQqs%I3nxu7!4tBSN+n7zFE2X=aGQKS|9|uI(=k(6Sco#!)zzJy zog$Iw`1qKsCYivqvAx&DZ?Nz0@6o-XBVw^wuh-MtGMOwgG7_J?zP_HEoP2tELL=|* z@7~_tc12E3P7)Fl@Xw%Htu87m;;KnTu-1$has8$R1qCthCpY8>udlBhw8#+FpGmxk>rZ%HT^&l+ z)YPz8>+$h1K0ZD-H`i*lK0Q6*KM^f0Eu1vT1W(A)YJasTDHICN&(D99G?`2%Cnt{M zGMP*$6rw4OMuQ6S`TWhz&7W_Q2_CwFmrA85ER{+v7K>x%`T03NKfkoJ)NZ_|r-!1b zp`jssu(Gle6--P_e1Cs;x-R}4{OaloaCpw}kgUtgOZ=84Gc$92eH{<1t*s>|Ckutb z)6-LYe1CCqQ6LZ$78cqSyt%mv4h}{I+uPfnuA7{kj57H3@Y~xP;{*KCk;PCLUy1H5_m4XhO1}2OC zM!3NBLp8J6Twh<0UrEsL`Fxp7W-u83*O@b1OyL2<*VmU^F1O2w#p1QKHPC_6u)p15 zjm2VFSy|cJ+cTTZzP`TE(a||MIUyk-zi!3d-Q9(S1(V4X6ci+pNK#W%+5H*%8)W^t lkOjZ*0T5Y}K?P10z~8Mkqnkt5xVZoT002ovPDHLkV1lRtljHyZ delta 1324 zcmV+{1=ISg3#bc_B!6#7L_t(|ob8!ONP1fsz^^IN;39)+Q$zznkQG?b3`JxSEdp0f zkS-z@6}*aeQ4|ExfDrA2AczJC(x515paI<|jZ{?7JlSNE-M6^!opA%7fMS@(|_yr$gIU;86O|7tgK|B zTCLV>Hq$iy*T3L%)oL{wpP8BAbILsC{4*0MYierB&(DvdD5+Gsva*84XJ=>8j-w5zL&3HkW= zm`o=2bhTQI)_*E3EyY8fot+d#_4oI?-=s^tBr7W`i^4kB?C)l}g1SeY-BQ-#bRNOl}cqqLpffxVX6JPC*df zkQEgbg^u_3_ITIX-riQJRK2~u_4W142u;&!wfgn-)!i03!uvCc7jgaxudS^`nbpH+wD(JPx!Ayb91x%O>)5-vVRN)1Im<0Bqt{)zskJ7zdtxQa2Qf5 zl@f^rMfG|;swor-H#RohY?2FJ+QBOn3Y4r+C~P(xJLmcNIWI4-q@;u$>hA8QC~9zU z5Ff0ls6aIn5)$6u-`NxK=inC?7l6}qgqLKUpP%FRD;XIXYinzG!s_a3Qc{vcA~`%f z{C_rgVPQce5)~8_d>g;Mz77r!Mm5{o+SuzRB_*K*d_Vm5_Qv@{{{tq=YPFh7CcR!i zG&F?E>hJH@>2yY;(TO#)*&G`ii_eOQi76~BEGjBu#zR6vmY0|DDLXqmTCJ8zZfa`M z>2zqnespxSw6s)QT#OggYPH{7_x1ImXn$sA=FZN}?(S|?Rh2@a0G&7kOcwKXaDwxP zYF4YYuC5N>NiYb7LZwo9baeE+IY&4N;Ry_OyIrMHeM^wZWUH&Ipc7}{f4jpQo6WYo zyu7uwWwly;eSM>%qO!BI@h_Ce+1%WmpP#?KzYhuulFQ{ODJlH^4E+tV{#?l4_mf@) iHkQ+8Ka@EKHf+G0000 diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png index 9e5fe70e539e1640d6e9d807c2841487369d25b3..2adb517a759aab4a06e83cc7ab604f97f88c1d27 100644 GIT binary patch delta 1321 zcmV+^1=jkm3z!R#B!6E?L_t(|ob8!gD7$YQz@M?jY-TITS+c}#M9yjCh8%CWb4U_e zNwFw7U2x&>6G=HVLdt~;Tu~G$q?Dv(=A102Vk5M}@8YfZ|CpKgyUqN6^?a`0J>T#1 zeZSAAchBDU9m`}gAp$d8e!GEx2_g#skp+Or0zhN|AhG}uS(9%CBY)s~Su8tfYis-Y zB{&Z@C_5n`fj-{e-ews3MU+bA?CdN;NGui?6&2ZA&h#C0=GE1e#c_mCMn(o6YHVzz z##>ukzpekHG&D3=?9eut?pXqXz~A4WVRB?-B*V;~W@%~3fz_DG0^z^iYE84u<;YAp3pGKo`W@bhrk(l+sX0!A2^Uu%E zt(?MQ+ZIC#g(5sWoXh1peATd92YYyYd@Ly`Sy)&=2nmJ4x_`PliA1u#zCJiOsMqV~ z=jYef)|Qr*;^X7#DN|EZCnqPZt*v;S!otGr>}={-p->zg9Q5|~QXjLjvI+|e^?JQX zBoc{4R&H)=#5Tt!;Fb0I`kIoGLM8M0{Jp(BIy5mcLDvZk419QaFq^xxv*Y6O-`*;f zN{vP%9aXE-C9=i3p+Q=4LxPWP&qfsnu!@heIcyot>E_q^73Q(YUy{&lR4Yo&o{_=o}u8 zcYJ(oGMT8ysl2>A{93c2Lxwm*7QX5D`udthH5!dbB!8k4yuH0G7MYuy!_SzOmbSUM z$!4=7BO{-lp6u+9AFruQOJUl$`D2K!Os!nchF8YX2C{(Fb_IJn-XUOvP z^+k*c3k&y`Zf|eLN2p)G zA8h?2&VP{Q=H^Bp-`w2TQ_9=h8~?bnvNAFA+U4Q=ksi>%+qJDmU_xJZ^b9Z-l z-GANPc|6|H(GfmgUS5tD%*@O*n;0D(O-Ef_U8!r_zTb2yURw|W)gM%$C zE!EZ4=p(hYwVj=vV`F1q&E448u>N8?G&I!gJ2*Z*UR6~^C)C&1D-;SUG(9~nm&?s6 z2!+C)o}QVR87im0zu)ZY%gV|M4Gl#I(SJE0V6yu9`mC&$!piiUo13z-GW<@0g2UmI zmX_Y$-kLY}b$up17z_rPOvYld@Q|06*YNN#Xw@{VZ%^!SbUNL}#>U~{p;oKqa=Bu$ zSSpp`kB5%s?CflHb@lrCn$PEp#p3kzbnEX0t?&!7{$0r5_W+1207Mo5A`1YK1(SOP f9g}JWC>G!^b@Y-wy!^oM00000NkvXXu0mjf+`_2U delta 1325 zcmV+|1=9MM3$6>0B!6{DL_t(|ob8%RNE=-gfNwNDVq#1{B=|%UMBRu|A1xBajSqZm zbf*>3q9TZ(K|wUS(1nkRq6=Gm;S0fbp{p!(ApsRZkRl-(>kBbj8?Z^mN|NbfVE${= zWKN9ZU%20Da?iPE&ix!OGn4Qrib4c-c)fN5{}MzN03r(jk&{yeEPsG&S-fY^(b4hp zOW+PZC_6PZl{wzr++-PgU35C#!omVVNTE=amXi^X%;G(Q$-Oc6K%%YH4Yq z$JJ`J%la=(b93{fO}YWwJxeSWhlYl-Opc3-W0}bT8jZ$NrP#^>t^fZc>*C^KXJ^M# z)!534jg4hr6qNj;Xn!;s%5&c$yWZfdtE&-0`T6bbOUt3!n9v-$@ ztxHQwYiny7jV2`}h0dRzp8o#*yIQTri+5_zKQ1or`uf@FVn090pY?)yc`pvzlWKN3!r+=Rbe`&^PfQe*gYmTwHv2celO0-PF{?jGN8o zs;Vl_bm~?!{axy;U-Me>2qY=+BnM_4RMF^qt@^Xjiqobn;p|rF#tJO-6 ztgNiy)x=`)>FMdi^Wx9aNl8g=FP4V8$DMke*Ey^109vg zWPd+@{=_G4Z*Tki`wNA_{r!D>yt1+qua=XO^YEmJiHS^tx3@Qa&Dw0Xsi`T8#R6V3 zC|jq~4G#~ux3|~U)}s3~G&FQ~caM#YIhn52YMsBEj*gDT{|6@~CTePGm}pZ|)5OFC z9h#Y$QK?i8(SSia77^z~LZ|_FUpQFE3ob>G9_Fz8`)a zF9aJi48uj#`17wX@IOIW0F(tlSpbv;Kv@8k1wdHPlFc@50 zTWdC(+uPfIzyIdurd%#ZB9Uk`s%e@{moxY3x^B}73`2onFesWA78b<8@Aq$PY>4yD z&W_5EAu#uzo}SvY0z&D@nw*>zhj2J74sN&GESiUr0G6{DJ_rPJx7qoc2{ud}nW z-QC@%r>ED~*MEb9gKReY_V#AeU(&kQrWFuMvP?cdKU1lc&*#hM^TRb13WY!*u)4ac z>$+Jim&@UBI2w&UKR-*GTdmgMvCe?Wy1l&>&to7E7_LDYL;d2kz12GqZ+kkj7BI_Z7D%8;u5ZPn*DG6^ljjJbFAHYkg{J zO4Mg(XU&FqJTA`3WKw;UbZjn{gYIb)SY|CRFN;`NUthP@x3;!iF4y?@`0??v*&vES zmij2^*ndPK0o~IsP`kNYE*6VDK0aEDnx@Uq&u6pQ@9*z+yIrf*etv!o!>HHmJ)fXz zwYs;rcXD!K?bLNWlgS(&9=^Z7_ZvIZqcCDHS*21*D2~Nq*81GsoTy)3UJjlh?cCqr z7sX5_Gu&8bz&?hZPN(P4^RnG;D^9R>_ItbfSbv9r)P8(?Y;JCPz258VYim89&-;A7 zm6es3mzTj4q@52B51~*f5{dMDGf*Gv3~*+hpP#4G=}M*YYlAe2teIsN`(+li*8xV8 zS<(h+5lUdRR^?3RU*7G%;5iLDZ?Ku04NK9 ivXeFgU6GT_A delta 797 zcmV+&1LFLa2dM{;Bx!I-L_t(|ob8&uOY2Y=#^W7at^F`vq*bv+7e&xP#|nO-C|JZv zXz^bVw2O`oB8ZzqJ9Ke!cGAJ2D2+Oc=%fL$Iw>e}91eusHr|{kG?5F>Z#up_yzk-H z^d(?hgb*507wxb4}CqX#|E)K-YB{9vB#q2VK`ECMM*0etur1i3hB?o12?Hjet;kvclo8JS3Az zc?g940z<{9VHnwLc5Q9#})I+E0s##0z!y#xvcB@=;)|vnpUw=sdRO9rBbQO%ge^( z{r$ab+W^319UUFX=TX;nZvi1hJRX;st*tGqI5{~fiwg@2jmg2mLD#kcfW11jwYAk9 z(150Cp-`yq;95*160U83g8;Q_?JUm$$4%t80UdWGdf_I5$$EW#&15o>NMv_+*IwV> z-;YM4LqkK=YSjuHA0Kyib|#a_)6-L^TVvbX+gs?KK7q+PJUo=oV>leP*CUaLtoQcz zT7gU^BhNE4Gf=n2wuM3gx~ETIJ8O7&SibUDZ={mx>1j>VT3cIx*VosrfGkQ`P`Adm zGb*exxHu@Mt^^QKA->m{H)b#*VoryUtdCqo12@)Z_w4%)zZ?^ z#>R#{Y?@{+ms?$3eSUs+Z0qS!Xfl|rlamvvIGs-0>wSHFvc9*s=i0T?62IXlZS;JAha zva{6Z219dpmSfG!wXT_XKs!}}X00000NkvXXu0mjfa-);( diff --git a/Tests/images/variation_tiny_name.png b/Tests/images/variation_tiny_name.png index a0c6ffe3f1fb36b6460cfc29b08f5a2bff4694df..69f1550dbfc17edec9ff38ae07da108746c95e27 100644 GIT binary patch delta 1009 zcmVYCj6o1meu8#pM4upN zA$@?THZ9t^P%2A_Xi?Cj%-%jk`-tdGvME7n7vhlAc@_gAGvmxXJZd~}e#_(afA0L6 zJI%fI2LNmY8(qIWf&U4Sjv0SsAz+fVwYBB*`J7H?C=?2XLQbdC=ktw@j;h6(nVGk@ zw{}b}nN0FL?{GM#r>9?EU)#|F2UwBGWGq7@Nm3NWG73d1m1=Ae2n4FtYHM3yv6dxC z5{vZH)6>?@ED#82S{xrAx3&cq>t&eF=UHSuKR>e!V@JR4US3{W+X8=!wJg8i&k`gE zg3V^Txw)B|nsPWCgM)+X>+9d&-);Ma_SJY&L6H1OS)I)l8Nqwqr2KQrX_#)=b*va;YC39UX+9?O{(ke3i;I7YKR-W)vBhGsO%@nI$ZGax(A(Rqzvgf_JUcr(IXT(*+i+}b%u=h( zX1lz+G>pw;GHtTJNDGc=YPFiaSSpnsA0Nx*a;a1*m&=Nx*o?MBi9~|qI8E{C>B%rQ z7K{1)ewLTycERqoz7eS-2m%1aVlm4u>U()tm-c6KdwYLtSTv8_+uLi`&0`8e*7^Cl zUI749DwX;9`R?v+b6?qPR=>@>xVSJZn#ZbrI}c(RK0G{VD(Zm*$8r1n`=6hmD=RA| zU(e_B#bQxYyuZIUESkqM9)z?Uge?90QDen!w@*w=n0(#u_v<&9JkJ{z&0`r4LRt^| zO<1i~H3wdD>2$hrfTAe+U9pP#UT%2!X?Y^5kKNtfZOytuf{+COg+d_`iEtb@G&Hoa zv7sLz%W^atjRlL*M`_ve~Rw6vZ+MMLL~s9U>42)a&*34uQp5RxlXk zk$!r5+TNK30s+ks$H&L*9RiE>ZTR%`#3SqR@sX#EANIa`etvH65Lm2#W%+zQo*=*9 zZ?oBMZf<5~W*iR3@bK`)#>Usz7u(r<0>~-PZEa1Hrg;K}LLo&_bc=4c+h{m8_Ghq`1puj3%H#3q&RYnl?y2ZY}J|kId?9ad@OJ#d|+i229M@NxJMEAt}e*fa);`{r% z+S%LNYowqRS(g8{tWv3@nY72_0f2*p1Iv@2oSgjp`~-lpu`wQ5CnqNZ0|Q2%uB@zd z%7-ZkS^0ckGigbZ03Z|!@tl7?p9g?UCS%#DJ^@EZN7;|uZuk0s`g)_$=#UO`5VB-h z)=b@Qw*!FPZs(CD%d(oYwY4RRqHd2M2(ehKR4VDlHWPk}|GKh*!60zoS!ZWwYEG?I zySloXpPy%Y4i69S?(Wn!Hny4YTOeey3qVm60bpoo$kJ5vnRR`AJvcaM-n_K5^!@$K z#+J+F4q4y?AE0s#MT2&N9t+T75WHKoTf~NTJ@W93<5(%Hr$McfhDfqoM zmnKd8em?*t5(&#u)R%Gq=i@}C=@gmbwffBg#G>fkB^Vl)m4+j)#h@!tf}AM z-|H4lVmUWL+73b%`*PG;vD@uaQ&T2~tCtVE#uPS4Km-#O!9P?q VD3n|!qC)@x002ovPDHLkV1fYv>6) +/* round a 26.6 pixel coordinate to the nearest integer */ +#define PIXEL(x) ((((x)+32) & -64)>>6) static PyObject* geterror(int code) @@ -612,6 +612,7 @@ font_getsize(FontObject* self, PyObject* args) { 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 */ @@ -619,7 +620,6 @@ font_getsize(FontObject* self, PyObject* args) FT_Face face; FT_Glyph glyph; FT_BBox bbox; /* glyph bounding box */ - FT_Vector pen; GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i, count; /* glyph_info index and length */ int horizontal_dir; /* is primary axis horizontal? */ @@ -662,8 +662,8 @@ font_getsize(FontObject* self, PyObject* args) face = self->face; if (horizontal_dir) { - pen.x = position + glyph_info[i].x_offset; - pen.y = glyph_info[i].y_offset; + px = PIXEL(position + glyph_info[i].x_offset); + py = PIXEL(glyph_info[i].y_offset); position += glyph_info[i].x_advance; advanced = PIXEL(position); @@ -671,8 +671,8 @@ font_getsize(FontObject* self, PyObject* args) x_max = advanced; } } else { - pen.x = glyph_info[i].x_offset; - pen.y = position + glyph_info[i].y_offset; + px = PIXEL(glyph_info[i].x_offset); + py = PIXEL(position + glyph_info[i].y_offset); position += glyph_info[i].y_advance; advanced = PIXEL(position); @@ -680,7 +680,6 @@ font_getsize(FontObject* self, PyObject* args) y_min = advanced; } } - FT_Set_Transform(face, NULL, &pen); error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { @@ -693,15 +692,19 @@ font_getsize(FontObject* self, PyObject* args) } 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; } @@ -716,7 +719,6 @@ font_getsize(FontObject* self, PyObject* args) x_anchor = y_anchor = 0; if (face) { - FT_Set_Transform(face, NULL, NULL); if (horizontal_dir) { x_anchor = 0; y_anchor = PIXEL(self->face->size->metrics.ascender); @@ -737,6 +739,7 @@ static PyObject* font_render(FontObject* self, 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; @@ -744,7 +747,6 @@ font_render(FontObject* self, PyObject* args) FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; - FT_Vector pen; FT_Stroker stroker = NULL; GlyphInfo *glyph_info = NULL; /* computed text layout */ size_t i, count; /* glyph_info index and length */ @@ -803,9 +805,8 @@ font_render(FontObject* self, PyObject* args) */ x = y = x_min = y_max = 0; for (i = 0; i < count; i++) { - pen.x = x + glyph_info[i].x_offset; - pen.y = y + glyph_info[i].y_offset; - FT_Set_Transform(self->face, NULL, &pen); + 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) { @@ -815,11 +816,11 @@ font_render(FontObject* self, PyObject* args) glyph_slot = self->face->glyph; bitmap = glyph_slot->bitmap; - if (glyph_slot->bitmap_top > y_max) { - y_max = glyph_slot->bitmap_top; + if (glyph_slot->bitmap_top + py > y_max) { + y_max = glyph_slot->bitmap_top + py; } - if (glyph_slot->bitmap_left < x_min) { - x_min = glyph_slot->bitmap_left; + if (glyph_slot->bitmap_left + px < x_min) { + x_min = glyph_slot->bitmap_left + px; } x += glyph_info[i].x_advance; @@ -827,17 +828,16 @@ font_render(FontObject* self, PyObject* args) } /* set pen position to text origin */ - x = (-x_min + stroke_width) * 64; - y = (-y_max + (-stroke_width)) * 64; + x = (-x_min + stroke_width) << 6; + y = (-y_max + (-stroke_width)) << 6; if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; } for (i = 0; i < count; i++) { - pen.x = x + glyph_info[i].x_offset; - pen.y = y + glyph_info[i].y_offset; - FT_Set_Transform(self->face, NULL, &pen); + 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) { @@ -861,12 +861,12 @@ font_render(FontObject* self, PyObject* args) bitmap_glyph = (FT_BitmapGlyph)glyph; bitmap = bitmap_glyph->bitmap; - xx = bitmap_glyph->left; - yy = -bitmap_glyph->top; + xx = px + bitmap_glyph->left; + yy = -(py + bitmap_glyph->top); } else { bitmap = glyph_slot->bitmap; - xx = glyph_slot->bitmap_left; - yy = -glyph_slot->bitmap_top; + xx = px + glyph_slot->bitmap_left; + yy = -(py + glyph_slot->bitmap_top); } /* clip glyph bitmap width to target image bounds */ @@ -916,7 +916,6 @@ font_render(FontObject* self, PyObject* args) } } - FT_Set_Transform(self->face, NULL, NULL); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; From d1edf86953c37131bd340a28c76ac8638ef0742f Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 9 Sep 2020 05:35:55 +0200 Subject: [PATCH 09/10] tests cleanup --- Tests/test_imagedraw.py | 2 +- Tests/test_imagefont.py | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 58a4f1c9693..cc8bd24383c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1039,7 +1039,7 @@ def test_stroke_descender(): draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") # Assert - assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.78) + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) @skip_unless_feature("freetype2") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9ded460d3ea..89a0613dfc2 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 @@ def test_rotated_transposed_font_get_mask(self): mask = transposed_font.getmask(text) # Assert - assert mask.size in ((13, 107), (13, 108)) + assert mask.size == self.metrics["mask"][::-1] def test_unrotated_transposed_font_get_mask(self): # Arrange @@ -356,7 +371,7 @@ def test_unrotated_transposed_font_get_mask(self): mask = transposed_font.getmask(text) # Assert - assert mask.size in ((107, 13), (108, 13)) + assert mask.size == self.metrics["mask"] def test_free_type_font_get_name(self): # Arrange @@ -400,7 +415,7 @@ def test_free_type_font_get_mask(self): mask = font.getmask(text) # Assert - assert mask.size in ((107, 13), (108, 13)) + assert mask.size == self.metrics["mask"] def test_load_path_not_found(self): # Arrange From a501ba930884d8eaac6f56a4af251be2a26db211 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 11 Sep 2020 01:14:00 +0200 Subject: [PATCH 10/10] update link in comment --- src/PIL/ImageFont.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 25320d43d84..a61a32c253d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -259,7 +259,8 @@ def getsize( :return: (width, height) """ - # vertical offset is added for historical reasons, see discussion in #4789 + # 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,