From 446938586dc50a1e8aed1971ba876e85a65f5ac9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 8 Jun 2020 13:27:15 +0200 Subject: [PATCH 1/2] Switch from a hand-written glyph outline decomposer to FreeType's one. Instead of walking through FT_Outline ourselves, use FreeType's FT_Outline_Decompose to decompose glyphs into paths. Fixes incorrect positioning of first and last control points of certain contours; compare e.g. ```python from pylab import * from matplotlib.textpath import TextPath from matplotlib.patches import PathPatch rcParams["mathtext.fontset"] = "stixsans" path = TextPath((0, 0), r"$\delta$", size=72) plot(*path.vertices.T, ".-", lw=.5) gca().add_patch(PathPatch(path, alpha=.5)) gca().set(aspect="equal") show() ``` before and after the patch (before the patch, the inner loop was not properly closed). (Technically, I believe the earlier bug affected start/end-of-contours that are implicitly created per the ttf format at the midpoint between two conic control points.) A few SVG files need to be updated too. --- .../test_mathtext/mathtext_cm_69.svg | 243 ++-- .../test_mathtext/mathtext_dejavuserif_69.svg | 185 +-- .../test_mathtext/mathtext_stix_05.svg | 450 +++--- .../test_mathtext/mathtext_stix_26.svg | 531 +++---- .../test_mathtext/mathtext_stix_69.svg | 209 +-- .../test_mathtext/mathtext_stixsans_05.svg | 350 ++--- .../test_mathtext/mathtext_stixsans_26.svg | 483 ++++--- .../test_mathtext/mathtext_stixsans_28.svg | 351 ++--- .../test_mathtext/mathtext_stixsans_37.svg | 1052 +++++++------- .../test_mathtext/mathtext_stixsans_40.svg | 495 +++---- .../test_mathtext/mathtext_stixsans_41.svg | 1232 +++++++++-------- .../test_mathtext/mathtext_stixsans_69.svg | 183 +-- src/ft2font.cpp | 430 ++---- src/ft2font.h | 7 +- src/ft2font_wrapper.cpp | 14 +- 15 files changed, 3154 insertions(+), 3061 deletions(-) diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg index 7a63312d15d4..21c6e291cdbd 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_69.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:33.473355 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,124 +30,128 @@ z - - + + - + - + - - - - - +" id="STIXGeneral-Italic-c5" transform="scale(0.015625)"/> + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_69.svg index 18d953ca5660..9a3e21d549e9 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_69.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_69.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:42.291943 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,94 +30,98 @@ z - - + + - + - + - - - - - +" id="DejaVuSerif-Italic-c5" transform="scale(0.015625)"/> + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_05.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_05.svg index 926dea2b92e5..c13a83ae034c 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_05.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_05.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:34.129013 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,220 +30,231 @@ z - - - - + + - + - - + + - - - - - - - - - - - - - - - - - - - - - - +M 4077 768 +L 307 768 +L 307 1190 +L 4077 1190 +L 4077 768 +z +" id="STIXGeneral-Regular-3d" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_26.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_26.svg index 34c8efe0cd47..adc8bed4cb06 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_26.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_26.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:34.626436 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,264 +30,282 @@ z - - + + - - - - - - - - - + - - + - - - +" id="STIXGeneral-Regular-301" transform="scale(0.015625)"/> + + + + + + + + + + + - + - + - - - + + + - + - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg index 25e468a1f39c..9738cc21a69b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_69.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:35.788326 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,107 +30,111 @@ z - - + + - + - + - - - - - +" id="STIXGeneral-Italic-c5" transform="scale(0.015625)"/> + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_05.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_05.svg index 98b9bd65970b..f4634ce76c62 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_05.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_05.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:36.331853 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,172 +30,183 @@ z - - - - + + - + - + - - + - + - - - - - - - - - - - - - - - - - - - - +" id="STIXGeneral-Regular-3c" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_26.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_26.svg index 01a95a9c3782..5a1ed4923774 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_26.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_26.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:36.792928 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,243 +30,261 @@ z - - + + - - - + - - - - - - + - + - - - - +M 1203 1875 +L 2579 1875 +Q 2605 1978 2605 2086 +Q 2605 2509 2093 2509 +Q 1837 2509 1587 2349 +Q 1338 2189 1203 1875 +z +" id="STIXGeneral-Italic-1d626" transform="scale(0.015625)"/> + + + + + + + + + + - + - + - + - + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_28.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_28.svg index 6b002332b5dc..65a11fcbd59d 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_28.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_28.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:36.857412 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,172 +30,184 @@ z - - + + - - + - - - + + - - - - - - - - - - - +" id="STIXGeneral-Regular-1d7e8" transform="scale(0.015625)"/> + + + + + + + + + + + + - + + + + + 2020-08-05T13:43:37.056437 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,508 +30,539 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + 2020-08-05T13:43:37.190526 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,235 +30,253 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_41.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_41.svg index ac4809cf0045..8b097236395d 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_41.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_41.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:37.223168 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,597 +30,628 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg index 6f13a8fd413f..8242b10930c7 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_69.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-05T13:43:37.975730 + image/svg+xml + + + Matplotlib v3.3.0rc1.post530.dev0+g4269804ce, https://matplotlib.org/ + + + + + - + @@ -19,94 +30,98 @@ z - - + + - + - + - - - - - +" id="STIXGeneral-Italic-c5" transform="scale(0.015625)"/> + + + + diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 817289945f72..720d8a622df5 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -9,6 +9,7 @@ #include "ft2font.h" #include "mplutils.h" +#include "numpy_cpp.h" #include "py_exceptions.h" #ifndef M_PI @@ -168,11 +169,6 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_dirty = true; } -inline double conv(long v) -{ - return v / 64.; -} - static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) { FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); @@ -188,325 +184,133 @@ static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) } -int FT2Font::get_path_count() +// ft_outline_decomposer should be passed to FT_Outline_Decompose. On the +// first pass, vertices and codes are set to NULL, and index is simply +// incremented for each vertex that should be inserted, so that it is set, at +// the end, to the total number of vertices. On a second pass, vertices and +// codes should point to correctly sized arrays, and index set again to zero, +// to get fill vertices and codes with the outline decomposition. +struct ft_outline_decomposer { - // get the glyph as a path, a list of (COMMAND, *args) as described in matplotlib.path - // this code is from agg's decompose_ft_outline with minor modifications - - if (!face->glyph) { - throw std::runtime_error("No glyph loaded"); - } - - FT_Outline &outline = face->glyph->outline; - - FT_Vector v_last; - FT_Vector v_control; - FT_Vector v_start; - - FT_Vector *point; - FT_Vector *limit; - char *tags; - - int n; // index of contour in outline - int first; // index of first point in contour - char tag; // current point's state - int count; - - count = 0; - first = 0; - for (n = 0; n < outline.n_contours; n++) { - int last; // index of last point in contour - bool starts_with_last; - - last = outline.contours[n]; - limit = outline.points + last; - - v_start = outline.points[first]; - v_last = outline.points[last]; - - v_control = v_start; - - point = outline.points + first; - tags = outline.tags + first; - tag = FT_CURVE_TAG(tags[0]); - - // A contour cannot start with a cubic control point! - if (tag == FT_CURVE_TAG_CUBIC) { - throw std::runtime_error("A contour cannot start with a cubic control point"); - } else if (tag == FT_CURVE_TAG_CONIC) { - starts_with_last = true; - } else { - starts_with_last = false; - } - - count++; - - while (point < limit) { - if (!starts_with_last) { - point++; - tags++; - } - starts_with_last = false; - - tag = FT_CURVE_TAG(tags[0]); - switch (tag) { - case FT_CURVE_TAG_ON: // emit a single line_to - { - count++; - continue; - } - - case FT_CURVE_TAG_CONIC: // consume conic arcs - { - Count_Do_Conic: - if (point < limit) { - point++; - tags++; - tag = FT_CURVE_TAG(tags[0]); - - if (tag == FT_CURVE_TAG_ON) { - count += 2; - continue; - } - - if (tag != FT_CURVE_TAG_CONIC) { - throw std::runtime_error("Invalid font"); - } - - count += 2; - - goto Count_Do_Conic; - } - - count += 2; - - goto Count_Close; - } - - default: // FT_CURVE_TAG_CUBIC - { - if (point + 1 > limit || FT_CURVE_TAG(tags[1]) != FT_CURVE_TAG_CUBIC) { - throw std::runtime_error("Invalid font"); - } - - point += 2; - tags += 2; - - if (point <= limit) { - count += 3; - continue; - } + int index; + double* vertices; + unsigned char* codes; +}; - count += 3; - - goto Count_Close; - } - } +static int +ft_outline_move_to(FT_Vector const* to, void* user) +{ + ft_outline_decomposer* d = reinterpret_cast(user); + if (d->codes) { + if (d->index) { + // Appending ENDPOLY is important to make patheffects work. + *(d->vertices++) = 0; + *(d->vertices++) = 0; + *(d->codes++) = ENDPOLY; } - - Count_Close: - count++; - first = last + 1; + *(d->vertices++) = to->x / 64.; + *(d->vertices++) = to->y / 64.; + *(d->codes++) = MOVETO; } - - return count; + d->index += d->index ? 2 : 1; + return 0; } -void FT2Font::get_path(double *outpoints, unsigned char *outcodes) +static int +ft_outline_line_to(FT_Vector const* to, void* user) { - FT_Outline &outline = face->glyph->outline; - bool flip_y = false; // todo, pass me as kwarg - - FT_Vector v_last; - FT_Vector v_control; - FT_Vector v_start; - - FT_Vector *point; - FT_Vector *limit; - char *tags; - - int n; // index of contour in outline - int first; // index of first point in contour - char tag; // current point's state - - first = 0; - for (n = 0; n < outline.n_contours; n++) { - int last; // index of last point in contour - bool starts_with_last; - - last = outline.contours[n]; - limit = outline.points + last; - - v_start = outline.points[first]; - v_last = outline.points[last]; - - v_control = v_start; - - point = outline.points + first; - tags = outline.tags + first; - tag = FT_CURVE_TAG(tags[0]); - - double x, y; - if (tag != FT_CURVE_TAG_ON) { - x = conv(v_last.x); - y = flip_y ? -conv(v_last.y) : conv(v_last.y); - starts_with_last = true; - } else { - x = conv(v_start.x); - y = flip_y ? -conv(v_start.y) : conv(v_start.y); - starts_with_last = false; - } - - *(outpoints++) = x; - *(outpoints++) = y; - *(outcodes++) = MOVETO; + ft_outline_decomposer* d = reinterpret_cast(user); + if (d->codes) { + *(d->vertices++) = to->x / 64.; + *(d->vertices++) = to->y / 64.; + *(d->codes++) = LINETO; + } + d->index++; + return 0; +} - while (point < limit) { - if (!starts_with_last) { - point++; - tags++; - } - starts_with_last = false; - - tag = FT_CURVE_TAG(tags[0]); - switch (tag) { - case FT_CURVE_TAG_ON: // emit a single line_to - { - double x = conv(point->x); - double y = flip_y ? -conv(point->y) : conv(point->y); - *(outpoints++) = x; - *(outpoints++) = y; - *(outcodes++) = LINETO; - continue; - } +static int +ft_outline_conic_to(FT_Vector const* control, FT_Vector const* to, void* user) +{ + ft_outline_decomposer* d = reinterpret_cast(user); + if (d->codes) { + *(d->vertices++) = control->x / 64.; + *(d->vertices++) = control->y / 64.; + *(d->vertices++) = to->x / 64.; + *(d->vertices++) = to->y / 64.; + *(d->codes++) = CURVE3; + *(d->codes++) = CURVE3; + } + d->index += 2; + return 0; +} - case FT_CURVE_TAG_CONIC: // consume conic arcs - { - v_control.x = point->x; - v_control.y = point->y; - - Do_Conic: - if (point < limit) { - FT_Vector vec; - FT_Vector v_middle; - - point++; - tags++; - tag = FT_CURVE_TAG(tags[0]); - - vec.x = point->x; - vec.y = point->y; - - if (tag == FT_CURVE_TAG_ON) { - double xctl = conv(v_control.x); - double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y); - double xto = conv(vec.x); - double yto = flip_y ? -conv(vec.y) : conv(vec.y); - *(outpoints++) = xctl; - *(outpoints++) = yctl; - *(outpoints++) = xto; - *(outpoints++) = yto; - *(outcodes++) = CURVE3; - *(outcodes++) = CURVE3; - continue; - } - - v_middle.x = (v_control.x + vec.x) / 2; - v_middle.y = (v_control.y + vec.y) / 2; - - double xctl = conv(v_control.x); - double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y); - double xto = conv(v_middle.x); - double yto = flip_y ? -conv(v_middle.y) : conv(v_middle.y); - *(outpoints++) = xctl; - *(outpoints++) = yctl; - *(outpoints++) = xto; - *(outpoints++) = yto; - *(outcodes++) = CURVE3; - *(outcodes++) = CURVE3; - - v_control = vec; - goto Do_Conic; - } - double xctl = conv(v_control.x); - double yctl = flip_y ? -conv(v_control.y) : conv(v_control.y); - double xto = conv(v_start.x); - double yto = flip_y ? -conv(v_start.y) : conv(v_start.y); - - *(outpoints++) = xctl; - *(outpoints++) = yctl; - *(outpoints++) = xto; - *(outpoints++) = yto; - *(outcodes++) = CURVE3; - *(outcodes++) = CURVE3; - - goto Close; - } +static int +ft_outline_cubic_to( + FT_Vector const* c1, FT_Vector const* c2, FT_Vector const* to, void* user) +{ + ft_outline_decomposer* d = reinterpret_cast(user); + if (d->codes) { + *(d->vertices++) = c1->x / 64.; + *(d->vertices++) = c1->y / 64.; + *(d->vertices++) = c2->x / 64.; + *(d->vertices++) = c2->y / 64.; + *(d->vertices++) = to->x / 64.; + *(d->vertices++) = to->y / 64.; + *(d->codes++) = CURVE4; + *(d->codes++) = CURVE4; + *(d->codes++) = CURVE4; + } + d->index += 3; + return 0; +} - default: // FT_CURVE_TAG_CUBIC - { - FT_Vector vec1, vec2; - - vec1.x = point[0].x; - vec1.y = point[0].y; - vec2.x = point[1].x; - vec2.y = point[1].y; - - point += 2; - tags += 2; - - if (point <= limit) { - FT_Vector vec; - - vec.x = point->x; - vec.y = point->y; - - double xctl1 = conv(vec1.x); - double yctl1 = flip_y ? -conv(vec1.y) : conv(vec1.y); - double xctl2 = conv(vec2.x); - double yctl2 = flip_y ? -conv(vec2.y) : conv(vec2.y); - double xto = conv(vec.x); - double yto = flip_y ? -conv(vec.y) : conv(vec.y); - - (*outpoints++) = xctl1; - (*outpoints++) = yctl1; - (*outpoints++) = xctl2; - (*outpoints++) = yctl2; - (*outpoints++) = xto; - (*outpoints++) = yto; - (*outcodes++) = CURVE4; - (*outcodes++) = CURVE4; - (*outcodes++) = CURVE4; - continue; - } - - double xctl1 = conv(vec1.x); - double yctl1 = flip_y ? -conv(vec1.y) : conv(vec1.y); - double xctl2 = conv(vec2.x); - double yctl2 = flip_y ? -conv(vec2.y) : conv(vec2.y); - double xto = conv(v_start.x); - double yto = flip_y ? -conv(v_start.y) : conv(v_start.y); - (*outpoints++) = xctl1; - (*outpoints++) = yctl1; - (*outpoints++) = xctl2; - (*outpoints++) = yctl2; - (*outpoints++) = xto; - (*outpoints++) = yto; - (*outcodes++) = CURVE4; - (*outcodes++) = CURVE4; - (*outcodes++) = CURVE4; - - goto Close; - } - } - } +static FT_Outline_Funcs ft_outline_funcs = { + ft_outline_move_to, + ft_outline_line_to, + ft_outline_conic_to, + ft_outline_cubic_to}; - Close: - (*outpoints++) = 0.0; - (*outpoints++) = 0.0; - (*outcodes++) = ENDPOLY; - first = last + 1; - } +PyObject* +FT2Font::get_path() +{ + if (!face->glyph) { + PyErr_SetString(PyExc_RuntimeError, "No glyph loaded"); + return NULL; + } + ft_outline_decomposer decomposer = {}; + if (FT_Error error = + FT_Outline_Decompose( + &face->glyph->outline, &ft_outline_funcs, &decomposer)) { + PyErr_Format(PyExc_RuntimeError, + "FT_Outline_Decompose failed with error 0x%x", error); + return NULL; + } + if (!decomposer.index) { // Don't append ENDPOLY to null glyphs. + npy_intp vertices_dims[2] = { 0, 2 }; + numpy::array_view vertices(vertices_dims); + npy_intp codes_dims[1] = { 0 }; + numpy::array_view codes(codes_dims); + return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); + } + npy_intp vertices_dims[2] = { decomposer.index + 1, 2 }; + numpy::array_view vertices(vertices_dims); + npy_intp codes_dims[1] = { decomposer.index + 1 }; + numpy::array_view codes(codes_dims); + decomposer.index = 0; + decomposer.vertices = vertices.data(); + decomposer.codes = codes.data(); + if (FT_Error error = + FT_Outline_Decompose( + &face->glyph->outline, &ft_outline_funcs, &decomposer)) { + PyErr_Format(PyExc_RuntimeError, + "FT_Outline_Decompose failed with error 0x%x", error); + return NULL; + } + *(decomposer.vertices++) = 0; + *(decomposer.vertices++) = 0; + *(decomposer.codes++) = ENDPOLY; + return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); } FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face(NULL) diff --git a/src/ft2font.h b/src/ft2font.h index ea48e7f49bae..0863f3450b36 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -10,11 +10,15 @@ extern "C" { #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_OUTLINE_H #include FT_SFNT_NAMES_H #include FT_TYPE1_TABLES_H #include FT_TRUETYPE_TABLES_H } +#define PY_SSIZE_T_CLEAN +#include + /* By definition, FT_FIXED as 2 16bit values stored in a single long. */ @@ -87,8 +91,7 @@ class FT2Font void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); void get_glyph_name(unsigned int glyph_number, char *buffer); long get_name_index(char *name); - int get_path_count(); - void get_path(double *outpoints, unsigned char *outcodes); + PyObject* get_path(); FT_Face &get_face() { diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index ba361ac0279a..ea8d1e19237a 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1370,19 +1370,7 @@ const char *PyFT2Font_get_path__doc__ = static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args, PyObject *kwds) { - int count; - - CALL_CPP("get_path", (count = self->x->get_path_count())); - - npy_intp vertices_dims[2] = { count, 2 }; - numpy::array_view vertices(vertices_dims); - - npy_intp codes_dims[1] = { count }; - numpy::array_view codes(codes_dims); - - self->x->get_path(vertices.data(), codes.data()); - - return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); + CALL_CPP("get_path", return self->x->get_path()); } const char *PyFT2Font_get_image__doc__ = From 19359c11c5778b625d0e9e6b8a26f04655342830 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 8 Jun 2020 15:22:15 +0200 Subject: [PATCH 2/2] Stop using ttconv for pdf. Quite a bit of work goes into reproducing quirks of the old ttconv code in order not to update baseline images, but this can easily be dropped once the new baseline images system goes in. figure_align_label.pdf gets modified because of tiny differences in outline extraction (due to the ttconv emulation); there's already the svg test that tests the fixed-dpi case so I feel OK deleting it. This also fixes ttc subsetting for pdf output (cf. changes in test_font_manager.py). --- lib/matplotlib/backends/backend_pdf.py | 58 +++++++++++++++--- .../test_figure/figure_align_labels.pdf | Bin 11021 -> 0 bytes lib/matplotlib/tests/test_figure.py | 2 +- lib/matplotlib/tests/test_font_manager.py | 3 +- src/_path.h | 55 +++++++++-------- 5 files changed, 79 insertions(+), 39 deletions(-) delete mode 100644 lib/matplotlib/tests/baseline_images/test_figure/figure_align_labels.pdf diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e8e8ece1310c..bdd55eeeb8b4 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -42,7 +42,6 @@ from matplotlib.path import Path from matplotlib.dates import UTC from matplotlib import _path -from matplotlib import _ttconv from . import _backend_pdf_ps _log = logging.getLogger(__name__) @@ -556,6 +555,52 @@ def _flush(self): self.compressobj = None +def _get_pdf_charprocs(font_path, glyph_ids): + font = get_font(font_path, hinting_factor=1) + conv = 1000 / font.units_per_EM # Conversion to PS units (1/1000's). + procs = {} + for glyph_id in glyph_ids: + g = font.load_glyph(glyph_id, LOAD_NO_SCALE) + # NOTE: We should be using round(), but instead use + # "(x+.5).astype(int)" to keep backcompat with the old ttconv code + # (this is different for negative x's). + d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int) + v, c = font.get_path() + v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's). + # Backcompat with old ttconv code: control points between two quads are + # omitted if they are exactly at the midpoint between the control of + # the quad before and the quad after, but ttconv used to interpolate + # *after* conversion to PS units, causing floating point errors. Here + # we reproduce ttconv's logic, detecting these "implicit" points and + # re-interpolating them. Note that occasionally (e.g. with DejaVu Sans + # glyph "0") a point detected as "implicit" is actually explicit, and + # will thus be shifted by 1. + quads, = np.nonzero(c == 3) + quads_on = quads[1::2] + quads_mid_on = np.array( + sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int) + implicit = quads_mid_on[ + (v[quads_mid_on] # As above, use astype(int), not // division + == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int)) + .all(axis=1)] + if (font.postscript_name, glyph_id) in [ + ("DejaVuSerif-Italic", 77), # j + ("DejaVuSerif-Italic", 135), # \AA + ]: + v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1). + v = (v * conv + .5).astype(int) # As above re: truncation vs rounding. + v[implicit] = (( # Fix implicit points; again, truncate. + (v[implicit - 1] + v[implicit + 1]) / 2).astype(int)) + procs[font.get_glyph_name(glyph_id)] = ( + " ".join(map(str, d1)).encode("ascii") + b" d1\n" + + _path.convert_to_string( + Path(v, c), None, None, False, None, -1, + # no code for quad Beziers triggers auto-conversion to cubics. + [b"m", b"l", b"", b"c", b"h"], True) + + b"f") + return procs + + class PdfFile: """PDF file object.""" @@ -1089,15 +1134,8 @@ def get_char_width(charcode): differencesArray.append(Name(name)) last_c = c - # Make the charprocs array (using ttconv to generate the - # actual outlines) - try: - rawcharprocs = _ttconv.get_pdf_charprocs( - os.fsencode(filename), glyph_ids) - except RuntimeError: - _log.warning("The PDF backend does not currently support the " - "selected font.") - raise + # Make the charprocs array. + rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] diff --git a/lib/matplotlib/tests/baseline_images/test_figure/figure_align_labels.pdf b/lib/matplotlib/tests/baseline_images/test_figure/figure_align_labels.pdf deleted file mode 100644 index c1763f68be19210a13a4e5a4fb92ce81e7fede67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11021 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 NfrP+dN;=Ar{{v= str && *q == '0'; --q) { - // Rewind through all the zeros - } - - // If the end is a decimal point, delete that too - if (q >= str && *q == '.') { - --q; - } - - // Truncate the string - ++q; - *q = 0; - - try { + if (precision == -1) { + // Special-case for compat with old ttconv code, which *truncated* + // values with a cast to int instead of rounding them as printf + // would do. The only point where non-integer values arise is from + // quad2cubic conversion (as we already perform a first truncation + // on Python's side), which can introduce additional floating point + // error (by adding 2/3 delta-x and then 1/3 delta-x), so compensate by + // first rounding to the closest 1/3 and then truncating. + char str[255]; + PyOS_snprintf(str, 255, "%d", (int)(round(val * 3)) / 3); buffer += str; - } catch (std::bad_alloc& e) { + } else { + char *str = PyOS_double_to_string( + val, format_code, precision, Py_DTSF_ADD_DOT_0, NULL); + // Delete trailing zeros and decimal point + char *c = str + strlen(str) - 1; // Start at last character. + // Rewind through all the zeros and, if present, the trailing decimal + // point. Py_DTSF_ADD_DOT_0 ensures we won't go past the start of str. + while (*c == '0') { + --c; + } + if (*c == '.') { + --c; + } + try { + buffer.append(str, c + 1); + } catch (std::bad_alloc& e) { + PyMem_Free(str); + throw e; + } PyMem_Free(str); - throw e; } - PyMem_Free(str); }