From 8caff885c195fd65588c5dae6c4feaf5e57c44f7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 3 May 2025 03:04:33 -0400 Subject: [PATCH 01/58] Remove ttconv backwards-compatibility code --- lib/matplotlib/backends/backend_pdf.py | 38 +++++---------------- src/_path.h | 47 ++++++++++---------------- 2 files changed, 25 insertions(+), 60 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 8db640d888b1..53bf10c90b48 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -617,40 +617,18 @@ def _get_pdf_charprocs(font_path, glyph_ids): procs = {} for glyph_id in glyph_ids: g = font.load_glyph(glyph_id, LoadFlags.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) + d1 = [ + round(g.horiAdvance * conv), 0, + # Round bbox corners *outwards*, so that they indeed bound the glyph. + math.floor(g.bbox[0] * conv), math.floor(g.bbox[1] * conv), + math.ceil(g.bbox[2] * conv), math.ceil(g.bbox[3] * conv), + ] 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)) + v = (v * 64 * conv).round() # Back to TrueType's internal units (1/64's). 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, + Path(v, c), None, None, False, None, 0, # no code for quad Beziers triggers auto-conversion to cubics. [b"m", b"l", b"", b"c", b"h"], True) + b"f") diff --git a/src/_path.h b/src/_path.h index c03703776760..1b54426c7e81 100644 --- a/src/_path.h +++ b/src/_path.h @@ -1066,38 +1066,25 @@ void quad2cubic(double x0, double y0, void __add_number(double val, char format_code, int precision, std::string& buffer) { - 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; - } else { - char *str = PyOS_double_to_string( - val, format_code, precision, Py_DTSF_ADD_DOT_0, nullptr); - // 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; - } + char *str = PyOS_double_to_string( + val, format_code, precision, Py_DTSF_ADD_DOT_0, nullptr); + // 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); } From c44db77b9fb1318934767cfa01397ba6b81e30f7 Mon Sep 17 00:00:00 2001 From: Wiliam Date: Tue, 26 Nov 2024 19:24:26 +0100 Subject: [PATCH 02/58] Fix center of rotation with rotation_mode='anchor' --- lib/matplotlib/backends/backend_agg.py | 18 ++++++++++++++---- .../test_text/rotation_anchor.png | Bin 0 -> 15484 bytes lib/matplotlib/tests/test_text.py | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b435ae565ce4..f25b89e2b053 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -197,10 +197,20 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): xo, yo = font.get_bitmap_offset() xo /= 64.0 yo /= 64.0 - xd = d * sin(radians(angle)) - yd = d * cos(radians(angle)) - x = round(x + xo + xd) - y = round(y + yo + yd) + + rad = radians(angle) + xd = d * sin(rad) + yd = d * cos(rad) + # Rotating the offset vector ensures text rotates around the anchor point. + # Without this, rotated text offsets incorrectly, causing a horizontal shift. + # Applying the 2D rotation matrix. + rotated_xo = xo * cos(rad) - yo * sin(rad) + rotated_yo = xo * sin(rad) + yo * cos(rad) + # Subtract rotated_yo to account for the inverted y-axis in computer graphics, + # compared to the mathematical convention. + x = round(x + rotated_xo + xd) + y = round(y - rotated_yo + yd) + self._renderer.draw_text_image(font, x, y + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): diff --git a/lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png b/lib/matplotlib/tests/baseline_images/test_text/rotation_anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..3dad1f9a19f754558bcaae58122536d4544bd710 GIT binary patch literal 15484 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

e-(#m58!y>EXEBxmOCkFyGfXFjdMYq~`*< z6WjNf@2x3a@_dzxJM|ldFxN`-l9a5g*FRV2tC>occ?1-)J1k=bCX*Nx>iNx|t@h3E zMW#dpd8n0*tlw0nn9;ut4mTu$fyfHz&=fnKJa~nay?DO0X%tV`42tke49HXI|INX{ znodkW@SCZbnSkYt7)JhDIr)z&XGS2D(Nlm(JlRy{-L--6Pj z1|{`}cok227M%XmqTGaVbsY~}u9aQY`u?8ot>+@5&H1k~OPUt8EP?t=jF=THtRK<6 z>^LJC{J*C@PEP6Hdi|q@w$53!p+o_BG*xE+I!gpe`XG?5(dyW=JqaBYBt_QS;R!<> z?{hw=b5m^8=KEDXG4(0gsr9@K))9%Wa3d+yU+YP98% z2Ral7yEdXgbM0N~;^3ahh12xSl+e)C20CmAZJidu02hwf!X78mM-%xJ4oCg8C_O2B zO946W_0g}XK5T0*-Esfox2d72S>SfG+#X|UdiG|#fWbKCT0RU5Yj>mGBiGqb0Cj(l z+IlOPh>#^nIf{{OptSTXrD2_qJLBSV{?DI?;Tqv%0ngmhW%JJ3^9vb~%xhfjD5Q5g z4$cf$erCj5Fogg&j$VD1cNcIlxmSaY0p5)Q&0MG`Am8ddP~I#!-%Jm|gBA7S71>~8 z-!$^t1~#;C9f%^nZ>`pyogZ;w?&Jb@O7!&=!!7G`+kE zH0|lJYfVtNxOY5;S3ua_Xa~Gv6-B5arr@+{am35 zms~L*fEEY)r)#Z= z>}S=TjfQg)&WUY$j*OWF3d{eYp+s`Ud8HPwR&%M{DW9cbImr5H^X-OJSc`Snmvp_^ z+HiF?ZWrIZlZ;$f{a!(j`lKI8$#k+Fznj5FPms)%*rD!4^_QD%-8nUbDTkLfZtqe& z+%8Tx$19}I7lBzg+k_xXNJzkGpt;zcIB9N8y0W9)V|?s)urTU8ryS`i)+J4eZh70- z1fCL2=x&Q-v^Ra4i(idOFzfa^KV0DIZhluSLio6wra)zfbx4W?s~4qX)#KP?MH@ATUqM;WYv54?EJDPNz`W8 zBvD3C#p$5<_3HlNV42g};k%k6Hmwv*S)Wxie&D^1Go|$YVt)%$jI6ia)pimmyBl-u zle@kZ(e{#(1HYb&Bpo`u!P~yEjURXG2O}bw4W-70z`Ww&2>5OY9()I~2{>yeFq;Vq zqC&GM45g@}m5JJ!`FgDWgf7{<_u-9utlBZ=@Pl0!RL8|vP?-eK@0{#zH CCYEdf literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 407d7a96be4d..906484c8a526 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -301,6 +301,22 @@ def test_alignment(): ax.set_yticks([]) +@image_comparison(baseline_images=['rotation_anchor.png'], style='mpl20', + remove_text=True) +def test_rotation_mode_anchor(): + fig, ax = plt.subplots() + + ax.plot([0, 1], lw=0) + ax.axvline(.5, linewidth=.5, color='.5') + ax.axhline(.5, linewidth=.5, color='.5') + + N = 4 + for r in range(N): + ax.text(.5, .5, 'pP', color=f'C{r}', size=100, + rotation=r/N*360, rotation_mode='anchor', + verticalalignment='center_baseline') + + @image_comparison(['axes_titles.png']) def test_axes_titles(): # Related to issue #3327 From 41cc933f33a87fe7f2b69fe0ffe9a82e60c1389b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 12 Jun 2025 18:42:01 -0400 Subject: [PATCH 03/58] Remove fallback code for glyph indices Glyph indices are specific to each font. It does not make sense to fall back based on glyph index to another font. This could only really be populated by calling `FT2Font.set_text`, but even that was fragile. If a fallback font was used for a character with the same glyph index as a previous character in the main font, then these lookups could be overwritten to the fallback instead of the main font, with a completely different character! Fortunately, nothing actually uses or requires a fallback through glyph indices. --- src/ft2font.cpp | 65 +++++------------------------------------ src/ft2font.h | 8 ++--- src/ft2font_wrapper.cpp | 12 +++----- 3 files changed, 14 insertions(+), 71 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index bdfa2873ca80..9726e96233ab 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -253,7 +253,6 @@ void FT2Font::clear() } glyphs.clear(); - glyph_to_font.clear(); char_to_font.clear(); for (auto & fallback : fallbacks) { @@ -287,35 +286,13 @@ void FT2Font::select_charmap(unsigned long i) FT_CHECK(FT_Select_Charmap, face, (FT_Encoding)i); } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, - bool fallback = false) -{ - if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && - glyph_to_font.find(right) != glyph_to_font.end()) { - FT2Font *left_ft_object = glyph_to_font[left]; - FT2Font *right_ft_object = glyph_to_font[right]; - if (left_ft_object != right_ft_object) { - // we do not know how to do kerning between different fonts - return 0; - } - // if left_ft_object is the same as right_ft_object, - // do the exact same thing which set_text does. - return right_ft_object->get_kerning(left, right, mode, false); - } - else - { - FT_Vector delta; - return get_kerning(left, right, mode, delta); - } -} - -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, - FT_Vector &delta) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode) { if (!FT_HAS_KERNING(face)) { return 0; } + FT_Vector delta; if (!FT_Get_Kerning(face, left, right, mode, &delta)) { return (int)(delta.x) / (hinting_factor << kerning_factor); } else { @@ -364,7 +341,7 @@ void FT2Font::set_text( std::set glyph_seen_fonts; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, glyph_to_font, codepoint, flags, + char_to_font, codepoint, flags, charcode_error, glyph_error, glyph_seen_fonts, false); if (!was_found) { ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); @@ -372,8 +349,7 @@ void FT2Font::set_text( // come back to top-most font ft_object_with_glyph = this; char_to_font[codepoint] = ft_object_with_glyph; - glyph_to_font[glyph_index] = ft_object_with_glyph; - ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); + ft_object_with_glyph->load_glyph(glyph_index, flags); } else if (ft_object_with_glyph->warn_if_used) { ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); } @@ -383,8 +359,7 @@ void FT2Font::set_text( ft_object_with_glyph->has_kerning() && // if the font knows how to kern previous && glyph_index // and we really have 2 glyphs ) { - FT_Vector delta; - pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT, delta); + pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT); } // extract glyph image and store it in our table @@ -434,7 +409,7 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool FT_Error charcode_error, glyph_error; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, - glyphs, char_to_font, glyph_to_font, + glyphs, char_to_font, charcode, flags, charcode_error, glyph_error, glyph_seen_fonts, true); if (!was_found) { @@ -493,7 +468,6 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_UInt &final_glyph_index, std::vector &parent_glyphs, std::unordered_map &parent_char_to_font, - std::unordered_map &parent_glyph_to_font, long charcode, FT_Int32 flags, FT_Error &charcode_error, @@ -523,7 +497,6 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, // need to store this for anytime a character is loaded from a parent // FT2Font object or to generate a mapping of individual characters to fonts ft_object_with_glyph = this; - parent_glyph_to_font[final_glyph_index] = this; parent_char_to_font[charcode] = this; parent_glyphs.push_back(thisGlyph); return true; @@ -532,7 +505,7 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, for (auto & fallback : fallbacks) { bool was_found = fallback->load_char_with_fallback( ft_object_with_glyph, final_glyph_index, parent_glyphs, - parent_char_to_font, parent_glyph_to_font, charcode, flags, + parent_char_to_font, charcode, flags, charcode_error, glyph_error, glyph_seen_fonts, override); if (was_found) { return true; @@ -542,21 +515,6 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, } } -void FT2Font::load_glyph(FT_UInt glyph_index, - FT_Int32 flags, - FT2Font *&ft_object, - bool fallback = false) -{ - // cache is only for parent FT2Font - if (fallback && glyph_to_font.find(glyph_index) != glyph_to_font.end()) { - ft_object = glyph_to_font[glyph_index]; - } else { - ft_object = this; - } - - ft_object->load_glyph(glyph_index, flags); -} - void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) { FT_CHECK(FT_Load_Glyph, face, glyph_index, flags); @@ -644,15 +602,8 @@ void FT2Font::draw_glyph_to_bitmap( draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, - bool fallback = false) +void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer) { - if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { - // cache is only for parent FT2Font - FT2Font *ft_object = glyph_to_font[glyph_number]; - ft_object->get_glyph_name(glyph_number, buffer, false); - return; - } if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ diff --git a/src/ft2font.h b/src/ft2font.h index 8db0239ed4fd..262ff395ac5d 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -108,22 +108,19 @@ class FT2Font void select_charmap(unsigned long i); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, std::vector &xys); - int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback); - int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta); + int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_UInt &final_glyph_index, std::vector &parent_glyphs, std::unordered_map &parent_char_to_font, - std::unordered_map &parent_glyph_to_font, long charcode, FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, std::set &glyph_seen_fonts, bool override); - void load_glyph(FT_UInt glyph_index, FT_Int32 flags, FT2Font *&ft_object, bool fallback); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); @@ -132,7 +129,7 @@ class FT2Font void draw_glyph_to_bitmap( py::array_t im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); + void get_glyph_name(unsigned int glyph_number, std::string &buffer); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); void get_path(std::vector &vertices, std::vector &codes); @@ -176,7 +173,6 @@ class FT2Font FT_Vector pen; /* untransformed origin */ std::vector glyphs; std::vector fallbacks; - std::unordered_map glyph_to_font; std::unordered_map char_to_font; FT_BBox bbox; FT_Pos advance; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index ca2db6aa0e5b..cb816efff9a9 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -618,7 +618,6 @@ static int PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, std::variant mode_or_int) { - bool fallback = true; FT_Kerning_Mode mode; if (auto value = std::get_if(&mode_or_int)) { @@ -636,7 +635,7 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, throw py::type_error("mode must be Kerning or int"); } - return self->x->get_kerning(left, right, mode, fallback); + return self->x->get_kerning(left, right, mode); } const char *PyFT2Font_get_fontmap__doc__ = R"""( @@ -834,8 +833,6 @@ static PyGlyph * PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) { - bool fallback = true; - FT2Font *ft_object = nullptr; LoadFlags flags; if (auto value = std::get_if(&flags_or_int)) { @@ -853,9 +850,9 @@ PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, throw py::type_error("flags must be LoadFlags or int"); } - self->x->load_glyph(glyph_index, static_cast(flags), ft_object, fallback); + self->x->load_glyph(glyph_index, static_cast(flags)); - return PyGlyph_from_FT2Font(ft_object); + return PyGlyph_from_FT2Font(self->x); } const char *PyFT2Font_get_width_height__doc__ = R"""( @@ -1022,10 +1019,9 @@ static py::str PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) { std::string buffer; - bool fallback = true; buffer.resize(128); - self->x->get_glyph_name(glyph_number, buffer, fallback); + self->x->get_glyph_name(glyph_number, buffer); return buffer; } From 389373eca101a613ffb7f88271d5eb9c10712005 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 30 Jun 2025 17:44:09 -0400 Subject: [PATCH 04/58] ci: Preload existing test images from text-overhaul-figures branch This allows checking that there are no _new_ failures, without committing the new figures to the repo until the branch is complete. --- .appveyor.yml | 20 ++++++++++++++++++++ .github/workflows/tests.yml | 19 +++++++++++++++++++ azure-pipelines.yml | 19 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index c3fcb0ea9591..3e3a3b884d18 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -60,6 +60,26 @@ install: - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 - micromamba activate mpl-dev +before_test: + - git config --global user.name 'Matplotlib' + - git config --global user.email 'nobody@matplotlib.org' + - git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + - git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + - ps: | + $conflicts = git diff --name-only --diff-filter=U ` + lib/matplotlib/tests/baseline_images ` + lib/mpl_toolkits/*/tests/baseline_images + if ($conflicts) { + git checkout --ours -- $conflicts + git add -- $conflicts + } + git status + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + - git commit -m "Preload test images from branch text-overhaul-figures" + test_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85ace93445b6..53d47346c6eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -95,6 +95,25 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Preload test images + run: | + git config --global user.name 'Matplotlib' + git config --global user.email 'nobody@matplotlib.org' + git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + conflicts=$(git diff --name-only --diff-filter=U \ + lib/matplotlib/tests/baseline_images \ + lib/mpl_toolkits/*/tests/baseline_images) + if [ -n "${conflicts}" ]; then + git checkout --ours -- "${conflicts}" + git add -- "${conflicts}" + fi + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + git commit -m 'Preload test images from branch text-overhaul-figures' + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d68a9d36f0d3..a5a0e965e97b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,6 +68,25 @@ stages: architecture: 'x64' displayName: 'Use Python $(python.version)' + - bash: | + git config --global user.name 'Matplotlib' + git config --global user.email 'nobody@matplotlib.org' + git fetch https://github.com/QuLogic/matplotlib.git text-overhaul-figures:text-overhaul-figures + git merge --no-commit text-overhaul-figures || true + # If there are any conflicts in baseline images, then pick "ours", + # which should be the updated images in the PR. + conflicts=$(git diff --name-only --diff-filter=U \ + lib/matplotlib/tests/baseline_images \ + lib/mpl_toolkits/*/tests/baseline_images) + if [ -n "${conflicts}" ]; then + git checkout --ours -- "${conflicts}" + git add -- "${conflicts}" + fi + # If committing fails, there were conflicts other than the baseline images, + # which should not be allowed to happen, and should fail the build. + git commit -m 'Preload test images from branch text-overhaul-figures' + displayName: Preload test images + - bash: | choco install ninja displayName: 'Install dependencies' From a01860688d2d1a01c1f808983c15a170aa90f099 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 3 Jun 2025 00:32:10 -0400 Subject: [PATCH 05/58] Add typing to AFM parser Also, check some expected conditions at parse time instead of somewhere during use of the data. --- lib/matplotlib/_afm.py | 231 ++++++++++++++------------ lib/matplotlib/backends/backend_ps.py | 2 +- lib/matplotlib/tests/test_afm.py | 49 +++--- 3 files changed, 154 insertions(+), 128 deletions(-) diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py index 9094206c2d7c..352d3c42247e 100644 --- a/lib/matplotlib/_afm.py +++ b/lib/matplotlib/_afm.py @@ -27,9 +27,10 @@ being used. """ -from collections import namedtuple +import inspect import logging import re +from typing import BinaryIO, NamedTuple, TypedDict from ._mathtext_data import uni2type1 @@ -37,7 +38,7 @@ _log = logging.getLogger(__name__) -def _to_int(x): +def _to_int(x: bytes | str) -> int: # Some AFM files have floats where we are expecting ints -- there is # probably a better way to handle this (support floats, round rather than # truncate). But I don't know what the best approach is now and this @@ -46,7 +47,7 @@ def _to_int(x): return int(float(x)) -def _to_float(x): +def _to_float(x: bytes | str) -> float: # Some AFM files use "," instead of "." as decimal separator -- this # shouldn't be ambiguous (unless someone is wicked enough to use "," as # thousands separator...). @@ -57,27 +58,56 @@ def _to_float(x): return float(x.replace(',', '.')) -def _to_str(x): +def _to_str(x: bytes) -> str: return x.decode('utf8') -def _to_list_of_ints(s): +def _to_list_of_ints(s: bytes) -> list[int]: s = s.replace(b',', b' ') return [_to_int(val) for val in s.split()] -def _to_list_of_floats(s): +def _to_list_of_floats(s: bytes | str) -> list[float]: return [_to_float(val) for val in s.split()] -def _to_bool(s): +def _to_bool(s: bytes) -> bool: if s.lower().strip() in (b'false', b'0', b'no'): return False else: return True -def _parse_header(fh): +class FontMetricsHeader(TypedDict, total=False): + StartFontMetrics: float + FontName: str + FullName: str + FamilyName: str + Weight: str + ItalicAngle: float + IsFixedPitch: bool + FontBBox: list[int] + UnderlinePosition: float + UnderlineThickness: float + Version: str + # Some AFM files have non-ASCII characters (which are not allowed by the spec). + # Given that there is actually no public API to even access this field, just return + # it as straight bytes. + Notice: bytes + EncodingScheme: str + CapHeight: float # Is the second version a mistake, or + Capheight: float # do some AFM files contain 'Capheight'? -JKS + XHeight: float + Ascender: float + Descender: float + StdHW: float + StdVW: float + StartCharMetrics: int + CharacterSet: str + Characters: int + + +def _parse_header(fh: BinaryIO) -> FontMetricsHeader: """ Read the font metrics header (up to the char metrics). @@ -98,34 +128,15 @@ def _parse_header(fh): * '-168 -218 1000 898' -> [-168, -218, 1000, 898] """ header_converters = { - b'StartFontMetrics': _to_float, - b'FontName': _to_str, - b'FullName': _to_str, - b'FamilyName': _to_str, - b'Weight': _to_str, - b'ItalicAngle': _to_float, - b'IsFixedPitch': _to_bool, - b'FontBBox': _to_list_of_ints, - b'UnderlinePosition': _to_float, - b'UnderlineThickness': _to_float, - b'Version': _to_str, - # Some AFM files have non-ASCII characters (which are not allowed by - # the spec). Given that there is actually no public API to even access - # this field, just return it as straight bytes. - b'Notice': lambda x: x, - b'EncodingScheme': _to_str, - b'CapHeight': _to_float, # Is the second version a mistake, or - b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS - b'XHeight': _to_float, - b'Ascender': _to_float, - b'Descender': _to_float, - b'StdHW': _to_float, - b'StdVW': _to_float, - b'StartCharMetrics': _to_int, - b'CharacterSet': _to_str, - b'Characters': _to_int, + bool: _to_bool, + bytes: lambda x: x, + float: _to_float, + int: _to_int, + list[int]: _to_list_of_ints, + str: _to_str, } - d = {} + header_value_types = inspect.get_annotations(FontMetricsHeader) + d: FontMetricsHeader = {} first_line = True for line in fh: line = line.rstrip() @@ -147,14 +158,16 @@ def _parse_header(fh): else: val = b'' try: - converter = header_converters[key] - except KeyError: + key_str = _to_str(key) + value_type = header_value_types[key_str] + except (KeyError, UnicodeDecodeError): _log.error("Found an unknown keyword in AFM header (was %r)", key) continue try: - d[key] = converter(val) + converter = header_converters[value_type] + d[key_str] = converter(val) # type: ignore[literal-required] except ValueError: - _log.error('Value error parsing header in AFM: %s, %s', key, val) + _log.error('Value error parsing header in AFM: %r, %r', key, val) continue if key == b'StartCharMetrics': break @@ -163,8 +176,8 @@ def _parse_header(fh): return d -CharMetrics = namedtuple('CharMetrics', 'width, name, bbox') -CharMetrics.__doc__ = """ +class CharMetrics(NamedTuple): + """ Represents the character metrics of a single character. Notes @@ -172,13 +185,20 @@ def _parse_header(fh): The fields do currently only describe a subset of character metrics information defined in the AFM standard. """ + + width: float + name: str + bbox: tuple[int, int, int, int] + + CharMetrics.width.__doc__ = """The character width (WX).""" CharMetrics.name.__doc__ = """The character name (N).""" CharMetrics.bbox.__doc__ = """ The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" -def _parse_char_metrics(fh): +def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics], + dict[str, CharMetrics]]: """ Parse the given filehandle for character metrics information. @@ -198,12 +218,12 @@ def _parse_char_metrics(fh): """ required_keys = {'C', 'WX', 'N', 'B'} - ascii_d = {} - name_d = {} - for line in fh: + ascii_d: dict[int, CharMetrics] = {} + name_d: dict[str, CharMetrics] = {} + for bline in fh: # We are defensively letting values be utf8. The spec requires # ascii, but there are non-compliant fonts in circulation - line = _to_str(line.rstrip()) # Convert from byte-literal + line = _to_str(bline.rstrip()) if line.startswith('EndCharMetrics'): return ascii_d, name_d # Split the metric line into a dictionary, keyed by metric identifiers @@ -214,8 +234,9 @@ def _parse_char_metrics(fh): num = _to_int(vals['C']) wx = _to_float(vals['WX']) name = vals['N'] - bbox = _to_list_of_floats(vals['B']) - bbox = list(map(int, bbox)) + bbox = tuple(map(int, _to_list_of_floats(vals['B']))) + if len(bbox) != 4: + raise RuntimeError(f'Bad parse: bbox has {len(bbox)} elements, should be 4') metrics = CharMetrics(wx, name, bbox) # Workaround: If the character name is 'Euro', give it the # corresponding character code, according to WinAnsiEncoding (see PDF @@ -230,7 +251,7 @@ def _parse_char_metrics(fh): raise RuntimeError('Bad parse') -def _parse_kern_pairs(fh): +def _parse_kern_pairs(fh: BinaryIO) -> dict[tuple[str, str], float]: """ Return a kern pairs dictionary. @@ -242,12 +263,11 @@ def _parse_kern_pairs(fh): d['A', 'y'] = -50 """ - line = next(fh) if not line.startswith(b'StartKernPairs'): - raise RuntimeError('Bad start of kern pairs data: %s' % line) + raise RuntimeError(f'Bad start of kern pairs data: {line!r}') - d = {} + d: dict[tuple[str, str], float] = {} for line in fh: line = line.rstrip() if not line: @@ -257,21 +277,26 @@ def _parse_kern_pairs(fh): return d vals = line.split() if len(vals) != 4 or vals[0] != b'KPX': - raise RuntimeError('Bad kern pairs line: %s' % line) + raise RuntimeError(f'Bad kern pairs line: {line!r}') c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3]) d[(c1, c2)] = val raise RuntimeError('Bad kern pairs parse') -CompositePart = namedtuple('CompositePart', 'name, dx, dy') -CompositePart.__doc__ = """ - Represents the information on a composite element of a composite char.""" +class CompositePart(NamedTuple): + """Represents the information on a composite element of a composite char.""" + + name: bytes + dx: float + dy: float + + CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'.""" CompositePart.dx.__doc__ = """x-displacement of the part from the origin.""" CompositePart.dy.__doc__ = """y-displacement of the part from the origin.""" -def _parse_composites(fh): +def _parse_composites(fh: BinaryIO) -> dict[bytes, list[CompositePart]]: """ Parse the given filehandle for composites information. @@ -292,11 +317,11 @@ def _parse_composites(fh): will be represented as:: - composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0), - CompositePart(name='acute', dx=160, dy=170)] + composites[b'Aacute'] = [CompositePart(name=b'A', dx=0, dy=0), + CompositePart(name=b'acute', dx=160, dy=170)] """ - composites = {} + composites: dict[bytes, list[CompositePart]] = {} for line in fh: line = line.rstrip() if not line: @@ -306,6 +331,9 @@ def _parse_composites(fh): vals = line.split(b';') cc = vals[0].split() name, _num_parts = cc[1], _to_int(cc[2]) + if len(vals) != _num_parts + 2: # First element is 'CC', last is empty. + raise RuntimeError(f'Bad composites parse: expected {_num_parts} parts, ' + f'but got {len(vals) - 2}') pccParts = [] for s in vals[1:-1]: pcc = s.split() @@ -316,7 +344,8 @@ def _parse_composites(fh): raise RuntimeError('Bad composites parse') -def _parse_optional(fh): +def _parse_optional(fh: BinaryIO) -> tuple[dict[tuple[str, str], float], + dict[bytes, list[CompositePart]]]: """ Parse the optional fields for kern pair data and composites. @@ -329,44 +358,38 @@ def _parse_optional(fh): A dict containing composite information. May be empty. See `._parse_composites`. """ - optional = { - b'StartKernData': _parse_kern_pairs, - b'StartComposites': _parse_composites, - } - - d = {b'StartKernData': {}, - b'StartComposites': {}} + kern_data: dict[tuple[str, str], float] = {} + composites: dict[bytes, list[CompositePart]] = {} for line in fh: line = line.rstrip() if not line: continue - key = line.split()[0] - - if key in optional: - d[key] = optional[key](fh) + match line.split()[0]: + case b'StartKernData': + kern_data = _parse_kern_pairs(fh) + case b'StartComposites': + composites = _parse_composites(fh) - return d[b'StartKernData'], d[b'StartComposites'] + return kern_data, composites class AFM: - def __init__(self, fh): + def __init__(self, fh: BinaryIO): """Parse the AFM file in file object *fh*.""" self._header = _parse_header(fh) self._metrics, self._metrics_by_name = _parse_char_metrics(fh) self._kern, self._composite = _parse_optional(fh) - def get_str_bbox_and_descent(self, s): + def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]: """Return the string bounding box and the maximal descent.""" if not len(s): return 0, 0, 0, 0, 0 - total_width = 0 - namelast = None - miny = 1e9 + total_width = 0.0 + namelast = '' + miny = 1_000_000_000 maxy = 0 left = 0 - if not isinstance(s, str): - s = _to_str(s) for c in s: if c == '\n': continue @@ -386,11 +409,11 @@ def get_str_bbox_and_descent(self, s): return left, miny, total_width, maxy - miny, -miny - def get_glyph_name(self, glyph_ind): # For consistency with FT2Font. + def get_glyph_name(self, glyph_ind: int) -> str: # For consistency with FT2Font. """Get the name of the glyph, i.e., ord(';') is 'semicolon'.""" return self._metrics[glyph_ind].name - def get_char_index(self, c): # For consistency with FT2Font. + def get_char_index(self, c: int) -> int: # For consistency with FT2Font. """ Return the glyph index corresponding to a character code point. @@ -398,38 +421,38 @@ def get_char_index(self, c): # For consistency with FT2Font. """ return c - def get_width_char(self, c): + def get_width_char(self, c: int) -> float: """Get the width of the character code from the character metric WX field.""" return self._metrics[c].width - def get_width_from_char_name(self, name): + def get_width_from_char_name(self, name: str) -> float: """Get the width of the character from a type1 character name.""" return self._metrics_by_name[name].width - def get_kern_dist_from_name(self, name1, name2): + def get_kern_dist_from_name(self, name1: str, name2: str) -> float: """ Return the kerning pair distance (possibly 0) for chars *name1* and *name2*. """ return self._kern.get((name1, name2), 0) - def get_fontname(self): + def get_fontname(self) -> str: """Return the font name, e.g., 'Times-Roman'.""" - return self._header[b'FontName'] + return self._header['FontName'] @property - def postscript_name(self): # For consistency with FT2Font. + def postscript_name(self) -> str: # For consistency with FT2Font. return self.get_fontname() - def get_fullname(self): + def get_fullname(self) -> str: """Return the font full name, e.g., 'Times-Roman'.""" - name = self._header.get(b'FullName') + name = self._header.get('FullName') if name is None: # use FontName as a substitute - name = self._header[b'FontName'] + name = self._header['FontName'] return name - def get_familyname(self): + def get_familyname(self) -> str: """Return the font family name, e.g., 'Times'.""" - name = self._header.get(b'FamilyName') + name = self._header.get('FamilyName') if name is not None: return name @@ -440,26 +463,26 @@ def get_familyname(self): return re.sub(extras, '', name) @property - def family_name(self): # For consistency with FT2Font. + def family_name(self) -> str: # For consistency with FT2Font. """The font family name, e.g., 'Times'.""" return self.get_familyname() - def get_weight(self): + def get_weight(self) -> str: """Return the font weight, e.g., 'Bold' or 'Roman'.""" - return self._header[b'Weight'] + return self._header['Weight'] - def get_angle(self): + def get_angle(self) -> float: """Return the fontangle as float.""" - return self._header[b'ItalicAngle'] + return self._header['ItalicAngle'] - def get_capheight(self): + def get_capheight(self) -> float: """Return the cap height as float.""" - return self._header[b'CapHeight'] + return self._header['CapHeight'] - def get_xheight(self): + def get_xheight(self) -> float: """Return the xheight as float.""" - return self._header[b'XHeight'] + return self._header['XHeight'] - def get_underline_thickness(self): + def get_underline_thickness(self) -> float: """Return the underline thickness as float.""" - return self._header[b'UnderlineThickness'] + return self._header['UnderlineThickness'] diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index ea5868387918..368564a1518d 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -779,7 +779,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): .decode("ascii")) scale = 0.001 * prop.get_size_in_points() thisx = 0 - last_name = None # kerns returns 0 for None. + last_name = '' # kerns returns 0 for ''. for c in s: name = uni2type1.get(ord(c), f"uni{ord(c):04X}") try: diff --git a/lib/matplotlib/tests/test_afm.py b/lib/matplotlib/tests/test_afm.py index 80cf8ac60feb..bc1d587baf6b 100644 --- a/lib/matplotlib/tests/test_afm.py +++ b/lib/matplotlib/tests/test_afm.py @@ -47,20 +47,20 @@ def test_parse_header(): fh = BytesIO(AFM_TEST_DATA) header = _afm._parse_header(fh) assert header == { - b'StartFontMetrics': 2.0, - b'FontName': 'MyFont-Bold', - b'EncodingScheme': 'FontSpecific', - b'FullName': 'My Font Bold', - b'FamilyName': 'Test Fonts', - b'Weight': 'Bold', - b'ItalicAngle': 0.0, - b'IsFixedPitch': False, - b'UnderlinePosition': -100, - b'UnderlineThickness': 56.789, - b'Version': '001.000', - b'Notice': b'Copyright \xa9 2017 No one.', - b'FontBBox': [0, -321, 1234, 369], - b'StartCharMetrics': 3, + 'StartFontMetrics': 2.0, + 'FontName': 'MyFont-Bold', + 'EncodingScheme': 'FontSpecific', + 'FullName': 'My Font Bold', + 'FamilyName': 'Test Fonts', + 'Weight': 'Bold', + 'ItalicAngle': 0.0, + 'IsFixedPitch': False, + 'UnderlinePosition': -100, + 'UnderlineThickness': 56.789, + 'Version': '001.000', + 'Notice': b'Copyright \xa9 2017 No one.', + 'FontBBox': [0, -321, 1234, 369], + 'StartCharMetrics': 3, } @@ -69,20 +69,23 @@ def test_parse_char_metrics(): _afm._parse_header(fh) # position metrics = _afm._parse_char_metrics(fh) assert metrics == ( - {0: (250.0, 'space', [0, 0, 0, 0]), - 42: (1141.0, 'foo', [40, 60, 800, 360]), - 99: (583.0, 'bar', [40, -10, 543, 210]), - }, - {'space': (250.0, 'space', [0, 0, 0, 0]), - 'foo': (1141.0, 'foo', [40, 60, 800, 360]), - 'bar': (583.0, 'bar', [40, -10, 543, 210]), - }) + { + 0: _afm.CharMetrics(250.0, 'space', (0, 0, 0, 0)), + 42: _afm.CharMetrics(1141.0, 'foo', (40, 60, 800, 360)), + 99: _afm.CharMetrics(583.0, 'bar', (40, -10, 543, 210)), + }, + { + 'space': _afm.CharMetrics(250.0, 'space', (0, 0, 0, 0)), + 'foo': _afm.CharMetrics(1141.0, 'foo', (40, 60, 800, 360)), + 'bar': _afm.CharMetrics(583.0, 'bar', (40, -10, 543, 210)), + } + ) def test_get_familyname_guessed(): fh = BytesIO(AFM_TEST_DATA) font = _afm.AFM(fh) - del font._header[b'FamilyName'] # remove FamilyName, so we have to guess + del font._header['FamilyName'] # remove FamilyName, so we have to guess assert font.get_familyname() == 'My Font' From aff20cf00c41e61ee7b72fdd803a8b389530e076 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 8 Jul 2025 03:02:36 -0400 Subject: [PATCH 06/58] ci: Fix image preload with multiple conflicts --- .github/workflows/tests.yml | 4 ++-- azure-pipelines.yml | 4 ++-- lib/matplotlib/mlab.py | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53d47346c6eb..7c27ec84f86a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -107,8 +107,8 @@ jobs: lib/matplotlib/tests/baseline_images \ lib/mpl_toolkits/*/tests/baseline_images) if [ -n "${conflicts}" ]; then - git checkout --ours -- "${conflicts}" - git add -- "${conflicts}" + git checkout --ours -- ${conflicts} + git add -- ${conflicts} fi # If committing fails, there were conflicts other than the baseline images, # which should not be allowed to happen, and should fail the build. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a5a0e965e97b..eef71162f9cb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,8 +79,8 @@ stages: lib/matplotlib/tests/baseline_images \ lib/mpl_toolkits/*/tests/baseline_images) if [ -n "${conflicts}" ]; then - git checkout --ours -- "${conflicts}" - git add -- "${conflicts}" + git checkout --ours -- ${conflicts} + git add -- ${conflicts} fi # If committing fails, there were conflicts other than the baseline images, # which should not be allowed to happen, and should fail the build. diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index b4b4c3f96828..de890935c23b 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -219,9 +219,6 @@ def _stride_windows(x, n, noverlap=0): raise ValueError(f'n ({n}) and noverlap ({noverlap}) must be positive integers ' f'with n < noverlap and n <= x.size ({x.size})') - if n == 1 and noverlap == 0: - return x[np.newaxis] - step = n - noverlap shape = (n, (x.shape[-1]-noverlap)//step) strides = (x.strides[0], step*x.strides[0]) From 7b4d725306b2d5907a023c14a30474a04280f804 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 28 Mar 2025 00:09:04 -0400 Subject: [PATCH 07/58] Remove kerning_factor from tests --- .../mpl-data/stylelib/_classic_test_patch.mplstyle | 2 -- lib/matplotlib/tests/test_artist.py | 3 --- lib/matplotlib/tests/test_legend.py | 3 --- lib/matplotlib/tests/test_text.py | 5 ----- lib/mpl_toolkits/axisartist/tests/test_axis_artist.py | 9 --------- lib/mpl_toolkits/axisartist/tests/test_axislines.py | 6 ------ lib/mpl_toolkits/axisartist/tests/test_floating_axes.py | 3 --- .../axisartist/tests/test_grid_helper_curvelinear.py | 5 +---- 8 files changed, 1 insertion(+), 35 deletions(-) diff --git a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle index abd972925871..478ff5e415f9 100644 --- a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle @@ -1,8 +1,6 @@ # This patch should go on top of the "classic" style and exists solely to avoid # changing baseline images. -text.kerning_factor : 6 - ytick.alignment: center_baseline hatch.color: edge diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index 1367701ffe3e..d891609d4eb3 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -217,9 +217,6 @@ def test_remove(): @image_comparison(["default_edges.png"], remove_text=True, style='default') def test_default_edges(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2) ax1.plot(np.arange(10), np.arange(10), 'x', diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 9b100037cc41..7ef31bf64a53 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -258,9 +258,6 @@ def test_legend_expand(): @image_comparison(['hatching'], remove_text=True, style='default') def test_hatching(): # Remove legend texts when this image is regenerated. - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() # Patches diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 7e1a50df8a2f..911de1d4a59a 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -139,9 +139,6 @@ def test_multiline(): @image_comparison(['multiline2'], style='mpl20') def test_multiline2(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.set_xlim(0, 1.4) @@ -694,8 +691,6 @@ def test_annotation_units(fig_test, fig_ref): @image_comparison(['large_subscript_title.png'], style='mpl20') def test_large_subscript_title(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 plt.rcParams['axes.titley'] = None fig, axs = plt.subplots(1, 2, figsize=(9, 2.5), constrained_layout=True) diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index d44a61b6dd4a..7caf4fc21683 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -26,9 +26,6 @@ def test_ticks(): @image_comparison(['axis_artist_labelbase.png'], style='default') def test_labelbase(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.plot([0.5], [0.5], "o") @@ -43,9 +40,6 @@ def test_labelbase(): @image_comparison(['axis_artist_ticklabels.png'], style='default') def test_ticklabels(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.xaxis.set_visible(False) @@ -78,9 +72,6 @@ def test_ticklabels(): @image_comparison(['axis_artist.png'], style='default') def test_axis_artist(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() ax.xaxis.set_visible(False) diff --git a/lib/mpl_toolkits/axisartist/tests/test_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py index a1485d4f436b..c371d6453932 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -9,9 +9,6 @@ @image_comparison(['SubplotZero.png'], style='default') def test_SubplotZero(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig = plt.figure() ax = SubplotZero(fig, 1, 1, 1) @@ -30,9 +27,6 @@ def test_SubplotZero(): @image_comparison(['Subplot.png'], style='default') def test_Subplot(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig = plt.figure() ax = Subplot(fig, 1, 1, 1) diff --git a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index feb667af013e..3dd4309d199e 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -69,9 +69,6 @@ def test_curvelinear3(): # remove when image is regenerated. @image_comparison(['curvelinear4.png'], style='default', tol=0.9) def test_curvelinear4(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 - fig = plt.figure(figsize=(5, 5)) tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index 7d6554782fe6..f58a42471680 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -135,11 +135,8 @@ def test_polar_box(): ax1.grid(True) -# Remove tol & kerning_factor when this test image is regenerated. -@image_comparison(['axis_direction.png'], style='default', tol=0.13) +@image_comparison(['axis_direction.png'], style='default', tol=0.04) def test_axis_direction(): - plt.rcParams['text.kerning_factor'] = 6 - fig = plt.figure(figsize=(5, 5)) # PolarAxes.PolarTransform takes radian. However, we want our coordinate From 8255ae206b273524657a4c81c8c06162a31a27e0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 9 Apr 2025 05:03:23 -0400 Subject: [PATCH 08/58] Set text hinting to defaults Namely, `text.hinting` is now `default` instead of `force_autohint` (or `none` for classic tests) and `text.hinting_factor` is now 1, not 8. --- lib/matplotlib/mpl-data/matplotlibrc | 4 ++-- .../mpl-data/stylelib/_classic_test_patch.mplstyle | 3 +++ lib/matplotlib/testing/__init__.py | 11 +++++++++-- lib/matplotlib/tests/test_axes.py | 6 +++--- lib/matplotlib/tests/test_figure.py | 2 +- lib/matplotlib/tests/test_legend.py | 8 ++++---- lib/matplotlib/tests/test_polar.py | 2 +- lib/matplotlib/tests/test_text.py | 6 +++--- 8 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index ec649560ba3b..e1f66cce0c36 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -301,9 +301,9 @@ ## ("native" is a synonym.) ## - force_autohint: Use FreeType's auto-hinter. ("auto" is a synonym.) ## - no_hinting: Disable hinting. ("none" is a synonym.) -#text.hinting: force_autohint +#text.hinting: default -#text.hinting_factor: 8 # Specifies the amount of softness for hinting in the +#text.hinting_factor: 1 # Specifies the amount of softness for hinting in the # horizontal direction. A value of 1 will hint to full # pixels. A value of 2 will hint to half pixels etc. #text.kerning_factor: 0 # Specifies the scaling factor for kerning values. This diff --git a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle index 478ff5e415f9..3dc92f832b20 100644 --- a/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/_classic_test_patch.mplstyle @@ -4,3 +4,6 @@ ytick.alignment: center_baseline hatch.color: edge + +text.hinting: default +text.hinting_factor: 1 diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index d6affb1b039f..453cc631c0ea 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -19,8 +19,15 @@ def set_font_settings_for_testing(): mpl.rcParams['font.family'] = 'DejaVu Sans' - mpl.rcParams['text.hinting'] = 'none' - mpl.rcParams['text.hinting_factor'] = 8 + # We've changed the default for ourselves here, but for backwards-compatibility, use + # the old setting if not called in our own tests (which would set + # `_called_from_pytest` from our `conftest.py`). + if getattr(mpl, '_called_from_pytest', False): + mpl.rcParams['text.hinting'] = 'default' + mpl.rcParams['text.hinting_factor'] = 1 + else: + mpl.rcParams['text.hinting'] = 'none' + mpl.rcParams['text.hinting_factor'] = 8 def set_reproducibility_for_testing(): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c96173e340f7..60d507e2b999 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8272,8 +8272,8 @@ def test_normal_axes(): # test the axis bboxes target = [ - [123.375, 75.88888888888886, 983.25, 33.0], - [85.51388888888889, 99.99999999999997, 53.375, 993.0] + [124.0, 76.89, 982.0, 32.0], + [86.89, 100.5, 52.0, 992.0], ] for nn, b in enumerate(bbaxis): targetbb = mtransforms.Bbox.from_bounds(*target[nn]) @@ -8293,7 +8293,7 @@ def test_normal_axes(): targetbb = mtransforms.Bbox.from_bounds(*target) assert_array_almost_equal(bbax.bounds, targetbb.bounds, decimal=2) - target = [85.5138, 75.88888, 1021.11, 1017.11] + target = [86.89, 76.89, 1019.11, 1015.61] targetbb = mtransforms.Bbox.from_bounds(*target) assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=2) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index c5890a2963b3..cad77e2d00d7 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -814,7 +814,7 @@ def test_tightbbox(): ax.set_xlim(0, 1) t = ax.text(1., 0.5, 'This dangles over end') renderer = fig.canvas.get_renderer() - x1Nom0 = 9.035 # inches + x1Nom0 = 8.9375 # inches assert abs(t.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(ax.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(fig.get_tightbbox(renderer).x1 - x1Nom0) < 0.05 diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 7ef31bf64a53..9c43217cabb5 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -481,10 +481,10 @@ def test_figure_legend_outside(): todos += ['left ' + pos for pos in ['lower', 'center', 'upper']] todos += ['right ' + pos for pos in ['lower', 'center', 'upper']] - upperext = [20.347556, 27.722556, 790.583, 545.499] - lowerext = [20.347556, 71.056556, 790.583, 588.833] - leftext = [151.681556, 27.722556, 790.583, 588.833] - rightext = [20.347556, 27.722556, 659.249, 588.833] + upperext = [20.722556, 26.722556, 790.333, 545.999] + lowerext = [20.722556, 70.056556, 790.333, 589.333] + leftext = [152.056556, 26.722556, 790.333, 589.333] + rightext = [20.722556, 26.722556, 658.999, 589.333] axbb = [upperext, upperext, upperext, lowerext, lowerext, lowerext, leftext, leftext, leftext, diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index c0bf72b89eb0..83368f819242 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -328,7 +328,7 @@ def test_get_tightbbox_polar(): fig.canvas.draw() bb = ax.get_tightbbox(fig.canvas.get_renderer()) assert_allclose( - bb.extents, [107.7778, 29.2778, 539.7847, 450.7222], rtol=1e-03) + bb.extents, [108.27778, 28.7778, 539.7222, 451.2222], rtol=1e-03) @check_figures_equal() diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 911de1d4a59a..9b894a650bcf 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -720,14 +720,14 @@ def test_wrap(x, rotation, halign): s = 'This is a very long text that should be wrapped multiple times.' text = subfig.text(x, 0.7, s, wrap=True, rotation=rotation, ha=halign) fig.canvas.draw() - assert text._get_wrapped_text() == ('This is a very long\n' - 'text that should be\n' + assert text._get_wrapped_text() == ('This is a very long text\n' + 'that should be\n' 'wrapped multiple\n' 'times.') def test_mathwrap(): - fig = plt.figure(figsize=(6, 4)) + fig = plt.figure(figsize=(5, 4)) s = r'This is a very $\overline{\mathrm{long}}$ line of Mathtext.' text = fig.text(0, 0.5, s, size=40, wrap=True) fig.canvas.draw() From 89c054dc80e61425f0e07f192a62801cf6a22cc1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 27 Mar 2025 04:32:15 -0400 Subject: [PATCH 09/58] Update FreeType to 2.13.3 --- .pre-commit-config.yaml | 4 +- extern/meson.build | 13 +- lib/matplotlib/__init__.py | 2 +- lib/matplotlib/tests/test_bbox_tight.py | 2 +- lib/matplotlib/tests/test_figure.py | 5 +- lib/matplotlib/tests/test_ft2font.py | 6 +- lib/matplotlib/tests/test_mathtext.py | 16 +- lib/matplotlib/tests/test_text.py | 4 +- subprojects/freetype-2.13.3.wrap | 13 + subprojects/freetype-2.6.1.wrap | 10 - .../freetype-2.6.1-meson/LICENSE.build | 19 - .../builds/unix/ftconfig.h.in | 498 ---------- .../include/freetype/config/ftoption.h.in | 886 ------------------ .../freetype-2.6.1-meson/meson.build | 193 ---- .../freetype-2.6.1-meson/src/gzip/zconf.h | 284 ------ ...d655f1696da774b5cdd4c5effb312153232f.patch | 36 + 16 files changed, 80 insertions(+), 1911 deletions(-) create mode 100644 subprojects/freetype-2.13.3.wrap delete mode 100644 subprojects/freetype-2.6.1.wrap delete mode 100644 subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build delete mode 100644 subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in delete mode 100644 subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in delete mode 100644 subprojects/packagefiles/freetype-2.6.1-meson/meson.build delete mode 100644 subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h create mode 100644 subprojects/packagefiles/freetype-34aed655f1696da774b5cdd4c5effb312153232f.patch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86a9a0f45440..595d69f65b4a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,13 +20,13 @@ repos: - id: check-docstring-first exclude: lib/matplotlib/typing.py # docstring used for attribute flagged by check - id: end-of-file-fixer - exclude_types: [svg] + exclude_types: [diff, svg] - id: mixed-line-ending - id: name-tests-test args: ["--pytest-test-first"] - id: no-commit-to-branch # Default is master and main. - id: trailing-whitespace - exclude_types: [svg] + exclude_types: [diff, svg] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: diff --git a/extern/meson.build b/extern/meson.build index 5463183a9099..7f7c2511c3d5 100644 --- a/extern/meson.build +++ b/extern/meson.build @@ -13,11 +13,20 @@ else # must match the value in `lib/matplotlib.__init__.py`. Also update the docs # in `docs/devel/dependencies.rst`. Bump the cache key in # `.circleci/config.yml` when changing requirements. - LOCAL_FREETYPE_VERSION = '2.6.1' + LOCAL_FREETYPE_VERSION = '2.13.3' freetype_proj = subproject( f'freetype-@LOCAL_FREETYPE_VERSION@', - default_options: ['default_library=static']) + default_options: [ + 'default_library=static', + 'brotli=disabled', + 'bzip2=disabled', + 'harfbuzz=disabled', + 'mmap=auto', + 'png=disabled', + 'tests=disabled', + 'zlib=internal', + ]) freetype_dep = freetype_proj.get_variable('freetype_dep') endif diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index e98e8ea07502..008d4de77a3b 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1329,7 +1329,7 @@ def _val_or_rc(val, *rc_names): def _init_tests(): # The version of FreeType to install locally for running the tests. This must match # the value in `meson.build`. - LOCAL_FREETYPE_VERSION = '2.6.1' + LOCAL_FREETYPE_VERSION = '2.13.3' from matplotlib import ft2font if (ft2font.__freetype_version__ != LOCAL_FREETYPE_VERSION or diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 431ca70bf7ea..2ae94abcd7b2 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -47,7 +47,7 @@ def test_bbox_inches_tight(text_placeholders): @image_comparison(['bbox_inches_tight_suptile_legend'], savefig_kwarg={'bbox_inches': 'tight'}, - tol=0 if platform.machine() == 'x86_64' else 0.02) + tol=0 if platform.machine() == 'x86_64' else 0.022) def test_bbox_inches_tight_suptile_legend(): plt.plot(np.arange(10), label='a straight line') plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left') diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index cad77e2d00d7..900e184c6741 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -814,7 +814,7 @@ def test_tightbbox(): ax.set_xlim(0, 1) t = ax.text(1., 0.5, 'This dangles over end') renderer = fig.canvas.get_renderer() - x1Nom0 = 8.9375 # inches + x1Nom0 = 8.9875 # inches assert abs(t.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(ax.get_tightbbox(renderer).x1 - x1Nom0 * fig.dpi) < 2 assert abs(fig.get_tightbbox(renderer).x1 - x1Nom0) < 0.05 @@ -1376,7 +1376,8 @@ def test_subfigure_dpi(): @image_comparison(['test_subfigure_ss.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, tol=0.02) + savefig_kwarg={'facecolor': 'teal'}, + tol=0.022) def test_subfigure_ss(): # test assigning the subfigure via subplotspec np.random.seed(19680801) diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 8b448e17b7fd..0dc0667d0e84 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -708,10 +708,10 @@ def test_ft2font_get_sfnt_table(font_name, header): @pytest.mark.parametrize('left, right, unscaled, unfitted, default', [ # These are all the same class. - ('A', 'A', 57, 248, 256), ('A', 'À', 57, 248, 256), ('A', 'Á', 57, 248, 256), - ('A', 'Â', 57, 248, 256), ('A', 'Ã', 57, 248, 256), ('A', 'Ä', 57, 248, 256), + ('A', 'A', 57, 247, 256), ('A', 'À', 57, 247, 256), ('A', 'Á', 57, 247, 256), + ('A', 'Â', 57, 247, 256), ('A', 'Ã', 57, 247, 256), ('A', 'Ä', 57, 247, 256), # And a few other random ones. - ('D', 'A', -36, -156, -128), ('T', '.', -243, -1056, -1024), + ('D', 'A', -36, -156, -128), ('T', '.', -243, -1055, -1024), ('X', 'C', -149, -647, -640), ('-', 'J', 114, 495, 512), ]) def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 39c28dc9228c..4fc04a627dd5 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -568,14 +568,14 @@ def test_box_repr(): _mathtext.DejaVuSansFonts(fm.FontProperties(), LoadFlags.NO_HINTING), fontsize=12, dpi=100)) assert s == textwrap.dedent("""\ - Hlist[ + Hlist[ Hlist[], - Hlist[ - Hlist[ - Vlist[ - HCentered[ + Hlist[ + Hlist[ + Vlist[ + HCentered[ Glue, - Hlist[ + Hlist[ `1`, k2.36, ], @@ -584,9 +584,9 @@ def test_box_repr(): Vbox, Hrule, Vbox, - HCentered[ + HCentered[ Glue, - Hlist[ + Hlist[ `2`, k2.02, ], diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 9b894a650bcf..9d943fa9df13 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -720,8 +720,8 @@ def test_wrap(x, rotation, halign): s = 'This is a very long text that should be wrapped multiple times.' text = subfig.text(x, 0.7, s, wrap=True, rotation=rotation, ha=halign) fig.canvas.draw() - assert text._get_wrapped_text() == ('This is a very long text\n' - 'that should be\n' + assert text._get_wrapped_text() == ('This is a very long\n' + 'text that should be\n' 'wrapped multiple\n' 'times.') diff --git a/subprojects/freetype-2.13.3.wrap b/subprojects/freetype-2.13.3.wrap new file mode 100644 index 000000000000..68f688a35861 --- /dev/null +++ b/subprojects/freetype-2.13.3.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = freetype-2.13.3 +source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz +source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.13.3/freetype-2.13.3.tar.xz +source_filename = freetype-2.13.3.tar.xz +source_hash = 0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289 + +# https://gitlab.freedesktop.org/freetype/freetype/-/commit/34aed655f1696da774b5cdd4c5effb312153232f +diff_files = freetype-34aed655f1696da774b5cdd4c5effb312153232f.patch + +[provide] +freetype2 = freetype_dep +freetype = freetype_dep diff --git a/subprojects/freetype-2.6.1.wrap b/subprojects/freetype-2.6.1.wrap deleted file mode 100644 index 763362b84df0..000000000000 --- a/subprojects/freetype-2.6.1.wrap +++ /dev/null @@ -1,10 +0,0 @@ -[wrap-file] -source_url = https://download.savannah.gnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz -source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.6.1/freetype-2.6.1.tar.gz -source_filename = freetype-2.6.1.tar.gz -source_hash = 0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 - -patch_directory = freetype-2.6.1-meson - -[provide] -freetype-2.6.1 = freetype_dep diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build b/subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build deleted file mode 100644 index ec288041f388..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/LICENSE.build +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2018 The Meson development team - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in b/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in deleted file mode 100644 index 400f3a2a5bf2..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/builds/unix/ftconfig.h.in +++ /dev/null @@ -1,498 +0,0 @@ -/***************************************************************************/ -/* */ -/* ftconfig.in */ -/* */ -/* UNIX-specific configuration file (specification only). */ -/* */ -/* Copyright 1996-2015 by */ -/* David Turner, Robert Wilhelm, and Werner Lemberg. */ -/* */ -/* This file is part of the FreeType project, and may only be used, */ -/* modified, and distributed under the terms of the FreeType project */ -/* license, LICENSE.TXT. By continuing to use, modify, or distribute */ -/* this file you indicate that you have read the license and */ -/* understand and accept it fully. */ -/* */ -/***************************************************************************/ - - - /*************************************************************************/ - /* */ - /* This header file contains a number of macro definitions that are used */ - /* by the rest of the engine. Most of the macros here are automatically */ - /* determined at compile time, and you should not need to change it to */ - /* port FreeType, except to compile the library with a non-ANSI */ - /* compiler. */ - /* */ - /* Note however that if some specific modifications are needed, we */ - /* advise you to place a modified copy in your build directory. */ - /* */ - /* The build directory is usually `builds/', and contains */ - /* system-specific files that are always included first when building */ - /* the library. */ - /* */ - /*************************************************************************/ - -/* MESON: based on unix/ftconfig.in with but meson-friendly configuration defines */ - -#ifndef FTCONFIG_H_ -#define FTCONFIG_H_ - -#include -#include FT_CONFIG_OPTIONS_H -#include FT_CONFIG_STANDARD_LIBRARY_H - - -FT_BEGIN_HEADER - - - /*************************************************************************/ - /* */ - /* PLATFORM-SPECIFIC CONFIGURATION MACROS */ - /* */ - /* These macros can be toggled to suit a specific system. The current */ - /* ones are defaults used to compile FreeType in an ANSI C environment */ - /* (16bit compilers are also supported). Copy this file to your own */ - /* `builds/' directory, and edit it to port the engine. */ - /* */ - /*************************************************************************/ - - -#define HAVE_UNISTD_H @HAVE_UNISTD_H@ -#define HAVE_FCNTL_H @HAVE_FCNTL_H@ -#define HAVE_STDINT_H @HAVE_STDINT_H@ - - - /* There are systems (like the Texas Instruments 'C54x) where a `char' */ - /* has 16 bits. ANSI C says that sizeof(char) is always 1. Since an */ - /* `int' has 16 bits also for this system, sizeof(int) gives 1 which */ - /* is probably unexpected. */ - /* */ - /* `CHAR_BIT' (defined in limits.h) gives the number of bits in a */ - /* `char' type. */ - -#ifndef FT_CHAR_BIT -#define FT_CHAR_BIT CHAR_BIT -#endif - - -#undef FT_USE_AUTOCONF_SIZEOF_TYPES -#ifdef FT_USE_AUTOCONF_SIZEOF_TYPES - -#undef SIZEOF_INT -#undef SIZEOF_LONG -#define FT_SIZEOF_INT SIZEOF_INT -#define FT_SIZEOF_LONG SIZEOF_LONG - -#else /* !FT_USE_AUTOCONF_SIZEOF_TYPES */ - - /* Following cpp computation of the bit length of int and long */ - /* is copied from default include/freetype/config/ftconfig.h. */ - /* If any improvement is required for this file, it should be */ - /* applied to the original header file for the builders that */ - /* do not use configure script. */ - - /* The size of an `int' type. */ -#if FT_UINT_MAX == 0xFFFFUL -#define FT_SIZEOF_INT (16 / FT_CHAR_BIT) -#elif FT_UINT_MAX == 0xFFFFFFFFUL -#define FT_SIZEOF_INT (32 / FT_CHAR_BIT) -#elif FT_UINT_MAX > 0xFFFFFFFFUL && FT_UINT_MAX == 0xFFFFFFFFFFFFFFFFUL -#define FT_SIZEOF_INT (64 / FT_CHAR_BIT) -#else -#error "Unsupported size of `int' type!" -#endif - - /* The size of a `long' type. A five-byte `long' (as used e.g. on the */ - /* DM642) is recognized but avoided. */ -#if FT_ULONG_MAX == 0xFFFFFFFFUL -#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT) -#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFUL -#define FT_SIZEOF_LONG (32 / FT_CHAR_BIT) -#elif FT_ULONG_MAX > 0xFFFFFFFFUL && FT_ULONG_MAX == 0xFFFFFFFFFFFFFFFFUL -#define FT_SIZEOF_LONG (64 / FT_CHAR_BIT) -#else -#error "Unsupported size of `long' type!" -#endif - -#endif /* !FT_USE_AUTOCONF_SIZEOF_TYPES */ - - - /* FT_UNUSED is a macro used to indicate that a given parameter is not */ - /* used -- this is only used to get rid of unpleasant compiler warnings */ -#ifndef FT_UNUSED -#define FT_UNUSED( arg ) ( (arg) = (arg) ) -#endif - - - /*************************************************************************/ - /* */ - /* AUTOMATIC CONFIGURATION MACROS */ - /* */ - /* These macros are computed from the ones defined above. Don't touch */ - /* their definition, unless you know precisely what you are doing. No */ - /* porter should need to mess with them. */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Mac support */ - /* */ - /* This is the only necessary change, so it is defined here instead */ - /* providing a new configuration file. */ - /* */ -#if defined( __APPLE__ ) || ( defined( __MWERKS__ ) && defined( macintosh ) ) - /* no Carbon frameworks for 64bit 10.4.x */ - /* AvailabilityMacros.h is available since Mac OS X 10.2, */ - /* so guess the system version by maximum errno before inclusion */ -#include -#ifdef ECANCELED /* defined since 10.2 */ -#include "AvailabilityMacros.h" -#endif -#if defined( __LP64__ ) && \ - ( MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 ) -/undef FT_MACINTOSH -#endif - -#elif defined( __SC__ ) || defined( __MRC__ ) - /* Classic MacOS compilers */ -#include "ConditionalMacros.h" -#if TARGET_OS_MAC -#define FT_MACINTOSH 1 -#endif - -#endif - - - /* Fix compiler warning with sgi compiler */ -#if defined( __sgi ) && !defined( __GNUC__ ) -#if defined( _COMPILER_VERSION ) && ( _COMPILER_VERSION >= 730 ) -#pragma set woff 3505 -#endif -#endif - - - /*************************************************************************/ - /* */ - /*

*/ - /* basic_types */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int16 */ - /* */ - /* */ - /* A typedef for a 16bit signed integer type. */ - /* */ - typedef signed short FT_Int16; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt16 */ - /* */ - /* */ - /* A typedef for a 16bit unsigned integer type. */ - /* */ - typedef unsigned short FT_UInt16; - - /* */ - - - /* this #if 0 ... #endif clause is for documentation purposes */ -#if 0 - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int32 */ - /* */ - /* */ - /* A typedef for a 32bit signed integer type. The size depends on */ - /* the configuration. */ - /* */ - typedef signed XXX FT_Int32; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt32 */ - /* */ - /* A typedef for a 32bit unsigned integer type. The size depends on */ - /* the configuration. */ - /* */ - typedef unsigned XXX FT_UInt32; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_Int64 */ - /* */ - /* A typedef for a 64bit signed integer type. The size depends on */ - /* the configuration. Only defined if there is real 64bit support; */ - /* otherwise, it gets emulated with a structure (if necessary). */ - /* */ - typedef signed XXX FT_Int64; - - - /*************************************************************************/ - /* */ - /* */ - /* FT_UInt64 */ - /* */ - /* A typedef for a 64bit unsigned integer type. The size depends on */ - /* the configuration. Only defined if there is real 64bit support; */ - /* otherwise, it gets emulated with a structure (if necessary). */ - /* */ - typedef unsigned XXX FT_UInt64; - - /* */ - -#endif - -#if FT_SIZEOF_INT == 4 - - typedef signed int FT_Int32; - typedef unsigned int FT_UInt32; - -#elif FT_SIZEOF_LONG == 4 - - typedef signed long FT_Int32; - typedef unsigned long FT_UInt32; - -#else -#error "no 32bit type found -- please check your configuration files" -#endif - - - /* look up an integer type that is at least 32 bits */ -#if FT_SIZEOF_INT >= 4 - - typedef int FT_Fast; - typedef unsigned int FT_UFast; - -#elif FT_SIZEOF_LONG >= 4 - - typedef long FT_Fast; - typedef unsigned long FT_UFast; - -#endif - - - /* determine whether we have a 64-bit int type for platforms without */ - /* Autoconf */ -#if FT_SIZEOF_LONG == 8 - - /* FT_LONG64 must be defined if a 64-bit type is available */ -#define FT_LONG64 -#define FT_INT64 long -#define FT_UINT64 unsigned long - - /*************************************************************************/ - /* */ - /* A 64-bit data type may create compilation problems if you compile */ - /* in strict ANSI mode. To avoid them, we disable other 64-bit data */ - /* types if __STDC__ is defined. You can however ignore this rule */ - /* by defining the FT_CONFIG_OPTION_FORCE_INT64 configuration macro. */ - /* */ -#elif !defined( __STDC__ ) || defined( FT_CONFIG_OPTION_FORCE_INT64 ) - -#if defined( _MSC_VER ) && _MSC_VER >= 900 /* Visual C++ (and Intel C++) */ - - /* this compiler provides the __int64 type */ -#define FT_LONG64 -#define FT_INT64 __int64 -#define FT_UINT64 unsigned __int64 - -#elif defined( __BORLANDC__ ) /* Borland C++ */ - - /* XXXX: We should probably check the value of __BORLANDC__ in order */ - /* to test the compiler version. */ - - /* this compiler provides the __int64 type */ -#define FT_LONG64 -#define FT_INT64 __int64 -#define FT_UINT64 unsigned __int64 - -#elif defined( __WATCOMC__ ) /* Watcom C++ */ - - /* Watcom doesn't provide 64-bit data types */ - -#elif defined( __MWERKS__ ) /* Metrowerks CodeWarrior */ - -#define FT_LONG64 -#define FT_INT64 long long int -#define FT_UINT64 unsigned long long int - -#elif defined( __GNUC__ ) - - /* GCC provides the `long long' type */ -#define FT_LONG64 -#define FT_INT64 long long int -#define FT_UINT64 unsigned long long int - -#endif /* _MSC_VER */ - -#endif /* FT_SIZEOF_LONG == 8 */ - -#ifdef FT_LONG64 - typedef FT_INT64 FT_Int64; - typedef FT_UINT64 FT_UInt64; -#endif - - - /*************************************************************************/ - /* */ - /* miscellaneous */ - /* */ - /*************************************************************************/ - - -#define FT_BEGIN_STMNT do { -#define FT_END_STMNT } while ( 0 ) -#define FT_DUMMY_STMNT FT_BEGIN_STMNT FT_END_STMNT - - - /* typeof condition taken from gnulib's `intprops.h' header file */ -#if ( __GNUC__ >= 2 || \ - defined( __IBM__TYPEOF__ ) || \ - ( __SUNPRO_C >= 0x5110 && !__STDC__ ) ) -#define FT_TYPEOF( type ) (__typeof__ (type)) -#else -#define FT_TYPEOF( type ) /* empty */ -#endif - - -#ifdef FT_MAKE_OPTION_SINGLE_OBJECT - -#define FT_LOCAL( x ) static x -#define FT_LOCAL_DEF( x ) static x - -#else - -#ifdef __cplusplus -#define FT_LOCAL( x ) extern "C" x -#define FT_LOCAL_DEF( x ) extern "C" x -#else -#define FT_LOCAL( x ) extern x -#define FT_LOCAL_DEF( x ) x -#endif - -#endif /* FT_MAKE_OPTION_SINGLE_OBJECT */ - -#define FT_LOCAL_ARRAY( x ) extern const x -#define FT_LOCAL_ARRAY_DEF( x ) const x - - -#ifndef FT_BASE - -#ifdef __cplusplus -#define FT_BASE( x ) extern "C" x -#else -#define FT_BASE( x ) extern x -#endif - -#endif /* !FT_BASE */ - - -#ifndef FT_BASE_DEF - -#ifdef __cplusplus -#define FT_BASE_DEF( x ) x -#else -#define FT_BASE_DEF( x ) x -#endif - -#endif /* !FT_BASE_DEF */ - - -#ifndef FT_EXPORT - -#ifdef __cplusplus -#define FT_EXPORT( x ) extern "C" x -#else -#define FT_EXPORT( x ) extern x -#endif - -#endif /* !FT_EXPORT */ - - -#ifndef FT_EXPORT_DEF - -#ifdef __cplusplus -#define FT_EXPORT_DEF( x ) extern "C" x -#else -#define FT_EXPORT_DEF( x ) extern x -#endif - -#endif /* !FT_EXPORT_DEF */ - - -#ifndef FT_EXPORT_VAR - -#ifdef __cplusplus -#define FT_EXPORT_VAR( x ) extern "C" x -#else -#define FT_EXPORT_VAR( x ) extern x -#endif - -#endif /* !FT_EXPORT_VAR */ - - /* The following macros are needed to compile the library with a */ - /* C++ compiler and with 16bit compilers. */ - /* */ - - /* This is special. Within C++, you must specify `extern "C"' for */ - /* functions which are used via function pointers, and you also */ - /* must do that for structures which contain function pointers to */ - /* assure C linkage -- it's not possible to have (local) anonymous */ - /* functions which are accessed by (global) function pointers. */ - /* */ - /* */ - /* FT_CALLBACK_DEF is used to _define_ a callback function. */ - /* */ - /* FT_CALLBACK_TABLE is used to _declare_ a constant variable that */ - /* contains pointers to callback functions. */ - /* */ - /* FT_CALLBACK_TABLE_DEF is used to _define_ a constant variable */ - /* that contains pointers to callback functions. */ - /* */ - /* */ - /* Some 16bit compilers have to redefine these macros to insert */ - /* the infamous `_cdecl' or `__fastcall' declarations. */ - /* */ -#ifndef FT_CALLBACK_DEF -#ifdef __cplusplus -#define FT_CALLBACK_DEF( x ) extern "C" x -#else -#define FT_CALLBACK_DEF( x ) static x -#endif -#endif /* FT_CALLBACK_DEF */ - -#ifndef FT_CALLBACK_TABLE -#ifdef __cplusplus -#define FT_CALLBACK_TABLE extern "C" -#define FT_CALLBACK_TABLE_DEF extern "C" -#else -#define FT_CALLBACK_TABLE extern -#define FT_CALLBACK_TABLE_DEF /* nothing */ -#endif -#endif /* FT_CALLBACK_TABLE */ - - -FT_END_HEADER - - -#endif /* FTCONFIG_H_ */ - - -/* END */ diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in b/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in deleted file mode 100644 index 5df84c706800..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/include/freetype/config/ftoption.h.in +++ /dev/null @@ -1,886 +0,0 @@ -/***************************************************************************/ -/* */ -/* ftoption.h */ -/* */ -/* User-selectable configuration macros (specification only). */ -/* */ -/* Copyright 1996-2015 by */ -/* David Turner, Robert Wilhelm, and Werner Lemberg. */ -/* */ -/* This file is part of the FreeType project, and may only be used, */ -/* modified, and distributed under the terms of the FreeType project */ -/* license, LICENSE.TXT. By continuing to use, modify, or distribute */ -/* this file you indicate that you have read the license and */ -/* understand and accept it fully. */ -/* */ -/***************************************************************************/ - - -#ifndef FTOPTION_H_ -#define FTOPTION_H_ - - -#include - - -FT_BEGIN_HEADER - - /*************************************************************************/ - /* */ - /* USER-SELECTABLE CONFIGURATION MACROS */ - /* */ - /* This file contains the default configuration macro definitions for */ - /* a standard build of the FreeType library. There are three ways to */ - /* use this file to build project-specific versions of the library: */ - /* */ - /* - You can modify this file by hand, but this is not recommended in */ - /* cases where you would like to build several versions of the */ - /* library from a single source directory. */ - /* */ - /* - You can put a copy of this file in your build directory, more */ - /* precisely in `$BUILD/freetype/config/ftoption.h', where `$BUILD' */ - /* is the name of a directory that is included _before_ the FreeType */ - /* include path during compilation. */ - /* */ - /* The default FreeType Makefiles and Jamfiles use the build */ - /* directory `builds/' by default, but you can easily change */ - /* that for your own projects. */ - /* */ - /* - Copy the file to `$BUILD/ft2build.h' and modify it */ - /* slightly to pre-define the macro FT_CONFIG_OPTIONS_H used to */ - /* locate this file during the build. For example, */ - /* */ - /* #define FT_CONFIG_OPTIONS_H */ - /* #include */ - /* */ - /* will use `$BUILD/myftoptions.h' instead of this file for macro */ - /* definitions. */ - /* */ - /* Note also that you can similarly pre-define the macro */ - /* FT_CONFIG_MODULES_H used to locate the file listing of the modules */ - /* that are statically linked to the library at compile time. By */ - /* default, this file is . */ - /* */ - /* We highly recommend using the third method whenever possible. */ - /* */ - /*************************************************************************/ - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** G E N E R A L F R E E T Y P E 2 C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Uncomment the line below if you want to activate sub-pixel rendering */ - /* (a.k.a. LCD rendering, or ClearType) in this build of the library. */ - /* */ - /* Note that this feature is covered by several Microsoft patents */ - /* and should not be activated in any default build of the library. */ - /* */ - /* This macro has no impact on the FreeType API, only on its */ - /* _implementation_. For example, using FT_RENDER_MODE_LCD when calling */ - /* FT_Render_Glyph still generates a bitmap that is 3 times wider than */ - /* the original size in case this macro isn't defined; however, each */ - /* triplet of subpixels has R=G=B. */ - /* */ - /* This is done to allow FreeType clients to run unmodified, forcing */ - /* them to display normal gray-level anti-aliased glyphs. */ - /* */ -/* #define FT_CONFIG_OPTION_SUBPIXEL_RENDERING */ - - - /*************************************************************************/ - /* */ - /* Many compilers provide a non-ANSI 64-bit data type that can be used */ - /* by FreeType to speed up some computations. However, this will create */ - /* some problems when compiling the library in strict ANSI mode. */ - /* */ - /* For this reason, the use of 64-bit integers is normally disabled when */ - /* the __STDC__ macro is defined. You can however disable this by */ - /* defining the macro FT_CONFIG_OPTION_FORCE_INT64 here. */ - /* */ - /* For most compilers, this will only create compilation warnings when */ - /* building the library. */ - /* */ - /* ObNote: The compiler-specific 64-bit integers are detected in the */ - /* file `ftconfig.h' either statically or through the */ - /* `configure' script on supported platforms. */ - /* */ -#undef FT_CONFIG_OPTION_FORCE_INT64 - - - /*************************************************************************/ - /* */ - /* If this macro is defined, do not try to use an assembler version of */ - /* performance-critical functions (e.g. FT_MulFix). You should only do */ - /* that to verify that the assembler function works properly, or to */ - /* execute benchmark tests of the various implementations. */ -/* #define FT_CONFIG_OPTION_NO_ASSEMBLER */ - - - /*************************************************************************/ - /* */ - /* If this macro is defined, try to use an inlined assembler version of */ - /* the `FT_MulFix' function, which is a `hotspot' when loading and */ - /* hinting glyphs, and which should be executed as fast as possible. */ - /* */ - /* Note that if your compiler or CPU is not supported, this will default */ - /* to the standard and portable implementation found in `ftcalc.c'. */ - /* */ -#define FT_CONFIG_OPTION_INLINE_MULFIX - - - /*************************************************************************/ - /* */ - /* LZW-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `compress' program. This is mostly used to parse many of the PCF */ - /* files that come with various X11 distributions. The implementation */ - /* uses NetBSD's `zopen' to partially uncompress the file on the fly */ - /* (see src/lzw/ftgzip.c). */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#define FT_CONFIG_OPTION_USE_LZW - - - /*************************************************************************/ - /* */ - /* Gzip-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `gzip' program. This is mostly used to parse many of the PCF files */ - /* that come with XFree86. The implementation uses `zlib' to */ - /* partially uncompress the file on the fly (see src/gzip/ftgzip.c). */ - /* */ - /* Define this macro if you want to enable this `feature'. See also */ - /* the macro FT_CONFIG_OPTION_SYSTEM_ZLIB below. */ - /* */ -#define FT_CONFIG_OPTION_USE_ZLIB - - - /*************************************************************************/ - /* */ - /* ZLib library selection */ - /* */ - /* This macro is only used when FT_CONFIG_OPTION_USE_ZLIB is defined. */ - /* It allows FreeType's `ftgzip' component to link to the system's */ - /* installation of the ZLib library. This is useful on systems like */ - /* Unix or VMS where it generally is already available. */ - /* */ - /* If you let it undefined, the component will use its own copy */ - /* of the zlib sources instead. These have been modified to be */ - /* included directly within the component and *not* export external */ - /* function names. This allows you to link any program with FreeType */ - /* _and_ ZLib without linking conflicts. */ - /* */ - /* Do not #undef this macro here since the build system might define */ - /* it for certain configurations only. */ - /* */ -#mesondefine FT_CONFIG_OPTION_SYSTEM_ZLIB - - - /*************************************************************************/ - /* */ - /* Bzip2-compressed file support. */ - /* */ - /* FreeType now handles font files that have been compressed with the */ - /* `bzip2' program. This is mostly used to parse many of the PCF */ - /* files that come with XFree86. The implementation uses `libbz2' to */ - /* partially uncompress the file on the fly (see src/bzip2/ftbzip2.c). */ - /* Contrary to gzip, bzip2 currently is not included and need to use */ - /* the system available bzip2 implementation. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_BZIP2 - - - /*************************************************************************/ - /* */ - /* Define to disable the use of file stream functions and types, FILE, */ - /* fopen() etc. Enables the use of smaller system libraries on embedded */ - /* systems that have multiple system libraries, some with or without */ - /* file stream support, in the cases where file stream support is not */ - /* necessary such as memory loading of font files. */ - /* */ -/* #define FT_CONFIG_OPTION_DISABLE_STREAM_SUPPORT */ - - - /*************************************************************************/ - /* */ - /* PNG bitmap support. */ - /* */ - /* FreeType now handles loading color bitmap glyphs in the PNG format. */ - /* This requires help from the external libpng library. Uncompressed */ - /* color bitmaps do not need any external libraries and will be */ - /* supported regardless of this configuration. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_PNG - - - /*************************************************************************/ - /* */ - /* HarfBuzz support. */ - /* */ - /* FreeType uses the HarfBuzz library to improve auto-hinting of */ - /* OpenType fonts. If available, many glyphs not directly addressable */ - /* by a font's character map will be hinted also. */ - /* */ - /* Define this macro if you want to enable this `feature'. */ - /* */ -#mesondefine FT_CONFIG_OPTION_USE_HARFBUZZ - - - /*************************************************************************/ - /* */ - /* DLL export compilation */ - /* */ - /* When compiling FreeType as a DLL, some systems/compilers need a */ - /* special keyword in front OR after the return type of function */ - /* declarations. */ - /* */ - /* Two macros are used within the FreeType source code to define */ - /* exported library functions: FT_EXPORT and FT_EXPORT_DEF. */ - /* */ - /* FT_EXPORT( return_type ) */ - /* */ - /* is used in a function declaration, as in */ - /* */ - /* FT_EXPORT( FT_Error ) */ - /* FT_Init_FreeType( FT_Library* alibrary ); */ - /* */ - /* */ - /* FT_EXPORT_DEF( return_type ) */ - /* */ - /* is used in a function definition, as in */ - /* */ - /* FT_EXPORT_DEF( FT_Error ) */ - /* FT_Init_FreeType( FT_Library* alibrary ) */ - /* { */ - /* ... some code ... */ - /* return FT_Err_Ok; */ - /* } */ - /* */ - /* You can provide your own implementation of FT_EXPORT and */ - /* FT_EXPORT_DEF here if you want. If you leave them undefined, they */ - /* will be later automatically defined as `extern return_type' to */ - /* allow normal compilation. */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_EXPORT(x) extern x */ -/* #define FT_EXPORT_DEF(x) x */ - - - /*************************************************************************/ - /* */ - /* Glyph Postscript Names handling */ - /* */ - /* By default, FreeType 2 is compiled with the `psnames' module. This */ - /* module is in charge of converting a glyph name string into a */ - /* Unicode value, or return a Macintosh standard glyph name for the */ - /* use with the TrueType `post' table. */ - /* */ - /* Undefine this macro if you do not want `psnames' compiled in your */ - /* build of FreeType. This has the following effects: */ - /* */ - /* - The TrueType driver will provide its own set of glyph names, */ - /* if you build it to support postscript names in the TrueType */ - /* `post' table. */ - /* */ - /* - The Type 1 driver will not be able to synthesize a Unicode */ - /* charmap out of the glyphs found in the fonts. */ - /* */ - /* You would normally undefine this configuration macro when building */ - /* a version of FreeType that doesn't contain a Type 1 or CFF driver. */ - /* */ -#define FT_CONFIG_OPTION_POSTSCRIPT_NAMES - - - /*************************************************************************/ - /* */ - /* Postscript Names to Unicode Values support */ - /* */ - /* By default, FreeType 2 is built with the `PSNames' module compiled */ - /* in. Among other things, the module is used to convert a glyph name */ - /* into a Unicode value. This is especially useful in order to */ - /* synthesize on the fly a Unicode charmap from the CFF/Type 1 driver */ - /* through a big table named the `Adobe Glyph List' (AGL). */ - /* */ - /* Undefine this macro if you do not want the Adobe Glyph List */ - /* compiled in your `PSNames' module. The Type 1 driver will not be */ - /* able to synthesize a Unicode charmap out of the glyphs found in the */ - /* fonts. */ - /* */ -#define FT_CONFIG_OPTION_ADOBE_GLYPH_LIST - - - /*************************************************************************/ - /* */ - /* Support for Mac fonts */ - /* */ - /* Define this macro if you want support for outline fonts in Mac */ - /* format (mac dfont, mac resource, macbinary containing a mac */ - /* resource) on non-Mac platforms. */ - /* */ - /* Note that the `FOND' resource isn't checked. */ - /* */ -#define FT_CONFIG_OPTION_MAC_FONTS - - - /*************************************************************************/ - /* */ - /* Guessing methods to access embedded resource forks */ - /* */ - /* Enable extra Mac fonts support on non-Mac platforms (e.g. */ - /* GNU/Linux). */ - /* */ - /* Resource forks which include fonts data are stored sometimes in */ - /* locations which users or developers don't expected. In some cases, */ - /* resource forks start with some offset from the head of a file. In */ - /* other cases, the actual resource fork is stored in file different */ - /* from what the user specifies. If this option is activated, */ - /* FreeType tries to guess whether such offsets or different file */ - /* names must be used. */ - /* */ - /* Note that normal, direct access of resource forks is controlled via */ - /* the FT_CONFIG_OPTION_MAC_FONTS option. */ - /* */ -#ifdef FT_CONFIG_OPTION_MAC_FONTS -#define FT_CONFIG_OPTION_GUESSING_EMBEDDED_RFORK -#endif - - - /*************************************************************************/ - /* */ - /* Allow the use of FT_Incremental_Interface to load typefaces that */ - /* contain no glyph data, but supply it via a callback function. */ - /* This is required by clients supporting document formats which */ - /* supply font data incrementally as the document is parsed, such */ - /* as the Ghostscript interpreter for the PostScript language. */ - /* */ -#define FT_CONFIG_OPTION_INCREMENTAL - - - /*************************************************************************/ - /* */ - /* The size in bytes of the render pool used by the scan-line converter */ - /* to do all of its work. */ - /* */ -#define FT_RENDER_POOL_SIZE 16384L - - - /*************************************************************************/ - /* */ - /* FT_MAX_MODULES */ - /* */ - /* The maximum number of modules that can be registered in a single */ - /* FreeType library object. 32 is the default. */ - /* */ -#define FT_MAX_MODULES 32 - - - /*************************************************************************/ - /* */ - /* Debug level */ - /* */ - /* FreeType can be compiled in debug or trace mode. In debug mode, */ - /* errors are reported through the `ftdebug' component. In trace */ - /* mode, additional messages are sent to the standard output during */ - /* execution. */ - /* */ - /* Define FT_DEBUG_LEVEL_ERROR to build the library in debug mode. */ - /* Define FT_DEBUG_LEVEL_TRACE to build it in trace mode. */ - /* */ - /* Don't define any of these macros to compile in `release' mode! */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_DEBUG_LEVEL_ERROR */ -/* #define FT_DEBUG_LEVEL_TRACE */ - - - /*************************************************************************/ - /* */ - /* Autofitter debugging */ - /* */ - /* If FT_DEBUG_AUTOFIT is defined, FreeType provides some means to */ - /* control the autofitter behaviour for debugging purposes with global */ - /* boolean variables (consequently, you should *never* enable this */ - /* while compiling in `release' mode): */ - /* */ - /* _af_debug_disable_horz_hints */ - /* _af_debug_disable_vert_hints */ - /* _af_debug_disable_blue_hints */ - /* */ - /* Additionally, the following functions provide dumps of various */ - /* internal autofit structures to stdout (using `printf'): */ - /* */ - /* af_glyph_hints_dump_points */ - /* af_glyph_hints_dump_segments */ - /* af_glyph_hints_dump_edges */ - /* af_glyph_hints_get_num_segments */ - /* af_glyph_hints_get_segment_offset */ - /* */ - /* As an argument, they use another global variable: */ - /* */ - /* _af_debug_hints */ - /* */ - /* Please have a look at the `ftgrid' demo program to see how those */ - /* variables and macros should be used. */ - /* */ - /* Do not #undef these macros here since the build system might define */ - /* them for certain configurations only. */ - /* */ -/* #define FT_DEBUG_AUTOFIT */ - - - /*************************************************************************/ - /* */ - /* Memory Debugging */ - /* */ - /* FreeType now comes with an integrated memory debugger that is */ - /* capable of detecting simple errors like memory leaks or double */ - /* deletes. To compile it within your build of the library, you */ - /* should define FT_DEBUG_MEMORY here. */ - /* */ - /* Note that the memory debugger is only activated at runtime when */ - /* when the _environment_ variable `FT2_DEBUG_MEMORY' is defined also! */ - /* */ - /* Do not #undef this macro here since the build system might define */ - /* it for certain configurations only. */ - /* */ -/* #define FT_DEBUG_MEMORY */ - - - /*************************************************************************/ - /* */ - /* Module errors */ - /* */ - /* If this macro is set (which is _not_ the default), the higher byte */ - /* of an error code gives the module in which the error has occurred, */ - /* while the lower byte is the real error code. */ - /* */ - /* Setting this macro makes sense for debugging purposes only, since */ - /* it would break source compatibility of certain programs that use */ - /* FreeType 2. */ - /* */ - /* More details can be found in the files ftmoderr.h and fterrors.h. */ - /* */ -#undef FT_CONFIG_OPTION_USE_MODULE_ERRORS - - - /*************************************************************************/ - /* */ - /* Position Independent Code */ - /* */ - /* If this macro is set (which is _not_ the default), FreeType2 will */ - /* avoid creating constants that require address fixups. Instead the */ - /* constants will be moved into a struct and additional intialization */ - /* code will be used. */ - /* */ - /* Setting this macro is needed for systems that prohibit address */ - /* fixups, such as BREW. */ - /* */ -#mesondefine FT_CONFIG_OPTION_PIC - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** S F N T D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_EMBEDDED_BITMAPS if you want to support */ - /* embedded bitmaps in all formats using the SFNT module (namely */ - /* TrueType & OpenType). */ - /* */ -#define TT_CONFIG_OPTION_EMBEDDED_BITMAPS - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_POSTSCRIPT_NAMES if you want to be able to */ - /* load and enumerate the glyph Postscript names in a TrueType or */ - /* OpenType file. */ - /* */ - /* Note that when you do not compile the `PSNames' module by undefining */ - /* the above FT_CONFIG_OPTION_POSTSCRIPT_NAMES, the `sfnt' module will */ - /* contain additional code used to read the PS Names table from a font. */ - /* */ - /* (By default, the module uses `PSNames' to extract glyph names.) */ - /* */ -#define TT_CONFIG_OPTION_POSTSCRIPT_NAMES - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_SFNT_NAMES if your applications need to */ - /* access the internal name table in a SFNT-based format like TrueType */ - /* or OpenType. The name table contains various strings used to */ - /* describe the font, like family name, copyright, version, etc. It */ - /* does not contain any glyph name though. */ - /* */ - /* Accessing SFNT names is done through the functions declared in */ - /* `ftsnames.h'. */ - /* */ -#define TT_CONFIG_OPTION_SFNT_NAMES - - - /*************************************************************************/ - /* */ - /* TrueType CMap support */ - /* */ - /* Here you can fine-tune which TrueType CMap table format shall be */ - /* supported. */ -#define TT_CONFIG_CMAP_FORMAT_0 -#define TT_CONFIG_CMAP_FORMAT_2 -#define TT_CONFIG_CMAP_FORMAT_4 -#define TT_CONFIG_CMAP_FORMAT_6 -#define TT_CONFIG_CMAP_FORMAT_8 -#define TT_CONFIG_CMAP_FORMAT_10 -#define TT_CONFIG_CMAP_FORMAT_12 -#define TT_CONFIG_CMAP_FORMAT_13 -#define TT_CONFIG_CMAP_FORMAT_14 - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** T R U E T Y P E D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_BYTECODE_INTERPRETER if you want to compile */ - /* a bytecode interpreter in the TrueType driver. */ - /* */ - /* By undefining this, you will only compile the code necessary to load */ - /* TrueType glyphs without hinting. */ - /* */ - /* Do not #undef this macro here, since the build system might */ - /* define it for certain configurations only. */ - /* */ -#define TT_CONFIG_OPTION_BYTECODE_INTERPRETER - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_SUBPIXEL_HINTING if you want to compile */ - /* EXPERIMENTAL subpixel hinting support into the TrueType driver. This */ - /* replaces the native TrueType hinting mechanism when anything but */ - /* FT_RENDER_MODE_MONO is requested. */ - /* */ - /* Enabling this causes the TrueType driver to ignore instructions under */ - /* certain conditions. This is done in accordance with the guide here, */ - /* with some minor differences: */ - /* */ - /* http://www.microsoft.com/typography/cleartype/truetypecleartype.aspx */ - /* */ - /* By undefining this, you only compile the code necessary to hint */ - /* TrueType glyphs with native TT hinting. */ - /* */ - /* This option requires TT_CONFIG_OPTION_BYTECODE_INTERPRETER to be */ - /* defined. */ - /* */ -/* #define TT_CONFIG_OPTION_SUBPIXEL_HINTING */ - - - /*************************************************************************/ - /* */ - /* If you define TT_CONFIG_OPTION_UNPATENTED_HINTING, a special version */ - /* of the TrueType bytecode interpreter is used that doesn't implement */ - /* any of the patented opcodes and algorithms. The patents related to */ - /* TrueType hinting have expired worldwide since May 2010; this option */ - /* is now deprecated. */ - /* */ - /* Note that the TT_CONFIG_OPTION_UNPATENTED_HINTING macro is *ignored* */ - /* if you define TT_CONFIG_OPTION_BYTECODE_INTERPRETER; in other words, */ - /* either define TT_CONFIG_OPTION_BYTECODE_INTERPRETER or */ - /* TT_CONFIG_OPTION_UNPATENTED_HINTING but not both at the same time. */ - /* */ - /* This macro is only useful for a small number of font files (mostly */ - /* for Asian scripts) that require bytecode interpretation to properly */ - /* load glyphs. For all other fonts, this produces unpleasant results, */ - /* thus the unpatented interpreter is never used to load glyphs from */ - /* TrueType fonts unless one of the following two options is used. */ - /* */ - /* - The unpatented interpreter is explicitly activated by the user */ - /* through the FT_PARAM_TAG_UNPATENTED_HINTING parameter tag */ - /* when opening the FT_Face. */ - /* */ - /* - FreeType detects that the FT_Face corresponds to one of the */ - /* `trick' fonts (e.g., `Mingliu') it knows about. The font engine */ - /* contains a hard-coded list of font names and other matching */ - /* parameters (see function `tt_face_init' in file */ - /* `src/truetype/ttobjs.c'). */ - /* */ - /* Here a sample code snippet for using FT_PARAM_TAG_UNPATENTED_HINTING. */ - /* */ - /* { */ - /* FT_Parameter parameter; */ - /* FT_Open_Args open_args; */ - /* */ - /* */ - /* parameter.tag = FT_PARAM_TAG_UNPATENTED_HINTING; */ - /* */ - /* open_args.flags = FT_OPEN_PATHNAME | FT_OPEN_PARAMS; */ - /* open_args.pathname = my_font_pathname; */ - /* open_args.num_params = 1; */ - /* open_args.params = ¶meter; */ - /* */ - /* error = FT_Open_Face( library, &open_args, index, &face ); */ - /* ... */ - /* } */ - /* */ -/* #define TT_CONFIG_OPTION_UNPATENTED_HINTING */ - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED to compile the */ - /* TrueType glyph loader to use Apple's definition of how to handle */ - /* component offsets in composite glyphs. */ - /* */ - /* Apple and MS disagree on the default behavior of component offsets */ - /* in composites. Apple says that they should be scaled by the scaling */ - /* factors in the transformation matrix (roughly, it's more complex) */ - /* while MS says they should not. OpenType defines two bits in the */ - /* composite flags array which can be used to disambiguate, but old */ - /* fonts will not have them. */ - /* */ - /* http://www.microsoft.com/typography/otspec/glyf.htm */ - /* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html */ - /* */ -#undef TT_CONFIG_OPTION_COMPONENT_OFFSET_SCALED - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_GX_VAR_SUPPORT if you want to include */ - /* support for Apple's distortable font technology (fvar, gvar, cvar, */ - /* and avar tables). This has many similarities to Type 1 Multiple */ - /* Masters support. */ - /* */ -#define TT_CONFIG_OPTION_GX_VAR_SUPPORT - - - /*************************************************************************/ - /* */ - /* Define TT_CONFIG_OPTION_BDF if you want to include support for */ - /* an embedded `BDF ' table within SFNT-based bitmap formats. */ - /* */ -#define TT_CONFIG_OPTION_BDF - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** T Y P E 1 D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* T1_MAX_DICT_DEPTH is the maximum depth of nest dictionaries and */ - /* arrays in the Type 1 stream (see t1load.c). A minimum of 4 is */ - /* required. */ - /* */ -#define T1_MAX_DICT_DEPTH 5 - - - /*************************************************************************/ - /* */ - /* T1_MAX_SUBRS_CALLS details the maximum number of nested sub-routine */ - /* calls during glyph loading. */ - /* */ -#define T1_MAX_SUBRS_CALLS 16 - - - /*************************************************************************/ - /* */ - /* T1_MAX_CHARSTRING_OPERANDS is the charstring stack's capacity. A */ - /* minimum of 16 is required. */ - /* */ - /* The Chinese font MingTiEG-Medium (CNS 11643 character set) needs 256. */ - /* */ -#define T1_MAX_CHARSTRINGS_OPERANDS 256 - - - /*************************************************************************/ - /* */ - /* Define this configuration macro if you want to prevent the */ - /* compilation of `t1afm', which is in charge of reading Type 1 AFM */ - /* files into an existing face. Note that if set, the T1 driver will be */ - /* unable to produce kerning distances. */ - /* */ -#undef T1_CONFIG_OPTION_NO_AFM - - - /*************************************************************************/ - /* */ - /* Define this configuration macro if you want to prevent the */ - /* compilation of the Multiple Masters font support in the Type 1 */ - /* driver. */ - /* */ -#undef T1_CONFIG_OPTION_NO_MM_SUPPORT - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** C F F D R I V E R C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Using CFF_CONFIG_OPTION_DARKENING_PARAMETER_{X,Y}{1,2,3,4} it is */ - /* possible to set up the default values of the four control points that */ - /* define the stem darkening behaviour of the (new) CFF engine. For */ - /* more details please read the documentation of the */ - /* `darkening-parameters' property of the cff driver module (file */ - /* `ftcffdrv.h'), which allows the control at run-time. */ - /* */ - /* Do *not* undefine these macros! */ - /* */ -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 500 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 400 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 1000 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 275 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 1667 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 275 - -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 2333 -#define CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 0 - - - /*************************************************************************/ - /* */ - /* CFF_CONFIG_OPTION_OLD_ENGINE controls whether the pre-Adobe CFF */ - /* engine gets compiled into FreeType. If defined, it is possible to */ - /* switch between the two engines using the `hinting-engine' property of */ - /* the cff driver module. */ - /* */ -/* #define CFF_CONFIG_OPTION_OLD_ENGINE */ - - - /*************************************************************************/ - /*************************************************************************/ - /**** ****/ - /**** A U T O F I T M O D U L E C O N F I G U R A T I O N ****/ - /**** ****/ - /*************************************************************************/ - /*************************************************************************/ - - - /*************************************************************************/ - /* */ - /* Compile autofit module with CJK (Chinese, Japanese, Korean) script */ - /* support. */ - /* */ -#define AF_CONFIG_OPTION_CJK - - /*************************************************************************/ - /* */ - /* Compile autofit module with Indic script support. */ - /* */ -#define AF_CONFIG_OPTION_INDIC - - /*************************************************************************/ - /* */ - /* Compile autofit module with warp hinting. The idea of the warping */ - /* code is to slightly scale and shift a glyph within a single dimension */ - /* so that as much of its segments are aligned (more or less) on the */ - /* grid. To find out the optimal scaling and shifting value, various */ - /* parameter combinations are tried and scored. */ - /* */ - /* This experimental option is active only if the rendering mode is */ - /* FT_RENDER_MODE_LIGHT; you can switch warping on and off with the */ - /* `warping' property of the auto-hinter (see file `ftautoh.h' for more */ - /* information; by default it is switched off). */ - /* */ -#define AF_CONFIG_OPTION_USE_WARPER - - /* */ - - - /* - * This macro is obsolete. Support has been removed in FreeType - * version 2.5. - */ -/* #define FT_CONFIG_OPTION_OLD_INTERNALS */ - - - /* - * This macro is defined if either unpatented or native TrueType - * hinting is requested by the definitions above. - */ -#ifdef TT_CONFIG_OPTION_BYTECODE_INTERPRETER -#define TT_USE_BYTECODE_INTERPRETER -#undef TT_CONFIG_OPTION_UNPATENTED_HINTING -#elif defined TT_CONFIG_OPTION_UNPATENTED_HINTING -#define TT_USE_BYTECODE_INTERPRETER -#endif - - - /* - * Check CFF darkening parameters. The checks are the same as in function - * `cff_property_set' in file `cffdrivr.c'. - */ -#if CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 < 0 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 < 0 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 < 0 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X1 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X2 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X3 > \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_X4 || \ - \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y1 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y2 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y3 > 500 || \ - CFF_CONFIG_OPTION_DARKENING_PARAMETER_Y4 > 500 -#error "Invalid CFF darkening parameters!" -#endif - -FT_END_HEADER - - -#endif /* FTOPTION_H_ */ - - -/* END */ diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build b/subprojects/packagefiles/freetype-2.6.1-meson/meson.build deleted file mode 100644 index 9a5180ef7586..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build +++ /dev/null @@ -1,193 +0,0 @@ -project('freetype2', 'c', - version: '2.6.1', - license: '(FTL OR GPL-2.0-or-later) AND BSD-3-Clause AND MIT AND MIT-Modern-Variant AND Zlib', - license_files: [ - 'docs/LICENSE.TXT', - 'docs/FTL.TXT', - 'docs/GPLv2.TXT', - ], - meson_version: '>=1.1.0') - -# NOTE about FreeType versions -# There are 3 versions numbers associated with each releases: -# - official release number (eg. 2.6.1) - accessible via -# FREETYPE_{MAJOR,MINOR,PATCH} macros from FT_FREETYPE_H -# - libtool-specific version number, this is what is returned by -# freetype-config --version / pkg-config --modversion (eg. 22.1.16) -# - the platform-specific shared object version number (eg. 6.16.1) -# See https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/VERSIONS.TXT -# for more information -release_version = meson.project_version() -libtool_version = '18.1.12' -so_version = '6.12.1' -so_soversion = '6' - -pkgmod = import('pkgconfig') - -cc = meson.get_compiler('c') - -base_sources = [ - 'src/autofit/autofit.c', - 'src/base/ftbase.c', - 'src/base/ftbbox.c', - 'src/base/ftbdf.c', - 'src/base/ftbitmap.c', - 'src/base/ftcid.c', - 'src/base/ftfntfmt.c', - 'src/base/ftfstype.c', - 'src/base/ftgasp.c', - 'src/base/ftglyph.c', - 'src/base/ftgxval.c', - 'src/base/ftinit.c', - 'src/base/ftlcdfil.c', - 'src/base/ftmm.c', - 'src/base/ftotval.c', - 'src/base/ftpatent.c', - 'src/base/ftpfr.c', - 'src/base/ftstroke.c', - 'src/base/ftsynth.c', - 'src/base/ftsystem.c', - 'src/base/fttype1.c', - 'src/base/ftwinfnt.c', - 'src/bdf/bdf.c', - 'src/bzip2/ftbzip2.c', - 'src/cache/ftcache.c', - 'src/cff/cff.c', - 'src/cid/type1cid.c', - 'src/gzip/ftgzip.c', - 'src/lzw/ftlzw.c', - 'src/pcf/pcf.c', - 'src/pfr/pfr.c', - 'src/psaux/psaux.c', - 'src/pshinter/pshinter.c', - 'src/psnames/psnames.c', - 'src/raster/raster.c', - 'src/sfnt/sfnt.c', - 'src/smooth/smooth.c', - 'src/truetype/truetype.c', - 'src/type1/type1.c', - 'src/type42/type42.c', - 'src/winfonts/winfnt.c', -] - -ft2build_h = [ - 'include/ft2build.h', -] - -ft_headers = [ - 'include/freetype/freetype.h', - 'include/freetype/ftadvanc.h', - 'include/freetype/ftautoh.h', - 'include/freetype/ftbbox.h', - 'include/freetype/ftbdf.h', - 'include/freetype/ftbitmap.h', - 'include/freetype/ftbzip2.h', - 'include/freetype/ftcache.h', - 'include/freetype/ftcffdrv.h', - 'include/freetype/ftchapters.h', - 'include/freetype/ftcid.h', - 'include/freetype/fterrdef.h', - 'include/freetype/fterrors.h', - 'include/freetype/ftfntfmt.h', - 'include/freetype/ftgasp.h', - 'include/freetype/ftglyph.h', - 'include/freetype/ftgxval.h', - 'include/freetype/ftgzip.h', - 'include/freetype/ftimage.h', - 'include/freetype/ftincrem.h', - 'include/freetype/ftlcdfil.h', - 'include/freetype/ftlist.h', - 'include/freetype/ftlzw.h', - 'include/freetype/ftmac.h', - 'include/freetype/ftmm.h', - 'include/freetype/ftmodapi.h', - 'include/freetype/ftmoderr.h', - 'include/freetype/ftotval.h', - 'include/freetype/ftoutln.h', - 'include/freetype/ftpfr.h', - 'include/freetype/ftrender.h', - 'include/freetype/ftsizes.h', - 'include/freetype/ftsnames.h', - 'include/freetype/ftstroke.h', - 'include/freetype/ftsynth.h', - 'include/freetype/ftsystem.h', - 'include/freetype/fttrigon.h', - 'include/freetype/ftttdrv.h', - 'include/freetype/fttypes.h', - 'include/freetype/ftwinfnt.h', - 'include/freetype/t1tables.h', - 'include/freetype/ttnameid.h', - 'include/freetype/tttables.h', - 'include/freetype/tttags.h', - 'include/freetype/ttunpat.h', -] - -ft_config_headers = [ - 'include/freetype/config/ftconfig.h', - 'include/freetype/config/ftheader.h', - 'include/freetype/config/ftmodule.h', - 'include/freetype/config/ftoption.h', - 'include/freetype/config/ftstdlib.h', -] - -if host_machine.system() == 'windows' - base_sources += [ - 'builds/windows/ftdebug.c', - ] -else - base_sources += [ - 'src/base/ftdebug.c', - ] -endif - -c_args = [ - '-DFT2_BUILD_LIBRARY', - '-DFT_CONFIG_CONFIG_H=', - '-DFT_CONFIG_OPTIONS_H=' -] - -check_headers = [] - -if ['linux', 'darwin', 'cygwin'].contains(host_machine.system()) - check_headers += [ - ['unistd.h'], - ['fcntl.h'], - ['stdint.h'], - ] - ftconfig_h_in = files('builds/unix/ftconfig.h.in') -else - ftconfig_h_in = files('include/freetype/config/ftconfig.h') -endif - -conf = configuration_data() -deps = [] -incbase = include_directories(['include']) - -foreach check : check_headers - name = check[0] - - if cc.has_header(name) - conf.set('HAVE_@0@'.format(name.to_upper().underscorify()), 1) - endif -endforeach - -configure_file(input: ftconfig_h_in, - output: 'ftconfig.h', - configuration: conf) - -ft_config_headers += [configure_file(input: 'include/freetype/config/ftoption.h.in', - output: 'ftoption.h', - configuration: conf)] - -libfreetype = static_library('freetype', base_sources, - include_directories: incbase, - dependencies: deps, - c_args: c_args, - gnu_symbol_visibility: 'inlineshidden', -) - -freetype_dep = declare_dependency( - link_with: libfreetype, - include_directories : incbase, - dependencies: deps, -) diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h b/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h deleted file mode 100644 index d88a82a2eec8..000000000000 --- a/subprojects/packagefiles/freetype-2.6.1-meson/src/gzip/zconf.h +++ /dev/null @@ -1,284 +0,0 @@ -/* zconf.h -- configuration of the zlib compression library - * Copyright (C) 1995-2002 Jean-loup Gailly. - * For conditions of distribution and use, see copyright notice in zlib.h - */ - -/* @(#) $Id$ */ - -#ifndef _ZCONF_H -#define _ZCONF_H - -/* - * If you *really* need a unique prefix for all types and library functions, - * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it. - */ -#ifdef Z_PREFIX -# define deflateInit_ z_deflateInit_ -# define deflate z_deflate -# define deflateEnd z_deflateEnd -# define inflateInit_ z_inflateInit_ -# define inflate z_inflate -# define inflateEnd z_inflateEnd -# define deflateInit2_ z_deflateInit2_ -# define deflateSetDictionary z_deflateSetDictionary -# define deflateCopy z_deflateCopy -# define deflateReset z_deflateReset -# define deflateParams z_deflateParams -# define inflateInit2_ z_inflateInit2_ -# define inflateSetDictionary z_inflateSetDictionary -# define inflateSync z_inflateSync -# define inflateSyncPoint z_inflateSyncPoint -# define inflateReset z_inflateReset -# define compress z_compress -# define compress2 z_compress2 -# define uncompress z_uncompress -# define adler32 z_adler32 -# define crc32 z_crc32 -# define get_crc_table z_get_crc_table - -# define Byte z_Byte -# define uInt z_uInt -# define uLong z_uLong -# define Bytef z_Bytef -# define charf z_charf -# define intf z_intf -# define uIntf z_uIntf -# define uLongf z_uLongf -# define voidpf z_voidpf -# define voidp z_voidp -#endif - -#if (defined(_WIN32) || defined(__WIN32__)) && !defined(WIN32) -# define WIN32 -#endif -#if defined(__GNUC__) || defined(WIN32) || defined(__386__) || defined(i386) -# ifndef __32BIT__ -# define __32BIT__ -# endif -#endif -#if defined(__MSDOS__) && !defined(MSDOS) -# define MSDOS -#endif - -/* WinCE doesn't have errno.h */ -#ifdef _WIN32_WCE -# define NO_ERRNO_H -#endif - - -/* - * Compile with -DMAXSEG_64K if the alloc function cannot allocate more - * than 64k bytes at a time (needed on systems with 16-bit int). - */ -#if defined(MSDOS) && !defined(__32BIT__) -# define MAXSEG_64K -#endif -#ifdef MSDOS -# define UNALIGNED_OK -#endif - -#if (defined(MSDOS) || defined(_WINDOWS) || defined(WIN32)) && !defined(STDC) -# define STDC -#endif -#if defined(__STDC__) || defined(__cplusplus) || defined(__OS2__) -# ifndef STDC -# define STDC -# endif -#endif - -#ifndef STDC -# ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */ -# define const -# endif -#endif - -/* Some Mac compilers merge all .h files incorrectly: */ -#if defined(__MWERKS__) || defined(applec) ||defined(THINK_C) ||defined(__SC__) -# define NO_DUMMY_DECL -#endif - -/* Old Borland C and LCC incorrectly complains about missing returns: */ -#if defined(__BORLANDC__) && (__BORLANDC__ < 0x500) -# define NEED_DUMMY_RETURN -#endif - -#if defined(__LCC__) -# define NEED_DUMMY_RETURN -#endif - -/* Maximum value for memLevel in deflateInit2 */ -#ifndef MAX_MEM_LEVEL -# ifdef MAXSEG_64K -# define MAX_MEM_LEVEL 8 -# else -# define MAX_MEM_LEVEL 9 -# endif -#endif - -/* Maximum value for windowBits in deflateInit2 and inflateInit2. - * WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files - * created by gzip. (Files created by minigzip can still be extracted by - * gzip.) - */ -#ifndef MAX_WBITS -# define MAX_WBITS 15 /* 32K LZ77 window */ -#endif - -/* The memory requirements for deflate are (in bytes): - (1 << (windowBits+2)) + (1 << (memLevel+9)) - that is: 128K for windowBits=15 + 128K for memLevel = 8 (default values) - plus a few kilobytes for small objects. For example, if you want to reduce - the default memory requirements from 256K to 128K, compile with - make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7" - Of course this will generally degrade compression (there's no free lunch). - - The memory requirements for inflate are (in bytes) 1 << windowBits - that is, 32K for windowBits=15 (default value) plus a few kilobytes - for small objects. -*/ - - /* Type declarations */ - -#ifndef OF /* function prototypes */ -# ifdef STDC -# define OF(args) args -# else -# define OF(args) () -# endif -#endif - -/* The following definitions for FAR are needed only for MSDOS mixed - * model programming (small or medium model with some far allocations). - * This was tested only with MSC; for other MSDOS compilers you may have - * to define NO_MEMCPY in zutil.h. If you don't need the mixed model, - * just define FAR to be empty. - */ -#if (defined(M_I86SM) || defined(M_I86MM)) && !defined(__32BIT__) - /* MSC small or medium model */ -# define SMALL_MEDIUM -# ifdef _MSC_VER -# define FAR _far -# else -# define FAR far -# endif -#endif -#if defined(__BORLANDC__) && (defined(__SMALL__) || defined(__MEDIUM__)) -# ifndef __32BIT__ -# define SMALL_MEDIUM -# define FAR _far -# endif -#endif - -/* Compile with -DZLIB_DLL for Windows DLL support */ -#if defined(ZLIB_DLL) -# if defined(_WINDOWS) || defined(WINDOWS) -# ifdef FAR -# undef FAR -# endif -# include -# define ZEXPORT(x) x WINAPI -# ifdef WIN32 -# define ZEXPORTVA(x) x WINAPIV -# else -# define ZEXPORTVA(x) x FAR _cdecl _export -# endif -# endif -# if defined (__BORLANDC__) -# if (__BORLANDC__ >= 0x0500) && defined (WIN32) -# include -# define ZEXPORT(x) x __declspec(dllexport) WINAPI -# define ZEXPORTRVA(x) x __declspec(dllexport) WINAPIV -# else -# if defined (_Windows) && defined (__DLL__) -# define ZEXPORT(x) x _export -# define ZEXPORTVA(x) x _export -# endif -# endif -# endif -#endif - - -#ifndef ZEXPORT -# define ZEXPORT(x) static x -#endif -#ifndef ZEXPORTVA -# define ZEXPORTVA(x) static x -#endif -#ifndef ZEXTERN -# define ZEXTERN(x) static x -#endif -#ifndef ZEXTERNDEF -# define ZEXTERNDEF(x) static x -#endif - -#ifndef FAR -# define FAR -#endif - -#if !defined(__MACTYPES__) -typedef unsigned char Byte; /* 8 bits */ -#endif -typedef unsigned int uInt; /* 16 bits or more */ -typedef unsigned long uLong; /* 32 bits or more */ - -#ifdef SMALL_MEDIUM - /* Borland C/C++ and some old MSC versions ignore FAR inside typedef */ -# define Bytef Byte FAR -#else - typedef Byte FAR Bytef; -#endif -typedef char FAR charf; -typedef int FAR intf; -typedef uInt FAR uIntf; -typedef uLong FAR uLongf; - -#ifdef STDC - typedef void FAR *voidpf; - typedef void *voidp; -#else - typedef Byte FAR *voidpf; - typedef Byte *voidp; -#endif - -#ifdef HAVE_UNISTD_H -# include /* for off_t */ -# include /* for SEEK_* and off_t */ -# define z_off_t off_t -#endif -#ifndef SEEK_SET -# define SEEK_SET 0 /* Seek from beginning of file. */ -# define SEEK_CUR 1 /* Seek from current position. */ -# define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ -#endif -#ifndef z_off_t -# define z_off_t long -#endif - -/* MVS linker does not support external names larger than 8 bytes */ -#if defined(__MVS__) -# pragma map(deflateInit_,"DEIN") -# pragma map(deflateInit2_,"DEIN2") -# pragma map(deflateEnd,"DEEND") -# pragma map(inflateInit_,"ININ") -# pragma map(inflateInit2_,"ININ2") -# pragma map(inflateEnd,"INEND") -# pragma map(inflateSync,"INSY") -# pragma map(inflateSetDictionary,"INSEDI") -# pragma map(inflate_blocks,"INBL") -# pragma map(inflate_blocks_new,"INBLNE") -# pragma map(inflate_blocks_free,"INBLFR") -# pragma map(inflate_blocks_reset,"INBLRE") -# pragma map(inflate_codes_free,"INCOFR") -# pragma map(inflate_codes,"INCO") -# pragma map(inflate_fast,"INFA") -# pragma map(inflate_flush,"INFLU") -# pragma map(inflate_mask,"INMA") -# pragma map(inflate_set_dictionary,"INSEDI2") -# pragma map(inflate_copyright,"INCOPY") -# pragma map(inflate_trees_bits,"INTRBI") -# pragma map(inflate_trees_dynamic,"INTRDY") -# pragma map(inflate_trees_fixed,"INTRFI") -# pragma map(inflate_trees_free,"INTRFR") -#endif - -#endif /* _ZCONF_H */ diff --git a/subprojects/packagefiles/freetype-34aed655f1696da774b5cdd4c5effb312153232f.patch b/subprojects/packagefiles/freetype-34aed655f1696da774b5cdd4c5effb312153232f.patch new file mode 100644 index 000000000000..c00baa702f65 --- /dev/null +++ b/subprojects/packagefiles/freetype-34aed655f1696da774b5cdd4c5effb312153232f.patch @@ -0,0 +1,36 @@ +From 34aed655f1696da774b5cdd4c5effb312153232f Mon Sep 17 00:00:00 2001 +From: Benoit Pierre +Date: Sat, 12 Oct 2024 10:49:46 +0000 +Subject: [PATCH] * meson.build: Fix `bzip2` option handling. + +--- + meson.build | 11 ++++++++--- + 1 file changed, 8 insertions(+), 3 deletions(-) + +diff --git a/meson.build b/meson.build +index 72b7f9900..2e8d5355e 100644 +--- a/meson.build ++++ b/meson.build +@@ -320,11 +320,16 @@ else + endif + + # BZip2 support. +-bzip2_dep = dependency('bzip2', required: false) ++bzip2_dep = dependency( ++ 'bzip2', ++ required: get_option('bzip2').disabled() ? get_option('bzip2') : false, ++) + if not bzip2_dep.found() +- bzip2_dep = cc.find_library('bz2', ++ bzip2_dep = cc.find_library( ++ 'bz2', + has_headers: ['bzlib.h'], +- required: get_option('bzip2')) ++ required: get_option('bzip2'), ++ ) + endif + + if bzip2_dep.found() +-- +GitLab + From 7787153a524475e60350ca2d87b2caf00905c6aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 23 Apr 2025 05:36:31 -0400 Subject: [PATCH 10/58] Bump minimum meson-python to 0.13.2 --- pyproject.toml | 4 ++-- requirements/testing/minver.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b580feff930e..18d99e3111e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ requires-python = ">=3.11" [project.optional-dependencies] # Should be a copy of the build dependencies below. dev = [ - "meson-python>=0.13.1,!=0.17.*", + "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ @@ -72,7 +72,7 @@ build-backend = "mesonpy" requires = [ # meson-python 0.17.x breaks symlinks in sdists. You can remove this pin if # you really need it and aren't using an sdist. - "meson-python>=0.13.1,!=0.17.*", + "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", "setuptools_scm>=7", ] diff --git a/requirements/testing/minver.txt b/requirements/testing/minver.txt index ee55f6c7b1bf..3b6aea9e7ca3 100644 --- a/requirements/testing/minver.txt +++ b/requirements/testing/minver.txt @@ -5,7 +5,7 @@ cycler==0.10 fonttools==4.22.0 importlib-resources==3.2.0 kiwisolver==1.3.2 -meson-python==0.13.1 +meson-python==0.13.2 meson==1.1.0 numpy==1.25.0 packaging==20.0 From 972a82173fba27b00696fbcb5cc4a20caacb6bd0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 2 Jul 2025 17:36:02 -0400 Subject: [PATCH 11/58] ci: Purge Strawberry Perl from Windows builders We don't use Perl, and it includes a completely busted version of `patch`. --- .github/workflows/cibuildwheel.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index fececb0dfc40..dc05ec5483fd 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -140,6 +140,10 @@ jobs: name: cibw-sdist path: dist/ + - name: Purge Strawberry Perl + if: startsWith(matrix.os, 'windows-') + run: Remove-Item -Recurse C:\Strawberry + - name: Build wheels for CPython 3.14 uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 with: From 5fc955939dd074beea4554b6e2b668061fb46073 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Jul 2025 04:42:42 -0400 Subject: [PATCH 12/58] Don't set a default size for FT2Font In the interest of handling non-scalable fonts and reducing font initialization, drop the default size from the `FT2Font` constructor. Non-scalable fonts are sometimes used for bitmap-backed emoji fonts. When we start supporting collection fonts (`.ttc`), then setting a size is a waste, as we will just need to read the count of fonts within. The renderer method `Renderer.draw_text` always sets a size immediately after creating the font object, so this doesn't affect anything in most cases. Only the direct `FT2Font` tests need changes. --- doc/api/next_api_changes/behavior/30318-ES.rst | 9 +++++++++ lib/matplotlib/tests/test_ft2font.py | 7 ++++++- src/ft2font.cpp | 6 ------ 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30318-ES.rst diff --git a/doc/api/next_api_changes/behavior/30318-ES.rst b/doc/api/next_api_changes/behavior/30318-ES.rst new file mode 100644 index 000000000000..805901dcb21d --- /dev/null +++ b/doc/api/next_api_changes/behavior/30318-ES.rst @@ -0,0 +1,9 @@ +FT2Font no longer sets a default size +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the interest of handling non-scalable fonts and reducing font initialization, the +`.FT2Font` constructor no longer sets a default size. Non-scalable fonts are sometimes +used for bitmap-backed emoji fonts. + +If metrics are important (i.e., if you are loading character glyphs, or setting a text +string), then explicitly call `.FT2Font.set_size` beforehand. diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 0dc0667d0e84..b39df1f52996 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -188,8 +188,8 @@ def test_ft2font_clear(): def test_ft2font_set_size(): file = fm.findfont('DejaVu Sans') - # Default is 12pt @ 72 dpi. font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1) + font.set_size(12, 72) font.set_text('ABabCDcd') orig = font.get_width_height() font.set_size(24, 72) @@ -757,6 +757,7 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): def test_ft2font_set_text(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(12, 72) xys = font.set_text('') np.testing.assert_array_equal(xys, np.empty((0, 2))) assert font.get_width_height() == (0, 0) @@ -778,6 +779,7 @@ def test_ft2font_set_text(): def test_ft2font_loading(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(12, 72) for glyph in [font.load_char(ord('M')), font.load_glyph(font.get_char_index(ord('M')))]: assert glyph is not None @@ -818,11 +820,13 @@ def test_ft2font_drawing(): expected *= 255 file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(12, 72) font.set_text('M') font.draw_glyphs_to_bitmap(antialiased=False) image = font.get_image() np.testing.assert_array_equal(image, expected) font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(12, 72) glyph = font.load_char(ord('M')) image = np.zeros(expected.shape, np.uint8) font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) @@ -832,6 +836,7 @@ def test_ft2font_drawing(): def test_ft2font_get_path(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font.set_size(12, 72) vertices, codes = font.get_path() assert vertices.shape == (0, 2) assert codes.shape == (0, ) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index ca8881d98c50..1d03ecf10b56 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -221,12 +221,6 @@ FT2Font::FT2Font(FT_Open_Args &open_args, if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } - try { - set_size(12., 72.); // Set a default fontsize 12 pt at 72dpi. - } catch (...) { - FT_Done_Face(face); - throw; - } // Set fallbacks std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); } From 7a628e5b752da6e5b549a5180e3e1ebb4f142b9d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Jul 2025 02:25:58 -0400 Subject: [PATCH 13/58] Deprecate font_manager.is_opentype_cff_font According to the docs, it was used for PostScript and PDF which "cannot subset those fonts". However, that is no longer true, and there are no users of this function. --- doc/api/next_api_changes/deprecations/30329-ES.rst | 4 ++++ lib/matplotlib/font_manager.py | 2 +- lib/matplotlib/tests/test_font_manager.py | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30329-ES.rst diff --git a/doc/api/next_api_changes/deprecations/30329-ES.rst b/doc/api/next_api_changes/deprecations/30329-ES.rst new file mode 100644 index 000000000000..8d5060c4821b --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30329-ES.rst @@ -0,0 +1,4 @@ +``font_manager.is_opentype_cff_font`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is no replacement. diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index ab6b495631de..79e088b85998 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1539,7 +1539,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, return _cached_realpath(result) -@lru_cache +@_api.deprecated("3.11") def is_opentype_cff_font(filename): """ Return whether the given font is a Postscript Compact Font Format Font diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 24421b8e30b3..b15647644e04 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -67,12 +67,14 @@ def test_json_serialization(tmp_path): def test_otf(): fname = '/usr/share/fonts/opentype/freefont/FreeMono.otf' if Path(fname).exists(): - assert is_opentype_cff_font(fname) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + assert is_opentype_cff_font(fname) for f in fontManager.ttflist: if 'otf' in f.fname: with open(f.fname, 'rb') as fd: res = fd.read(4) == b'OTTO' - assert res == is_opentype_cff_font(f.fname) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + assert res == is_opentype_cff_font(f.fname) @pytest.mark.skipif(sys.platform == "win32" or not has_fclist, From 42c108a850fb88e78b9a3d990d826ce536a0d6e0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Jul 2025 20:43:18 -0400 Subject: [PATCH 14/58] Deprecate setting text kerning factor to any non-None value This factor existed only to preserve test images, but as of #29816, it is set to 0 (i.e., disabled and providing default behaviour). In the future, with libraqm, it will have no effect no matter its setting (because we won't be applying kerning ourselves at all.) --- .../deprecations/30322-ES.rst | 7 +++++ doc/users/prev_whats_new/whats_new_3.2.0.rst | 29 ++++++------------- lib/matplotlib/__init__.py | 2 ++ lib/matplotlib/ft2font.pyi | 2 +- lib/matplotlib/mpl-data/matplotlibrc | 7 ++--- lib/matplotlib/rcsetup.py | 2 +- lib/matplotlib/tests/test_ft2font.py | 20 ++++++++----- src/ft2font_wrapper.cpp | 20 +++++++------ 8 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30322-ES.rst diff --git a/doc/api/next_api_changes/deprecations/30322-ES.rst b/doc/api/next_api_changes/deprecations/30322-ES.rst new file mode 100644 index 000000000000..b9c4964e58c8 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30322-ES.rst @@ -0,0 +1,7 @@ +Font kerning factor is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Due to internal changes to support complex text rendering, the kerning factor on fonts is +no longer used. Setting the ``text.kerning_factor`` rcParam (which existed only for +backwards-compatibility) to any value other than None is deprecated, and the rcParam will +be removed in the future. diff --git a/doc/users/prev_whats_new/whats_new_3.2.0.rst b/doc/users/prev_whats_new/whats_new_3.2.0.rst index 12d7fab3af90..3519245642a8 100644 --- a/doc/users/prev_whats_new/whats_new_3.2.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.2.0.rst @@ -52,26 +52,15 @@ triangle meshes. Kerning adjustments now use correct values ------------------------------------------ -Due to an error in how kerning adjustments were applied, previous versions of -Matplotlib would under-correct kerning. This version will now correctly apply -kerning (for fonts supported by FreeType). To restore the old behavior (e.g., -for test images), you may set :rc:`text.kerning_factor` to 6 (instead of 0). -Other values have undefined behavior. - -.. plot:: - - import matplotlib.pyplot as plt - - # Use old kerning values: - plt.rcParams['text.kerning_factor'] = 6 - fig, ax = plt.subplots() - ax.text(0.0, 0.05, 'BRAVO\nAWKWARD\nVAT\nW.Test', fontsize=56) - ax.set_title('Before (text.kerning_factor = 6)') - -Note how the spacing between characters is uniform between their bounding boxes -(above). With corrected kerning (below), slanted characters (e.g., AV or VA) -will be spaced closer together, as well as various other character pairs, -depending on font support (e.g., T and e, or the period after the W). +Due to an error in how kerning adjustments were applied, previous versions of Matplotlib +would under-correct kerning. This version will now correctly apply kerning (for fonts +supported by FreeType). To restore the old behavior (e.g., for test images), you may set +the ``text.kerning_factor`` rcParam to 6 (instead of 0). Other values have undefined +behavior. + +With corrected kerning (below), slanted characters (e.g., AV or VA) will be spaced closer +together, as well as various other character pairs, depending on font support (e.g., T +and e, or the period after the W). .. plot:: diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 008d4de77a3b..e343e60b5fa1 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -751,6 +751,8 @@ def __setitem__(self, key, val): f"a list of valid parameters)") from err except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None + if key == "text.kerning_factor" and cval is not None: + _api.warn_deprecated("3.11", name="text.kerning_factor", obj_type="rcParam") self._set(key, cval) def __getitem__(self, key): diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index a413cd3c1a76..5257893b380a 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -191,7 +191,7 @@ class FT2Font(Buffer): hinting_factor: int = ..., *, _fallback_list: list[FT2Font] | None = ..., - _kerning_factor: int = ... + _kerning_factor: int | None = ... ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 0ab5cfadf291..fb96656c5303 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -306,10 +306,9 @@ #text.hinting_factor: 1 # Specifies the amount of softness for hinting in the # horizontal direction. A value of 1 will hint to full # pixels. A value of 2 will hint to half pixels etc. -#text.kerning_factor: 0 # Specifies the scaling factor for kerning values. This - # is provided solely to allow old test images to remain - # unchanged. Set to 6 to obtain previous behavior. - # Values other than 0 or 6 have no defined meaning. +#text.kerning_factor: None # Specifies the scaling factor for kerning values. Values + # other than 0, 6, or None have no defined meaning. + # This setting is deprecated. #text.antialiased: True # If True (default), the text will be antialiased. # This only affects raster outputs. #text.parse_math: True # Use mathtext if there is an even number of unescaped diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 80d25659888e..b4224d169815 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1042,7 +1042,7 @@ def _convert_validator_spec(key, conv): "text.hinting": ["default", "no_autohint", "force_autohint", "no_hinting", "auto", "native", "either", "none"], "text.hinting_factor": validate_int, - "text.kerning_factor": validate_int, + "text.kerning_factor": validate_int_or_None, "text.antialiased": validate_bool, "text.parse_math": validate_bool, diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index b39df1f52996..5dd96ce9cafe 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -168,6 +168,12 @@ def test_ft2font_invalid_args(tmp_path): # kerning_factor argument. with pytest.raises(TypeError, match='incompatible constructor arguments'): ft2font.FT2Font(file, _kerning_factor=1.3) + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='text.kerning_factor rcParam was deprecated .+ 3.11'): + mpl.rcParams['text.kerning_factor'] = 0 + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match='_kerning_factor parameter was deprecated .+ 3.11'): + ft2font.FT2Font(file, _kerning_factor=123) def test_ft2font_clear(): @@ -188,7 +194,7 @@ def test_ft2font_clear(): def test_ft2font_set_size(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=1) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) font.set_text('ABabCDcd') orig = font.get_width_height() @@ -717,7 +723,7 @@ def test_ft2font_get_sfnt_table(font_name, header): def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): file = fm.findfont('DejaVu Sans') # With unscaled, these settings should produce exact values found in FontForge. - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(100, 100) assert font.get_kerning(font.get_char_index(ord(left)), font.get_char_index(ord(right)), @@ -756,7 +762,7 @@ def test_ft2font_get_kerning(left, right, unscaled, unfitted, default): def test_ft2font_set_text(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) xys = font.set_text('') np.testing.assert_array_equal(xys, np.empty((0, 2))) @@ -778,7 +784,7 @@ def test_ft2font_set_text(): def test_ft2font_loading(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) for glyph in [font.load_char(ord('M')), font.load_glyph(font.get_char_index(ord('M')))]: @@ -819,13 +825,13 @@ def test_ft2font_drawing(): ]) expected *= 255 file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) font.set_text('M') font.draw_glyphs_to_bitmap(antialiased=False) image = font.get_image() np.testing.assert_array_equal(image, expected) - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) glyph = font.load_char(ord('M')) image = np.zeros(expected.shape, np.uint8) @@ -835,7 +841,7 @@ def test_ft2font_drawing(): def test_ft2font_get_path(): file = fm.findfont('DejaVu Sans') - font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) + font = ft2font.FT2Font(file, hinting_factor=1) font.set_size(12, 72) vertices, codes = font.get_path() assert vertices.shape == (0, 2) diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index cb816efff9a9..5ba4bec36874 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -432,12 +432,6 @@ const char *PyFT2Font_init__doc__ = R"""( .. warning:: This API is both private and provisional: do not use it directly. - _kerning_factor : int, optional - Used to adjust the degree of kerning. - - .. warning:: - This API is private: do not use it directly. - _warn_if_used : bool, optional Used to trigger missing glyph warnings. @@ -448,11 +442,19 @@ const char *PyFT2Font_init__doc__ = R"""( static PyFT2Font * PyFT2Font_init(py::object filename, long hinting_factor = 8, std::optional> fallback_list = std::nullopt, - int kerning_factor = 0, bool warn_if_used = false) + std::optional kerning_factor = std::nullopt, + bool warn_if_used = false) { if (hinting_factor <= 0) { throw py::value_error("hinting_factor must be greater than 0"); } + if (kerning_factor) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.11", "name"_a="_kerning_factor", "obj_type"_a="parameter"); + } else { + kerning_factor = 0; + } PyFT2Font *self = new PyFT2Font(); self->x = nullptr; @@ -500,7 +502,7 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn, warn_if_used); - self->x->set_kerning_factor(kerning_factor); + self->x->set_kerning_factor(*kerning_factor); return self; } @@ -1605,7 +1607,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), "filename"_a, "hinting_factor"_a=8, py::kw_only(), - "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, + "_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(), "_warn_if_used"_a=false, PyFT2Font_init__doc__) .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) From 3a0a7734466db95809e72d7912d7a075b0ee4d12 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 3 Jun 2025 18:59:00 -0400 Subject: [PATCH 15/58] TYP: Make glyph indices distinct from character codes Previously, these were both typed as `int`, which means you could mix and match them erroneously. While the character code can't be made a distinct type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means these can't be fully swapped. Unfortunately, you can still go back to the base type, so glyph indices still work as character codes. But this is still sufficient to catch errors such as the wrong call to `FT2Font.get_kerning` in `_mathtext.py`. --- .../next_api_changes/development/30143-ES.rst | 7 +++++ lib/matplotlib/_afm.py | 19 +++++++------ lib/matplotlib/_mathtext.py | 28 +++++++++---------- lib/matplotlib/_mathtext_data.py | 18 +++++++----- lib/matplotlib/_text_helpers.py | 4 +-- lib/matplotlib/dviread.pyi | 7 +++-- lib/matplotlib/ft2font.pyi | 22 +++++++++------ lib/matplotlib/tests/test_ft2font.py | 5 ++-- src/ft2font_wrapper.cpp | 3 ++ 9 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 doc/api/next_api_changes/development/30143-ES.rst diff --git a/doc/api/next_api_changes/development/30143-ES.rst b/doc/api/next_api_changes/development/30143-ES.rst new file mode 100644 index 000000000000..2d79ad6bbe9d --- /dev/null +++ b/doc/api/next_api_changes/development/30143-ES.rst @@ -0,0 +1,7 @@ +Glyph indices now typed distinctly from character codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, character codes and glyph indices were both typed as `int`, which means you +could mix and match them erroneously. While the character code can't be made a distinct +type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means +these can't be fully swapped. diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py index 352d3c42247e..3d7f7a44baca 100644 --- a/lib/matplotlib/_afm.py +++ b/lib/matplotlib/_afm.py @@ -30,9 +30,10 @@ import inspect import logging import re -from typing import BinaryIO, NamedTuple, TypedDict +from typing import BinaryIO, NamedTuple, TypedDict, cast from ._mathtext_data import uni2type1 +from .ft2font import CharacterCodeType, GlyphIndexType _log = logging.getLogger(__name__) @@ -197,7 +198,7 @@ class CharMetrics(NamedTuple): The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" -def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics], +def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[CharacterCodeType, CharMetrics], dict[str, CharMetrics]]: """ Parse the given filehandle for character metrics information. @@ -218,7 +219,7 @@ def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics], """ required_keys = {'C', 'WX', 'N', 'B'} - ascii_d: dict[int, CharMetrics] = {} + ascii_d: dict[CharacterCodeType, CharMetrics] = {} name_d: dict[str, CharMetrics] = {} for bline in fh: # We are defensively letting values be utf8. The spec requires @@ -409,19 +410,21 @@ def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]: return left, miny, total_width, maxy - miny, -miny - def get_glyph_name(self, glyph_ind: int) -> str: # For consistency with FT2Font. + def get_glyph_name(self, # For consistency with FT2Font. + glyph_ind: GlyphIndexType) -> str: """Get the name of the glyph, i.e., ord(';') is 'semicolon'.""" - return self._metrics[glyph_ind].name + return self._metrics[cast(CharacterCodeType, glyph_ind)].name - def get_char_index(self, c: int) -> int: # For consistency with FT2Font. + def get_char_index(self, # For consistency with FT2Font. + c: CharacterCodeType) -> GlyphIndexType: """ Return the glyph index corresponding to a character code point. Note, for AFM fonts, we treat the glyph index the same as the codepoint. """ - return c + return cast(GlyphIndexType, c) - def get_width_char(self, c: int) -> float: + def get_width_char(self, c: CharacterCodeType) -> float: """Get the width of the character code from the character metric WX field.""" return self._metrics[c].width diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 19ddbb6d0883..afaa9ade6018 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -37,7 +37,8 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import Glyph + from .ft2font import CharacterCodeType, Glyph + ParserElement.enable_packrat() _log = logging.getLogger("matplotlib.mathtext") @@ -47,7 +48,7 @@ # FONTS -def get_unicode_index(symbol: str) -> int: # Publicly exported. +def get_unicode_index(symbol: str) -> CharacterCodeType: # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. @@ -85,7 +86,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, int, float, float]] + glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -212,7 +213,7 @@ class FontInfo(NamedTuple): fontsize: float postscript_name: str metrics: FontMetrics - num: int + num: CharacterCodeType glyph: Glyph offset: float @@ -365,7 +366,7 @@ def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float, return 0. def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: raise NotImplementedError # The return value of _get_info is cached per-instance. @@ -459,7 +460,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -551,7 +552,7 @@ class UnicodeFonts(TruetypeFonts): # Some glyphs are not present in the `cmr10` font, and must be brought in # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at # which they are found in `cmsy10`. - _cmr10_substitutions = { + _cmr10_substitutions: dict[CharacterCodeType, CharacterCodeType] = { 0x00D7: 0x00A3, # Multiplication sign. 0x2212: 0x00A1, # Minus sign. } @@ -594,11 +595,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag _slanted_symbols = set(r"\int \oint".split()) def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: return fontname, uniindex def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: try: uniindex = get_unicode_index(sym) found_symbol = True @@ -607,8 +608,7 @@ def _get_glyph(self, fontname: str, font_class: str, found_symbol = False _log.warning("No TeX to Unicode mapping for %a.", sym) - fontname, uniindex = self._map_virtual_font( - fontname, font_class, uniindex) + fontname, uniindex = self._map_virtual_font(fontname, font_class, uniindex) new_fontname = fontname @@ -693,7 +693,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _get_glyph(self, fontname: str, font_class: str, - sym: str) -> tuple[FT2Font, int, bool]: + sym: str) -> tuple[FT2Font, CharacterCodeType, bool]: # Override prime symbol to use Bakoma. if sym == r'\prime': return self.bakoma._get_glyph(fontname, font_class, sym) @@ -783,7 +783,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag self.fontmap[name] = fullpath def _map_virtual_font(self, fontname: str, font_class: str, - uniindex: int) -> tuple[str, int]: + uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]: # Handle these "fonts" that are actually embedded in # other fonts. font_mapping = stix_virtual_fonts.get(fontname) @@ -1170,7 +1170,7 @@ def __init__(self, elements: T.Sequence[Node]): self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching self.glue_order = 0 # The order of infinity (0 - 3) for the glue - def __repr__(self): + def __repr__(self) -> str: return "{}[{}]".format( super().__repr__(), self.width, self.height, diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 5819ee743044..0451791e9f26 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -3,9 +3,12 @@ """ from __future__ import annotations -from typing import overload +from typing import TypeAlias, overload -latex_to_bakoma = { +from .ft2font import CharacterCodeType + + +latex_to_bakoma: dict[str, tuple[str, CharacterCodeType]] = { '\\__sqrt__' : ('cmex10', 0x70), '\\bigcap' : ('cmex10', 0x5c), '\\bigcup' : ('cmex10', 0x5b), @@ -241,7 +244,7 @@ # Automatically generated. -type12uni = { +type12uni: dict[str, CharacterCodeType] = { 'aring' : 229, 'quotedblright' : 8221, 'V' : 86, @@ -475,7 +478,7 @@ # for key in sd: # print("{0:24} : {1: dict[str, float]: ... @property - def index(self) -> int: ... # type: ignore[override] + def index(self) -> GlyphIndexType: ... # type: ignore[override] @property - def glyph_name_or_index(self) -> int | str: ... + def glyph_name_or_index(self) -> GlyphIndexType | str: ... class Dvi: file: io.BufferedReader diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 5257893b380a..a4ddc84358c1 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,6 +1,6 @@ from enum import Enum, Flag import sys -from typing import BinaryIO, Literal, TypedDict, final, overload, cast +from typing import BinaryIO, Literal, NewType, TypeAlias, TypedDict, cast, final, overload from typing_extensions import Buffer # < Py 3.12 import numpy as np @@ -9,6 +9,12 @@ from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str +# We can't change the type hints for standard library chr/ord, so character codes are a +# simple type alias. +CharacterCodeType: TypeAlias = int +# But glyph indices are internal, so use a distinct type hint. +GlyphIndexType = NewType('GlyphIndexType', int) + class FaceFlags(Flag): SCALABLE = cast(int, ...) FIXED_SIZES = cast(int, ...) @@ -202,13 +208,13 @@ class FT2Font(Buffer): ) -> None: ... def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ... def get_bitmap_offset(self) -> tuple[int, int]: ... - def get_char_index(self, codepoint: int) -> int: ... - def get_charmap(self) -> dict[int, int]: ... + def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ... + def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ... def get_descent(self) -> int: ... - def get_glyph_name(self, index: int) -> str: ... + def get_glyph_name(self, index: GlyphIndexType) -> str: ... def get_image(self) -> NDArray[np.uint8]: ... - def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ... - def get_name_index(self, name: str) -> int: ... + def get_kerning(self, left: GlyphIndexType, right: GlyphIndexType, mode: Kerning) -> int: ... + def get_name_index(self, name: str) -> GlyphIndexType: ... def get_num_glyphs(self) -> int: ... def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ... def get_ps_font_info( @@ -230,8 +236,8 @@ class FT2Font(Buffer): @overload def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ... def get_width_height(self) -> tuple[int, int]: ... - def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ... - def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ... + def load_char(self, charcode: CharacterCodeType, flags: LoadFlags = ...) -> Glyph: ... + def load_glyph(self, glyphindex: GlyphIndexType, flags: LoadFlags = ...) -> Glyph: ... def select_charmap(self, i: int) -> None: ... def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 5dd96ce9cafe..6b405287e5d7 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,6 +1,7 @@ import itertools import io from pathlib import Path +from typing import cast import numpy as np import pytest @@ -241,7 +242,7 @@ def enc(name): assert unic == after # This is just a random sample from FontForge. - glyph_names = { + glyph_names = cast(dict[str, ft2font.GlyphIndexType], { 'non-existent-glyph-name': 0, 'plusminus': 115, 'Racute': 278, @@ -253,7 +254,7 @@ def enc(name): 'uni2A02': 4464, 'u1D305': 5410, 'u1F0A1': 5784, - } + }) for name, index in glyph_names.items(): assert font.get_name_index(name) == index if name == 'non-existent-glyph-name': diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 5ba4bec36874..31202f018e42 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1774,5 +1774,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + auto py_int = py::module_::import("builtins").attr("int"); + m.attr("CharacterCodeType") = py_int; + m.attr("GlyphIndexType") = py_int; m.def("__getattr__", ft2font__getattr__); } From c6e690489637b25e8bd883cc9299751462a1da96 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Jun 2025 23:18:52 -0400 Subject: [PATCH 16/58] Fix kerning of mathtext The `FontInfo.num` value returned by `TruetypeFonts._get_info` is a character code, but `FT2Font.get_kerning` takes *glyph indices*, meaning that kerning was likely off in most cases. --- lib/matplotlib/_mathtext.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index afaa9ade6018..78f8913cd65a 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -426,7 +426,9 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64 + return font.get_kerning(font.get_char_index(info1.num), + font.get_char_index(info2.num), + Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) From 733cd7d99a3534be25dd46cd2c05b93749ec1b39 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 24 Jul 2025 15:46:13 -0400 Subject: [PATCH 17/58] Update test images for previous changes --- .../test_mathtext/mathtext_cm_21.svg | 1476 +++++++++-------- .../test_mathtext/mathtext_cm_23.png | Bin 3144 -> 2823 bytes .../test_mathtext/mathtext_cm_23.svg | 599 +++---- .../test_mathtext/mathtext_dejavusans_21.svg | 907 +++++----- .../test_mathtext/mathtext_dejavusans_23.png | Bin 3122 -> 2822 bytes .../test_mathtext/mathtext_dejavusans_23.svg | 537 +++--- .../test_mathtext/mathtext_dejavusans_27.svg | 383 +++-- .../test_mathtext/mathtext_dejavusans_46.svg | 229 +-- .../test_mathtext/mathtext_dejavusans_49.svg | 211 +-- .../test_mathtext/mathtext_dejavusans_60.svg | 418 ++--- .../test_mathtext/mathtext_dejavuserif_21.svg | 1020 ++++++------ .../test_mathtext/mathtext_dejavuserif_23.png | Bin 3125 -> 2853 bytes .../test_mathtext/mathtext_dejavuserif_23.svg | 559 ++++--- .../test_mathtext/mathtext_dejavuserif_60.svg | 444 ++--- .../test_mathtext/mathtext_stix_21.svg | 1096 ++++++------ .../test_mathtext/mathtext_stix_23.png | Bin 3135 -> 2826 bytes .../test_mathtext/mathtext_stix_23.svg | 573 ++++--- .../test_mathtext/mathtext_stixsans_21.svg | 904 +++++----- .../test_mathtext/mathtext_stixsans_23.png | Bin 3099 -> 2808 bytes .../test_mathtext/mathtext_stixsans_23.svg | 539 +++--- 20 files changed, 5180 insertions(+), 4715 deletions(-) diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg index 6967f80a1186..a7195c665c14 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:36.846948 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,721 +26,752 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.png index 0317cb99e1c00d2d126a11b341ffe67100702976..c2076843da4aa5e8c399a405e432f818e61b17b9 100644 GIT binary patch literal 2823 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 VBU01<;(}=nVq@uGK{CI3`ybYCRIUI3 literal 3144 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 diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg index 9d57faac5f18..09dd81f56563 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:33.031781 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,297 +26,317 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg index 90f9b2cec969..b6236288603d 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.551077 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,437 +26,467 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.png index d6802f84bfda1b1eb62f0b43e1655f44b1b690b2..5a615d92e166a98248600a46f67c6770f06c47e8 100644 GIT binary patch literal 2822 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 H&N2T6^r6IS diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg index 77ded780c3f1..4d7fdcc30954 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.658346 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,268 +26,288 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - + - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg index 7a7b7ec42c25..e73b1c5e872b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_27.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:39.585869 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,190 +26,202 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - + - - - + - - - + - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg index 0846b552246a..ca0439485b08 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_46.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.642097 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,115 +26,121 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - - - + - - - - - - +M 1381 2969 +Q 1594 3256 1914 3420 +Q 2234 3584 2584 3584 +Q 3122 3584 3439 3221 +Q 3756 2859 3756 2241 +Q 3756 1734 3570 1259 +Q 3384 784 3041 416 +Q 2816 172 2522 40 +Q 2228 -91 1906 -91 +Q 1566 -91 1316 65 +Q 1066 222 909 531 +L 806 0 +L 231 0 +L 1178 4863 +L 1753 4863 +L 1381 2969 +z +" transform="scale(0.015625)"/> + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg index 24db824fd37c..8287a2338258 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_49.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.896681 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,103 +26,109 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - + + - + - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg index 189491319c10..0bbef213526f 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:39.508765 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,209 +26,220 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + + - + - - - + - - + - - - - - - - - - - - - - +M 1959 2075 +Q 2384 2075 2632 2365 +Q 2881 2656 2881 3163 +Q 2881 3666 2632 3958 +Q 2384 4250 1959 4250 +Q 1534 4250 1286 3958 +Q 1038 3666 1038 3163 +Q 1038 2656 1286 2365 +Q 1534 2075 1959 2075 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg index e0721c9e47a4..3e7c6dc1c42c 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:40.642033 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,493 +26,524 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_23.png index b405dd438309c2f44abc7e9ee3bbf115dd84c202..c8bae48383aa5503ea433fd5f57972daf62d20ef 100644 GIT binary patch literal 2853 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 F@L%p)Jz)R< literal 3125 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 h|4(%OSDJW7 - - + + + + + + 2025-07-24T15:42:40.725227 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,278 +26,298 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - + - - - + - - + - - - - - - - - - - - - - - - - - - - - - +M 3022 2063 +Q 3016 2534 2758 2815 +Q 2500 3097 2075 3097 +Q 1594 3097 1305 2825 +Q 1016 2553 972 2059 +L 3022 2063 +z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg index a4fb4be582a4..cd7dfc34183b 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_60.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:42.172241 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,223 +26,234 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - + - - - + - - + - + - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg index 4623754e2963..045cc829e0cf 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.874726 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,531 +26,562 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_23.png index 1923648b80a3d4e1b4098614faa2b8b4fd08c3ad..d0233a4ee7a0136ab93656d81e101a97dfb49807 100644 GIT binary patch literal 2826 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 V?H43i=Rn&AF*iots5ZQv@E=cIIIsW! literal 3135 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 he?&|Fi#1P2Z4VcyJD7HQfj - - + + + + + + 2025-07-24T15:42:38.959357 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,284 +26,304 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - - + - - + - - - + - - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg index d61317816ad6..c3dd8722b044 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_21.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.040182 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,435 +26,466 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.png index a86119004e62ecf23497cd90cf563911bc3679d9..6f816c2ee723ac761b49e407d6a8eea1e088d8df 100644 GIT binary patch literal 2808 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 CAyDc7 literal 3099 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 B_&ERo diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg index 4e129aa6c87d..50bdb38d37b1 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_23.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-24T15:42:38.119948 + image/svg+xml + + + Matplotlib v3.11.0.dev1119+gc6e6904896.d20250724, https://matplotlib.org/ + + + + + - + @@ -15,269 +26,289 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - + - - - + - + - - + - - - + - - + - - - - - - - - - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + From fa62956b9e6b66fe6637e82bcde06e1bc04c6bb7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 16 Jul 2025 05:05:01 -0400 Subject: [PATCH 18/58] Split font opening/closing out of FT2Font constructor/destructor This makes it easier to do later refactors. --- src/ft2font.cpp | 25 ++++++++++++++++++------- src/ft2font.h | 5 +++-- src/ft2font_wrapper.cpp | 4 ++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 1d03ecf10b56..cc8e6f26caff 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -207,9 +206,7 @@ FT2Font::get_path(std::vector &vertices, std::vector &cod codes.push_back(CLOSEPOLY); } -FT2Font::FT2Font(FT_Open_Args &open_args, - long hinting_factor_, - std::vector &fallback_list, +FT2Font::FT2Font(long hinting_factor_, std::vector &fallback_list, FT2Font::WarnFunc warn, bool warn_if_used) : ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr), hinting_factor(hinting_factor_), @@ -217,22 +214,36 @@ FT2Font::FT2Font(FT_Open_Args &open_args, kerning_factor(0) { clear(); + // Set fallbacks + std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); +} + +FT2Font::~FT2Font() +{ + close(); +} + +void FT2Font::open(FT_Open_Args &open_args) +{ FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face); if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } - // Set fallbacks - std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); } -FT2Font::~FT2Font() +void FT2Font::close() { + // This should be idempotent, in case a user manually calls close before the + // destructor does. + for (auto & glyph : glyphs) { FT_Done_Glyph(glyph); } + glyphs.clear(); if (face) { FT_Done_Face(face); + face = nullptr; } } diff --git a/src/ft2font.h b/src/ft2font.h index 0881693e7557..a4443a0cd74d 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -99,10 +99,11 @@ class FT2Font typedef void (*WarnFunc)(FT_ULong charcode, std::set family_names); public: - FT2Font(FT_Open_Args &open_args, long hinting_factor, - std::vector &fallback_list, + FT2Font(long hinting_factor, std::vector &fallback_list, WarnFunc warn, bool warn_if_used); virtual ~FT2Font(); + void open(FT_Open_Args &open_args); + void close(); void clear(); void set_size(double ptsize, double dpi); void set_charmap(int i); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 31202f018e42..3678370b4c3a 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -499,10 +499,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->stream.close = nullptr; } - self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn, + self->x = new FT2Font(hinting_factor, fallback_fonts, ft_glyph_warn, warn_if_used); - self->x->set_kerning_factor(*kerning_factor); + self->x->open(open_args); return self; } From db17bafa6111ff27640fdb3b567be9f8ceb05859 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 17 Jul 2025 01:15:38 -0400 Subject: [PATCH 19/58] Make PyFT2Font a subclass of FT2Font --- src/ft2font.cpp | 26 ++-- src/ft2font.h | 14 +-- src/ft2font_wrapper.cpp | 272 ++++++++++++++-------------------------- 3 files changed, 115 insertions(+), 197 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index cc8e6f26caff..ebb7d5204d80 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -207,15 +207,13 @@ FT2Font::get_path(std::vector &vertices, std::vector &cod } FT2Font::FT2Font(long hinting_factor_, std::vector &fallback_list, - FT2Font::WarnFunc warn, bool warn_if_used) - : ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr), + bool warn_if_used) + : warn_if_used(warn_if_used), image({1, 1}), face(nullptr), fallbacks(fallback_list), hinting_factor(hinting_factor_), // set default kerning factor to 0, i.e., no kerning manipulation kerning_factor(0) { clear(); - // Set fallbacks - std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); } FT2Font::~FT2Font() @@ -234,7 +232,8 @@ void FT2Font::open(FT_Open_Args &open_args) void FT2Font::close() { // This should be idempotent, in case a user manually calls close before the - // destructor does. + // destructor does. Note for example, that PyFT2Font _does_ call this before the + // base destructor to ensure internal pointers are cleared early enough. for (auto & glyph : glyphs) { FT_Done_Glyph(glyph); @@ -544,10 +543,9 @@ FT_UInt FT2Font::get_char_index(FT_ULong charcode, bool fallback = false) return FT_Get_Char_Index(ft_object->get_face(), charcode); } -void FT2Font::get_width_height(long *width, long *height) +std::tuple FT2Font::get_width_height() { - *width = advance; - *height = bbox.yMax - bbox.yMin; + return {advance, bbox.yMax - bbox.yMin}; } long FT2Font::get_descent() @@ -555,10 +553,9 @@ long FT2Font::get_descent() return -bbox.yMin; } -void FT2Font::get_bitmap_offset(long *x, long *y) +std::tuple FT2Font::get_bitmap_offset() { - *x = bbox.xMin; - *y = 0; + return {bbox.xMin, 0}; } void FT2Font::draw_glyphs_to_bitmap(bool antialiased) @@ -607,8 +604,11 @@ void FT2Font::draw_glyph_to_bitmap( draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer) +std::string FT2Font::get_glyph_name(unsigned int glyph_number) { + std::string buffer; + buffer.resize(128); + if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ @@ -625,6 +625,8 @@ void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer) buffer.resize(len); } } + + return buffer; } long FT2Font::get_name_index(char *name) diff --git a/src/ft2font.h b/src/ft2font.h index a4443a0cd74d..80bc490f4bad 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -96,11 +97,9 @@ extern FT_Library _ft2Library; class FT2Font { - typedef void (*WarnFunc)(FT_ULong charcode, std::set family_names); - public: FT2Font(long hinting_factor, std::vector &fallback_list, - WarnFunc warn, bool warn_if_used); + bool warn_if_used); virtual ~FT2Font(); void open(FT_Open_Args &open_args); void close(); @@ -124,14 +123,14 @@ class FT2Font std::set &glyph_seen_fonts, bool override); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); - void get_width_height(long *width, long *height); - void get_bitmap_offset(long *x, long *y); + std::tuple get_width_height(); + std::tuple get_bitmap_offset(); long get_descent(); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap( py::array_t im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, std::string &buffer); + std::string get_glyph_name(unsigned int glyph_number); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); void get_path(std::vector &vertices, std::vector &codes); @@ -167,8 +166,9 @@ class FT2Font return FT_HAS_KERNING(face); } + protected: + virtual void ft_glyph_warn(FT_ULong charcode, std::set family_names) = 0; private: - WarnFunc ft_glyph_warn; bool warn_if_used; py::array_t image; FT_Face face; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 3678370b4c3a..99023836b001 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -331,16 +331,35 @@ PyGlyph_get_bbox(PyGlyph *self) * FT2Font * */ -struct PyFT2Font +class PyFT2Font final : public FT2Font { - FT2Font *x; + public: + using FT2Font::FT2Font; + py::object py_file; FT_StreamRec stream; py::list fallbacks; ~PyFT2Font() { - delete this->x; + // Because destructors are called from subclass up to base class, we need to + // explicitly close the font here. Otherwise, the instance attributes here will + // be destroyed before the font itself, but those are used in the close callback. + close(); + } + + void ft_glyph_warn(FT_ULong charcode, std::set family_names) + { + std::set::iterator it = family_names.begin(); + std::stringstream ss; + ss<<*it; + while(++it != family_names.end()){ + ss<<", "<<*it; + } + + auto text_helpers = py::module_::import("matplotlib._text_helpers"); + auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); + warn_on_missing_glyph(charcode, ss.str()); } }; @@ -402,21 +421,6 @@ close_file_callback(FT_Stream stream) PyErr_Restore(type, value, traceback); } -static void -ft_glyph_warn(FT_ULong charcode, std::set family_names) -{ - std::set::iterator it = family_names.begin(); - std::stringstream ss; - ss<<*it; - while(++it != family_names.end()){ - ss<<", "<<*it; - } - - auto text_helpers = py::module_::import("matplotlib._text_helpers"); - auto warn_on_missing_glyph = text_helpers.attr("warn_on_missing_glyph"); - warn_on_missing_glyph(charcode, ss.str()); -} - const char *PyFT2Font_init__doc__ = R"""( Parameters ---------- @@ -456,8 +460,23 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, kerning_factor = 0; } - PyFT2Font *self = new PyFT2Font(); - self->x = nullptr; + std::vector fallback_fonts; + if (fallback_list) { + // go through fallbacks to add them to our lists + std::copy(fallback_list->begin(), fallback_list->end(), + std::back_inserter(fallback_fonts)); + } + + auto self = new PyFT2Font(hinting_factor, fallback_fonts, warn_if_used); + self->set_kerning_factor(*kerning_factor); + + if (fallback_list) { + // go through fallbacks to add them to our lists + for (auto item : *fallback_list) { + self->fallbacks.append(item); + } + } + memset(&self->stream, 0, sizeof(FT_StreamRec)); self->stream.base = nullptr; self->stream.size = 0x7fffffff; // Unknown size. @@ -469,18 +488,6 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; - std::vector fallback_fonts; - if (fallback_list) { - // go through fallbacks to add them to our lists - for (auto item : *fallback_list) { - self->fallbacks.append(item); - // Also (locally) cache the underlying FT2Font objects. As long as - // the Python objects are kept alive, these pointer are good. - FT2Font *fback = item->x; - fallback_fonts.push_back(fback); - } - } - if (py::isinstance(filename) || py::isinstance(filename)) { self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; @@ -499,10 +506,7 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->stream.close = nullptr; } - self->x = new FT2Font(hinting_factor, fallback_fonts, ft_glyph_warn, - warn_if_used); - self->x->set_kerning_factor(*kerning_factor); - self->x->open(open_args); + self->open(open_args); return self; } @@ -520,12 +524,6 @@ PyFT2Font_fname(PyFT2Font *self) const char *PyFT2Font_clear__doc__ = "Clear all the glyphs, reset for a new call to `.set_text`."; -static void -PyFT2Font_clear(PyFT2Font *self) -{ - self->x->clear(); -} - const char *PyFT2Font_set_size__doc__ = R"""( Set the size of the text. @@ -537,12 +535,6 @@ const char *PyFT2Font_set_size__doc__ = R"""( The DPI used for rendering the text. )"""; -static void -PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) -{ - self->x->set_size(ptsize, dpi); -} - const char *PyFT2Font_set_charmap__doc__ = R"""( Make the i-th charmap current. @@ -561,12 +553,6 @@ const char *PyFT2Font_set_charmap__doc__ = R"""( .get_charmap )"""; -static void -PyFT2Font_set_charmap(PyFT2Font *self, int i) -{ - self->x->set_charmap(i); -} - const char *PyFT2Font_select_charmap__doc__ = R"""( Select a charmap by its FT_Encoding number. @@ -585,12 +571,6 @@ const char *PyFT2Font_select_charmap__doc__ = R"""( .get_charmap )"""; -static void -PyFT2Font_select_charmap(PyFT2Font *self, unsigned long i) -{ - self->x->select_charmap(i); -} - const char *PyFT2Font_get_kerning__doc__ = R"""( Get the kerning between two glyphs. @@ -637,7 +617,7 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, throw py::type_error("mode must be Kerning or int"); } - return self->x->get_kerning(left, right, mode); + return self->get_kerning(left, right, mode); } const char *PyFT2Font_get_fontmap__doc__ = R"""( @@ -671,7 +651,7 @@ PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) py::object target_font; int index; - if (self->x->get_char_fallback_index(code, index)) { + if (self->get_char_fallback_index(code, index)) { if (index >= 0) { target_font = self->fallbacks[index]; } else { @@ -733,7 +713,7 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 throw py::type_error("flags must be LoadFlags or int"); } - self->x->set_text(text, angle, static_cast(flags), xys); + self->set_text(text, angle, static_cast(flags), xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -745,12 +725,6 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 const char *PyFT2Font_get_num_glyphs__doc__ = "Return the number of loaded glyphs."; -static size_t -PyFT2Font_get_num_glyphs(PyFT2Font *self) -{ - return self->x->get_num_glyphs(); -} - const char *PyFT2Font_load_char__doc__ = R"""( Load character in current fontfile and set glyph. @@ -800,7 +774,7 @@ PyFT2Font_load_char(PyFT2Font *self, long charcode, throw py::type_error("flags must be LoadFlags or int"); } - self->x->load_char(charcode, static_cast(flags), ft_object, fallback); + self->load_char(charcode, static_cast(flags), ft_object, fallback); return PyGlyph_from_FT2Font(ft_object); } @@ -852,9 +826,9 @@ PyFT2Font_load_glyph(PyFT2Font *self, FT_UInt glyph_index, throw py::type_error("flags must be LoadFlags or int"); } - self->x->load_glyph(glyph_index, static_cast(flags)); + self->load_glyph(glyph_index, static_cast(flags)); - return PyGlyph_from_FT2Font(self->x); + return PyGlyph_from_FT2Font(self); } const char *PyFT2Font_get_width_height__doc__ = R"""( @@ -874,16 +848,6 @@ const char *PyFT2Font_get_width_height__doc__ = R"""( .get_descent )"""; -static py::tuple -PyFT2Font_get_width_height(PyFT2Font *self) -{ - long width, height; - - self->x->get_width_height(&width, &height); - - return py::make_tuple(width, height); -} - const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( Get the (x, y) offset for the bitmap if ink hangs left or below (0, 0). @@ -901,16 +865,6 @@ const char *PyFT2Font_get_bitmap_offset__doc__ = R"""( .get_descent )"""; -static py::tuple -PyFT2Font_get_bitmap_offset(PyFT2Font *self) -{ - long x, y; - - self->x->get_bitmap_offset(&x, &y); - - return py::make_tuple(x, y); -} - const char *PyFT2Font_get_descent__doc__ = R"""( Get the descent of the current string set by `.set_text`. @@ -928,12 +882,6 @@ const char *PyFT2Font_get_descent__doc__ = R"""( .get_width_height )"""; -static long -PyFT2Font_get_descent(PyFT2Font *self) -{ - return self->x->get_descent(); -} - const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( Draw the glyphs that were loaded by `.set_text` to the bitmap. @@ -949,12 +897,6 @@ const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = R"""( .draw_glyph_to_bitmap )"""; -static void -PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, bool antialiased = true) -{ - self->x->draw_glyphs_to_bitmap(antialiased); -} - const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( Draw a single glyph to the bitmap at pixel locations x, y. @@ -989,7 +931,7 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image, auto xd = _double_to_("x", vxd); auto yd = _double_to_("y", vyd); - self->x->draw_glyph_to_bitmap( + self->draw_glyph_to_bitmap( py::array_t{image}, xd, yd, glyph->glyphInd, antialiased); } @@ -1017,16 +959,6 @@ const char *PyFT2Font_get_glyph_name__doc__ = R"""( .get_name_index )"""; -static py::str -PyFT2Font_get_glyph_name(PyFT2Font *self, unsigned int glyph_number) -{ - std::string buffer; - - buffer.resize(128); - self->x->get_glyph_name(glyph_number, buffer); - return buffer; -} - const char *PyFT2Font_get_charmap__doc__ = R"""( Return a mapping of character codes to glyph indices in the font. @@ -1045,10 +977,10 @@ PyFT2Font_get_charmap(PyFT2Font *self) { py::dict charmap; FT_UInt index; - FT_ULong code = FT_Get_First_Char(self->x->get_face(), &index); + FT_ULong code = FT_Get_First_Char(self->get_face(), &index); while (index != 0) { charmap[py::cast(code)] = py::cast(index); - code = FT_Get_Next_Char(self->x->get_face(), code, &index); + code = FT_Get_Next_Char(self->get_face(), code, &index); } return charmap; } @@ -1060,6 +992,8 @@ const char *PyFT2Font_get_char_index__doc__ = R"""( ---------- codepoint : int A character code point in the current charmap (which defaults to Unicode.) + _fallback : bool + Whether to enable fallback fonts while searching for a character. Returns ------- @@ -1074,14 +1008,6 @@ const char *PyFT2Font_get_char_index__doc__ = R"""( .get_name_index )"""; -static FT_UInt -PyFT2Font_get_char_index(PyFT2Font *self, FT_ULong ccode) -{ - bool fallback = true; - - return self->x->get_char_index(ccode, fallback); -} - const char *PyFT2Font_get_sfnt__doc__ = R"""( Load the entire SFNT names table. @@ -1098,17 +1024,17 @@ const char *PyFT2Font_get_sfnt__doc__ = R"""( static py::dict PyFT2Font_get_sfnt(PyFT2Font *self) { - if (!(self->x->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { + if (!(self->get_face()->face_flags & FT_FACE_FLAG_SFNT)) { throw py::value_error("No SFNT name table"); } - size_t count = FT_Get_Sfnt_Name_Count(self->x->get_face()); + size_t count = FT_Get_Sfnt_Name_Count(self->get_face()); py::dict names; for (FT_UInt j = 0; j < count; ++j) { FT_SfntName sfnt; - FT_Error error = FT_Get_Sfnt_Name(self->x->get_face(), j, &sfnt); + FT_Error error = FT_Get_Sfnt_Name(self->get_face(), j, &sfnt); if (error) { throw py::value_error("Could not get SFNT name"); @@ -1143,12 +1069,6 @@ const char *PyFT2Font_get_name_index__doc__ = R"""( .get_glyph_name )"""; -static long -PyFT2Font_get_name_index(PyFT2Font *self, char *glyphname) -{ - return self->x->get_name_index(glyphname); -} - const char *PyFT2Font_get_ps_font_info__doc__ = R"""( Return the information in the PS Font Info structure. @@ -1173,7 +1093,7 @@ PyFT2Font_get_ps_font_info(PyFT2Font *self) { PS_FontInfoRec fontinfo; - FT_Error error = FT_Get_PS_Font_Info(self->x->get_face(), &fontinfo); + FT_Error error = FT_Get_PS_Font_Info(self->get_face(), &fontinfo); if (error) { throw py::value_error("Could not get PS font info"); } @@ -1225,7 +1145,7 @@ PyFT2Font_get_sfnt_table(PyFT2Font *self, std::string tagname) return std::nullopt; } - void *table = FT_Get_Sfnt_Table(self->x->get_face(), tag); + void *table = FT_Get_Sfnt_Table(self->get_face(), tag); if (!table) { return std::nullopt; } @@ -1408,7 +1328,7 @@ PyFT2Font_get_path(PyFT2Font *self) std::vector vertices; std::vector codes; - self->x->get_path(vertices, codes); + self->get_path(vertices, codes); py::ssize_t length = codes.size(); py::ssize_t vertices_dims[2] = { length, 2 }; @@ -1437,12 +1357,6 @@ const char *PyFT2Font_get_image__doc__ = R"""( .get_path )"""; -static py::array -PyFT2Font_get_image(PyFT2Font *self) -{ - return self->x->get_image(); -} - const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""( Return a list mapping CharString indices of a Type 1 font to FreeType glyph indices. @@ -1454,7 +1368,7 @@ const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""( static std::array PyFT2Font__get_type1_encoding_vector(PyFT2Font *self) { - auto face = self->x->get_face(); + auto face = self->get_face(); auto indices = std::array{}; for (auto i = 0u; i < indices.size(); ++i) { auto len = FT_Get_PS_Font_Value(face, PS_DICT_ENCODING_ENTRY, i, nullptr, 0); @@ -1610,12 +1524,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) "_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(), "_warn_if_used"_a=false, PyFT2Font_init__doc__) - .def("clear", &PyFT2Font_clear, PyFT2Font_clear__doc__) - .def("set_size", &PyFT2Font_set_size, "ptsize"_a, "dpi"_a, + .def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__) + .def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a, PyFT2Font_set_size__doc__) - .def("set_charmap", &PyFT2Font_set_charmap, "i"_a, + .def("set_charmap", &PyFT2Font::set_charmap, "i"_a, PyFT2Font_set_charmap__doc__) - .def("select_charmap", &PyFT2Font_select_charmap, "i"_a, + .def("select_charmap", &PyFT2Font::select_charmap, "i"_a, PyFT2Font_select_charmap__doc__) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) @@ -1624,19 +1538,20 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_set_text__doc__) .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, PyFT2Font_get_fontmap__doc__) - .def("get_num_glyphs", &PyFT2Font_get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) + .def("get_num_glyphs", &PyFT2Font::get_num_glyphs, + PyFT2Font_get_num_glyphs__doc__) .def("load_char", &PyFT2Font_load_char, "charcode"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_char__doc__) .def("load_glyph", &PyFT2Font_load_glyph, "glyph_index"_a, "flags"_a=LoadFlags::FORCE_AUTOHINT, PyFT2Font_load_glyph__doc__) - .def("get_width_height", &PyFT2Font_get_width_height, + .def("get_width_height", &PyFT2Font::get_width_height, PyFT2Font_get_width_height__doc__) - .def("get_bitmap_offset", &PyFT2Font_get_bitmap_offset, + .def("get_bitmap_offset", &PyFT2Font::get_bitmap_offset, PyFT2Font_get_bitmap_offset__doc__) - .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) - .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, + .def("get_descent", &PyFT2Font::get_descent, PyFT2Font_get_descent__doc__) + .def("draw_glyphs_to_bitmap", &PyFT2Font::draw_glyphs_to_bitmap, py::kw_only(), "antialiased"_a=true, PyFT2Font_draw_glyphs_to_bitmap__doc__); // The generated docstring uses an unqualified "Buffer" as type hint, @@ -1652,26 +1567,27 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_draw_glyph_to_bitmap__doc__); } cls - .def("get_glyph_name", &PyFT2Font_get_glyph_name, "index"_a, + .def("get_glyph_name", &PyFT2Font::get_glyph_name, "index"_a, PyFT2Font_get_glyph_name__doc__) .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__) - .def("get_char_index", &PyFT2Font_get_char_index, "codepoint"_a, + .def("get_char_index", &PyFT2Font::get_char_index, + "codepoint"_a, py::kw_only(), "_fallback"_a=true, PyFT2Font_get_char_index__doc__) .def("get_sfnt", &PyFT2Font_get_sfnt, PyFT2Font_get_sfnt__doc__) - .def("get_name_index", &PyFT2Font_get_name_index, "name"_a, + .def("get_name_index", &PyFT2Font::get_name_index, "name"_a, PyFT2Font_get_name_index__doc__) .def("get_ps_font_info", &PyFT2Font_get_ps_font_info, PyFT2Font_get_ps_font_info__doc__) .def("get_sfnt_table", &PyFT2Font_get_sfnt_table, "name"_a, PyFT2Font_get_sfnt_table__doc__) .def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__) - .def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__) + .def("get_image", &PyFT2Font::get_image, PyFT2Font_get_image__doc__) .def("_get_type1_encoding_vector", &PyFT2Font__get_type1_encoding_vector, PyFT2Font__get_type1_encoding_vector__doc__) .def_property_readonly( "postscript_name", [](PyFT2Font *self) { - if (const char *name = FT_Get_Postscript_Name(self->x->get_face())) { + if (const char *name = FT_Get_Postscript_Name(self->get_face())) { return name; } else { return "UNAVAILABLE"; @@ -1679,11 +1595,11 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "PostScript name of the font.") .def_property_readonly( "num_faces", [](PyFT2Font *self) { - return self->x->get_face()->num_faces; + return self->get_face()->num_faces; }, "Number of faces in file.") .def_property_readonly( "family_name", [](PyFT2Font *self) { - if (const char *name = self->x->get_face()->family_name) { + if (const char *name = self->get_face()->family_name) { return name; } else { return "UNAVAILABLE"; @@ -1691,7 +1607,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "Face family name.") .def_property_readonly( "style_name", [](PyFT2Font *self) { - if (const char *name = self->x->get_face()->style_name) { + if (const char *name = self->get_face()->style_name) { return name; } else { return "UNAVAILABLE"; @@ -1699,77 +1615,77 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "Style name.") .def_property_readonly( "face_flags", [](PyFT2Font *self) { - return static_cast(self->x->get_face()->face_flags); + return static_cast(self->get_face()->face_flags); }, "Face flags; see `.FaceFlags`.") .def_property_readonly( "style_flags", [](PyFT2Font *self) { - return static_cast(self->x->get_face()->style_flags & 0xffff); + return static_cast(self->get_face()->style_flags & 0xffff); }, "Style flags; see `.StyleFlags`.") .def_property_readonly( "num_named_instances", [](PyFT2Font *self) { - return (self->x->get_face()->style_flags & 0x7fff0000) >> 16; + return (self->get_face()->style_flags & 0x7fff0000) >> 16; }, "Number of named instances in the face.") .def_property_readonly( "num_glyphs", [](PyFT2Font *self) { - return self->x->get_face()->num_glyphs; + return self->get_face()->num_glyphs; }, "Number of glyphs in the face.") .def_property_readonly( "num_fixed_sizes", [](PyFT2Font *self) { - return self->x->get_face()->num_fixed_sizes; + return self->get_face()->num_fixed_sizes; }, "Number of bitmap in the face.") .def_property_readonly( "num_charmaps", [](PyFT2Font *self) { - return self->x->get_face()->num_charmaps; + return self->get_face()->num_charmaps; }, "Number of charmaps in the face.") .def_property_readonly( "scalable", [](PyFT2Font *self) { - return bool(FT_IS_SCALABLE(self->x->get_face())); + return bool(FT_IS_SCALABLE(self->get_face())); }, "Whether face is scalable; attributes after this one " "are only defined for scalable faces.") .def_property_readonly( "units_per_EM", [](PyFT2Font *self) { - return self->x->get_face()->units_per_EM; + return self->get_face()->units_per_EM; }, "Number of font units covered by the EM.") .def_property_readonly( "bbox", [](PyFT2Font *self) { - FT_BBox bbox = self->x->get_face()->bbox; + FT_BBox bbox = self->get_face()->bbox; return py::make_tuple(bbox.xMin, bbox.yMin, bbox.xMax, bbox.yMax); }, "Face global bounding box (xmin, ymin, xmax, ymax).") .def_property_readonly( "ascender", [](PyFT2Font *self) { - return self->x->get_face()->ascender; + return self->get_face()->ascender; }, "Ascender in 26.6 units.") .def_property_readonly( "descender", [](PyFT2Font *self) { - return self->x->get_face()->descender; + return self->get_face()->descender; }, "Descender in 26.6 units.") .def_property_readonly( "height", [](PyFT2Font *self) { - return self->x->get_face()->height; + return self->get_face()->height; }, "Height in 26.6 units; used to compute a default line spacing " "(baseline-to-baseline distance).") .def_property_readonly( "max_advance_width", [](PyFT2Font *self) { - return self->x->get_face()->max_advance_width; + return self->get_face()->max_advance_width; }, "Maximum horizontal cursor advance for all glyphs.") .def_property_readonly( "max_advance_height", [](PyFT2Font *self) { - return self->x->get_face()->max_advance_height; + return self->get_face()->max_advance_height; }, "Maximum vertical cursor advance for all glyphs.") .def_property_readonly( "underline_position", [](PyFT2Font *self) { - return self->x->get_face()->underline_position; + return self->get_face()->underline_position; }, "Vertical position of the underline bar.") .def_property_readonly( "underline_thickness", [](PyFT2Font *self) { - return self->x->get_face()->underline_thickness; + return self->get_face()->underline_thickness; }, "Thickness of the underline bar.") .def_property_readonly( "fname", &PyFT2Font_fname, "The original filename for this object.") .def_buffer([](PyFT2Font &self) -> py::buffer_info { - return self.x->get_image().request(); + return self.get_image().request(); }); m.attr("__freetype_version__") = version_string; From ad32f0dd9aa8d7c3974ff6d25e2dbabb4f986cc6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 21 Aug 2025 17:18:13 -0400 Subject: [PATCH 20/58] DOC: Fix missing references on text-overhaul branch I thought this was because this branch was missing d2d969ef9d01297728c15c0fdfa957852201834b, but it was actually a change introduced in #30324 as a separate (but similar-looking) issue. --- doc/missing-references.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/missing-references.json b/doc/missing-references.json index 1a3693c990e5..e89439b90483 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -126,10 +126,12 @@ "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" ], "numpy.typing.NDArray": [ + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.get_image:1", "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1" ], "numpy.uint8": [ - ":1" + ":1", + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.get_image:1" ] }, "py:obj": { From 584b1fd01dfe4c60e465220e2aef14966f5e9911 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 26 Aug 2025 04:18:02 -0400 Subject: [PATCH 21/58] Remove version from FreeType wrap file Latest Meson (1.9.0 at this time) is now erroring because of the Harfbuzz contains a differently-named wrap file that provides `freetype2`. This may be a bug upstream, but we really don't need the version in the filename. --- extern/meson.build | 8 +------- lib/matplotlib/__init__.py | 2 +- subprojects/{freetype-2.13.3.wrap => freetype2.wrap} | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) rename subprojects/{freetype-2.13.3.wrap => freetype2.wrap} (69%) diff --git a/extern/meson.build b/extern/meson.build index 7f7c2511c3d5..f4e14530369d 100644 --- a/extern/meson.build +++ b/extern/meson.build @@ -9,14 +9,8 @@ subdir('agg24-svn') if get_option('system-freetype') freetype_dep = dependency('freetype2', version: '>=9.11.3') else - # This is the version of FreeType to use when building a local version. It - # must match the value in `lib/matplotlib.__init__.py`. Also update the docs - # in `docs/devel/dependencies.rst`. Bump the cache key in - # `.circleci/config.yml` when changing requirements. - LOCAL_FREETYPE_VERSION = '2.13.3' - freetype_proj = subproject( - f'freetype-@LOCAL_FREETYPE_VERSION@', + 'freetype2', default_options: [ 'default_library=static', 'brotli=disabled', diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 651936dc19c2..dc6b703e942a 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1333,7 +1333,7 @@ def _val_or_rc(val, *rc_names): def _init_tests(): # The version of FreeType to install locally for running the tests. This must match - # the value in `meson.build`. + # the value in `subprojects/freetype2.wrap`. LOCAL_FREETYPE_VERSION = '2.13.3' from matplotlib import ft2font diff --git a/subprojects/freetype-2.13.3.wrap b/subprojects/freetype2.wrap similarity index 69% rename from subprojects/freetype-2.13.3.wrap rename to subprojects/freetype2.wrap index 68f688a35861..e1d0fb112ca9 100644 --- a/subprojects/freetype-2.13.3.wrap +++ b/subprojects/freetype2.wrap @@ -1,3 +1,7 @@ +# This is the version of FreeType to use when building a local version. It +# must match the value in `lib/matplotlib.__init__.py`. Also update the docs +# in `docs/devel/dependencies.rst`. Bump the cache key in +# `.circleci/config.yml` when changing requirements. [wrap-file] directory = freetype-2.13.3 source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz From 0635d3ab2445d2e8ea1a16a3882835b5b950e7a6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 18 Dec 2024 22:04:51 -0500 Subject: [PATCH 22/58] Add libraqm and its dependencies to build Also add some missing license entries in more places. --- LICENSE/LICENSE_FREETYPE | 46 ++++ LICENSE/LICENSE_HARFBUZZ | 42 ++++ LICENSE/LICENSE_LIBRAQM | 22 ++ LICENSE/LICENSE_SHEENBIDI | 202 ++++++++++++++++++ doc/project/license.rst | 32 +++ extern/meson.build | 29 +++ lib/matplotlib/ft2font.pyi | 1 + meson.build | 8 +- meson.options | 2 + src/ft2font.h | 2 + src/ft2font_wrapper.cpp | 1 + src/meson.build | 2 +- subprojects/harfbuzz.wrap | 13 ++ subprojects/libraqm-0.10.3.wrap | 8 + .../harfbuzz-11.2.0-bundle-freetype.patch | 36 ++++ .../libraqm-0.10.2-bundle-freetype.patch | 11 + subprojects/packagefiles/libraqm-203.patch | 27 +++ subprojects/sheenbidi.wrap | 5 + 18 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 LICENSE/LICENSE_FREETYPE create mode 100644 LICENSE/LICENSE_HARFBUZZ create mode 100644 LICENSE/LICENSE_LIBRAQM create mode 100755 LICENSE/LICENSE_SHEENBIDI create mode 100644 subprojects/harfbuzz.wrap create mode 100644 subprojects/libraqm-0.10.3.wrap create mode 100644 subprojects/packagefiles/harfbuzz-11.2.0-bundle-freetype.patch create mode 100644 subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch create mode 100644 subprojects/packagefiles/libraqm-203.patch create mode 100644 subprojects/sheenbidi.wrap diff --git a/LICENSE/LICENSE_FREETYPE b/LICENSE/LICENSE_FREETYPE new file mode 100644 index 000000000000..8b9ce9e2e6e3 --- /dev/null +++ b/LICENSE/LICENSE_FREETYPE @@ -0,0 +1,46 @@ +FREETYPE LICENSES +----------------- + +The FreeType 2 font engine is copyrighted work and cannot be used +legally without a software license. In order to make this project +usable to a vast majority of developers, we distribute it under two +mutually exclusive open-source licenses. + +This means that *you* must choose *one* of the two licenses described +below, then obey all its terms and conditions when using FreeType 2 in +any of your projects or products. + + - The FreeType License, found in the file `docs/FTL.TXT`, which is + similar to the original BSD license *with* an advertising clause + that forces you to explicitly cite the FreeType project in your + product's documentation. All details are in the license file. + This license is suited to products which don't use the GNU General + Public License. + + Note that this license is compatible to the GNU General Public + License version 3, but not version 2. + + - The GNU General Public License version 2, found in + `docs/GPLv2.TXT` (any later version can be used also), for + programs which already use the GPL. Note that the FTL is + incompatible with GPLv2 due to its advertisement clause. + +The contributed BDF and PCF drivers come with a license similar to +that of the X Window System. It is compatible to the above two +licenses (see files `src/bdf/README` and `src/pcf/README`). The same +holds for the source code files `src/base/fthash.c` and +`include/freetype/internal/fthash.h`; they were part of the BDF driver +in earlier FreeType versions. + +The gzip module uses the zlib license (see `src/gzip/zlib.h`) which +too is compatible to the above two licenses. + +The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code +taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses +the 'Old MIT' license, compatible to the above two licenses. + +The MD5 checksum support (only used for debugging in development +builds) is in the public domain. + + +--- end of LICENSE.TXT --- diff --git a/LICENSE/LICENSE_HARFBUZZ b/LICENSE/LICENSE_HARFBUZZ new file mode 100644 index 000000000000..1dd917e9f2e7 --- /dev/null +++ b/LICENSE/LICENSE_HARFBUZZ @@ -0,0 +1,42 @@ +HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. +For parts of HarfBuzz that are licensed under different licenses see individual +files names COPYING in subdirectories where applicable. + +Copyright © 2010-2022 Google, Inc. +Copyright © 2015-2020 Ebrahim Byagowi +Copyright © 2019,2020 Facebook, Inc. +Copyright © 2012,2015 Mozilla Foundation +Copyright © 2011 Codethink Limited +Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) +Copyright © 2009 Keith Stribley +Copyright © 2011 Martin Hosken and SIL International +Copyright © 2007 Chris Wilson +Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod +Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. +Copyright © 1998-2005 David Turner and Werner Lemberg +Copyright © 2016 Igalia S.L. +Copyright © 2022 Matthias Clasen +Copyright © 2018,2021 Khaled Hosny +Copyright © 2018,2019,2020 Adobe, Inc +Copyright © 2013-2015 Alexei Podtelezhnikov + +For full copyright notices consult the individual files in the package. + + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/LICENSE/LICENSE_LIBRAQM b/LICENSE/LICENSE_LIBRAQM new file mode 100644 index 000000000000..97e2489b7798 --- /dev/null +++ b/LICENSE/LICENSE_LIBRAQM @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright © 2015 Information Technology Authority (ITA) +Copyright © 2016-2023 Khaled Hosny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE/LICENSE_SHEENBIDI b/LICENSE/LICENSE_SHEENBIDI new file mode 100755 index 000000000000..d64569567334 --- /dev/null +++ b/LICENSE/LICENSE_SHEENBIDI @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/doc/project/license.rst b/doc/project/license.rst index eba9ef23cf62..2cad3f25b95d 100644 --- a/doc/project/license.rst +++ b/doc/project/license.rst @@ -71,6 +71,38 @@ Bundled software .. literalinclude:: ../../LICENSE/LICENSE_QT4_EDITOR :language: none +Rendering software +------------------ + +.. dropdown:: Agg + :class-container: sdd + + .. literalinclude:: ../../extern/agg24-svn/src/copying + :language: none + +.. dropdown:: FreeType + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_FREETYPE + :language: none + +.. dropdown:: Harfbuzz + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_HARFBUZZ + :language: none + +.. dropdown:: libraqm + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_LIBRAQM + :language: none + +.. dropdown:: SheenBidi + :class-container: sdd + + .. literalinclude:: ../../LICENSE/LICENSE_SHEENBIDI + :language: none .. _licenses-cmaps-styles: diff --git a/extern/meson.build b/extern/meson.build index f4e14530369d..2723baa47505 100644 --- a/extern/meson.build +++ b/extern/meson.build @@ -24,6 +24,35 @@ else freetype_dep = freetype_proj.get_variable('freetype_dep') endif +if get_option('system-libraqm') + libraqm_dep = dependency('raqm', version: '>=0.10.3') +else + subproject('harfbuzz', + default_options: [ + 'default_library=static', + 'cairo=disabled', + 'coretext=disabled', + 'directwrite=disabled', + 'fontations=disabled', + 'freetype=enabled', + 'gdi=disabled', + 'glib=disabled', + 'gobject=disabled', + 'harfruzz=disabled', + 'icu=disabled', + 'tests=disabled', + ] + ) + subproject('sheenbidi', default_options: ['default_library=static']) + libraqm_proj = subproject('libraqm-0.10.3', + default_options: [ + 'default_library=static', + 'sheenbidi=true', + ] + ) + libraqm_dep = libraqm_proj.get_variable('libraqm_dep') +endif + if get_option('system-qhull') qhull_dep = dependency('qhull_r', version: '>=8.0.2', required: false) if not qhull_dep.found() diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index a4ddc84358c1..55c076bb68b6 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -8,6 +8,7 @@ from numpy.typing import NDArray __freetype_build_type__: str __freetype_version__: str +__libraqm_version__: str # We can't change the type hints for standard library chr/ord, so character codes are a # simple type alias. diff --git a/meson.build b/meson.build index 54249473fe8e..239ae7827b73 100644 --- a/meson.build +++ b/meson.build @@ -7,18 +7,24 @@ project( '-m', 'setuptools_scm', check: true).stdout().strip(), # qt_editor backend is MIT # ResizeObserver at end of lib/matplotlib/backends/web_backend/js/mpl.js is CC0 - # Carlogo, STIX and Computer Modern is OFL + # Carlogo, STIX, Computer Modern, and Last Resort are OFL # DejaVu is Bitstream Vera and Public Domain license: 'PSF-2.0 AND MIT AND CC0-1.0 AND OFL-1.1 AND Bitstream-Vera AND Public-Domain', license_files: [ 'LICENSE/LICENSE', + 'extern/agg24-svn/src/copying', 'LICENSE/LICENSE_AMSFONTS', 'LICENSE/LICENSE_BAKOMA', 'LICENSE/LICENSE_CARLOGO', 'LICENSE/LICENSE_COLORBREWER', 'LICENSE/LICENSE_COURIERTEN', + 'LICENSE/LICENSE_FREETYPE', + 'LICENSE/LICENSE_HARFBUZZ', 'LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER', + 'LICENSE/LICENSE_LAST_RESORT_FONT', + 'LICENSE/LICENSE_LIBRAQM', 'LICENSE/LICENSE_QT4_EDITOR', + 'LICENSE/LICENSE_SHEENBIDI', 'LICENSE/LICENSE_SOLARIZED', 'LICENSE/LICENSE_STIX', 'LICENSE/LICENSE_YORICK', diff --git a/meson.options b/meson.options index d21cbedb9bb9..7e03ff405f85 100644 --- a/meson.options +++ b/meson.options @@ -7,6 +7,8 @@ # FreeType on AIX. option('system-freetype', type: 'boolean', value: false, description: 'Build against system version of FreeType') +option('system-libraqm', type: 'boolean', value: false, + description: 'Build against system version of libraqm') option('system-qhull', type: 'boolean', value: false, description: 'Build against system version of Qhull') diff --git a/src/ft2font.h b/src/ft2font.h index 80bc490f4bad..ffaf511ab9ca 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -26,6 +26,8 @@ extern "C" { #include FT_TRUETYPE_TABLES_H } +#include + namespace py = pybind11; // By definition, FT_FIXED as 2 16bit values stored in a single long. diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 99023836b001..65fcb4b7e013 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1690,6 +1690,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE; + m.attr("__libraqm_version__") = raqm_version_string(); auto py_int = py::module_::import("builtins").attr("int"); m.attr("CharacterCodeType") = py_int; m.attr("GlyphIndexType") = py_int; diff --git a/src/meson.build b/src/meson.build index d479a8b84aa2..8b52bf739c03 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,7 +53,7 @@ extension_data = { 'ft2font_wrapper.cpp', ), 'dependencies': [ - freetype_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, libraqm_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( diff --git a/subprojects/harfbuzz.wrap b/subprojects/harfbuzz.wrap new file mode 100644 index 000000000000..cc5e227f0ca2 --- /dev/null +++ b/subprojects/harfbuzz.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = harfbuzz-11.2.1 +source_url = https://github.com/harfbuzz/harfbuzz/releases/download/11.2.1/harfbuzz-11.2.1.tar.xz +source_filename = harfbuzz-11.2.1.tar.xz +source_hash = 093714c8548a285094685f0bdc999e202d666b59eeb3df2ff921ab68b8336a49 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/harfbuzz_11.2.1-1/harfbuzz-11.2.1.tar.xz +wrapdb_version = 11.2.1-1 + +# This patch allows using our bundled FreeType. +diff_files = harfbuzz-11.2.0-bundle-freetype.patch + +[provide] +dependency_names = harfbuzz, harfbuzz-cairo, harfbuzz-gobject, harfbuzz-icu, harfbuzz-subset diff --git a/subprojects/libraqm-0.10.3.wrap b/subprojects/libraqm-0.10.3.wrap new file mode 100644 index 000000000000..87061a231cba --- /dev/null +++ b/subprojects/libraqm-0.10.3.wrap @@ -0,0 +1,8 @@ +[wrap-file] +source_url = https://github.com/HOST-Oman/libraqm/archive/v0.10.3/libraqm-0.10.3.tar.gz +source_filename = libraqm-0.10.3.tar.gz +source_hash = fe1fe28b32f97ef97b325ca5d2defb0704da1ef048372ec20e85e1f587e20965 + +# First patch allows using our bundled FreeType. +# Second patch is for use as a subproject https://github.com/HOST-Oman/libraqm/pull/203 +diff_files = libraqm-0.10.2-bundle-freetype.patch, libraqm-203.patch diff --git a/subprojects/packagefiles/harfbuzz-11.2.0-bundle-freetype.patch b/subprojects/packagefiles/harfbuzz-11.2.0-bundle-freetype.patch new file mode 100644 index 000000000000..fa7be0b54afd --- /dev/null +++ b/subprojects/packagefiles/harfbuzz-11.2.0-bundle-freetype.patch @@ -0,0 +1,36 @@ +diff -uPNr harfbuzz-11.2.0.orig/meson.build harfbuzz-11.2.0/meson.build +--- harfbuzz-11.2.0.orig/meson.build 2025-04-28 08:56:32.000000000 -0400 ++++ harfbuzz-11.2.0/meson.build 2025-05-03 03:25:39.602646412 -0400 +@@ -115,31 +115,7 @@ + # Sadly, FreeType's versioning schemes are different between pkg-config and CMake + + # Try pkg-config name +- freetype_dep = dependency('freetype2', +- version: freetype_min_version, +- method: 'pkg-config', +- required: false, +- allow_fallback: false) +- if not freetype_dep.found() +- # Try cmake name +- freetype_dep = dependency('Freetype', +- version: freetype_min_version_actual, +- method: 'cmake', +- required: false, +- allow_fallback: false) +- # Subproject fallback +- if not freetype_dep.found() +- freetype_proj = subproject('freetype2', +- version: freetype_min_version_actual, +- required: get_option('freetype'), +- default_options: ['harfbuzz=disabled']) +- if freetype_proj.found() +- freetype_dep = freetype_proj.get_variable('freetype_dep') +- else +- freetype_dep = dependency('', required: false) +- endif +- endif +- endif ++ freetype_dep = dependency('freetype2', version: freetype_min_version) + endif + + glib_dep = dependency('glib-2.0', version: glib_min_version, required: get_option('glib')) diff --git a/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch b/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch new file mode 100644 index 000000000000..5e9a6b7f9ed5 --- /dev/null +++ b/subprojects/packagefiles/libraqm-0.10.2-bundle-freetype.patch @@ -0,0 +1,11 @@ +--- a/meson.build 2025-03-26 03:32:12.444735795 -0400 ++++ b/meson.build 2025-03-26 03:32:16.117435140 -0400 +@@ -45,8 +45,7 @@ + if not freetype.found() + freetype = dependency( + 'freetype2', + version: '>= @0@'.format(freetype_version[0]), +- method: 'pkg-config', + fallback: ['freetype2', 'freetype_dep'], + default_options: [ + 'png=disabled', diff --git a/subprojects/packagefiles/libraqm-203.patch b/subprojects/packagefiles/libraqm-203.patch new file mode 100644 index 000000000000..6628fec1d111 --- /dev/null +++ b/subprojects/packagefiles/libraqm-203.patch @@ -0,0 +1,27 @@ +From 8cedfc989998bb2cf23c2c1b40802effad72b0ed Mon Sep 17 00:00:00 2001 +From: Elliott Sales de Andrade +Date: Thu, 7 Aug 2025 18:07:15 -0400 +Subject: [PATCH] Add dependency override for use as a subproject + +--- + src/meson.build | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/src/meson.build b/src/meson.build +index 0a32f832..ca7c13d1 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -42,6 +42,13 @@ libraqm = library( + install: true, + ) + ++libraqm_dep = declare_dependency( ++ include_directories: include_directories('.'), ++ link_with: libraqm, ++) ++ ++meson.override_dependency(meson.project_name(), libraqm_dep) ++ + libraqm_test = static_library( + 'raqm-test', + 'raqm.c', diff --git a/subprojects/sheenbidi.wrap b/subprojects/sheenbidi.wrap new file mode 100644 index 000000000000..c58277d47499 --- /dev/null +++ b/subprojects/sheenbidi.wrap @@ -0,0 +1,5 @@ +[wrap-file] +directory = SheenBidi-2.9.0 +source_url = https://github.com/Tehreer/SheenBidi/archive/refs/tags/v2.9.0/sheenbidi-2.9.0.tar.gz +source_filename = sheenbidi-2.9.0.tar.gz +source_hash = e90ae142c6fc8b94366f3526f84b349a2c10137f87093db402fe51f6eace6d13 From b0ded3aadda70932fa2130df61ed9629cd7d54e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 26 Feb 2025 09:27:53 -0500 Subject: [PATCH 23/58] Implement text shaping with libraqm --- lib/matplotlib/_text_helpers.py | 17 ----- lib/matplotlib/tests/test_ft2font.py | 6 +- lib/matplotlib/tests/test_text.py | 24 +++---- src/ft2font.cpp | 97 ++++++++++++++++++---------- 4 files changed, 78 insertions(+), 66 deletions(-) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index 1a9b4e4c989c..a874c8f4bf81 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -25,23 +25,6 @@ def warn_on_missing_glyph(codepoint, fontnames): f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) " f"missing from font(s) {fontnames}.") - block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else - "Arabic" if 0x0600 <= codepoint <= 0x06ff else - "Devanagari" if 0x0900 <= codepoint <= 0x097f else - "Bengali" if 0x0980 <= codepoint <= 0x09ff else - "Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else - "Gujarati" if 0x0a80 <= codepoint <= 0x0aff else - "Oriya" if 0x0b00 <= codepoint <= 0x0b7f else - "Tamil" if 0x0b80 <= codepoint <= 0x0bff else - "Telugu" if 0x0c00 <= codepoint <= 0x0c7f else - "Kannada" if 0x0c80 <= codepoint <= 0x0cff else - "Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else - "Sinhala" if 0x0d80 <= codepoint <= 0x0dff else - None) - if block: - _api.warn_external( - f"Matplotlib currently does not support {block} natively.") - def layout(string, font, *, kern_mode=Kerning.DEFAULT): """ diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 6b405287e5d7..70e611e17bcc 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -775,9 +775,9 @@ def test_ft2font_set_text(): xys = font.set_text('AADAT.XC-J') np.testing.assert_array_equal( xys, - [(0, 0), (512, 0), (1024, 0), (1600, 0), (2112, 0), (2496, 0), (2688, 0), - (3200, 0), (3712, 0), (4032, 0)]) - assert font.get_width_height() == (4288, 768) + [(0, 0), (533, 0), (1045, 0), (1608, 0), (2060, 0), (2417, 0), (2609, 0), + (3065, 0), (3577, 0), (3940, 0)]) + assert font.get_width_height() == (4196, 768) assert font.get_num_glyphs() == 10 assert font.get_descent() == 192 assert font.get_bitmap_offset() == (6, 0) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 9d943fa9df13..bdf7ce72a2df 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -113,6 +113,18 @@ def find_matplotlib_font(**kw): ax.set_yticks([]) +@image_comparison(['complex.png']) +def test_complex_shaping(): + # Raqm is Arabic for writing; note that because Arabic is RTL, the characters here + # may seem to be in a different order than expected, but libraqm will order them + # correctly for us. + text = ( + 'Arabic: \N{Arabic Letter REH}\N{Arabic FATHA}\N{Arabic Letter QAF}' + '\N{Arabic SUKUN}\N{Arabic Letter MEEM}') + fig = plt.figure(figsize=(3, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + + @image_comparison(['multiline']) def test_multiline(): plt.figure() @@ -826,18 +838,6 @@ def test_pdf_kerning(): plt.figtext(0.1, 0.5, "ATATATATATATATATATA", size=30) -def test_unsupported_script(recwarn): - fig = plt.figure() - t = fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}") - fig.canvas.draw() - assert all(isinstance(warn.message, UserWarning) for warn in recwarn) - assert ( - [warn.message.args for warn in recwarn] == - [(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from font(s) " - + f"{t.get_fontname()}.",), - (r"Matplotlib currently does not support Bengali natively.",)]) - - # See gh-26152 for more information on this xfail @pytest.mark.xfail(pyparsing_version.release == (3, 1, 0), reason="Error messages are incorrect with pyparsing 3.1.0") diff --git a/src/ft2font.cpp b/src/ft2font.cpp index ebb7d5204d80..22199a0fd19b 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -227,6 +227,11 @@ void FT2Font::open(FT_Open_Args &open_args) if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } + + // This allows us to get back to our data if we need it, though it makes a pointer + // loop, so don't set a free-function for it. + face->generic.data = this; + face->generic.finalizer = nullptr; } void FT2Font::close() @@ -333,48 +338,69 @@ void FT2Font::set_text( bbox.xMin = bbox.yMin = 32000; bbox.xMax = bbox.yMax = -32000; - FT_UInt previous = 0; - FT2Font *previous_ft_object = nullptr; + auto rq = raqm_create(); + if (!rq) { + throw std::runtime_error("failed to compute text layout"); + } + [[maybe_unused]] auto const& rq_cleanup = + std::unique_ptr, decltype(&raqm_destroy)>( + rq, raqm_destroy); + + if (!raqm_set_text(rq, reinterpret_cast(text.data()), + text.size())) + { + throw std::runtime_error("failed to set text for layout"); + } + if (!raqm_set_freetype_face(rq, face)) { + throw std::runtime_error("failed to set text face for layout"); + } + if (!raqm_set_freetype_load_flags(rq, flags)) { + throw std::runtime_error("failed to set text flags for layout"); + } + if (!raqm_layout(rq)) { + throw std::runtime_error("failed to layout text"); + } - for (auto codepoint : text) { - FT_UInt glyph_index = 0; - FT_BBox glyph_bbox; - FT_Pos last_advance; + std::set glyph_seen_fonts; + glyph_seen_fonts.insert(face->family_name); - FT_Error charcode_error, glyph_error; - std::set glyph_seen_fonts; - FT2Font *ft_object_with_glyph = this; - bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, - char_to_font, codepoint, flags, - charcode_error, glyph_error, glyph_seen_fonts, false); - if (!was_found) { - ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); - // render missing glyph tofu - // come back to top-most font - ft_object_with_glyph = this; - char_to_font[codepoint] = ft_object_with_glyph; - ft_object_with_glyph->load_glyph(glyph_index, flags); - } else if (ft_object_with_glyph->warn_if_used) { - ft_glyph_warn((FT_ULong)codepoint, glyph_seen_fonts); - } + size_t num_glyphs = 0; + auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); - // retrieve kerning distance and move pen position - if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same - ft_object_with_glyph->has_kerning() && // if the font knows how to kern - previous && glyph_index // and we really have 2 glyphs - ) { - pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT); + for (size_t i = 0; i < num_glyphs; i++) { + auto const& rglyph = rq_glyphs[i]; + + // Warn for missing glyphs. + if (rglyph.index == 0) { + ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts); + continue; + } + FT2Font *wrapped_font = static_cast(rglyph.ftface->generic.data); + if (wrapped_font->warn_if_used) { + ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts); } // extract glyph image and store it in our table - FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1]; + FT_Error error; + error = FT_Load_Glyph(rglyph.ftface, rglyph.index, flags); + if (error) { + throw std::runtime_error("failed to load glyph"); + } + FT_Glyph thisGlyph; + error = FT_Get_Glyph(rglyph.ftface->glyph, &thisGlyph); + if (error) { + throw std::runtime_error("failed to get glyph"); + } + + pen.x += rglyph.x_offset; + pen.y += rglyph.y_offset; - last_advance = ft_object_with_glyph->get_face()->glyph->advance.x; FT_Glyph_Transform(thisGlyph, nullptr, &pen); FT_Glyph_Transform(thisGlyph, &matrix, nullptr); xys.push_back(pen.x); xys.push_back(pen.y); + FT_BBox glyph_bbox; FT_Glyph_Get_CBox(thisGlyph, FT_GLYPH_BBOX_SUBPIXELS, &glyph_bbox); bbox.xMin = std::min(bbox.xMin, glyph_bbox.xMin); @@ -382,11 +408,14 @@ void FT2Font::set_text( bbox.yMin = std::min(bbox.yMin, glyph_bbox.yMin); bbox.yMax = std::max(bbox.yMax, glyph_bbox.yMax); - pen.x += last_advance; - - previous = glyph_index; - previous_ft_object = ft_object_with_glyph; + if ((flags & FT_LOAD_NO_HINTING) != 0) { + pen.x += rglyph.x_advance - rglyph.x_offset; + } else { + pen.x += hinting_factor * rglyph.x_advance - rglyph.x_offset; + } + pen.y += rglyph.y_advance - rglyph.y_offset; + glyphs.push_back(thisGlyph); } FT_Vector_Transform(&pen, &matrix); From 98135232085af3c5585e4d7a077318f3d511306d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 3 Apr 2025 04:09:06 -0400 Subject: [PATCH 24/58] Implement font fallback for libraqm --- lib/matplotlib/tests/test_text.py | 12 +++++- src/ft2font.cpp | 63 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index bdf7ce72a2df..8dba63eeef32 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -121,8 +121,16 @@ def test_complex_shaping(): text = ( 'Arabic: \N{Arabic Letter REH}\N{Arabic FATHA}\N{Arabic Letter QAF}' '\N{Arabic SUKUN}\N{Arabic Letter MEEM}') - fig = plt.figure(figsize=(3, 1)) - fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + math_signs = '\N{N-ary Product}\N{N-ary Coproduct}\N{N-ary summation}\N{Integral}' + text = math_signs + text + math_signs + fig = plt.figure(figsize=(6, 2)) + fig.text(0.5, 0.75, text, size=32, ha='center', va='center') + # Also check fallback behaviour: + # - English should use cmr10 + # - Math signs should use DejaVu Sans Display (and thus be larger than the rest) + # - Arabic should use DejaVu Sans + fig.text(0.5, 0.25, text, size=32, ha='center', va='center', + family=['cmr10', 'DejaVu Sans Display', 'DejaVu Sans']) @image_comparison(['multiline']) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 22199a0fd19b..890fc61974b0 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -361,9 +362,71 @@ void FT2Font::set_text( throw std::runtime_error("failed to layout text"); } + std::vector> face_substitutions; std::set glyph_seen_fonts; glyph_seen_fonts.insert(face->family_name); + // Attempt to use fallback fonts if necessary. + for (auto const& fallback : fallbacks) { + size_t num_glyphs = 0; + auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); + bool new_fallback_used = false; + + // Sort clusters (n.b. std::map is ordered), as RTL text will be returned in + // display, not source, order. + std::map cluster_missing; + for (size_t i = 0; i < num_glyphs; i++) { + auto const& rglyph = rq_glyphs[i]; + + // Sometimes multiple glyphs are necessary for a single cluster; if any are + // not found, we want to "poison" the whole set and keep them missing. + cluster_missing[rglyph.cluster] |= (rglyph.index == 0); + } + + for (auto it = cluster_missing.cbegin(); it != cluster_missing.cend(); ) { + auto [cluster, missing] = *it; + ++it; // Early change so we can access the next cluster below. + if (missing) { + auto next = (it != cluster_missing.cend()) ? it->first : text.size(); + for (auto i = cluster; i < next; i++) { + face_substitutions.emplace_back(i, fallback->face); + } + new_fallback_used = true; + } + } + + if (!new_fallback_used) { + // If we never used a fallback, then we're good to go with the existing + // layout we have already made. + break; + } + + // If a fallback was used, then re-attempt the layout with the new fonts. + if (!fallback->warn_if_used) { + glyph_seen_fonts.insert(fallback->face->family_name); + } + + raqm_clear_contents(rq); + if (!raqm_set_text(rq, + reinterpret_cast(text.data()), + text.size())) + { + throw std::runtime_error("failed to set text for layout"); + } + if (!raqm_set_freetype_face(rq, face)) { + throw std::runtime_error("failed to set text face for layout"); + } + for (auto [cluster, fallback] : face_substitutions) { + raqm_set_freetype_face_range(rq, fallback, cluster, 1); + } + if (!raqm_set_freetype_load_flags(rq, flags)) { + throw std::runtime_error("failed to set text flags for layout"); + } + if (!raqm_layout(rq)) { + throw std::runtime_error("failed to layout text"); + } + } + size_t num_glyphs = 0; auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); From b36b97f3da663f65584203c8c1051e653e882338 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 Aug 2025 03:05:02 -0400 Subject: [PATCH 25/58] DOC: Add What's New entry for complex text layout --- doc/release/next_whats_new/libraqm.rst | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 doc/release/next_whats_new/libraqm.rst diff --git a/doc/release/next_whats_new/libraqm.rst b/doc/release/next_whats_new/libraqm.rst new file mode 100644 index 000000000000..8312f2f9432c --- /dev/null +++ b/doc/release/next_whats_new/libraqm.rst @@ -0,0 +1,42 @@ +Complex text layout with libraqm +-------------------------------- + +Text support has been extended to include complex text layout. This support includes: + +1. Languages that require advanced layout, such as Arabic or Hebrew. +2. Text that mixes left-to-right and right-to-left languages. + + .. plot:: + :show-source-link: False + + text = 'Here is some رَقْم in اَلْعَرَبِيَّةُ' + fig = plt.figure(figsize=(6, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + +3. Ligatures that combine several adjacent characters for improved legibility. + + .. plot:: + :show-source-link: False + + text = 'f\N{Hair Space}f\N{Hair Space}i \N{Rightwards Arrow} ffi' + fig = plt.figure(figsize=(3, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center') + +4. Combining multiple or double-width diacritics. + + .. plot:: + :show-source-link: False + + text = ( + 'a\N{Combining Circumflex Accent}\N{Combining Double Tilde}' + 'c\N{Combining Diaeresis}') + text = ' + '.join( + c if c in 'ac' else f'\N{Dotted Circle}{c}' + for c in text) + f' \N{Rightwards Arrow} {text}' + fig = plt.figure(figsize=(6, 1)) + fig.text(0.5, 0.5, text, size=32, ha='center', va='center', + # Builtin DejaVu Sans doesn't support multiple diacritics. + family=['Noto Sans', 'DejaVu Sans']) + +Note, all advanced features require corresponding font support, and may require +additional fonts over the builtin DejaVu Sans. From 04c8eefb7e42029e0194d84f3cb30fc5e31df061 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 26 Aug 2025 00:10:47 -0400 Subject: [PATCH 26/58] ci: Ignore coverage data from subprojects and generated files We only care about our own source files, so anything in `subprojects` or the `build` directory can be ignored, thus fixing the bugginess with Harfbuzz headers. --- .github/workflows/tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 343d497a4696..b2ba198589ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -397,13 +397,14 @@ jobs: if [[ "${{ runner.os }}" != 'macOS' ]]; then LCOV_IGNORE_ERRORS=',' # do not ignore any lcov errors by default if [[ "${{ matrix.os }}" = ubuntu-24.04 ]]; then - # filter mismatch and unused-entity errors detected by lcov 2.x - LCOV_IGNORE_ERRORS='mismatch,unused' + # filter mismatch errors detected by lcov 2.x + LCOV_IGNORE_ERRORS='mismatch' fi lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ - --capture --directory . --output-file coverage.info + --capture --directory . --exclude $PWD/subprojects --exclude $PWD/build \ + --output-file coverage.info lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ - --output-file coverage.info --extract coverage.info $PWD/src/'*' $PWD/lib/'*' + --output-file coverage.info --extract coverage.info $PWD/src/'*' lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ --list coverage.info find . -name '*.gc*' -delete From 258de53f55f0306400d7271a4c302cd37ab530de Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 30 Aug 2025 06:38:17 -0400 Subject: [PATCH 27/58] pdf: Simplify Type 3 font character encoding For a Type 3 font, its encoding is entirely defined by its `Encoding` dictionary (which we create), so there's no reason to use a specific encoding like `cp1252`. Instead, switch to Latin-1, which corresponds exactly to the first 256 character codes in Unicode, and can be mapped directly with `ord`. --- lib/matplotlib/backends/backend_pdf.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index ff351e301176..6682deffb00e 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1180,13 +1180,11 @@ def embedTTFType3(font, characters, descriptor): 'Widths': widthsObject } - from encodings import cp1252 - # Make the "Widths" array def get_char_width(charcode): - s = ord(cp1252.decoding_table[charcode]) width = font.load_char( - s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance + charcode, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning @@ -2331,9 +2329,13 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): self.draw_path(boxgc, path, mytrans, gc._rgb) def encode_string(self, s, fonttype): - if fonttype in (1, 3): - return s.encode('cp1252', 'replace') - return s.encode('utf-16be', 'replace') + match fonttype: + case 1: + return s.encode('cp1252', 'replace') + case 3: + return s.encode('latin-1', 'replace') + case _: + return s.encode('utf-16be', 'replace') def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited From f630b483e86d703afa37f5eaa25c88b8c753c639 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Jun 2025 15:49:39 -0400 Subject: [PATCH 28/58] Use glyph indices for font tracking in vector formats With libraqm, string layout produces glyph indices, not character codes, and font features may even produce different glyphs for the same character code (e.g., by picking a different Stylistic Set). Thus we cannot rely on character codes as unique items within a font, and must move toward glyph indices everywhere. --- .../next_api_changes/behavior/30335-ES.rst | 15 +++ lib/matplotlib/_mathtext.py | 15 ++- lib/matplotlib/_text_helpers.py | 16 +-- lib/matplotlib/backends/_backend_pdf_ps.py | 45 ++++--- lib/matplotlib/backends/backend_cairo.py | 10 +- lib/matplotlib/backends/backend_pdf.py | 111 +++++++++--------- lib/matplotlib/backends/backend_ps.py | 56 ++++----- lib/matplotlib/backends/backend_svg.py | 31 +++-- lib/matplotlib/dviread.pyi | 4 +- lib/matplotlib/tests/test_backend_pdf.py | 6 +- lib/matplotlib/tests/test_backend_svg.py | 2 +- lib/matplotlib/textpath.py | 53 ++++----- 12 files changed, 191 insertions(+), 173 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30335-ES.rst diff --git a/doc/api/next_api_changes/behavior/30335-ES.rst b/doc/api/next_api_changes/behavior/30335-ES.rst new file mode 100644 index 000000000000..26b059401e19 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30335-ES.rst @@ -0,0 +1,15 @@ +``mathtext.VectorParse`` now includes glyph indices +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a *path*-outputting `.MathTextParser`, in the return value of +`~.MathTextParser.parse`, (a `.VectorParse`), the *glyphs* field is now a list +containing tuples of: + +- font: `.FT2Font` +- fontsize: `float` +- character code: `int` +- glyph index: `int` +- x: `float` +- y: `float` + +Specifically, the glyph index was added after the character code. diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 234e5a238436..b85cdffd6d88 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -38,7 +38,7 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import CharacterCodeType, Glyph + from .ft2font import CharacterCodeType, Glyph, GlyphIndexType ParserElement.enable_packrat() @@ -87,7 +87,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]] + glyphs: list[tuple[FT2Font, float, CharacterCodeType, GlyphIndexType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -132,7 +132,8 @@ def __init__(self, box: Box): def to_vector(self) -> VectorParse: w, h, d = map( np.ceil, [self.box.width, self.box.height, self.box.depth]) - gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + gs = [(info.font, info.fontsize, info.num, info.glyph_index, + ox, h - oy + info.offset) for ox, oy, info in self.glyphs] rs = [(x1, h - y2, x2 - x1, y2 - y1) for x1, y1, x2, y2 in self.rects] @@ -215,6 +216,7 @@ class FontInfo(NamedTuple): postscript_name: str metrics: FontMetrics num: CharacterCodeType + glyph_index: GlyphIndexType glyph: Glyph offset: float @@ -375,7 +377,8 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, dpi: float) -> FontInfo: font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char(num, flags=self.load_glyph_flags) + glyph_index = font.get_char_index(num) + glyph = font.load_glyph(glyph_index, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) @@ -398,6 +401,7 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, postscript_name=font.postscript_name, metrics=metrics, num=num, + glyph_index=glyph_index, glyph=glyph, offset=offset ) @@ -427,8 +431,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(font.get_char_index(info1.num), - font.get_char_index(info2.num), + return font.get_kerning(info1.glyph_index, info2.glyph_index, Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index a874c8f4bf81..b9471c2c7e39 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -14,7 +14,7 @@ class LayoutItem: ft_object: FT2Font char: str - glyph_idx: GlyphIndexType + glyph_index: GlyphIndexType x: float prev_kern: float @@ -47,19 +47,19 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): LayoutItem """ x = 0 - prev_glyph_idx = None + prev_glyph_index = None char_to_font = font._get_fontmap(string) base_font = font for char in string: # This has done the fallback logic font = char_to_font.get(char, base_font) - glyph_idx = font.get_char_index(ord(char)) + glyph_index = font.get_char_index(ord(char)) kern = ( - base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64 - if prev_glyph_idx is not None else 0. + base_font.get_kerning(prev_glyph_index, glyph_index, kern_mode) / 64 + if prev_glyph_index is not None else 0. ) x += kern - glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING) - yield LayoutItem(font, char, glyph_idx, x, kern) + glyph = font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) + yield LayoutItem(font, char, glyph_index, x, kern) x += glyph.linearHoriAdvance / 65536 - prev_glyph_idx = glyph_idx + prev_glyph_index = glyph_index diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a2a878d54156..75f0a05ae0dc 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -2,9 +2,12 @@ Common functionality between the PDF and PS backends. """ +from __future__ import annotations + from io import BytesIO import functools import logging +import typing from fontTools import subset @@ -14,24 +17,29 @@ from ..backend_bases import RendererBase +if typing.TYPE_CHECKING: + from .ft2font import FT2Font, GlyphIndexType + from fontTools.ttLib import TTFont + + @functools.lru_cache(50) def _cached_get_afm_from_fname(fname): with open(fname, "rb") as fh: return AFM(fh) -def get_glyphs_subset(fontfile, characters): +def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: """ - Subset a TTF font + Subset a TTF font. - Reads the named fontfile and restricts the font to the characters. + Reads the named fontfile and restricts the font to the glyphs. Parameters ---------- fontfile : str Path to the font file - characters : str - Continuous set of characters to include in subset + glyphs : set[GlyphIndexType] + Set of glyph indices to include in subset. Returns ------- @@ -39,8 +47,8 @@ def get_glyphs_subset(fontfile, characters): An open font object representing the subset, which needs to be closed by the caller. """ - - options = subset.Options(glyph_names=True, recommended_glyphs=True) + options = subset.Options(glyph_names=True, recommended_glyphs=True, + retain_gids=True) # Prevent subsetting extra tables. options.drop_tables += [ @@ -71,7 +79,7 @@ def get_glyphs_subset(fontfile, characters): font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) - subsetter.populate(text=characters) + subsetter.populate(gids=glyphs) subsetter.subset(font) return font @@ -97,24 +105,25 @@ def font_as_file(font): class CharacterTracker: """ - Helper for font subsetting by the pdf and ps backends. + Helper for font subsetting by the PDF and PS backends. - Maintains a mapping of font paths to the set of character codepoints that - are being used from that font. + Maintains a mapping of font paths to the set of glyphs that are being used from that + font. """ - def __init__(self): - self.used = {} + def __init__(self) -> None: + self.used: dict[str, set[GlyphIndexType]] = {} - def track(self, font, s): + def track(self, font: FT2Font, s: str) -> None: """Record that string *s* is being typeset using font *font*.""" char_to_font = font._get_fontmap(s) for _c, _f in char_to_font.items(): - self.used.setdefault(_f.fname, set()).add(ord(_c)) + glyph_index = _f.get_char_index(ord(_c)) + self.used.setdefault(_f.fname, set()).add(glyph_index) - def track_glyph(self, font, glyph): - """Record that codepoint *glyph* is being typeset using font *font*.""" - self.used.setdefault(font.fname, set()).add(glyph) + def track_glyph(self, font: FT2Font, glyph_index: GlyphIndexType) -> None: + """Record that glyph index *glyph_index* is being typeset using font *font*.""" + self.used.setdefault(font.fname, set()).add(glyph_index) class RendererPDFPSBase(RendererBase): diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 7409cd35b394..e20ec3fc2313 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -8,6 +8,7 @@ import functools import gzip +import itertools import math import numpy as np @@ -248,13 +249,12 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if angle: ctx.rotate(np.deg2rad(-angle)) - for font, fontsize, idx, ox, oy in glyphs: + for (font, fontsize), font_glyphs in itertools.groupby( + glyphs, key=lambda info: (info[0], info[1])): ctx.new_path() - ctx.move_to(ox, -oy) - ctx.select_font_face( - *_cairo_font_args_from_font_prop(ttfFontProperty(font))) + ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_text(chr(idx)) + ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) for ox, oy, w, h in rects: ctx.new_path() diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 6682deffb00e..7f1905f96f12 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -19,6 +19,7 @@ import sys import time import types +import typing import warnings import zlib @@ -35,7 +36,8 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags +from matplotlib.ft2font import ( + FT2Font, FaceFlags, GlyphIndexType, Kerning, LoadFlags, StyleFlags) from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -611,12 +613,12 @@ def _flush(self): self.compressobj = None -def _get_pdf_charprocs(font_path, glyph_ids): +def _get_pdf_charprocs(font_path, glyph_indices): 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, LoadFlags.NO_SCALE) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) d1 = [ round(g.horiAdvance * conv), 0, # Round bbox corners *outwards*, so that they indeed bound the glyph. @@ -625,7 +627,7 @@ def _get_pdf_charprocs(font_path, glyph_ids): ] v, c = font.get_path() v = (v * 64 * conv).round() # Back to TrueType's internal units (1/64's). - procs[font.get_glyph_name(glyph_id)] = ( + procs[font.get_glyph_name(glyph_index)] = ( " ".join(map(str, d1)).encode("ascii") + b" d1\n" + _path.convert_to_string( Path(v, c), None, None, False, None, 0, @@ -960,9 +962,9 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - chars = self._character_tracker.used.get(filename) - if chars: - fonts[Fx] = self.embedTTF(filename, chars) + glyphs = self._character_tracker.used.get(filename) + if glyphs: + fonts[Fx] = self.embedTTF(filename, glyphs) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1136,9 +1138,8 @@ def _get_xobject_glyph_name(self, filename, glyph_name): end end""" - def embedTTF(self, filename, characters): + def embedTTF(self, filename, glyphs): """Embed the TTF font from the named file into the document.""" - font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1153,7 +1154,7 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, characters, descriptor): + def embedTTFType3(font, glyphs, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') @@ -1198,15 +1199,13 @@ def get_char_width(charcode): # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids # that we need from this font. - glyph_ids = [] differences = [] multi_byte_chars = set() - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph_ids.append(gind) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: glyph_name = font.get_glyph_name(gind) - if ccode <= 255: + ccode = charmap.get(gind) + if ccode is not None and ccode <= 255: differences.append((ccode, glyph_name)) else: multi_byte_chars.add(glyph_name) @@ -1220,7 +1219,7 @@ def get_char_width(charcode): last_c = c # Make the charprocs array. - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + rawcharprocs = _get_pdf_charprocs(filename, glyphs) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] @@ -1257,7 +1256,7 @@ def get_char_width(charcode): return fontdictObject - def embedTTFType42(font, characters, descriptor): + def embedTTFType42(font, glyphs, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1267,9 +1266,8 @@ def embedTTFType42(font, characters, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - subset_str = "".join(chr(c) for c in characters) - _log.debug("SUBSET %s characters: %s", filename, subset_str) - with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: + _log.debug("SUBSET %s characters: %s", filename, glyphs) + with _backend_pdf_ps.get_glyphs_subset(filename, glyphs) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( "SUBSET %s %d -> %d", filename, @@ -1317,11 +1315,11 @@ def embedTTFType42(font, characters, descriptor): cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph = font.load_char(ccode, - flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: + glyph = font.load_glyph(gind, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + ccode = charmap[gind] widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1359,14 +1357,13 @@ def embedTTFType42(font, characters, descriptor): (len(unicode_groups), b"\n".join(unicode_bfrange))) # Add XObjects for unsupported chars - glyph_ids = [] - for ccode in characters: - if not _font_supports_glyph(fonttype, ccode): - gind = full_font.get_char_index(ccode) - glyph_ids.append(gind) + glyph_indices = [ + glyph_index for glyph_index in glyphs + if not _font_supports_glyph(fonttype, charmap[glyph_index]) + ] bbox = [cvt(x, nearest=False) for x in full_font.bbox] - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + rawcharprocs = _get_pdf_charprocs(filename, glyph_indices) for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] charprocDict = {'Type': Name('XObject'), @@ -1448,9 +1445,9 @@ def embedTTFType42(font, characters, descriptor): } if fonttype == 3: - return embedTTFType3(font, characters, descriptor) + return embedTTFType3(font, glyphs, descriptor) elif fonttype == 42: - return embedTTFType42(font, characters, descriptor) + return embedTTFType42(font, glyphs, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" @@ -2214,13 +2211,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): unsupported_chars = [] self.file.output(Op.begin_text) - for font, fontsize, num, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, num) + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + self.file._character_tracker.track_glyph(font, glyph_index) fontname = font.fname - if not _font_supports_glyph(fonttype, num): + if not _font_supports_glyph(fonttype, ccode): # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in # Type 42) must be emitted separately (below). - unsupported_chars.append((font, fontsize, ox, oy, num)) + unsupported_chars.append((font, fontsize, ox, oy, glyph_index)) else: self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy @@ -2228,13 +2225,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.file.output(self.file.fontName(fontname), fontsize, Op.selectfont) prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), + self.file.output(self.encode_string(chr(ccode), fonttype), Op.show) self.file.output(Op.end_text) - for font, fontsize, ox, oy, num in unsupported_chars: - self._draw_xobject_glyph( - font, fontsize, font.get_char_index(num), ox, oy) + for font, fontsize, ox, oy, glyph_index in unsupported_chars: + self._draw_xobject_glyph(font, fontsize, glyph_index, ox, oy) # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: @@ -2266,13 +2262,17 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # one single-character string, but later it may have longer # strings interspersed with kern amounts. oldfont, seq = None, [] - for x1, y1, dvifont, glyph, width in page.text: - if dvifont != oldfont: - pdfname = self.file.dviFontName(dvifont) - seq += [['font', pdfname, dvifont.size]] - oldfont = dvifont - seq += [['text', x1, y1, [bytes([glyph])], x1+width]] - self.file._character_tracker.track(dvifont, chr(glyph)) + for text in page.text: + if text.font != oldfont: + pdfname = self.file.dviFontName(text.font) + seq += [['font', pdfname, text.font.size]] + oldfont = text.font + seq += [['text', text.x, text.y, [bytes([text.glyph])], text.x+text.width]] + # TODO: This should use glyph indices, not character codes, but will be + # fixed soon. + self.file._character_tracker.track_glyph(text.font, + typing.cast('GlyphIndexType', + text.glyph)) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one @@ -2401,7 +2401,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): singlebyte_chunks[-1][2].append(item.char) prev_was_multibyte = False else: - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_idx)) + multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) prev_was_multibyte = True # Do the rotation and global translation as a single matrix # concatenation up front @@ -2411,7 +2411,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): -math.sin(a), math.cos(a), x, y, Op.concat_matrix) # Emit all the 1-byte characters in a BT/ET group. - self.file.output(Op.begin_text) prev_start_x = 0 for ft_object, start_x, kerns_or_chars in singlebyte_chunks: @@ -2428,15 +2427,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): prev_start_x = start_x self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. - for ft_object, start_x, glyph_idx in multibyte_glyphs: + for ft_object, start_x, glyph_index in multibyte_glyphs: self._draw_xobject_glyph( - ft_object, fontsize, glyph_idx, start_x, 0 + ft_object, fontsize, glyph_index, start_x, 0 ) self.file.output(Op.grestore) - def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): + def _draw_xobject_glyph(self, font, fontsize, glyph_index, x, y): """Draw a multibyte character from a Type 3 font as an XObject.""" - glyph_name = font.get_glyph_name(glyph_idx) + glyph_name = font.get_glyph_name(glyph_index) name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 368564a1518d..060a40c08e8b 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -88,16 +88,16 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, chars): +def _font_to_ps_type3(font_path, glyph_indices): """ - Subset *chars* from the font at *font_path* into a Type 3 font. + Subset *glyphs_indices* from the font at *font_path* into a Type 3 font. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. Returns ------- @@ -106,7 +106,6 @@ def _font_to_ps_type3(font_path, chars): verbatim into a PostScript file. """ font = get_font(font_path, hinting_factor=1) - glyph_ids = [font.get_char_index(c) for c in chars] preamble = """\ %!PS-Adobe-3.0 Resource-Font @@ -123,9 +122,9 @@ def _font_to_ps_type3(font_path, chars): """.format(font_name=font.postscript_name, inv_units_per_em=1 / font.units_per_EM, bbox=" ".join(map(str, font.bbox)), - encoding=" ".join(f"/{font.get_glyph_name(glyph_id)}" - for glyph_id in glyph_ids), - num_glyphs=len(glyph_ids) + 1) + encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}" + for glyph_index in glyph_indices), + num_glyphs=len(glyph_indices) + 1) postamble = """ end readonly def @@ -146,12 +145,12 @@ def _font_to_ps_type3(font_path, chars): """ entries = [] - for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { - "name": font.get_glyph_name(glyph_id), + "name": font.get_glyph_name(glyph_index), "bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])), } + _path.convert_to_string( @@ -169,21 +168,20 @@ def _font_to_ps_type3(font_path, chars): return preamble + "\n".join(entries) + postamble -def _font_to_ps_type42(font_path, chars, fh): +def _font_to_ps_type42(font_path, glyph_indices, fh): """ - Subset *chars* from the font at *font_path* into a Type 42 font at *fh*. + Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. fh : file-like Where to write the font. """ - subset_str = ''.join(chr(c) for c in chars) - _log.debug("SUBSET %s characters: %s", font_path, subset_str) + _log.debug("SUBSET %s characters: %s", font_path, glyph_indices) try: kw = {} # fix this once we support loading more fonts from a collection @@ -191,7 +189,7 @@ def _font_to_ps_type42(font_path, chars, fh): if font_path.endswith('.ttc'): kw['fontNumber'] = 0 with (fontTools.ttLib.TTFont(font_path, **kw) as font, - _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset): + _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, @@ -775,8 +773,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) - ps_name = (font.postscript_name.encode("ascii", "replace") - .decode("ascii")) + ps_name = font.postscript_name.encode("ascii", "replace").decode("ascii") scale = 0.001 * prop.get_size_in_points() thisx = 0 last_name = '' # kerns returns 0 for ''. @@ -799,7 +796,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): for item in _text_helpers.layout(s, font): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) - glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) + glyph_name = item.ft_object.get_glyph_name(item.glyph_index) stream.append((ps_name, item.x, glyph_name)) self.set_color(*gc.get_rgb()) @@ -828,13 +825,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{x:g} {y:g} translate\n" f"{angle:g} rotate\n") lastfont = None - for font, fontsize, num, ox, oy in glyphs: - self._character_tracker.track_glyph(font, num) + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + self._character_tracker.track_glyph(font, glyph_index) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - glyph_name = font.get_glyph_name(font.get_char_index(num)) + glyph_name = font.get_glyph_name(glyph_index) self._pswriter.write( f"{ox:g} {oy:g} moveto\n" f"/{glyph_name} glyphshow\n") @@ -1072,19 +1069,18 @@ def print_figure_impl(fh): print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for font_path, chars \ - in ps_renderer._character_tracker.used.items(): - if not chars: + for font_path, glyphs in ps_renderer._character_tracker.used.items(): + if not glyphs: continue fonttype = mpl.rcParams['ps.fonttype'] # Can't use more than 255 chars from a single Type 3 font. - if len(chars) > 255: + if len(glyphs) > 255: fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, chars)) + fh.write(_font_to_ps_type3(font_path, glyphs)) else: # Type 42 only. - _font_to_ps_type42(font_path, chars, fh) + _font_to_ps_type42(font_path, glyphs, fh) print("end", file=fh) print("%%EndProlog", file=fh) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..7b94a2b9ba2b 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1023,19 +1023,19 @@ def _update_glyph_map_defs(self, glyph_map_new): writer = self.writer if glyph_map_new: writer.start('defs') - for char_id, (vertices, codes) in glyph_map_new.items(): - char_id = self._adjust_char_id(char_id) + for glyph_repr, (vertices, codes) in glyph_map_new.items(): + glyph_repr = self._adjust_glyph_repr(glyph_repr) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( Path(vertices * 64, codes), simplify=False) writer.element( - 'path', id=char_id, d=path_data, + 'path', id=glyph_repr, d=path_data, transform=_generate_transform([('scale', (1 / 64,))])) writer.end('defs') self._glyph_map.update(glyph_map_new) - def _adjust_char_id(self, char_id): - return char_id.replace("%20", "_") + def _adjust_glyph_repr(self, glyph_repr): + return glyph_repr.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # docstring inherited @@ -1067,19 +1067,18 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) - _glyphs = text2path.get_glyphs_with_font( + glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) - glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) - for glyph_id, xposition, yposition, scale in glyph_info: + for glyph_repr, xposition, yposition, scale in glyph_info: writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{glyph_id}'}) + attrib={'xlink:href': f'#{glyph_repr}'}) else: if ismath == "TeX": @@ -1091,15 +1090,15 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) - for char_id, xposition, yposition, scale in glyph_info: - char_id = self._adjust_char_id(char_id) + for glyph_repr, xposition, yposition, scale in glyph_info: + glyph_repr = self._adjust_glyph_repr(glyph_repr) writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{char_id}'}) + attrib={'xlink:href': f'#{glyph_repr}'}) for verts, codes in rects: path = Path(verts, codes) @@ -1223,7 +1222,7 @@ def _get_all_quoted_names(prop): # Sort the characters by font, and output one tspan for each. spans = {} - for font, fontsize, thetext, new_x, new_y in glyphs: + for font, fontsize, ccode, glyph_index, new_x, new_y in glyphs: entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes @@ -1238,9 +1237,9 @@ def _get_all_quoted_names(prop): if entry.stretch != 'normal': font_style['font-stretch'] = entry.stretch style = _generate_css({**font_style, **color_style}) - if thetext == 32: - thetext = 0xa0 # non-breaking space - spans.setdefault(style, []).append((new_x, -new_y, thetext)) + if ccode == 32: + ccode = 0xa0 # non-breaking space + spans.setdefault(style, []).append((new_x, -new_y, ccode)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 6ddc463295a9..c1e911a88355 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -8,7 +8,7 @@ from collections.abc import Generator from typing import NamedTuple from typing import Self -from .ft2font import GlyphIndexType +from .ft2font import CharacterCodeType, GlyphIndexType class _dvistate(Enum): @@ -35,7 +35,7 @@ class Text(NamedTuple): x: int y: int font: DviFont - glyph: int + glyph: CharacterCodeType width: int @property def font_path(self) -> Path: ... diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index f126fb543e78..2dc22fd9170e 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -361,13 +361,13 @@ def test_glyphs_subset(): # non-subsetted FT2Font nosubfont = FT2Font(fpath) nosubfont.set_text(chars) + nosubcmap = nosubfont.get_charmap() # subsetted FT2Font - with get_glyphs_subset(fpath, chars) as subset: + glyph_indices = {nosubcmap[ord(c)] for c in chars} + with get_glyphs_subset(fpath, glyph_indices) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) - - nosubcmap = nosubfont.get_charmap() subcmap = subfont.get_charmap() # all unique chars must be available in subsetted font diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 2c64b7c24b3e..e865dbbe92da 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -216,7 +216,7 @@ def test_unicode_won(): tree = xml.etree.ElementTree.fromstring(buf) ns = 'http://www.w3.org/2000/svg' - won_id = 'SFSS1728-8e' + won_id = 'SFSS1728-232' assert len(tree.findall(f'.//{{{ns}}}path[@d][@id="{won_id}"]')) == 1 assert f'#{won_id}' in tree.find(f'.//{{{ns}}}use').attrib.values() diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..626568ba134e 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -39,11 +39,9 @@ def _get_font(self, prop): def _get_hinting_flag(self): return LoadFlags.NO_HINTING - def _get_char_id(self, font, ccode): - """ - Return a unique id for the given font and character-code set. - """ - return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}") + def _get_glyph_repr(self, font, glyph): + """Return a unique id for the given font and glyph index.""" + return urllib.parse.quote(f"{font.postscript_name}-{glyph:x}") def get_text_width_height_descent(self, s, prop, ismath): fontsize = prop.get_size_in_points() @@ -114,8 +112,8 @@ def get_text_path(self, prop, s, ismath=False): glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) verts, codes = [], [] - for glyph_id, xposition, yposition, scale in glyph_info: - verts1, codes1 = glyph_map[glyph_id] + for glyph_repr, xposition, yposition, scale in glyph_info: + verts1, codes1 = glyph_map[glyph_repr] verts.extend(verts1 * scale + [xposition, yposition]) codes.extend(codes1) for verts1, codes1 in rects: @@ -144,20 +142,20 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, glyph_map_new = glyph_map xpositions = [] - glyph_ids = [] + glyph_reprs = [] for item in _text_helpers.layout(s, font): - char_id = self._get_char_id(item.ft_object, ord(item.char)) - glyph_ids.append(char_id) + glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) + glyph_reprs.append(glyph_repr) xpositions.append(item.x) - if char_id not in glyph_map: - glyph_map_new[char_id] = item.ft_object.get_path() + if glyph_repr not in glyph_map: + glyph_map_new[glyph_repr] = item.ft_object.get_path() ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, @@ -182,20 +180,20 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, xpositions = [] ypositions = [] - glyph_ids = [] + glyph_reprs = [] sizes = [] - for font, fontsize, ccode, ox, oy in glyphs: - char_id = self._get_char_id(font, ccode) - if char_id not in glyph_map: + for font, fontsize, ccode, glyph_index, ox, oy in glyphs: + glyph_repr = self._get_glyph_repr(font, glyph_index) + if glyph_repr not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LoadFlags.NO_HINTING) - glyph_map_new[char_id] = font.get_path() + font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) + glyph_map_new[glyph_repr] = font.get_path() xpositions.append(ox) ypositions.append(oy) - glyph_ids.append(char_id) + glyph_reprs.append(glyph_repr) size = fontsize / self.FONT_SCALE sizes.append(size) @@ -208,7 +206,7 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, Path.CLOSEPOLY] myrects.append((vert1, code1)) - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_glyphs_tex(self, prop, s, glyph_map=None, @@ -228,21 +226,20 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, else: glyph_map_new = glyph_map - glyph_ids, xpositions, ypositions, sizes = [], [], [], [] + glyph_reprs, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. - t1_encodings = {} for text in page.text: font = get_font(text.font_path) - char_id = self._get_char_id(font, text.glyph) - if char_id not in glyph_map: + glyph_repr = self._get_glyph_repr(font, text.index) + if glyph_repr not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_glyph(text.index, flags=LoadFlags.TARGET_LIGHT) - glyph_map_new[char_id] = font.get_path() + glyph_map_new[glyph_repr] = font.get_path() - glyph_ids.append(char_id) + glyph_reprs.append(glyph_repr) xpositions.append(text.x) ypositions.append(text.y) sizes.append(text.font_size / self.FONT_SCALE) @@ -257,7 +254,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, Path.CLOSEPOLY] myrects.append((vert1, code1)) - return (list(zip(glyph_ids, xpositions, ypositions, sizes)), + return (list(zip(glyph_reprs, xpositions, ypositions, sizes)), glyph_map_new, myrects) From f192c8794f410e3dc7c052477c0e0a359182d980 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Sep 2025 05:08:35 -0400 Subject: [PATCH 29/58] pdf/ps: Track full character map in CharacterTracker By tracking both character codes and glyph indices, we can handle producing multiple font subsets if needed by a file format. --- lib/matplotlib/backends/_backend_pdf_ps.py | 108 ++++++++++++++++++--- lib/matplotlib/backends/backend_pdf.py | 52 +++++----- lib/matplotlib/backends/backend_ps.py | 13 +-- 3 files changed, 123 insertions(+), 50 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 75f0a05ae0dc..1fdcccbab61a 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -18,7 +18,7 @@ if typing.TYPE_CHECKING: - from .ft2font import FT2Font, GlyphIndexType + from .ft2font import CharacterCodeType, FT2Font, GlyphIndexType from fontTools.ttLib import TTFont @@ -107,23 +107,103 @@ class CharacterTracker: """ Helper for font subsetting by the PDF and PS backends. - Maintains a mapping of font paths to the set of glyphs that are being used from that - font. - """ + Maintains a mapping of font paths to the set of characters and glyphs that are being + used from that font. + + Attributes + ---------- + subset_size : int + The size at which characters are grouped into subsets. + used : dict[tuple[str, int], dict[CharacterCodeType, GlyphIndexType]] + A dictionary of font files to character maps. + + The key is a font filename and subset within that font. - def __init__(self) -> None: - self.used: dict[str, set[GlyphIndexType]] = {} + The value is a dictionary mapping a character code to a glyph index. Note this + mapping is the inverse of FreeType, which maps glyph indices to character codes. - def track(self, font: FT2Font, s: str) -> None: - """Record that string *s* is being typeset using font *font*.""" + If *subset_size* is not set, then there will only be one subset per font + filename. + """ + + def __init__(self, subset_size: int = 0): + """ + Parameters + ---------- + subset_size : int, optional + The maximum size that is supported for an embedded font. If provided, then + characters will be grouped into these sized subsets. + """ + self.used: dict[tuple[str, int], dict[CharacterCodeType, GlyphIndexType]] = {} + self.subset_size = subset_size + + def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: + """ + Record that string *s* is being typeset using font *font*. + + Parameters + ---------- + font : FT2Font + A font that is being used for the provided string. + s : str + The string that should be marked as tracked by the provided font. + + Returns + ------- + list[tuple[int, CharacterCodeType]] + A list of subset and character code pairs corresponding to the input string. + If a *subset_size* is specified on this instance, then the character code + will correspond with the given subset (and not necessarily the string as a + whole). If *subset_size* is not specified, then the subset will always be 0 + and the character codes will be returned from the string unchanged. + """ + font_glyphs = [] char_to_font = font._get_fontmap(s) for _c, _f in char_to_font.items(): - glyph_index = _f.get_char_index(ord(_c)) - self.used.setdefault(_f.fname, set()).add(glyph_index) - - def track_glyph(self, font: FT2Font, glyph_index: GlyphIndexType) -> None: - """Record that glyph index *glyph_index* is being typeset using font *font*.""" - self.used.setdefault(font.fname, set()).add(glyph_index) + charcode = ord(_c) + glyph_index = _f.get_char_index(charcode) + if self.subset_size != 0: + subset = charcode // self.subset_size + subset_charcode = charcode % self.subset_size + else: + subset = 0 + subset_charcode = charcode + self.used.setdefault((_f.fname, subset), {})[subset_charcode] = glyph_index + font_glyphs.append((subset, subset_charcode)) + return font_glyphs + + def track_glyph( + self, font: FT2Font, charcode: CharacterCodeType, + glyph: GlyphIndexType) -> tuple[int, CharacterCodeType]: + """ + Record character code *charcode* at glyph index *glyph* as using font *font*. + + Parameters + ---------- + font : FT2Font + A font that is being used for the provided string. + charcode : CharacterCodeType + The character code to record. + glyph : GlyphIndexType + The corresponding glyph index to record. + + Returns + ------- + subset : int + The subset in which the returned character code resides. If *subset_size* + was not specified on this instance, then this is always 0. + subset_charcode : CharacterCodeType + The character code within the above subset. If *subset_size* was not + specified on this instance, then this is just *charcode* unmodified. + """ + if self.subset_size != 0: + subset = charcode // self.subset_size + subset_charcode = charcode % self.subset_size + else: + subset = 0 + subset_charcode = charcode + self.used.setdefault((font.fname, subset), {})[subset_charcode] = glyph + return (subset, subset_charcode) class RendererPDFPSBase(RendererBase): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7f1905f96f12..153e03639d84 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -19,7 +19,6 @@ import sys import time import types -import typing import warnings import zlib @@ -36,8 +35,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import ( - FT2Font, FaceFlags, GlyphIndexType, Kerning, LoadFlags, StyleFlags) +from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -962,9 +960,9 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - glyphs = self._character_tracker.used.get(filename) - if glyphs: - fonts[Fx] = self.embedTTF(filename, glyphs) + charmap = self._character_tracker.used.get((filename, 0)) + if charmap: + fonts[Fx] = self.embedTTF(filename, charmap) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1006,8 +1004,9 @@ def _embedTeXFont(self, dvifont): # Reduce the font to only the glyphs used in the document, get the encoding # for that subset, and compute various properties based on the encoding. - chars = frozenset(self._character_tracker.used[dvifont.fname]) - t1font = t1font.subset(chars, self._get_subset_prefix(chars)) + charmap = self._character_tracker.used[(dvifont.fname, 0)] + chars = frozenset(charmap.keys()) + t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) fontdict['BaseFont'] = Name(t1font.prop['FontName']) # createType1Descriptor writes the font data as a side effect fontdict['FontDescriptor'] = self.createType1Descriptor(t1font) @@ -1138,7 +1137,7 @@ def _get_xobject_glyph_name(self, filename, glyph_name): end end""" - def embedTTF(self, filename, glyphs): + def embedTTF(self, filename, charmap): """Embed the TTF font from the named file into the document.""" font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1154,7 +1153,7 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, glyphs, descriptor): + def embedTTFType3(font, charmap, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') @@ -1201,10 +1200,8 @@ def get_char_width(charcode): # that we need from this font. differences = [] multi_byte_chars = set() - charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} - for gind in glyphs: + for ccode, gind in charmap.items(): glyph_name = font.get_glyph_name(gind) - ccode = charmap.get(gind) if ccode is not None and ccode <= 255: differences.append((ccode, glyph_name)) else: @@ -1219,7 +1216,7 @@ def get_char_width(charcode): last_c = c # Make the charprocs array. - rawcharprocs = _get_pdf_charprocs(filename, glyphs) + rawcharprocs = _get_pdf_charprocs(filename, charmap.values()) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] @@ -1256,7 +1253,7 @@ def get_char_width(charcode): return fontdictObject - def embedTTFType42(font, glyphs, descriptor): + def embedTTFType42(font, charmap, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1266,8 +1263,9 @@ def embedTTFType42(font, glyphs, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - _log.debug("SUBSET %s characters: %s", filename, glyphs) - with _backend_pdf_ps.get_glyphs_subset(filename, glyphs) as subset: + _log.debug("SUBSET %s characters: %s", filename, charmap) + with _backend_pdf_ps.get_glyphs_subset(filename, + charmap.values()) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( "SUBSET %s %d -> %d", filename, @@ -1315,11 +1313,9 @@ def embedTTFType42(font, glyphs, descriptor): cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 - charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} - for gind in glyphs: + for ccode, gind in charmap.items(): glyph = font.load_glyph(gind, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) - ccode = charmap[gind] widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1358,8 +1354,8 @@ def embedTTFType42(font, glyphs, descriptor): # Add XObjects for unsupported chars glyph_indices = [ - glyph_index for glyph_index in glyphs - if not _font_supports_glyph(fonttype, charmap[glyph_index]) + glyph_index for ccode, glyph_index in charmap.items() + if not _font_supports_glyph(fonttype, ccode) ] bbox = [cvt(x, nearest=False) for x in full_font.bbox] @@ -1445,9 +1441,9 @@ def embedTTFType42(font, glyphs, descriptor): } if fonttype == 3: - return embedTTFType3(font, glyphs, descriptor) + return embedTTFType3(font, charmap, descriptor) elif fonttype == 42: - return embedTTFType42(font, glyphs, descriptor) + return embedTTFType42(font, charmap, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" @@ -2212,7 +2208,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.file.output(Op.begin_text) for font, fontsize, ccode, glyph_index, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, glyph_index) + self.file._character_tracker.track_glyph(font, ccode, glyph_index) fontname = font.fname if not _font_supports_glyph(fonttype, ccode): # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in @@ -2268,11 +2264,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): seq += [['font', pdfname, text.font.size]] oldfont = text.font seq += [['text', text.x, text.y, [bytes([text.glyph])], text.x+text.width]] - # TODO: This should use glyph indices, not character codes, but will be - # fixed soon. - self.file._character_tracker.track_glyph(text.font, - typing.cast('GlyphIndexType', - text.glyph)) + self.file._character_tracker.track_glyph(text.font, text.glyph, text.index) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 060a40c08e8b..b0180de20f9f 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -826,7 +826,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{angle:g} rotate\n") lastfont = None for font, fontsize, ccode, glyph_index, ox, oy in glyphs: - self._character_tracker.track_glyph(font, glyph_index) + self._character_tracker.track_glyph(font, ccode, glyph_index) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( @@ -1069,18 +1069,19 @@ def print_figure_impl(fh): print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for font_path, glyphs in ps_renderer._character_tracker.used.items(): - if not glyphs: + for (font, subset_index), charmap in \ + ps_renderer._character_tracker.used.items(): + if not charmap: continue fonttype = mpl.rcParams['ps.fonttype'] # Can't use more than 255 chars from a single Type 3 font. - if len(glyphs) > 255: + if len(charmap) > 255: fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, glyphs)) + fh.write(_font_to_ps_type3(font, charmap.values())) else: # Type 42 only. - _font_to_ps_type42(font_path, glyphs, fh) + _font_to_ps_type42(font, charmap.values(), fh) print("end", file=fh) print("%%EndProlog", file=fh) From 3e97c0d649550d805c25c71bf49f5f456fec284f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 29 Aug 2025 04:16:48 -0400 Subject: [PATCH 30/58] pdf: Merge loops for single byte text chunk output Currently, we split text into single byte chunks and multi-byte glyphs, then iterate through single byte chunks for output and multi-byte glyphs for output. Instead, output the single byte chunks as we finish them, then do the multi-byte glyphs at the end. --- lib/matplotlib/backends/backend_pdf.py | 65 +++++++++++++------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 153e03639d84..560d85db682d 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2376,25 +2376,14 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # the regular text show command (TJ) with appropriate kerning between # chunks, whereas multibyte characters use the XObject command (Do). else: - # List of (ft_object, start_x, [prev_kern, char, char, ...]), - # w/o zero kerns. - singlebyte_chunks = [] - # List of (ft_object, start_x, glyph_index). - multibyte_glyphs = [] - prev_was_multibyte = True - prev_font = font - for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): - if _font_supports_glyph(fonttype, ord(item.char)): - if prev_was_multibyte or item.ft_object != prev_font: - singlebyte_chunks.append((item.ft_object, item.x, [])) - prev_font = item.ft_object - if item.prev_kern: - singlebyte_chunks[-1][2].append(item.prev_kern) - singlebyte_chunks[-1][2].append(item.char) - prev_was_multibyte = False - else: - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) - prev_was_multibyte = True + def output_singlebyte_chunk(kerns_or_chars): + self.file.output( + # See pdf spec "Text space details" for the 1000/fontsize + # (aka. 1000/T_fs) factor. + [(-1000 * next(group) / fontsize) if tp == float # a kern + else self.encode_string("".join(group), fonttype) + for tp, group in itertools.groupby(kerns_or_chars, type)], + Op.showkern) # Do the rotation and global translation as a single matrix # concatenation up front self.file.output(Op.gsave) @@ -2402,21 +2391,33 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.file.output(math.cos(a), math.sin(a), -math.sin(a), math.cos(a), x, y, Op.concat_matrix) + # List of [prev_kern, char, char, ...] w/o zero kerns. + singlebyte_chunk = [] + # List of (ft_object, start_x, glyph_index). + multibyte_glyphs = [] + prev_font = None + prev_start_x = 0 # Emit all the 1-byte characters in a BT/ET group. self.file.output(Op.begin_text) - prev_start_x = 0 - for ft_object, start_x, kerns_or_chars in singlebyte_chunks: - ft_name = self.file.fontName(ft_object.fname) - self.file.output(ft_name, fontsize, Op.selectfont) - self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0) - self.file.output( - # See pdf spec "Text space details" for the 1000/fontsize - # (aka. 1000/T_fs) factor. - [-1000 * next(group) / fontsize if tp == float # a kern - else self.encode_string("".join(group), fonttype) - for tp, group in itertools.groupby(kerns_or_chars, type)], - Op.showkern) - prev_start_x = start_x + for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): + if _font_supports_glyph(fonttype, ord(item.char)): + if item.ft_object != prev_font: + if singlebyte_chunk: + output_singlebyte_chunk(singlebyte_chunk) + ft_name = self.file.fontName(item.ft_object.fname) + self.file.output(ft_name, fontsize, Op.selectfont) + self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) + singlebyte_chunk = [] + prev_font = item.ft_object + prev_start_x = item.x + if item.prev_kern: + singlebyte_chunk.append(item.prev_kern) + singlebyte_chunk.append(item.char) + else: + prev_font = None + multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) + if singlebyte_chunk: + output_singlebyte_chunk(singlebyte_chunk) self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. for ft_object, start_x, glyph_index in multibyte_glyphs: From b2364882c3776a7531e00b50c0e8957219fce32b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 17 Sep 2025 22:39:24 -0400 Subject: [PATCH 31/58] MNT: Ignore differing stub for GlyphIndexType This may be an upstream bug [1], but until that is determined, ignore the error to get CI working. [1] https://github.com/python/mypy/issues/19877 --- ci/mypy-stubtest-allowlist.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 46ec06e0a9f1..c8e8e60bc3c9 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -49,3 +49,7 @@ matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability matplotlib\.inset\.InsetIndicator\.__getitem__ + +# Avoid a regression in NewType handling for stubtest +# https://github.com/python/mypy/issues/19877 +matplotlib\.ft2font\.GlyphIndexType\.__init__ From 18ffa029b95d747ece48ec0a53c1340f84858b74 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 01:20:05 -0400 Subject: [PATCH 32/58] Add os.PathLike support to FT2Font constructor, and FontManager Since we pass the filename to `io.open`, we can accept everything it can. Also, fix the return value of `FT2Font.fname`, which could be `bytes` if that was initially provided. --- lib/matplotlib/font_manager.py | 6 ++--- lib/matplotlib/font_manager.pyi | 14 +++++----- lib/matplotlib/ft2font.pyi | 5 ++-- lib/matplotlib/tests/test_font_manager.py | 33 ++++++++++++++++++----- lib/matplotlib/tests/test_ft2font.py | 22 +++++++++++++++ src/ft2font_wrapper.cpp | 13 +++++---- 6 files changed, 70 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 79e088b85998..47339d4491dd 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1611,10 +1611,10 @@ def get_font(font_filepaths, hinting_factor=None): Parameters ---------- - font_filepaths : Iterable[str, Path, bytes], str, Path, bytes + font_filepaths : Iterable[str, bytes, os.PathLike], str, bytes, os.PathLike Relative or absolute paths to the font files to be used. - If a single string, bytes, or `pathlib.Path`, then it will be treated + If a single string, bytes, or `os.PathLike`, then it will be treated as a list with that entry only. If more than one filepath is passed, then the returned FT2Font object @@ -1626,7 +1626,7 @@ def get_font(font_filepaths, hinting_factor=None): `.ft2font.FT2Font` """ - if isinstance(font_filepaths, (str, Path, bytes)): + if isinstance(font_filepaths, (str, bytes, os.PathLike)): paths = (_cached_realpath(font_filepaths),) else: paths = tuple(_cached_realpath(fname) for fname in font_filepaths) diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index e865f67384cd..f5e3910e5f63 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -24,7 +24,7 @@ def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ... def win32FontDirectory() -> str: ... def _get_fontconfig_fonts() -> list[Path]: ... def findSystemFonts( - fontpaths: Iterable[str | os.PathLike | Path] | None = ..., fontext: str = ... + fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ... ) -> list[str]: ... @dataclass class FontEntry: @@ -50,7 +50,7 @@ class FontProperties: weight: int | str | None = ..., stretch: int | str | None = ..., size: float | str | None = ..., - fname: str | os.PathLike | Path | None = ..., + fname: str | os.PathLike | None = ..., math_fontfamily: str | None = ..., ) -> None: ... def __hash__(self) -> int: ... @@ -72,7 +72,7 @@ class FontProperties: def set_weight(self, weight: int | str | None) -> None: ... def set_stretch(self, stretch: int | str | None) -> None: ... def set_size(self, size: float | str | None) -> None: ... - def set_file(self, file: str | os.PathLike | Path | None) -> None: ... + def set_file(self, file: str | os.PathLike | None) -> None: ... def set_fontconfig_pattern(self, pattern: str) -> None: ... def get_math_fontfamily(self) -> str: ... def set_math_fontfamily(self, fontfamily: str | None) -> None: ... @@ -83,8 +83,8 @@ class FontProperties: set_slant = set_style get_size_in_points = get_size -def json_dump(data: FontManager, filename: str | Path | os.PathLike) -> None: ... -def json_load(filename: str | Path | os.PathLike) -> FontManager: ... +def json_dump(data: FontManager, filename: str | os.PathLike) -> None: ... +def json_load(filename: str | os.PathLike) -> FontManager: ... class FontManager: __version__: str @@ -93,7 +93,7 @@ class FontManager: afmlist: list[FontEntry] ttflist: list[FontEntry] def __init__(self, size: float | None = ..., weight: str = ...) -> None: ... - def addfont(self, path: str | Path | os.PathLike) -> None: ... + def addfont(self, path: str | os.PathLike) -> None: ... @property def defaultFont(self) -> dict[str, str]: ... def get_default_weight(self) -> str: ... @@ -120,7 +120,7 @@ class FontManager: def is_opentype_cff_font(filename: str) -> bool: ... def get_font( - font_filepaths: Iterable[str | Path | bytes] | str | Path | bytes, + font_filepaths: Iterable[str | bytes | os.PathLike] | str | bytes | os.PathLike, hinting_factor: int | None = ..., ) -> ft2font.FT2Font: ... diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 55c076bb68b6..98b4b1f7cc4d 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -1,4 +1,5 @@ from enum import Enum, Flag +from os import PathLike import sys from typing import BinaryIO, Literal, NewType, TypeAlias, TypedDict, cast, final, overload from typing_extensions import Buffer # < Py 3.12 @@ -194,7 +195,7 @@ class _SfntPcltDict(TypedDict): class FT2Font(Buffer): def __init__( self, - filename: str | BinaryIO, + filename: str | bytes | PathLike | BinaryIO, hinting_factor: int = ..., *, _fallback_list: list[FT2Font] | None = ..., @@ -256,7 +257,7 @@ class FT2Font(Buffer): @property def family_name(self) -> str: ... @property - def fname(self) -> str: ... + def fname(self) -> str | bytes: ... @property def height(self) -> int: ... @property diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index b15647644e04..d51eb8d9837f 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -1,4 +1,4 @@ -from io import BytesIO, StringIO +from io import BytesIO import gc import multiprocessing import os @@ -137,6 +137,32 @@ def test_find_noto(): fig.savefig(BytesIO(), format=fmt) +def test_find_valid(): + class PathLikeClass: + def __init__(self, filename): + self.filename = filename + + def __fspath__(self): + return self.filename + + file_str = findfont('DejaVu Sans') + file_bytes = os.fsencode(file_str) + + font = get_font(file_str) + assert font.fname == file_str + font = get_font(file_bytes) + assert font.fname == file_bytes + font = get_font(PathLikeClass(file_str)) + assert font.fname == file_str + font = get_font(PathLikeClass(file_bytes)) + assert font.fname == file_bytes + + # Note, fallbacks are not currently accessible. + font = get_font([file_str, file_bytes, + PathLikeClass(file_str), PathLikeClass(file_bytes)]) + assert font.fname == file_str + + def test_find_invalid(tmp_path): with pytest.raises(FileNotFoundError): @@ -148,11 +174,6 @@ def test_find_invalid(tmp_path): with pytest.raises(FileNotFoundError): get_font(bytes(tmp_path / 'non-existent-font-name.ttf')) - # Not really public, but get_font doesn't expose non-filename constructor. - from matplotlib.ft2font import FT2Font - with pytest.raises(TypeError, match='font file or a binary-mode file'): - FT2Font(StringIO()) # type: ignore[arg-type] - @pytest.mark.skipif(sys.platform != 'linux' or not has_fclist, reason='only Linux with fontconfig installed') diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 70e611e17bcc..e78a3894076a 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -1,5 +1,6 @@ import itertools import io +import os from pathlib import Path from typing import cast @@ -134,6 +135,27 @@ def test_ft2font_stix_bold_attrs(): assert font.bbox == (4, -355, 1185, 2095) +def test_ft2font_valid_args(): + class PathLikeClass: + def __init__(self, filename): + self.filename = filename + + def __fspath__(self): + return self.filename + + file_str = fm.findfont('DejaVu Sans') + file_bytes = os.fsencode(file_str) + + font = ft2font.FT2Font(file_str) + assert font.fname == file_str + font = ft2font.FT2Font(file_bytes) + assert font.fname == file_bytes + font = ft2font.FT2Font(PathLikeClass(file_str)) + assert font.fname == file_str + font = ft2font.FT2Font(PathLikeClass(file_bytes)) + assert font.fname == file_bytes + + def test_ft2font_invalid_args(tmp_path): # filename argument. with pytest.raises(TypeError, match='to a font file or a binary-mode file object'): diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 65fcb4b7e013..3471203311b3 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -424,7 +424,7 @@ close_file_callback(FT_Stream stream) const char *PyFT2Font_init__doc__ = R"""( Parameters ---------- - filename : str or file-like + filename : str, bytes, os.PathLike, or io.BinaryIO The source of the font data in a format (ttf or ttc) that FreeType can read. hinting_factor : int, optional @@ -488,7 +488,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; - if (py::isinstance(filename) || py::isinstance(filename)) { + auto PathLike = py::module_::import("os").attr("PathLike"); + if (py::isinstance(filename) || py::isinstance(filename) || + py::isinstance(filename, PathLike)) + { self->py_file = py::module_::import("io").attr("open")(filename, "rb"); self->stream.close = &close_file_callback; } else { @@ -511,13 +514,13 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, return self; } -static py::str +static py::object PyFT2Font_fname(PyFT2Font *self) { - if (self->stream.close) { // Called passed a filename to the constructor. + if (self->stream.close) { // User passed a filename to the constructor. return self->py_file.attr("name"); } else { - return py::cast(self->py_file); + return self->py_file; } } From 7ce8eae7264dcef06278d675afd5b23b86c8c93b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 22 Mar 2025 05:15:06 -0400 Subject: [PATCH 33/58] Add language parameter to Text objects --- doc/release/next_whats_new/text_language.rst | 37 +++++++++++++ lib/matplotlib/_text_helpers.py | 7 ++- lib/matplotlib/backends/backend_agg.py | 3 +- lib/matplotlib/backends/backend_pdf.py | 6 ++- lib/matplotlib/backends/backend_ps.py | 3 +- lib/matplotlib/ft2font.pyi | 7 ++- lib/matplotlib/mpl-data/matplotlibrc | 5 ++ lib/matplotlib/rcsetup.py | 1 + lib/matplotlib/tests/test_ft2font.py | 31 +++++++++++ lib/matplotlib/tests/test_text.py | 57 ++++++++++++++++++++ lib/matplotlib/text.py | 38 +++++++++++++ lib/matplotlib/text.pyi | 4 +- lib/matplotlib/textpath.py | 12 +++-- lib/matplotlib/textpath.pyi | 5 +- src/ft2font.cpp | 24 ++++++++- src/ft2font.h | 6 ++- src/ft2font_wrapper.cpp | 22 ++++++-- 17 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 doc/release/next_whats_new/text_language.rst diff --git a/doc/release/next_whats_new/text_language.rst b/doc/release/next_whats_new/text_language.rst new file mode 100644 index 000000000000..1d4668587b43 --- /dev/null +++ b/doc/release/next_whats_new/text_language.rst @@ -0,0 +1,37 @@ +Specifying text language +------------------------ + +OpenType fonts may support language systems which can be used to select different +typographic conventions, e.g., localized variants of letters that share a single Unicode +code point, or different default font features. The text API now supports setting a +language to be used and may be set/get with: + +- `matplotlib.text.Text.set_language` / `matplotlib.text.Text.get_language` +- Any API that creates a `.Text` object by passing the *language* argument (e.g., + ``plt.xlabel(..., language=...)``) + +The language of the text must be in a format accepted by libraqm, namely `a BCP47 +language code `_. If None or +unset, then no particular language will be implied, and default font settings will be +used. + +For example, Matplotlib's default font ``DejaVu Sans`` supports language-specific glyphs +in the Serbian and Macedonian languages in the Cyrillic alphabet (vs Russian), +or the Sámi family of languages in the Latin alphabet (vs English). + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(7, 3)) + + char = '\U00000431' + fig.text(0.5, 0.8, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.6, f'Serbian: {char}', fontsize=40, language='sr') + fig.text(1, 0.6, f'Russian: {char}', fontsize=40, language='ru', + horizontalalignment='right') + + char = '\U0000014a' + fig.text(0.5, 0.3, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.1, f'Inari Sámi: {char}', fontsize=40, language='smn') + fig.text(1, 0.1, f'English: {char}', fontsize=40, language='en', + horizontalalignment='right') diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index b9471c2c7e39..fa5d36bc99c8 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -26,7 +26,7 @@ def warn_on_missing_glyph(codepoint, fontnames): f"missing from font(s) {fontnames}.") -def layout(string, font, *, kern_mode=Kerning.DEFAULT): +def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None): """ Render *string* with *font*. @@ -41,6 +41,9 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): The font. kern_mode : Kerning A FreeType kerning mode. + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Yields ------ @@ -48,7 +51,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): """ x = 0 prev_glyph_index = None - char_to_font = font._get_fontmap(string) + char_to_font = font._get_fontmap(string) # TODO: Pass in language. base_font = font for char in string: # This has done the fallback logic diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index feb4b0c8be01..2da422a88e84 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -190,7 +190,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): font = self._prepare_font(prop) # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=get_hinting_flag()) + font.set_text(s, 0, flags=get_hinting_flag(), + language=mtext.get_language() if mtext is not None else None) font.draw_glyphs_to_bitmap( antialiased=gc.get_antialiased()) d = font.get_descent() / 64.0 diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index aaacc9589391..ebbc70eb68c8 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2338,6 +2338,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): return self.draw_mathtext(gc, x, y, s, prop, angle) fontsize = prop.get_size_in_points() + language = mtext.get_language() if mtext is not None else None if mpl.rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) @@ -2348,7 +2349,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: - font.set_text(s) + font.set_text(s, language=language) width, height = font.get_width_height() self.file._annotations[-1][1].append(_get_link_annotation( gc, x, y, width / 64, height / 64, angle)) @@ -2398,7 +2399,8 @@ def output_singlebyte_chunk(kerns_or_chars): prev_start_x = 0 # Emit all the 1-byte characters in a BT/ET group. self.file.output(Op.begin_text) - for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): + for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED, + language=language): if _font_supports_glyph(fonttype, ord(item.char)): if item.ft_object != prev_font: if singlebyte_chunk: diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index b0180de20f9f..14518a38c4ef 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -791,9 +791,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): thisx += width * scale else: + language = mtext.get_language() if mtext is not None else None font = self._get_font_ttf(prop) self._character_tracker.track(font, s) - for item in _text_helpers.layout(s, font): + for item in _text_helpers.layout(s, font, language=language): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) glyph_name = item.ft_object.get_glyph_name(item.glyph_index) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 55c076bb68b6..91d8d6a38818 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -243,7 +243,12 @@ class FT2Font(Buffer): def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... def set_text( - self, string: str, angle: float = ..., flags: LoadFlags = ... + self, + string: str, + angle: float = ..., + flags: LoadFlags = ..., + *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> NDArray[np.float64]: ... @property def ascender(self) -> int: ... diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 223eed396535..66a2569ca6f7 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -292,6 +292,11 @@ ## for more information on text properties #text.color: black +## The language of the text in a format accepted by libraqm, namely `a BCP47 language +## code `_. If None, then no +## particular language will be implied, and default font settings will be used. +#text.language: None + ## FreeType hinting flag ("foo" corresponds to FT_LOAD_FOO); may be one of the ## following (Proprietary Matplotlib-specific synonyms are given in parentheses, ## but their use is discouraged): diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b4224d169815..586365dcf3f2 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1045,6 +1045,7 @@ def _convert_validator_spec(key, conv): "text.kerning_factor": validate_int_or_None, "text.antialiased": validate_bool, "text.parse_math": validate_bool, + "text.language": validate_string_or_None, "mathtext.cal": validate_font_properties, "mathtext.rm": validate_font_properties, diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 70e611e17bcc..c464dddc051f 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -783,6 +783,37 @@ def test_ft2font_set_text(): assert font.get_bitmap_offset() == (6, 0) +@pytest.mark.parametrize( + 'input', + [ + [1, 2, 3], + [(1, 2)], + [('en', 'foo', 2)], + [('en', 1, 'foo')], + ], + ids=[ + 'nontuple', + 'wrong length', + 'wrong start type', + 'wrong end type', + ], +) +def test_ft2font_language_invalid(input): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + with pytest.raises(TypeError): + font.set_text('foo', language=input) + + +def test_ft2font_language(): + # This is just a smoke test. + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_text('foo') + font.set_text('foo', language='en') + font.set_text('foo', language=[('en', 1, 2)]) + + def test_ft2font_loading(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 8dba63eeef32..4d8a7a59c731 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1202,3 +1202,60 @@ def test_ytick_rotation_mode(): tick.set_rotation(angle) plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) + + +@pytest.mark.parametrize( + 'input, match', + [ + ([1, 2, 3], 'must be list of tuple'), + ([(1, 2)], 'must be list of tuple'), + ([('en', 'foo', 2)], 'start location must be int'), + ([('en', 1, 'foo')], 'end location must be int'), + ], +) +def test_text_language_invalid(input, match): + with pytest.raises(TypeError, match=match): + Text(0, 0, 'foo', language=input) + + +@image_comparison(baseline_images=['language.png'], remove_text=False, style='mpl20') +def test_text_language(): + fig = plt.figure(figsize=(5, 3)) + + t = fig.text(0, 0.8, 'Default', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.55, 'Lang A', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.3, 'Lang B', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.05, 'Mixed', fontsize=32) + assert t.get_language() is None + + # DejaVu Sans supports language-specific glyphs in the Serbian and Macedonian + # languages in the Cyrillic alphabet. + cyrillic = '\U00000431' + t = fig.text(0.4, 0.8, cyrillic, fontsize=32) + assert t.get_language() is None + t = fig.text(0.4, 0.55, cyrillic, fontsize=32, language='sr') + assert t.get_language() == 'sr' + t = fig.text(0.4, 0.3, cyrillic, fontsize=32) + t.set_language('ru') + assert t.get_language() == 'ru' + t = fig.text(0.4, 0.05, cyrillic * 4, fontsize=32, + language=[('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)]) + assert t.get_language() == (('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)) + + # Or the Sámi family of languages in the Latin alphabet. + latin = '\U0000014a' + t = fig.text(0.7, 0.8, latin, fontsize=32) + assert t.get_language() is None + with plt.rc_context({'text.language': 'en'}): + t = fig.text(0.7, 0.55, latin, fontsize=32) + assert t.get_language() == 'en' + t = fig.text(0.7, 0.3, latin, fontsize=32, language='smn') + assert t.get_language() == 'smn' + # Tuples are not documented, but we'll allow it. + t = fig.text(0.7, 0.05, latin * 4, fontsize=32) + t.set_language((('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4))) + assert t.get_language() == ( + ('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4)) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index acde4fb179a2..4d80f9874941 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2,6 +2,7 @@ Classes for including text in a figure. """ +from collections.abc import Sequence import functools import logging import math @@ -136,6 +137,7 @@ def __init__(self, super().__init__() self._x, self._y = x, y self._text = '' + self.set_language(None) self._reset_visual_defaults( text=text, color=color, @@ -1422,6 +1424,42 @@ def _va_for_angle(self, angle): return 'baseline' if anchor_at_left else 'top' return 'top' if anchor_at_left else 'baseline' + def get_language(self): + """Return the language this Text is in.""" + return self._language + + def set_language(self, language): + """ + Set the language of the text. + + Parameters + ---------- + language : str or None + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + If None, then defaults to :rc:`text.language`. + """ + _api.check_isinstance((Sequence, str, None), language=language) + language = mpl._val_or_rc(language, 'text.language') + + if not cbook.is_scalar_or_string(language): + language = tuple(language) + for val in language: + if not isinstance(val, tuple) or len(val) != 3: + raise TypeError('language must be list of tuple, not {language!r}') + sublang, start, end = val + if not isinstance(sublang, str): + raise TypeError( + 'sub-language specification must be str, not {sublang!r}') + if not isinstance(start, int): + raise TypeError('start location must be int, not {start!r}') + if not isinstance(end, int): + raise TypeError('end location must be int, not {end!r}') + + self._language = language + self.stale = True + class OffsetFrom: """Callable helper class for working with `Annotation`.""" diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 41c7b761ae32..eb3c076b1c5c 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -14,7 +14,7 @@ from .transforms import ( Transform, ) -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from typing import Any, Literal from .typing import ColorType, CoordsType @@ -108,6 +108,8 @@ class Text(Artist): def set_antialiased(self, antialiased: bool) -> None: ... def _ha_for_angle(self, angle: Any) -> Literal['center', 'right', 'left'] | None: ... def _va_for_angle(self, angle: Any) -> Literal['center', 'top', 'baseline'] | None: ... + def get_language(self) -> str | tuple[tuple[str, int, int], ...] | None: ... + def set_language(self, language: str | Sequence[tuple[str, int, int]] | None) -> None: ... class OffsetFrom: def __init__( diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 6a24197d3e43..6f6f4daa4cfa 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -67,7 +67,7 @@ def get_text_width_height_descent(self, s, prop, ismath): d /= 64.0 return w * scale, h * scale, d * scale - def get_text_path(self, prop, s, ismath=False): + def get_text_path(self, prop, s, ismath=False, *, language=None): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). @@ -80,6 +80,9 @@ def get_text_path(self, prop, s, ismath=False): The text to be converted. ismath : {False, True, "TeX"} If True, use mathtext parser. If "TeX", use tex for rendering. + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Returns ------- @@ -107,7 +110,8 @@ def get_text_path(self, prop, s, ismath=False): glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) - glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) + glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s, + language=language) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) @@ -128,7 +132,7 @@ def get_text_path(self, prop, s, ismath=False): return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, - return_new_glyphs_only=False): + return_new_glyphs_only=False, *, language=None): """ Convert string *s* to vertices and codes using the provided ttf font. """ @@ -143,7 +147,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_reprs = [] - for item in _text_helpers.layout(s, font): + for item in _text_helpers.layout(s, font, language=language): glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) glyph_reprs.append(glyph_repr) xpositions.append(item.x) diff --git a/lib/matplotlib/textpath.pyi b/lib/matplotlib/textpath.pyi index 34d4e92ac47e..b83b337aa541 100644 --- a/lib/matplotlib/textpath.pyi +++ b/lib/matplotlib/textpath.pyi @@ -16,7 +16,8 @@ class TextToPath: self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"] ) -> tuple[float, float, float]: ... def get_text_path( - self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ... + self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ..., *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> list[np.ndarray]: ... def get_glyphs_with_font( self, @@ -24,6 +25,8 @@ class TextToPath: s: str, glyph_map: dict[str, tuple[np.ndarray, np.ndarray]] | None = ..., return_new_glyphs_only: bool = ..., + *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> tuple[ list[tuple[str, float, float, float]], dict[str, tuple[np.ndarray, np.ndarray]], diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 890fc61974b0..e3352e901c2b 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -319,7 +319,9 @@ void FT2Font::set_kerning_factor(int factor) } void FT2Font::set_text( - std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, + LanguageType languages, + std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -358,6 +360,16 @@ void FT2Font::set_text( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language between {} and {} characters "_s + "to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } if (!raqm_layout(rq)) { throw std::runtime_error("failed to layout text"); } @@ -422,6 +434,16 @@ void FT2Font::set_text( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language between {} and {} characters "_s + "to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } if (!raqm_layout(rq)) { throw std::runtime_error("failed to layout text"); } diff --git a/src/ft2font.h b/src/ft2font.h index ffaf511ab9ca..b468804b4830 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -100,6 +101,9 @@ extern FT_Library _ft2Library; class FT2Font { public: + using LanguageRange = std::tuple; + using LanguageType = std::optional>; + FT2Font(long hinting_factor, std::vector &fallback_list, bool warn_if_used); virtual ~FT2Font(); @@ -110,7 +114,7 @@ class FT2Font void set_charmap(int i); void select_charmap(unsigned long i); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, - std::vector &xys); + LanguageType languages, std::vector &xys); int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 65fcb4b7e013..186bf7864dc2 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -693,7 +693,8 @@ const char *PyFT2Font_set_text__doc__ = R"""( static py::array_t PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, - std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT, + std::variant languages_or_str = nullptr) { std::vector xys; LoadFlags flags; @@ -713,7 +714,21 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 throw py::type_error("flags must be LoadFlags or int"); } - self->set_text(text, angle, static_cast(flags), xys); + FT2Font::LanguageType languages; + if (auto value = std::get_if(&languages_or_str)) { + languages = std::move(*value); + } else if (auto value = std::get_if(&languages_or_str)) { + languages = std::vector{ + FT2Font::LanguageRange{*value, 0, text.size()} + }; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("languages must be str or list of tuple"); + } + + self->set_text(text, angle, static_cast(flags), languages, xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -1534,7 +1549,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) .def("set_text", &PyFT2Font_set_text, - "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, + "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), + "language"_a=nullptr, PyFT2Font_set_text__doc__) .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, PyFT2Font_get_fontmap__doc__) From b35e5cd9ded818b83a9feeb43a41606cf56219a0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 23 May 2025 07:24:32 -0400 Subject: [PATCH 34/58] ft2font: Split layouting from set_text The former may be used even on PS/PDF backend where nothing is rendered. --- src/ft2font.cpp | 52 ++++++++++++++++++++++++++++--------------------- src/ft2font.h | 3 +++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index e3352e901c2b..6f3db040f17d 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -318,29 +318,13 @@ void FT2Font::set_kerning_factor(int factor) } } -void FT2Font::set_text( - std::u32string_view text, double angle, FT_Int32 flags, +std::vector FT2Font::layout( + std::u32string_view text, FT_Int32 flags, LanguageType languages, - std::vector &xys) + std::set& glyph_seen_fonts) { - FT_Matrix matrix; /* transformation matrix */ - - angle = angle * (2 * M_PI / 360.0); - - // this computes width and height in subpixels so we have to multiply by 64 - double cosangle = cos(angle) * 0x10000L; - double sinangle = sin(angle) * 0x10000L; - - matrix.xx = (FT_Fixed)cosangle; - matrix.xy = (FT_Fixed)-sinangle; - matrix.yx = (FT_Fixed)sinangle; - matrix.yy = (FT_Fixed)cosangle; - clear(); - bbox.xMin = bbox.yMin = 32000; - bbox.xMax = bbox.yMax = -32000; - auto rq = raqm_create(); if (!rq) { throw std::runtime_error("failed to compute text layout"); @@ -375,7 +359,6 @@ void FT2Font::set_text( } std::vector> face_substitutions; - std::set glyph_seen_fonts; glyph_seen_fonts.insert(face->family_name); // Attempt to use fallback fonts if necessary. @@ -452,9 +435,34 @@ void FT2Font::set_text( size_t num_glyphs = 0; auto const& rq_glyphs = raqm_get_glyphs(rq, &num_glyphs); - for (size_t i = 0; i < num_glyphs; i++) { - auto const& rglyph = rq_glyphs[i]; + return std::vector(rq_glyphs, rq_glyphs + num_glyphs); +} + +void FT2Font::set_text( + std::u32string_view text, double angle, FT_Int32 flags, + LanguageType languages, + std::vector &xys) +{ + FT_Matrix matrix; /* transformation matrix */ + + angle = angle * (2 * M_PI / 360.0); + + // this computes width and height in subpixels so we have to multiply by 64 + double cosangle = cos(angle) * 0x10000L; + double sinangle = sin(angle) * 0x10000L; + + matrix.xx = (FT_Fixed)cosangle; + matrix.xy = (FT_Fixed)-sinangle; + matrix.yx = (FT_Fixed)sinangle; + matrix.yy = (FT_Fixed)cosangle; + + std::set glyph_seen_fonts; + auto rq_glyphs = layout(text, flags, languages, glyph_seen_fonts); + + bbox.xMin = bbox.yMin = 32000; + bbox.xMax = bbox.yMax = -32000; + for (auto const& rglyph : rq_glyphs) { // Warn for missing glyphs. if (rglyph.index == 0) { ft_glyph_warn(text[rglyph.cluster], glyph_seen_fonts); diff --git a/src/ft2font.h b/src/ft2font.h index b468804b4830..841c66cfb5ee 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -113,6 +113,9 @@ class FT2Font void set_size(double ptsize, double dpi); void set_charmap(int i); void select_charmap(unsigned long i); + std::vector layout(std::u32string_view text, FT_Int32 flags, + LanguageType languages, + std::set& glyph_seen_fonts); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, LanguageType languages, std::vector &xys); int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); From e422defc007d6b06106a7bdc2dd792d2295c6fd1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Apr 2025 02:30:06 -0400 Subject: [PATCH 35/58] Remove dead code from Auto{Height,Width}Char --- lib/matplotlib/_mathtext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index b85cdffd6d88..594439813b53 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -1531,7 +1531,7 @@ class AutoHeightChar(Hlist): """ def __init__(self, c: str, height: float, depth: float, state: ParserState, - always: bool = False, factor: float | None = None): + factor: float | None = None): alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) x_height = state.fontset.get_xheight(state.font, state.fontsize, state.dpi) @@ -1568,7 +1568,7 @@ class AutoWidthChar(Hlist): always just return a scaled version of the glyph. """ - def __init__(self, c: str, width: float, state: ParserState, always: bool = False, + def __init__(self, c: str, width: float, state: ParserState, char_class: type[Char] = Char): alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) @@ -2706,7 +2706,7 @@ def sqrt(self, toks: ParseResults) -> T.Any: # the height so it doesn't seem cramped height = body.height - body.shift_amount + 5 * thickness depth = body.depth + body.shift_amount - check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True) + check = AutoHeightChar(r'\__sqrt__', height, depth, state) height = check.height - check.shift_amount depth = check.depth + check.shift_amount From 3ba2c1321cb565a1e7103d3f9f502132e1a9b324 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 17 Apr 2025 17:04:39 -0400 Subject: [PATCH 36/58] Fix auto-sized glyphs with BaKoMa fonts If the larger glyphs for an auto-sized character in `cmex10` uses a character that is in the `latex_to_bakoma` table, then it will be mapped an extra time into `cmr10` (usually). Thus we end up with a large version of a "normal" character, such as an exclamation point. Instead map these glyphs through the `latex_to_bakoma` table by using their glyph names as "commands". This ensures they don't get double-mapped to the wrong font and fixes the following issues: - slash (/) uses a comma at the larger sizes - right parenthesis uses an exclamation point at the largest size - left and right braces use parentheses at the largest size - right floor uses a percentage sign at the largest size - left ceiling uses an ampersand at the largest size Also, drop the regular size braces, as they are the same as the first `big`-sized version. --- lib/matplotlib/_mathtext.py | 84 ++++++++++----------------- lib/matplotlib/_mathtext_data.py | 71 ++++++++++++++++++++++ lib/matplotlib/tests/test_mathtext.py | 13 ++++- 3 files changed, 114 insertions(+), 54 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 594439813b53..d628b18aebf4 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -482,60 +482,40 @@ def _get_glyph(self, fontname: str, font_class: str, else: return self._stix_fallback._get_glyph(fontname, font_class, sym) - # The Bakoma fonts contain many pre-sized alternatives for the - # delimiters. The AutoSizedChar class will use these alternatives - # and select the best (closest sized) glyph. + # The Bakoma fonts contain many pre-sized alternatives for the delimiters. The + # Auto(Height|Width)Char classes will use these alternatives and select the best + # (closest sized) glyph. + _latex_sizes = ('big', 'Big', 'bigg', 'Bigg') _size_alternatives = { - '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'), - ('ex', '\xb5'), ('ex', '\xc3')], - ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'), - ('ex', '\xb6'), ('ex', '\x21')], - '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'), - ('ex', '\xbd'), ('ex', '\x28')], - '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'), - ('ex', '\xbe'), ('ex', '\x29')], - # The fourth size of '[' is mysteriously missing from the BaKoMa - # font, so I've omitted it for both '[' and ']' - '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'), - ('ex', '\x22')], - ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'), - ('ex', '\x23')], - r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'), - ('ex', '\xb9'), ('ex', '\x24')], - r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'), - ('ex', '\xba'), ('ex', '\x25')], - r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'), - ('ex', '\xbb'), ('ex', '\x26')], - r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'), - ('ex', '\xbc'), ('ex', '\x27')], - r'\langle': [('ex', '\xad'), ('ex', '\x44'), - ('ex', '\xbf'), ('ex', '\x2a')], - r'\rangle': [('ex', '\xae'), ('ex', '\x45'), - ('ex', '\xc0'), ('ex', '\x2b')], - r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'), - ('ex', '\x72'), ('ex', '\x73')], - r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'), - ('ex', '\xc2'), ('ex', '\x2d')], - r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'), - ('ex', '\xcb'), ('ex', '\x2c')], - r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'), - ('ex', '\x64')], - r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'), - ('ex', '\x67')], - r'<': [('cal', 'h'), ('ex', 'D')], - r'>': [('cal', 'i'), ('ex', 'E')] - } + '(': [('rm', '('), *[('ex', fr'\__parenleft{s}__') for s in _latex_sizes]], + ')': [('rm', ')'), *[('ex', fr'\__parenright{s}__') for s in _latex_sizes]], + '{': [('ex', fr'\__braceleft{s}__') for s in _latex_sizes], + '}': [('ex', fr'\__braceright{s}__') for s in _latex_sizes], + '[': [('rm', '['), *[('ex', fr'\__bracketleft{s}__') for s in _latex_sizes]], + ']': [('rm', ']'), *[('ex', fr'\__bracketright{s}__') for s in _latex_sizes]], + '<': [('cal', r'\__angbracketleft__'), + *[('ex', fr'\__angbracketleft{s}__') for s in _latex_sizes]], + '>': [('cal', r'\__angbracketright__'), + *[('ex', fr'\__angbracketright{s}__') for s in _latex_sizes]], + r'\lfloor': [('ex', fr'\__floorleft{s}__') for s in _latex_sizes], + r'\rfloor': [('ex', fr'\__floorright{s}__') for s in _latex_sizes], + r'\lceil': [('ex', fr'\__ceilingleft{s}__') for s in _latex_sizes], + r'\rceil': [('ex', fr'\__ceilingright{s}__') for s in _latex_sizes], + r'\__sqrt__': [('ex', fr'\__radical{s}__') for s in _latex_sizes], + r'\backslash': [('ex', fr'\__backslash{s}__') for s in _latex_sizes], + r'/': [('rm', '/'), *[('ex', fr'\__slash{s}__') for s in _latex_sizes]], + r'\widehat': [('rm', '\x5e'), ('ex', r'\__hatwide__'), ('ex', r'\__hatwider__'), + ('ex', r'\__hatwidest__')], + r'\widetilde': [('rm', '\x7e'), ('ex', r'\__tildewide__'), + ('ex', r'\__tildewider__'), ('ex', r'\__tildewidest__')], + } - for alias, target in [(r'\leftparen', '('), - (r'\rightparen', ')'), - (r'\leftbrace', '{'), - (r'\rightbrace', '}'), - (r'\leftbracket', '['), - (r'\rightbracket', ']'), - (r'\{', '{'), - (r'\}', '}'), - (r'\[', '['), - (r'\]', ']')]: + for alias, target in [(r'\leftparen', '('), (r'\rightparen', ')'), + (r'\leftbrace', '{'), (r'\rightbrace', '}'), + (r'\leftbracket', '['), (r'\rightbracket', ']'), + (r'\langle', '<'), (r'\rangle', '>'), + (r'\{', '{'), (r'\}', '}'), + (r'\[', '['), (r'\]', ']')]: _size_alternatives[alias] = _size_alternatives[target] def get_sized_alternatives_for_symbol(self, fontname: str, diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index 0451791e9f26..f8b7c9ac2c33 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -36,6 +36,75 @@ '{' : ('cmex10', 0xa9), '}' : ('cmex10', 0xaa), + '\\__angbracketleft__' : ('cmsy10', 0x68), + '\\__angbracketright__' : ('cmsy10', 0x69), + '\\__angbracketleftbig__' : ('cmex10', 0xad), + '\\__angbracketleftBig__' : ('cmex10', 0x44), + '\\__angbracketleftbigg__' : ('cmex10', 0xbf), + '\\__angbracketleftBigg__' : ('cmex10', 0x2a), + '\\__angbracketrightbig__' : ('cmex10', 0xae), + '\\__angbracketrightBig__' : ('cmex10', 0x45), + '\\__angbracketrightbigg__' : ('cmex10', 0xc0), + '\\__angbracketrightBigg__' : ('cmex10', 0x2b), + '\\__backslashbig__' : ('cmex10', 0xb2), + '\\__backslashBig__' : ('cmex10', 0x2f), + '\\__backslashbigg__' : ('cmex10', 0xc2), + '\\__backslashBigg__' : ('cmex10', 0x2d), + '\\__braceleftbig__' : ('cmex10', 0xa9), + '\\__braceleftBig__' : ('cmex10', 0x6e), + '\\__braceleftbigg__' : ('cmex10', 0xbd), + '\\__braceleftBigg__' : ('cmex10', 0x28), + '\\__bracerightbig__' : ('cmex10', 0xaa), + '\\__bracerightBig__' : ('cmex10', 0x6f), + '\\__bracerightbigg__' : ('cmex10', 0xbe), + '\\__bracerightBigg__' : ('cmex10', 0x29), + '\\__bracketleftbig__' : ('cmex10', 0xa3), + '\\__bracketleftBig__' : ('cmex10', 0x68), + '\\__bracketleftbigg__' : ('cmex10', 0x2219), + '\\__bracketleftBigg__' : ('cmex10', 0x22), + '\\__bracketrightbig__' : ('cmex10', 0xa4), + '\\__bracketrightBig__' : ('cmex10', 0x69), + '\\__bracketrightbigg__' : ('cmex10', 0xb8), + '\\__bracketrightBigg__' : ('cmex10', 0x23), + '\\__ceilingleftbig__' : ('cmex10', 0xa7), + '\\__ceilingleftBig__' : ('cmex10', 0x6c), + '\\__ceilingleftbigg__' : ('cmex10', 0xbb), + '\\__ceilingleftBigg__' : ('cmex10', 0x26), + '\\__ceilingrightbig__' : ('cmex10', 0xa8), + '\\__ceilingrightBig__' : ('cmex10', 0x6d), + '\\__ceilingrightbigg__' : ('cmex10', 0xbc), + '\\__ceilingrightBigg__' : ('cmex10', 0x27), + '\\__floorleftbig__' : ('cmex10', 0xa5), + '\\__floorleftBig__' : ('cmex10', 0x6a), + '\\__floorleftbigg__' : ('cmex10', 0xb9), + '\\__floorleftBigg__' : ('cmex10', 0x24), + '\\__floorrightbig__' : ('cmex10', 0xa6), + '\\__floorrightBig__' : ('cmex10', 0x6b), + '\\__floorrightbigg__' : ('cmex10', 0xba), + '\\__floorrightBigg__' : ('cmex10', 0x25), + '\\__hatwide__' : ('cmex10', 0x62), + '\\__hatwider__' : ('cmex10', 0x63), + '\\__hatwidest__' : ('cmex10', 0x64), + '\\__parenleftbig__' : ('cmex10', 0xa1), + '\\__parenleftBig__' : ('cmex10', 0xb3), + '\\__parenleftbigg__' : ('cmex10', 0xb5), + '\\__parenleftBigg__' : ('cmex10', 0xc3), + '\\__parenrightbig__' : ('cmex10', 0xa2), + '\\__parenrightBig__' : ('cmex10', 0xb4), + '\\__parenrightbigg__' : ('cmex10', 0xb6), + '\\__parenrightBigg__' : ('cmex10', 0x21), + '\\__radicalbig__' : ('cmex10', 0x70), + '\\__radicalBig__' : ('cmex10', 0x71), + '\\__radicalbigg__' : ('cmex10', 0x72), + '\\__radicalBigg__' : ('cmex10', 0x73), + '\\__slashbig__' : ('cmex10', 0xb1), + '\\__slashBig__' : ('cmex10', 0x2e), + '\\__slashbigg__' : ('cmex10', 0xc1), + '\\__slashBigg__' : ('cmex10', 0x2c), + '\\__tildewide__' : ('cmex10', 0x65), + '\\__tildewider__' : ('cmex10', 0x66), + '\\__tildewidest__' : ('cmex10', 0x67), + ',' : ('cmmi10', 0x3b), '.' : ('cmmi10', 0x3a), '/' : ('cmmi10', 0x3d), @@ -1112,6 +1181,8 @@ '|' : 0x2016, '}' : 0x7d, } +tex2uni['__angbracketleft__'] = tex2uni['langle'] +tex2uni['__angbracketright__'] = tex2uni['rangle'] # Each element is a 4-tuple of the form: # src_start, src_end, dst_font, dst_start diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 4fc04a627dd5..5d0245bc5049 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -125,12 +125,21 @@ r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799 r'$\left(X\right)_{a}^{b}$', # github issue 7615 r'$\dfrac{\$100.00}{y}$', # github issue #1888 - r'$a=-b-c$' # github issue #28180 + r'$a=-b-c$', # github issue #28180 ] # 'svgastext' tests switch svg output to embed text as text (rather than as # paths). svgastext_math_tests = [ r'$-$-', + # Check all AutoHeightChar substitutions. + *[ + r'$\left' + lc + r' M \middle/ ? \middle\backslash ? \right' + rc + ' ' + # Normal size. + r'\left' + lc + r' \frac{M}{B} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # big size. + r'\left' + lc + r' \frac{\frac{M}{I}}{B} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # bigg size. + r'\left' + lc + r' \frac{\frac{M}{I}}{\frac{B}{U}} \middle/ ? \middle\backslash ? \right' + rc + ' ' + # Big size. + r'\left' + lc + r'\frac{\frac{\frac{M}{I}}{N}}{\frac{\frac{B}{U}}{G}} \middle/ ? \middle\backslash ? \right' + rc + '$' # Bigg size. + for lc, rc in ['()', '[]', '<>', (r'\{', r'\}'), (r'\lfloor', r'\rfloor'), (r'\lceil', r'\rceil')] + ], ] # 'lightweight' tests test only a single fontset (dejavusans, which is the # default) and only png outputs, in order to minimize the size of baseline @@ -237,7 +246,7 @@ def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): mpl.rcParams['svg.fonttype'] = 'none' # Minimize image size. fig = plt.figure(figsize=(5.25, 0.75)) fig.patch.set(visible=False) # Minimize image size. - fig.text(0.5, 0.5, text, + fig.text(0.5, 0.5, text, fontsize=16, horizontalalignment='center', verticalalignment='center') From b70fb888657ce566950a5f83b3e6e88b4c44404d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Sep 2025 05:21:04 -0400 Subject: [PATCH 37/58] pdf: Improve text with characters outside embedded font limits For character codes outside the embedded font limits (256 for type 3 and 65536 for type 42), we output them as XObjects instead of using text commands. But there is nothing in the PDF spec that requires any specific encoding like this. Since we now support subsetting all fonts before embedding, split each font into groups based on the maximum character code (e.g., 256-entry groups for type 3), then switch text strings to a different font subset and re-map character codes to it when necessary. This means all text is true text (albeit with some strange encoding), and we no longer need any XObjects for glyphs. For users of non-English text, this means it will become selectable and copyable again. Fixes #21797 --- .../deprecations/30512-ES.rst | 3 + lib/matplotlib/backends/backend_pdf.py | 245 +++++------------- 2 files changed, 71 insertions(+), 177 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30512-ES.rst diff --git a/doc/api/next_api_changes/deprecations/30512-ES.rst b/doc/api/next_api_changes/deprecations/30512-ES.rst new file mode 100644 index 000000000000..f235964c5502 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30512-ES.rst @@ -0,0 +1,3 @@ +``PdfFile.multi_byte_charprocs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated with no replacement. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index ebbc70eb68c8..06fa09793553 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -19,7 +19,6 @@ import sys import time import types -import warnings import zlib import numpy as np @@ -369,19 +368,10 @@ def pdfRepr(obj): "objects") -def _font_supports_glyph(fonttype, glyph): - """ - Returns True if the font is able to provide codepoint *glyph* in a PDF. - - For a Type 3 font, this method returns True only for single-byte - characters. For Type 42 fonts this method return True if the character is - from the Basic Multilingual Plane. - """ - if fonttype == 3: - return glyph <= 255 - if fonttype == 42: - return glyph <= 65535 - raise NotImplementedError() +_FONT_MAX_GLYPH = { + 3: 256, + 42: 65536, +} class Reference: @@ -700,7 +690,8 @@ def __init__(self, filename, metadata=None): self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1)) self._fontNames = {} # maps filenames to internal font names self._dviFontInfo = {} # maps pdf names to dvifonts - self._character_tracker = _backend_pdf_ps.CharacterTracker() + self._character_tracker = _backend_pdf_ps.CharacterTracker( + _FONT_MAX_GLYPH.get(mpl.rcParams['pdf.fonttype'], 0)) self.alphaStates = {} # maps alpha values to graphics state objects self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) @@ -715,7 +706,6 @@ def __init__(self, filename, metadata=None): self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) self.markers = {} - self.multi_byte_charprocs = {} self.paths = [] @@ -742,6 +732,7 @@ def __init__(self, filename, metadata=None): self.writeObject(self.resourceObject, resources) fontNames = _api.deprecated("3.11")(property(lambda self: self._fontNames)) + multi_byte_charprocs = _api.deprecated("3.11")(property(lambda _: {})) type1Descriptors = _api.deprecated("3.11")(property(lambda _: {})) @_api.deprecated("3.11") @@ -829,7 +820,7 @@ def toStr(n, base): @staticmethod def _get_subsetted_psname(ps_name, charmap): - return PdfFile._get_subset_prefix(frozenset(charmap.keys())) + ps_name + return PdfFile._get_subset_prefix(frozenset(charmap.values())) + ps_name def finalize(self): """Write out the various deferred objects and the pdf end matter.""" @@ -845,8 +836,6 @@ def finalize(self): name: ob for image, name, ob in self._images.values()} for tup in self.markers.values(): xobjects[tup[0]] = tup[1] - for name, value in self.multi_byte_charprocs.items(): - xobjects[name] = value for name, path, trans, ob, join, cap, padding, filled, stroked \ in self.paths: xobjects[name] = ob @@ -903,7 +892,7 @@ def _write_annotations(self): for annotsObject, annotations in self._annotations: self.writeObject(annotsObject, annotations) - def fontName(self, fontprop): + def fontName(self, fontprop, subset=0): """ Select a font based on fontprop and return a name suitable for ``Op.selectfont``. If fontprop is a string, it will be interpreted @@ -920,13 +909,13 @@ def fontName(self, fontprop): filenames = _fontManager._find_fonts_by_props(fontprop) first_Fx = None for fname in filenames: - Fx = self._fontNames.get(fname) + Fx = self._fontNames.get((fname, subset)) if not first_Fx: first_Fx = Fx if Fx is None: Fx = next(self._internal_font_seq) - self._fontNames[fname] = Fx - _log.debug('Assigning font %s = %r', Fx, fname) + self._fontNames[(fname, subset)] = Fx + _log.debug('Assigning font %s (subset %d) = %r', Fx, subset, fname) if not first_Fx: first_Fx = Fx @@ -950,9 +939,8 @@ def writeFonts(self): for pdfname, dvifont in sorted(self._dviFontInfo.items()): _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) - for filename in sorted(self._fontNames): - Fx = self._fontNames[filename] - _log.debug('Embedding font %s.', filename) + for (filename, subset), Fx in sorted(self._fontNames.items()): + _log.debug('Embedding font %s:%d.', filename, subset) if filename.endswith('.afm'): # from pdf.use14corefonts _log.debug('Writing AFM font.') @@ -960,7 +948,7 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - charmap = self._character_tracker.used.get((filename, 0)) + charmap = self._character_tracker.used.get((filename, subset)) if charmap: fonts[Fx] = self.embedTTF(filename, charmap) self.writeObject(self.fontObject, fonts) @@ -1108,13 +1096,6 @@ def createType1Descriptor(self, t1font, fontfile=None): return fontdescObject - def _get_xobject_glyph_name(self, filename, glyph_name): - Fx = self.fontName(filename) - return "-".join([ - Fx.name.decode(), - os.path.splitext(os.path.basename(filename))[0], - glyph_name]) - _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin begincmap @@ -1159,7 +1140,7 @@ def embedTTFType3(font, charmap, descriptor): fontdictObject = self.reserveObject('font dictionary') charprocsObject = self.reserveObject('character procs') differencesArray = [] - firstchar, lastchar = 0, 255 + firstchar, lastchar = min(charmap), max(charmap) bbox = [cvt(x, nearest=False) for x in font.bbox] fontdict = { @@ -1181,32 +1162,19 @@ def embedTTFType3(font, charmap, descriptor): # Make the "Widths" array def get_char_width(charcode): - width = font.load_char( - charcode, + width = font.load_glyph( + charmap.get(charcode, 0), flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) - with warnings.catch_warnings(): - # Ignore 'Required glyph missing from current font' warning - # from ft2font: here we're just building the widths table, but - # the missing glyphs may not even be used in the actual string. - warnings.filterwarnings("ignore") - widths = [get_char_width(charcode) - for charcode in range(firstchar, lastchar+1)] + widths = [get_char_width(charcode) + for charcode in range(firstchar, lastchar+1)] descriptor['MaxWidth'] = max(widths) - # Make the "Differences" array, sort the ccodes < 255 from - # the multi-byte ccodes, and build the whole set of glyph ids - # that we need from this font. - differences = [] - multi_byte_chars = set() - for ccode, gind in charmap.items(): - glyph_name = font.get_glyph_name(gind) - if ccode is not None and ccode <= 255: - differences.append((ccode, glyph_name)) - else: - multi_byte_chars.add(glyph_name) - differences.sort() - + # Make the "Differences" array with the whole set of character codes that we + # need from this font. + differences = sorted([ + (ccode, font.get_glyph_name(gind)) for ccode, gind in charmap.items() + ]) last_c = -2 for c, name in differences: if c != last_c + 1: @@ -1219,30 +1187,9 @@ def get_char_width(charcode): charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] - charprocDict = {} - # The 2-byte characters are used as XObjects, so they - # need extra info in their dictionary - if charname in multi_byte_chars: - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} - # Each glyph includes bounding box information, - # but xpdf and ghostscript can't handle it in a - # Form XObject (they segfault!!!), so we remove it - # from the stream here. It's not needed anyway, - # since the Form XObject includes it in its BBox - # value. - stream = stream[stream.find(b"d1") + 2:] charprocObject = self.reserveObject('charProc') - self.outputStream(charprocObject, stream, extra=charprocDict) - - # Send the glyphs with ccode > 255 to the XObject dictionary, - # and the others to the font itself - if charname in multi_byte_chars: - name = self._get_xobject_glyph_name(filename, charname) - self.multi_byte_charprocs[name] = charprocObject - else: - charprocs[charname] = charprocObject + self.outputStream(charprocObject, stream) + charprocs[charname] = charprocObject # Write everything out self.writeObject(fontdictObject, fontdict) @@ -1271,9 +1218,6 @@ def embedTTFType42(font, charmap, descriptor): os.stat(filename).st_size, fontdata.getbuffer().nbytes ) - # We need this ref for XObjects - full_font = font - # reload the font object from the subset # (all the necessary data could probably be obtained directly # using fontLib.ttLib) @@ -1351,32 +1295,6 @@ def embedTTFType42(font, charmap, descriptor): unicode_cmap = (self._identityToUnicodeCMap % (len(unicode_groups), b"\n".join(unicode_bfrange))) - # Add XObjects for unsupported chars - glyph_indices = [ - glyph_index for ccode, glyph_index in charmap.items() - if not _font_supports_glyph(fonttype, ccode) - ] - - bbox = [cvt(x, nearest=False) for x in full_font.bbox] - rawcharprocs = _get_pdf_charprocs(filename, glyph_indices) - for charname in sorted(rawcharprocs): - stream = rawcharprocs[charname] - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} - # Each glyph includes bounding box information, - # but xpdf and ghostscript can't handle it in a - # Form XObject (they segfault!!!), so we remove it - # from the stream here. It's not needed anyway, - # since the Form XObject includes it in its BBox - # value. - stream = stream[stream.find(b"d1") + 2:] - charprocObject = self.reserveObject('charProc') - self.outputStream(charprocObject, stream, extra=charprocDict) - - name = self._get_xobject_glyph_name(filename, charname) - self.multi_byte_charprocs[name] = charprocObject - # CIDToGIDMap stream cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be") self.outputStream(cidToGidMapObject, cid_to_gid_map) @@ -1396,10 +1314,7 @@ def embedTTFType42(font, charmap, descriptor): # Beginning of main embedTTF function... - ps_name = self._get_subsetted_psname( - font.postscript_name, - font.get_charmap() - ) + ps_name = self._get_subsetted_psname(font.postscript_name, charmap) ps_name = ps_name.encode('ascii', 'replace') ps_name = Name(ps_name) pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} @@ -2203,30 +2118,22 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.check_gc(gc, gc._rgb) prev_font = None, None oldx, oldy = 0, 0 - unsupported_chars = [] self.file.output(Op.begin_text) for font, fontsize, ccode, glyph_index, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, ccode, glyph_index) + subset_index, subset_charcode = self.file._character_tracker.track_glyph( + font, ccode, glyph_index) fontname = font.fname - if not _font_supports_glyph(fonttype, ccode): - # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in - # Type 42) must be emitted separately (below). - unsupported_chars.append((font, fontsize, ox, oy, glyph_index)) - else: - self._setup_textpos(ox, oy, 0, oldx, oldy) - oldx, oldy = ox, oy - if (fontname, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname), fontsize, - Op.selectfont) - prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(ccode), fonttype), - Op.show) + self._setup_textpos(ox, oy, 0, oldx, oldy) + oldx, oldy = ox, oy + if (fontname, subset_index, fontsize) != prev_font: + self.file.output(self.file.fontName(fontname, subset_index), fontsize, + Op.selectfont) + prev_font = fontname, subset_index, fontsize + self.file.output(self._encode_glyphs([subset_charcode], fonttype), + Op.show) self.file.output(Op.end_text) - for font, fontsize, ox, oy, glyph_index in unsupported_chars: - self._draw_xobject_glyph(font, fontsize, glyph_index, ox, oy) - # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: self.file.output(Op.gsave, ox, oy, width, height, @@ -2319,6 +2226,11 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): [0, 0]], pathops) self.draw_path(boxgc, path, mytrans, gc._rgb) + def _encode_glyphs(self, subset, fonttype): + if fonttype in (1, 3): + return bytes(subset) + return b''.join(glyph.to_bytes(2, 'big') for glyph in subset) + def encode_string(self, s, fonttype): match fonttype: case 1: @@ -2345,7 +2257,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fonttype = 1 else: font = self._get_font_ttf(prop) - self.file._character_tracker.track(font, s) fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: @@ -2365,23 +2276,23 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # A sequence of characters is broken into multiple chunks. The chunking # serves two purposes: - # - For Type 3 fonts, there is no way to access multibyte characters, - # as they cannot have a CIDMap. Therefore, in this case we break - # the string into chunks, where each chunk contains either a string - # of consecutive 1-byte characters or a single multibyte character. - # - A sequence of 1-byte characters is split into chunks to allow for - # kerning adjustments between consecutive chunks. + # - For Type 3 fonts, there is no way to access multibyte characters, as they + # cannot have a CIDMap. Therefore, in this case we break the string into + # chunks, where each chunk contains a string of consecutive 1-byte + # characters in a 256-character subset of the font. A distinct version of + # the original font is created for each 256-character subset. + # - A sequence of characters is split into chunks to allow for kerning + # adjustments between consecutive chunks. # - # Each chunk is emitted with a separate command: 1-byte characters use - # the regular text show command (TJ) with appropriate kerning between - # chunks, whereas multibyte characters use the XObject command (Do). + # Each chunk is emitted with the regular text show command (TJ) with appropriate + # kerning between chunks. else: def output_singlebyte_chunk(kerns_or_chars): self.file.output( # See pdf spec "Text space details" for the 1000/fontsize # (aka. 1000/T_fs) factor. [(-1000 * next(group) / fontsize) if tp == float # a kern - else self.encode_string("".join(group), fonttype) + else self._encode_glyphs(group, fonttype) for tp, group in itertools.groupby(kerns_or_chars, type)], Op.showkern) # Do the rotation and global translation as a single matrix @@ -2393,51 +2304,31 @@ def output_singlebyte_chunk(kerns_or_chars): x, y, Op.concat_matrix) # List of [prev_kern, char, char, ...] w/o zero kerns. singlebyte_chunk = [] - # List of (ft_object, start_x, glyph_index). - multibyte_glyphs = [] prev_font = None prev_start_x = 0 - # Emit all the 1-byte characters in a BT/ET group. + # Emit all the characters in a BT/ET group. self.file.output(Op.begin_text) for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED, language=language): - if _font_supports_glyph(fonttype, ord(item.char)): - if item.ft_object != prev_font: - if singlebyte_chunk: - output_singlebyte_chunk(singlebyte_chunk) - ft_name = self.file.fontName(item.ft_object.fname) - self.file.output(ft_name, fontsize, Op.selectfont) - self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) - singlebyte_chunk = [] - prev_font = item.ft_object - prev_start_x = item.x - if item.prev_kern: - singlebyte_chunk.append(item.prev_kern) - singlebyte_chunk.append(item.char) - else: - prev_font = None - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) + subset, charcode = self.file._character_tracker.track_glyph( + item.ft_object, ord(item.char), item.glyph_index) + if (item.ft_object, subset) != prev_font: + if singlebyte_chunk: + output_singlebyte_chunk(singlebyte_chunk) + ft_name = self.file.fontName(item.ft_object.fname, subset) + self.file.output(ft_name, fontsize, Op.selectfont) + self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) + singlebyte_chunk = [] + prev_font = (item.ft_object, subset) + prev_start_x = item.x + if item.prev_kern: + singlebyte_chunk.append(item.prev_kern) + singlebyte_chunk.append(charcode) if singlebyte_chunk: output_singlebyte_chunk(singlebyte_chunk) self.file.output(Op.end_text) - # Then emit all the multibyte characters, one at a time. - for ft_object, start_x, glyph_index in multibyte_glyphs: - self._draw_xobject_glyph( - ft_object, fontsize, glyph_index, start_x, 0 - ) self.file.output(Op.grestore) - def _draw_xobject_glyph(self, font, fontsize, glyph_index, x, y): - """Draw a multibyte character from a Type 3 font as an XObject.""" - glyph_name = font.get_glyph_name(glyph_index) - name = self.file._get_xobject_glyph_name(font.fname, glyph_name) - self.file.output( - Op.gsave, - 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, - Name(name), Op.use_xobject, - Op.grestore, - ) - def new_gc(self): # docstring inherited return GraphicsContextPdf(self.file) From 1c4af68657f0017055beada1c7172666cf147001 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Sep 2025 01:17:42 -0400 Subject: [PATCH 38/58] pdf: Correct Unicode mapping for out-of-range font chunks For Type 3 fonts, add a `ToUnicode` mapping (which was added in PDF 1.2), and for Type 42 fonts, correct the Unicode encoding, which should be UTF-16BE, not UCS2. --- lib/matplotlib/backends/_backend_pdf_ps.py | 19 ++++++ lib/matplotlib/backends/backend_pdf.py | 76 +++++++++++++--------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 1fdcccbab61a..1dde801d8665 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -205,6 +205,25 @@ def track_glyph( self.used.setdefault((font.fname, subset), {})[subset_charcode] = glyph return (subset, subset_charcode) + def subset_to_unicode(self, index: int, + charcode: CharacterCodeType) -> CharacterCodeType: + """ + Map a subset index and character code to a Unicode character code. + + Parameters + ---------- + index : int + The subset index within a font. + charcode : CharacterCodeType + The character code within a subset to map back. + + Returns + ------- + CharacterCodeType + The Unicode character code corresponding to the subsetted one. + """ + return index * self.subset_size + charcode + class RendererPDFPSBase(RendererBase): # The following attributes must be defined by the subclasses: diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 06fa09793553..0f7720b1022f 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -950,7 +950,7 @@ def writeFonts(self): _log.debug('Writing TrueType font.') charmap = self._character_tracker.used.get((filename, subset)) if charmap: - fonts[Fx] = self.embedTTF(filename, charmap) + fonts[Fx] = self.embedTTF(filename, subset, charmap) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1117,7 +1117,7 @@ def createType1Descriptor(self, t1font, fontfile=None): end end""" - def embedTTF(self, filename, charmap): + def embedTTF(self, filename, subset_index, charmap): """Embed the TTF font from the named file into the document.""" font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1133,12 +1133,40 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, charmap, descriptor): + def generate_unicode_cmap(subset_index, charmap): + # Make the ToUnicode CMap. + last_ccode = -2 + unicode_groups = [] + for ccode in sorted(charmap.keys()): + if ccode != last_ccode + 1: + unicode_groups.append([ccode, ccode]) + else: + unicode_groups[-1][1] = ccode + last_ccode = ccode + + width = 2 if fonttype == 3 else 4 + unicode_bfrange = [] + for start, end in unicode_groups: + real_start = self._character_tracker.subset_to_unicode(subset_index, + start) + real_end = self._character_tracker.subset_to_unicode(subset_index, end) + real_values = ' '.join('<%s>' % chr(x).encode('utf-16be').hex() + for x in range(real_start, real_end+1)) + unicode_bfrange.append( + f'<{start:0{width}x}> <{end:0{width}x}> [{real_values}]') + unicode_cmap = (self._identityToUnicodeCMap % + (len(unicode_groups), + '\n'.join(unicode_bfrange).encode('ascii'))) + + return unicode_cmap + + def embedTTFType3(font, subset_index, charmap, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') fontdictObject = self.reserveObject('font dictionary') charprocsObject = self.reserveObject('character procs') + toUnicodeMapObject = self.reserveObject('ToUnicode map') differencesArray = [] firstchar, lastchar = min(charmap), max(charmap) bbox = [cvt(x, nearest=False) for x in font.bbox] @@ -1157,8 +1185,9 @@ def embedTTFType3(font, charmap, descriptor): 'Encoding': { 'Type': Name('Encoding'), 'Differences': differencesArray}, - 'Widths': widthsObject - } + 'Widths': widthsObject, + 'ToUnicode': toUnicodeMapObject, + } # Make the "Widths" array def get_char_width(charcode): @@ -1191,15 +1220,18 @@ def get_char_width(charcode): self.outputStream(charprocObject, stream) charprocs[charname] = charprocObject + unicode_cmap = generate_unicode_cmap(subset_index, charmap) + # Write everything out self.writeObject(fontdictObject, fontdict) self.writeObject(fontdescObject, descriptor) self.writeObject(widthsObject, widths) self.writeObject(charprocsObject, charprocs) + self.outputStream(toUnicodeMapObject, unicode_cmap) return fontdictObject - def embedTTFType42(font, charmap, descriptor): + def embedTTFType42(font, subset_index, charmap, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1209,12 +1241,12 @@ def embedTTFType42(font, charmap, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - _log.debug("SUBSET %s characters: %s", filename, charmap) + _log.debug("SUBSET %s:%d characters: %s", filename, subset_index, charmap) with _backend_pdf_ps.get_glyphs_subset(filename, charmap.values()) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s %d -> %d", filename, + "SUBSET %s:%d %d -> %d", filename, subset_index, os.stat(filename).st_size, fontdata.getbuffer().nbytes ) @@ -1251,8 +1283,7 @@ def embedTTFType42(font, charmap, descriptor): fontfileObject, fontdata.getvalue(), extra={'Length1': fontdata.getbuffer().nbytes}) - # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap - # at the same time + # Make the 'W' (Widths) array and CidToGidMap at the same time. cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 @@ -1260,8 +1291,7 @@ def embedTTFType42(font, charmap, descriptor): glyph = font.load_glyph(gind, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) widths.append((ccode, cvt(glyph.horiAdvance))) - if ccode < 65536: - cid_to_gid_map[ccode] = chr(gind) + cid_to_gid_map[ccode] = chr(gind) max_ccode = max(ccode, max_ccode) widths.sort() cid_to_gid_map = cid_to_gid_map[:max_ccode + 1] @@ -1269,37 +1299,21 @@ def embedTTFType42(font, charmap, descriptor): last_ccode = -2 w = [] max_width = 0 - unicode_groups = [] for ccode, width in widths: if ccode != last_ccode + 1: w.append(ccode) w.append([width]) - unicode_groups.append([ccode, ccode]) else: w[-1].append(width) - unicode_groups[-1][1] = ccode max_width = max(max_width, width) last_ccode = ccode - unicode_bfrange = [] - for start, end in unicode_groups: - # Ensure the CID map contains only chars from BMP - if start > 65535: - continue - end = min(65535, end) - - unicode_bfrange.append( - b"<%04x> <%04x> [%s]" % - (start, end, - b" ".join(b"<%04x>" % x for x in range(start, end+1)))) - unicode_cmap = (self._identityToUnicodeCMap % - (len(unicode_groups), b"\n".join(unicode_bfrange))) - # CIDToGIDMap stream cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be") self.outputStream(cidToGidMapObject, cid_to_gid_map) # ToUnicode CMap + unicode_cmap = generate_unicode_cmap(subset_index, charmap) self.outputStream(toUnicodeMapObject, unicode_cmap) descriptor['MaxWidth'] = max_width @@ -1355,9 +1369,9 @@ def embedTTFType42(font, charmap, descriptor): } if fonttype == 3: - return embedTTFType3(font, charmap, descriptor) + return embedTTFType3(font, subset_index, charmap, descriptor) elif fonttype == 42: - return embedTTFType42(font, charmap, descriptor) + return embedTTFType42(font, subset_index, charmap, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" From 6cedcf7094696567e9ee761a43d2935b3c5bb577 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 3 Sep 2025 01:57:17 -0400 Subject: [PATCH 39/58] TST: Add emoji to multi-font text These characters are outside the BMP and should test subset splitting for type 42 output in PDF. --- lib/matplotlib/testing/__init__.py | 4 +++- lib/matplotlib/tests/test_backend_svg.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 6a9351ede7f6..d3a6265fab3b 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -276,11 +276,13 @@ def _gen_multi_font_text(): latin1_supplement = [chr(x) for x in range(start, 0xFF+1)] latin_extended_A = [chr(x) for x in range(0x100, 0x17F+1)] latin_extended_B = [chr(x) for x in range(0x180, 0x24F+1)] + non_basic_multilingual_plane = [chr(x) for x in range(0x1F600, 0x1F610)] count = itertools.count(start - 0xA0) non_basic_characters = '\n'.join( ''.join(line) for _, line in itertools.groupby( # Replace with itertools.batched for Py3.12+. - [*latin1_supplement, *latin_extended_A, *latin_extended_B], + [*latin1_supplement, *latin_extended_A, *latin_extended_B, + *non_basic_multilingual_plane], key=lambda x: next(count) // 32) # 32 characters per line. ) test_str = f"""There are basic characters diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index e865dbbe92da..bcac62854580 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -526,7 +526,7 @@ def test_svg_metadata(): @image_comparison(["multi_font_aspath.svg"]) -def test_multi_font_type3(): +def test_multi_font_aspath(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('svg', fonttype='path') @@ -537,7 +537,7 @@ def test_multi_font_type3(): @image_comparison(["multi_font_astext.svg"]) -def test_multi_font_type42(): +def test_multi_font_astext(): fonts, test_str = _gen_multi_font_text() plt.rc('font', family=fonts, size=16) plt.rc('svg', fonttype='none') From c908bbfcc05b74766d98848dc617997582ed2d13 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 19 Sep 2025 03:01:02 -0400 Subject: [PATCH 40/58] DOC: Add a release note for PDF font embedding fixes --- doc/release/next_whats_new/pdf_fonts.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/release/next_whats_new/pdf_fonts.rst diff --git a/doc/release/next_whats_new/pdf_fonts.rst b/doc/release/next_whats_new/pdf_fonts.rst new file mode 100644 index 000000000000..4d8665386a72 --- /dev/null +++ b/doc/release/next_whats_new/pdf_fonts.rst @@ -0,0 +1,10 @@ +Improved font embedding in PDF +------------------------------ + +Both Type 3 and Type 42 fonts (see :ref:`fonts` for more details) are now +embedded into PDFs without limitation. Fonts may be split into multiple +embedded subsets in order to satisfy format limits. Additionally, a corrected +Unicode mapping is added for each. + +This means that *all* text should now be selectable and copyable in PDF viewers +that support doing so. From 50f76ffb9661450fb8818bf9937f849df0beea84 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 26 Sep 2025 22:34:18 -0400 Subject: [PATCH 41/58] Deduplicate `CharacterTracker.track` implementation No need to repeat the calculation of subset blocks, but instead offload it to `track_glyph`. --- lib/matplotlib/backends/_backend_pdf_ps.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 1dde801d8665..2c34304cea3a 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -157,20 +157,10 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: whole). If *subset_size* is not specified, then the subset will always be 0 and the character codes will be returned from the string unchanged. """ - font_glyphs = [] - char_to_font = font._get_fontmap(s) - for _c, _f in char_to_font.items(): - charcode = ord(_c) - glyph_index = _f.get_char_index(charcode) - if self.subset_size != 0: - subset = charcode // self.subset_size - subset_charcode = charcode % self.subset_size - else: - subset = 0 - subset_charcode = charcode - self.used.setdefault((_f.fname, subset), {})[subset_charcode] = glyph_index - font_glyphs.append((subset, subset_charcode)) - return font_glyphs + return [ + self.track_glyph(f, ord(c), f.get_char_index(ord(c))) + for c, f in font._get_fontmap(s).items() + ] def track_glyph( self, font: FT2Font, charcode: CharacterCodeType, From 8274e1733a0deb3fd3ca81c3ee05a97f2d59c81f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 27 Sep 2025 04:22:40 -0400 Subject: [PATCH 42/58] pdf/ps: Compress subsetted font blocks Instead of splitting fonts into `subset_size` blocks and writing text as character code modulo `subset_size`, compress the blocks by doing two things: 1. Preserve the character code if it lies in the first block. This keeps ASCII (for Type 3) and the Basic Multilingual Plane (for Type 42) as their normal codes. 2. Push everything else into the next spot in the next block, splitting by `subset_size` as necessary. This should reduce the number of additional font subsets to embed. --- lib/matplotlib/backends/_backend_pdf_ps.py | 102 ++++++++++++++++++--- lib/matplotlib/backends/backend_pdf.py | 24 +++-- lib/matplotlib/backends/backend_ps.py | 28 +++--- 3 files changed, 116 insertions(+), 38 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 2c34304cea3a..381f7477278c 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -103,6 +103,58 @@ def font_as_file(font): return fh +class GlyphMap: + """ + A two-way glyph mapping. + + The forward glyph map is from (character code, glyph index)-pairs to (subset index, + subset character code)-pairs. + + The inverse glyph map is from to (subset index, subset character code)-pairs to + (character code, glyph index)-pairs. + """ + + def __init__(self) -> None: + self._forward: dict[tuple[CharacterCodeType, GlyphIndexType], + tuple[int, CharacterCodeType]] = {} + self._inverse: dict[tuple[int, CharacterCodeType], + tuple[CharacterCodeType, GlyphIndexType]] = {} + + def get(self, charcode: CharacterCodeType, + glyph_index: GlyphIndexType) -> tuple[int, CharacterCodeType] | None: + """ + Get the forward mapping from a (character code, glyph index)-pair. + + This may return *None* if the pair is not currently mapped. + """ + return self._forward.get((charcode, glyph_index)) + + def iget(self, subset: int, + subset_charcode: CharacterCodeType) -> tuple[CharacterCodeType, + GlyphIndexType]: + """Get the inverse mapping from a (subset, subset charcode)-pair.""" + return self._inverse[(subset, subset_charcode)] + + def add(self, charcode: CharacterCodeType, glyph_index: GlyphIndexType, subset: int, + subset_charcode: CharacterCodeType) -> None: + """ + Add a mapping to this instance. + + Parameters + ---------- + charcode : CharacterCodeType + The character code to record. + glyph : GlyphIndexType + The corresponding glyph index to record. + subset : int + The subset in which the subset character code resides. + subset_charcode : CharacterCodeType + The subset character code within the above subset. + """ + self._forward[(charcode, glyph_index)] = (subset, subset_charcode) + self._inverse[(subset, subset_charcode)] = (charcode, glyph_index) + + class CharacterTracker: """ Helper for font subsetting by the PDF and PS backends. @@ -114,16 +166,20 @@ class CharacterTracker: ---------- subset_size : int The size at which characters are grouped into subsets. - used : dict[tuple[str, int], dict[CharacterCodeType, GlyphIndexType]] + used : dict A dictionary of font files to character maps. - The key is a font filename and subset within that font. + The key is a font filename. - The value is a dictionary mapping a character code to a glyph index. Note this - mapping is the inverse of FreeType, which maps glyph indices to character codes. + The value is a list of dictionaries, each mapping at most *subset_size* + character codes to glyph indices. Note this mapping is the inverse of FreeType, + which maps glyph indices to character codes. If *subset_size* is not set, then there will only be one subset per font filename. + glyph_maps : dict + A dictionary of font files to glyph maps. You probably will want to use the + `.subset_to_unicode` method instead of this attribute. """ def __init__(self, subset_size: int = 0): @@ -134,7 +190,8 @@ def __init__(self, subset_size: int = 0): The maximum size that is supported for an embedded font. If provided, then characters will be grouped into these sized subsets. """ - self.used: dict[tuple[str, int], dict[CharacterCodeType, GlyphIndexType]] = {} + self.used: dict[str, list[dict[CharacterCodeType, GlyphIndexType]]] = {} + self.glyph_maps: dict[str, GlyphMap] = {} self.subset_size = subset_size def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: @@ -186,25 +243,42 @@ def track_glyph( The character code within the above subset. If *subset_size* was not specified on this instance, then this is just *charcode* unmodified. """ - if self.subset_size != 0: - subset = charcode // self.subset_size - subset_charcode = charcode % self.subset_size + glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap()) + if result := glyph_map.get(charcode, glyph): + return result + + subset_maps = self.used.setdefault(font.fname, [{}]) + # Default to preserving the character code as it was. + use_next_charmap = ( + self.subset_size != 0 + # But start filling a new subset if outside the first block; this preserves + # ASCII (for Type 3) or the Basic Multilingual Plane (for Type 42). + and charcode >= self.subset_size + ) + if use_next_charmap: + if len(subset_maps) == 1 or len(subset_maps[-1]) == self.subset_size: + subset_maps.append({}) + subset = len(subset_maps) - 1 + subset_charcode = len(subset_maps[-1]) else: subset = 0 subset_charcode = charcode - self.used.setdefault((font.fname, subset), {})[subset_charcode] = glyph + subset_maps[subset][subset_charcode] = glyph + glyph_map.add(charcode, glyph, subset, subset_charcode) return (subset, subset_charcode) - def subset_to_unicode(self, index: int, - charcode: CharacterCodeType) -> CharacterCodeType: + def subset_to_unicode(self, fontname: str, subset: int, + subset_charcode: CharacterCodeType) -> CharacterCodeType: """ Map a subset index and character code to a Unicode character code. Parameters ---------- - index : int + fontname : str + The name of the font, from the *used* dictionary key. + subset : int The subset index within a font. - charcode : CharacterCodeType + subset_charcode : CharacterCodeType The character code within a subset to map back. Returns @@ -212,7 +286,7 @@ def subset_to_unicode(self, index: int, CharacterCodeType The Unicode character code corresponding to the subsetted one. """ - return index * self.subset_size + charcode + return self.glyph_maps[fontname].iget(subset, subset_charcode)[0] class RendererPDFPSBase(RendererBase): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 0f7720b1022f..fa2131e53899 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -948,9 +948,8 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - charmap = self._character_tracker.used.get((filename, subset)) - if charmap: - fonts[Fx] = self.embedTTF(filename, subset, charmap) + charmap = self._character_tracker.used[filename][subset] + fonts[Fx] = self.embedTTF(filename, subset, charmap) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -992,8 +991,11 @@ def _embedTeXFont(self, dvifont): # Reduce the font to only the glyphs used in the document, get the encoding # for that subset, and compute various properties based on the encoding. - charmap = self._character_tracker.used[(dvifont.fname, 0)] - chars = frozenset(charmap.keys()) + charmap = self._character_tracker.used[dvifont.fname][0] + chars = { + self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode) + for ccode in charmap + } t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) fontdict['BaseFont'] = Name(t1font.prop['FontName']) # createType1Descriptor writes the font data as a side effect @@ -1144,14 +1146,16 @@ def generate_unicode_cmap(subset_index, charmap): unicode_groups[-1][1] = ccode last_ccode = ccode + def _to_unicode(ccode): + real_ccode = self._character_tracker.subset_to_unicode( + filename, subset_index, ccode) + unicodestr = chr(real_ccode).encode('utf-16be').hex() + return f'<{unicodestr}>' + width = 2 if fonttype == 3 else 4 unicode_bfrange = [] for start, end in unicode_groups: - real_start = self._character_tracker.subset_to_unicode(subset_index, - start) - real_end = self._character_tracker.subset_to_unicode(subset_index, end) - real_values = ' '.join('<%s>' % chr(x).encode('utf-16be').hex() - for x in range(real_start, real_end+1)) + real_values = ' '.join(_to_unicode(x) for x in range(start, end+1)) unicode_bfrange.append( f'<{start:0{width}x}> <{end:0{width}x}> [{real_values}]') unicode_cmap = (self._identityToUnicodeCMap % diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 14518a38c4ef..fe32ea7d2559 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1065,24 +1065,24 @@ def print_figure_impl(fh): Ndict = len(_psDefs) print("%%BeginProlog", file=fh) if not mpl.rcParams['ps.useafm']: - Ndict += len(ps_renderer._character_tracker.used) + Ndict += sum(map(len, ps_renderer._character_tracker.used.values())) print("/mpldict %d dict def" % Ndict, file=fh) print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for (font, subset_index), charmap in \ - ps_renderer._character_tracker.used.items(): - if not charmap: - continue - fonttype = mpl.rcParams['ps.fonttype'] - # Can't use more than 255 chars from a single Type 3 font. - if len(charmap) > 255: - fonttype = 42 - fh.flush() - if fonttype == 3: - fh.write(_font_to_ps_type3(font, charmap.values())) - else: # Type 42 only. - _font_to_ps_type42(font, charmap.values(), fh) + for font, subsets in ps_renderer._character_tracker.used.items(): + for charmap in subsets: + if not charmap: + continue + fonttype = mpl.rcParams['ps.fonttype'] + # Can't use more than 255 chars from a single Type 3 font. + if len(charmap) > 255: + fonttype = 42 + fh.flush() + if fonttype == 3: + fh.write(_font_to_ps_type3(font, charmap.values())) + else: # Type 42 only. + _font_to_ps_type42(font, charmap.values(), fh) print("end", file=fh) print("%%EndProlog", file=fh) From 70dc3880a7f04dd92ea18974a995034af95b60ab Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 27 Sep 2025 04:35:05 -0400 Subject: [PATCH 43/58] pdf: Fix first-block characters using multiple glyph representations If mixing languages, sometimes a single character may use different glyphs in one document. In that case, we need to give it a new character code in the next subset, since subset 0 is preserving character codes. --- lib/matplotlib/backends/_backend_pdf_ps.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 381f7477278c..30a611335d92 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -251,9 +251,15 @@ def track_glyph( # Default to preserving the character code as it was. use_next_charmap = ( self.subset_size != 0 - # But start filling a new subset if outside the first block; this preserves - # ASCII (for Type 3) or the Basic Multilingual Plane (for Type 42). - and charcode >= self.subset_size + and ( + # But start filling a new subset if outside the first block; this + # preserves ASCII (for Type 3) or the Basic Multilingual Plane (for + # Type 42). + charcode >= self.subset_size + # Or, use a new subset if the character code is already mapped for the + # first block. This means it's using an alternate glyph. + or charcode in subset_maps[0] + ) ) if use_next_charmap: if len(subset_maps) == 1 or len(subset_maps[-1]) == self.subset_size: From df670cf4509173d392b5bf852c98af643bb0da14 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 27 Sep 2025 05:32:38 -0400 Subject: [PATCH 44/58] pdf: Support multi-character glyphs when subsetting For ligatures or complex shapings, multiple characters may map to a single glyph. In this case, we still want to output a single character code for the string using the font subset, but the `ToUnicode` map should give back all the characters. --- lib/matplotlib/backends/_backend_pdf_ps.py | 48 +++++++++++++--------- lib/matplotlib/backends/backend_pdf.py | 11 ++--- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 30a611335d92..242a8716552d 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -107,11 +107,11 @@ class GlyphMap: """ A two-way glyph mapping. - The forward glyph map is from (character code, glyph index)-pairs to (subset index, - subset character code)-pairs. + The forward glyph map is from (character string, glyph index)-pairs to + (subset index, subset character code)-pairs. The inverse glyph map is from to (subset index, subset character code)-pairs to - (character code, glyph index)-pairs. + (character string, glyph index)-pairs. """ def __init__(self) -> None: @@ -120,22 +120,21 @@ def __init__(self) -> None: self._inverse: dict[tuple[int, CharacterCodeType], tuple[CharacterCodeType, GlyphIndexType]] = {} - def get(self, charcode: CharacterCodeType, + def get(self, charcodes: str, glyph_index: GlyphIndexType) -> tuple[int, CharacterCodeType] | None: """ - Get the forward mapping from a (character code, glyph index)-pair. + Get the forward mapping from a (character string, glyph index)-pair. This may return *None* if the pair is not currently mapped. """ - return self._forward.get((charcode, glyph_index)) + return self._forward.get((charcodes, glyph_index)) def iget(self, subset: int, - subset_charcode: CharacterCodeType) -> tuple[CharacterCodeType, - GlyphIndexType]: + subset_charcode: CharacterCodeType) -> tuple[str, GlyphIndexType]: """Get the inverse mapping from a (subset, subset charcode)-pair.""" return self._inverse[(subset, subset_charcode)] - def add(self, charcode: CharacterCodeType, glyph_index: GlyphIndexType, subset: int, + def add(self, charcode: str, glyph_index: GlyphIndexType, subset: int, subset_charcode: CharacterCodeType) -> None: """ Add a mapping to this instance. @@ -219,9 +218,8 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: for c, f in font._get_fontmap(s).items() ] - def track_glyph( - self, font: FT2Font, charcode: CharacterCodeType, - glyph: GlyphIndexType) -> tuple[int, CharacterCodeType]: + def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, + glyph: GlyphIndexType) -> tuple[int, CharacterCodeType]: """ Record character code *charcode* at glyph index *glyph* as using font *font*. @@ -229,8 +227,10 @@ def track_glyph( ---------- font : FT2Font A font that is being used for the provided string. - charcode : CharacterCodeType - The character code to record. + chars : str or CharacterCodeType + The character(s) to record. This may be a single character code, or multiple + characters in a string, if the glyph maps to several characters. It will be + normalized to a string internally. glyph : GlyphIndexType The corresponding glyph index to record. @@ -243,13 +243,21 @@ def track_glyph( The character code within the above subset. If *subset_size* was not specified on this instance, then this is just *charcode* unmodified. """ + if isinstance(chars, str): + charcode = ord(chars[0]) + else: + charcode = chars + chars = chr(chars) + glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap()) - if result := glyph_map.get(charcode, glyph): + if result := glyph_map.get(chars, glyph): return result subset_maps = self.used.setdefault(font.fname, [{}]) - # Default to preserving the character code as it was. use_next_charmap = ( + # Multi-character glyphs always go in the non-0 subset. + len(chars) > 1 or + # Default to preserving the character code as it was. self.subset_size != 0 and ( # But start filling a new subset if outside the first block; this @@ -270,11 +278,11 @@ def track_glyph( subset = 0 subset_charcode = charcode subset_maps[subset][subset_charcode] = glyph - glyph_map.add(charcode, glyph, subset, subset_charcode) + glyph_map.add(chars, glyph, subset, subset_charcode) return (subset, subset_charcode) def subset_to_unicode(self, fontname: str, subset: int, - subset_charcode: CharacterCodeType) -> CharacterCodeType: + subset_charcode: CharacterCodeType) -> str: """ Map a subset index and character code to a Unicode character code. @@ -289,8 +297,8 @@ def subset_to_unicode(self, fontname: str, subset: int, Returns ------- - CharacterCodeType - The Unicode character code corresponding to the subsetted one. + str + The Unicode character(s) corresponding to the subsetted character code. """ return self.glyph_maps[fontname].iget(subset, subset_charcode)[0] diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index fa2131e53899..19f04786eb26 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -993,7 +993,8 @@ def _embedTeXFont(self, dvifont): # for that subset, and compute various properties based on the encoding. charmap = self._character_tracker.used[dvifont.fname][0] chars = { - self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode) + # DVI type 1 fonts always map single glyph to single character. + ord(self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode)) for ccode in charmap } t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) @@ -1147,10 +1148,10 @@ def generate_unicode_cmap(subset_index, charmap): last_ccode = ccode def _to_unicode(ccode): - real_ccode = self._character_tracker.subset_to_unicode( + chars = self._character_tracker.subset_to_unicode( filename, subset_index, ccode) - unicodestr = chr(real_ccode).encode('utf-16be').hex() - return f'<{unicodestr}>' + hexstr = chars.encode('utf-16be').hex() + return f'<{hexstr}>' width = 2 if fonttype == 3 else 4 unicode_bfrange = [] @@ -2329,7 +2330,7 @@ def output_singlebyte_chunk(kerns_or_chars): for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED, language=language): subset, charcode = self.file._character_tracker.track_glyph( - item.ft_object, ord(item.char), item.glyph_index) + item.ft_object, item.char, item.glyph_index) if (item.ft_object, subset) != prev_font: if singlebyte_chunk: output_singlebyte_chunk(singlebyte_chunk) From ed5e07459235379bed74f9e6f51b4224e181e23f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 30 Sep 2025 00:50:43 -0400 Subject: [PATCH 45/58] ps: Fix font subset handling Previously, this was supposed to "upgrade" type 3 to type 42 if the number of glyphs overflowed. However, as `CharacterTracker` can suggest a new subset for other reasons (i.e., multiple glyphs for the same character or a glyph for multiple characters may go to a second subset), we do need proper subset handling here as well. Since that is now done, we can drop the "promotion" from type 3 to type 42, as we don't get too many glyphs in each embedded font. --- lib/matplotlib/backends/_backend_pdf_ps.py | 6 +++ lib/matplotlib/backends/backend_pdf.py | 8 +-- lib/matplotlib/backends/backend_ps.py | 58 +++++++++++++--------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 242a8716552d..0ff17a105c20 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -22,6 +22,12 @@ from fontTools.ttLib import TTFont +_FONT_MAX_GLYPH = { + 3: 256, + 42: 65536, +} + + @functools.lru_cache(50) def _cached_get_afm_from_fname(fname): with open(fname, "rb") as fh: diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 19f04786eb26..a850f229ab29 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -368,12 +368,6 @@ def pdfRepr(obj): "objects") -_FONT_MAX_GLYPH = { - 3: 256, - 42: 65536, -} - - class Reference: """ PDF reference object. @@ -691,7 +685,7 @@ def __init__(self, filename, metadata=None): self._fontNames = {} # maps filenames to internal font names self._dviFontInfo = {} # maps pdf names to dvifonts self._character_tracker = _backend_pdf_ps.CharacterTracker( - _FONT_MAX_GLYPH.get(mpl.rcParams['pdf.fonttype'], 0)) + _backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0)) self.alphaStates = {} # maps alpha values to graphics state objects self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index fe32ea7d2559..374e06da68e9 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -88,7 +88,7 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, glyph_indices): +def _font_to_ps_type3(font_path, subset_index, glyph_indices): """ Subset *glyphs_indices* from the font at *font_path* into a Type 3 font. @@ -96,6 +96,8 @@ def _font_to_ps_type3(font_path, glyph_indices): ---------- font_path : path-like Path to the font to be subsetted. + subset_index : int + The subset of the above font being created. glyph_indices : set[int] The glyphs to include in the subsetted font. @@ -111,7 +113,7 @@ def _font_to_ps_type3(font_path, glyph_indices): %!PS-Adobe-3.0 Resource-Font %%Creator: Converted from TrueType to Type 3 by Matplotlib. 10 dict begin -/FontName /{font_name} def +/FontName /{font_name}-{subset} def /PaintType 0 def /FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def /FontBBox [{bbox}] def @@ -119,7 +121,7 @@ def _font_to_ps_type3(font_path, glyph_indices): /Encoding [{encoding}] def /CharStrings {num_glyphs} dict dup begin /.notdef 0 def -""".format(font_name=font.postscript_name, +""".format(font_name=font.postscript_name, subset=subset_index, inv_units_per_em=1 / font.units_per_EM, bbox=" ".join(map(str, font.bbox)), encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}" @@ -168,7 +170,7 @@ def _font_to_ps_type3(font_path, glyph_indices): return preamble + "\n".join(entries) + postamble -def _font_to_ps_type42(font_path, glyph_indices, fh): +def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): """ Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*. @@ -176,12 +178,14 @@ def _font_to_ps_type42(font_path, glyph_indices, fh): ---------- font_path : path-like Path to the font to be subsetted. + subset_index : int + The subset of the above font being created. glyph_indices : set[int] The glyphs to include in the subsetted font. fh : file-like Where to write the font. """ - _log.debug("SUBSET %s characters: %s", font_path, glyph_indices) + _log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices) try: kw = {} # fix this once we support loading more fonts from a collection @@ -192,10 +196,10 @@ def _font_to_ps_type42(font_path, glyph_indices, fh): _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( - "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, - len(fontdata) + "SUBSET %s:%d %d -> %d", font_path, subset_index, + os.stat(font_path).st_size, len(fontdata) ) - fh.write(_serialize_type42(font, subset, fontdata)) + fh.write(_serialize_type42(font, subset_index, subset, fontdata)) except RuntimeError: _log.warning( "The PostScript backend does not currently support the selected font (%s).", @@ -203,7 +207,7 @@ def _font_to_ps_type42(font_path, glyph_indices, fh): raise -def _serialize_type42(font, subset, fontdata): +def _serialize_type42(font, subset_index, subset, fontdata): """ Output a PostScript Type-42 format representation of font @@ -211,6 +215,8 @@ def _serialize_type42(font, subset, fontdata): ---------- font : fontTools.ttLib.ttFont.TTFont The original font object + subset_index : int + The subset of the above font to be created. subset : fontTools.ttLib.ttFont.TTFont The subset font object fontdata : bytes @@ -231,7 +237,7 @@ def _serialize_type42(font, subset, fontdata): 10 dict begin /FontType 42 def /FontMatrix [1 0 0 1 0 0] def - /FontName /{name.getDebugName(6)} def + /FontName /{name.getDebugName(6)}-{subset_index} def /FontInfo 7 dict dup begin /FullName ({name.getDebugName(4)}) def /FamilyName ({name.getDebugName(1)}) def @@ -425,7 +431,8 @@ def __init__(self, width, height, pswriter, imagedpi=72): self._clip_paths = {} self._path_collection_id = 0 - self._character_tracker = _backend_pdf_ps.CharacterTracker() + self._character_tracker = _backend_pdf_ps.CharacterTracker( + _backend_pdf_ps._FONT_MAX_GLYPH.get(mpl.rcParams['ps.fonttype'], 0)) self._logwarn_once = functools.cache(_log.warning) def _is_transparent(self, rgb_or_rgba): @@ -793,12 +800,16 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): else: language = mtext.get_language() if mtext is not None else None font = self._get_font_ttf(prop) - self._character_tracker.track(font, s) for item in _text_helpers.layout(s, font, language=language): + # NOTE: We ignore the character code in the subset, because PS uses the + # glyph name to write text. The subset is only used to ensure that each + # one does not overflow format limits. + subset, _ = self._character_tracker.track_glyph( + item.ft_object, item.char, item.glyph_index) ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) glyph_name = item.ft_object.get_glyph_name(item.glyph_index) - stream.append((ps_name, item.x, glyph_name)) + stream.append((f'{ps_name}-{subset}', item.x, glyph_name)) self.set_color(*gc.get_rgb()) for ps_name, group in itertools. \ @@ -827,11 +838,15 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{angle:g} rotate\n") lastfont = None for font, fontsize, ccode, glyph_index, ox, oy in glyphs: - self._character_tracker.track_glyph(font, ccode, glyph_index) - if (font.postscript_name, fontsize) != lastfont: - lastfont = font.postscript_name, fontsize + # NOTE: We ignore the character code in the subset, because PS uses the + # glyph name to write text. The subset is only used to ensure that each one + # does not overflow format limits. + subset, _ = self._character_tracker.track_glyph( + font, ccode, glyph_index) + if (font.postscript_name, subset, fontsize) != lastfont: + lastfont = font.postscript_name, subset, fontsize self._pswriter.write( - f"/{font.postscript_name} {fontsize} selectfont\n") + f"/{font.postscript_name}-{subset} {fontsize} selectfont\n") glyph_name = font.get_glyph_name(glyph_index) self._pswriter.write( f"{ox:g} {oy:g} moveto\n" @@ -1071,18 +1086,15 @@ def print_figure_impl(fh): print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: for font, subsets in ps_renderer._character_tracker.used.items(): - for charmap in subsets: + for subset, charmap in enumerate(subsets): if not charmap: continue fonttype = mpl.rcParams['ps.fonttype'] - # Can't use more than 255 chars from a single Type 3 font. - if len(charmap) > 255: - fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font, charmap.values())) + fh.write(_font_to_ps_type3(font, subset, charmap.values())) else: # Type 42 only. - _font_to_ps_type42(font, charmap.values(), fh) + _font_to_ps_type42(font, subset, charmap.values(), fh) print("end", file=fh) print("%%EndProlog", file=fh) From 972a6888c4d0ebfc655bdc6dd7a46faa95eae917 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 1 Mar 2025 00:39:18 -0500 Subject: [PATCH 46/58] Add font feature API to FontProperties and Text Font features allow font designers to provide alternate glyphs or shaping within a single font. These features may be accessed via special tags corresponding to internal tables of glyphs. The mplcairo backend supports font features via an elaborate re-use of the font file path [1]. This commit adds the API to make this officially supported in the main user API. [1] https://github.com/matplotlib/mplcairo/blob/v0.6.1/README.rst#font-formats-and-features --- doc/release/next_whats_new/font_features.rst | 41 ++++++++++++++++++++ lib/matplotlib/_text_helpers.py | 6 ++- lib/matplotlib/backends/backend_agg.py | 1 + lib/matplotlib/backends/backend_pdf.py | 11 ++++-- lib/matplotlib/backends/backend_ps.py | 9 ++++- lib/matplotlib/font_manager.py | 2 +- lib/matplotlib/ft2font.pyi | 1 + lib/matplotlib/tests/test_ft2font.py | 13 +++++++ lib/matplotlib/tests/test_text.py | 16 ++++++++ lib/matplotlib/text.py | 40 +++++++++++++++++++ lib/matplotlib/text.pyi | 2 + lib/matplotlib/textpath.py | 11 +++--- lib/matplotlib/textpath.pyi | 8 +++- src/ft2font.cpp | 21 ++++++++-- src/ft2font.h | 2 + src/ft2font_wrapper.cpp | 12 +++++- 16 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 doc/release/next_whats_new/font_features.rst diff --git a/doc/release/next_whats_new/font_features.rst b/doc/release/next_whats_new/font_features.rst new file mode 100644 index 000000000000..022d36e1e21d --- /dev/null +++ b/doc/release/next_whats_new/font_features.rst @@ -0,0 +1,41 @@ +Specifying font feature tags +---------------------------- + +OpenType fonts may support feature tags that specify alternate glyph shapes or +substitutions to be made optionally. The text API now supports setting a list of feature +tags to be used with the associated font. Feature tags can be set/get with: + +- `matplotlib.text.Text.set_fontfeatures` / `matplotlib.text.Text.get_fontfeatures` +- Any API that creates a `.Text` object by passing the *fontfeatures* argument (e.g., + ``plt.xlabel(..., fontfeatures=...)``) + +Font feature strings are eventually passed to HarfBuzz, and so all `string formats +supported by hb_feature_from_string() +`__ are +supported. Note though that subranges are not explicitly supported and behaviour may +change in the future. + +For example, the default font ``DejaVu Sans`` enables Standard Ligatures (the ``'liga'`` +tag) by default, and also provides optional Discretionary Ligatures (the ``dlig`` tag.) +These may be toggled with ``+`` or ``-``. + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(7, 3)) + + fig.text(0.5, 0.85, 'Ligatures', fontsize=40, horizontalalignment='center') + + # Default has Standard Ligatures (liga). + fig.text(0, 0.6, 'Default: fi ffi fl st', fontsize=40) + + # Disable Standard Ligatures with -liga. + fig.text(0, 0.35, 'Disabled: fi ffi fl st', fontsize=40, + fontfeatures=['-liga']) + + # Enable Discretionary Ligatures with dlig. + fig.text(0, 0.1, 'Discretionary: fi ffi fl st', fontsize=40, + fontfeatures=['dlig']) + +Available font feature tags may be found at +https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index fa5d36bc99c8..e4e6bb03a145 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -26,7 +26,7 @@ def warn_on_missing_glyph(codepoint, fontnames): f"missing from font(s) {fontnames}.") -def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None): +def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=None): """ Render *string* with *font*. @@ -39,6 +39,8 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None): The string to be rendered. font : FT2Font The font. + features : tuple of str, optional + The font features to apply to the text. kern_mode : Kerning A FreeType kerning mode. language : str, optional @@ -51,7 +53,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None): """ x = 0 prev_glyph_index = None - char_to_font = font._get_fontmap(string) # TODO: Pass in language. + char_to_font = font._get_fontmap(string) # TODO: Pass in features and language. base_font = font for char in string: # This has done the fallback logic diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 2da422a88e84..43d40d1c0c68 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -191,6 +191,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=get_hinting_flag(), + features=mtext.get_fontfeatures() if mtext is not None else None, language=mtext.get_language() if mtext is not None else None) font.draw_glyphs_to_bitmap( antialiased=gc.get_antialiased()) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a850f229ab29..a5035d16e24f 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2263,7 +2263,11 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): return self.draw_mathtext(gc, x, y, s, prop, angle) fontsize = prop.get_size_in_points() - language = mtext.get_language() if mtext is not None else None + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None if mpl.rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) @@ -2273,7 +2277,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: - font.set_text(s, language=language) + font.set_text(s, features=features, language=language) width, height = font.get_width_height() self.file._annotations[-1][1].append(_get_link_annotation( gc, x, y, width / 64, height / 64, angle)) @@ -2321,7 +2325,8 @@ def output_singlebyte_chunk(kerns_or_chars): prev_start_x = 0 # Emit all the characters in a BT/ET group. self.file.output(Op.begin_text) - for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED, + for item in _text_helpers.layout(s, font, features=features, + kern_mode=Kerning.UNFITTED, language=language): subset, charcode = self.file._character_tracker.track_glyph( item.ft_object, item.char, item.glyph_index) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 374e06da68e9..2743da13aec5 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -798,9 +798,14 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): thisx += width * scale else: - language = mtext.get_language() if mtext is not None else None + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None font = self._get_font_ttf(prop) - for item in _text_helpers.layout(s, font, language=language): + for item in _text_helpers.layout(s, font, features=features, + language=language): # NOTE: We ignore the character code in the subset, because PS uses the # glyph name to write text. The subset is only used to ensure that each # one does not overflow format limits. diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 47339d4491dd..d1a96826fbf6 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -540,7 +540,7 @@ def afmFontProperty(fontpath, font): def _cleanup_fontproperties_init(init_method): """ - A decorator to limit the call signature to single a positional argument + A decorator to limit the call signature to a single positional argument or alternatively only keyword arguments. We still accept but deprecate all other call signatures. diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index fce67131e67b..a4d1c77061be 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -249,6 +249,7 @@ class FT2Font(Buffer): angle: float = ..., flags: LoadFlags = ..., *, + features: tuple[str] | None = ..., language: str | list[tuple[str, int, int]] | None = ..., ) -> NDArray[np.float64]: ... @property diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index e27a00b740e3..3c066a59e939 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -229,6 +229,19 @@ def test_ft2font_set_size(): assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig) +def test_ft2font_features(): + # Smoke test that these are accepted as intended. + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file) + font.set_text('foo', features=None) # unset + font.set_text('foo', features=['calt', 'dlig']) # list + font.set_text('foo', features=('calt', 'dlig')) # tuple + with pytest.raises(TypeError): + font.set_text('foo', features=123) + with pytest.raises(TypeError): + font.set_text('foo', features=[123, 456]) + + def test_ft2font_charmaps(): def enc(name): # We don't expose the encoding enum from FreeType, but can generate it here. diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 4d8a7a59c731..e3bec7c36910 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1204,6 +1204,22 @@ def test_ytick_rotation_mode(): plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) +@image_comparison(baseline_images=['features.png'], remove_text=False, style='mpl20') +def test_text_features(): + fig = plt.figure(figsize=(5, 1.5)) + t = fig.text(1, 0.7, 'Default: fi ffi fl st', + fontsize=32, horizontalalignment='right') + assert t.get_fontfeatures() is None + t = fig.text(1, 0.4, 'Disabled: fi ffi fl st', + fontsize=32, horizontalalignment='right', + fontfeatures=['-liga']) + assert t.get_fontfeatures() == ('-liga', ) + t = fig.text(1, 0.1, 'Discretionary: fi ffi fl st', + fontsize=32, horizontalalignment='right') + t.set_fontfeatures(['dlig']) + assert t.get_fontfeatures() == ('dlig', ) + + @pytest.mark.parametrize( 'input, match', [ diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 4d80f9874941..827b6bcb7667 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -137,6 +137,7 @@ def __init__(self, super().__init__() self._x, self._y = x, y self._text = '' + self._features = None self.set_language(None) self._reset_visual_defaults( text=text, @@ -849,6 +850,12 @@ def get_fontfamily(self): """ return self._fontproperties.get_family() + def get_fontfeatures(self): + """ + Return a tuple of font feature tags to enable. + """ + return self._features + def get_fontname(self): """ Return the font name as a string. @@ -1096,6 +1103,39 @@ def set_fontfamily(self, fontname): self._fontproperties.set_family(fontname) self.stale = True + def set_fontfeatures(self, features): + """ + Set the feature tags to enable on the font. + + Parameters + ---------- + features : list of str, or tuple of str, or None + A list of feature tags to be used with the associated font. These strings + are eventually passed to HarfBuzz, and so all `string formats supported by + hb_feature_from_string() + `__ + are supported. Note though that subranges are not explicitly supported and + behaviour may change in the future. + + For example, if your desired font includes Stylistic Sets which enable + various typographic alternates including one that you do not wish to use + (e.g., Contextual Ligatures), then you can pass the following to enable one + and not the other:: + + fp.set_features([ + 'ss01', # Use Stylistic Set 1. + '-clig', # But disable Contextural Ligatures. + ]) + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + """ + _api.check_isinstance((Sequence, None), features=features) + if features is not None: + features = tuple(features) + self._features = features + self.stale = True + def set_fontvariant(self, variant): """ Set the font variant. diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index eb3c076b1c5c..7992ecb20a8d 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -56,6 +56,7 @@ class Text(Artist): def get_color(self) -> ColorType: ... def get_fontproperties(self) -> FontProperties: ... def get_fontfamily(self) -> list[str]: ... + def get_fontfeatures(self) -> tuple[str, ...] | None: ... def get_fontname(self) -> str: ... def get_fontstyle(self) -> Literal["normal", "italic", "oblique"]: ... def get_fontsize(self) -> float | str: ... @@ -80,6 +81,7 @@ class Text(Artist): def set_multialignment(self, align: Literal["left", "center", "right"]) -> None: ... def set_linespacing(self, spacing: float) -> None: ... def set_fontfamily(self, fontname: str | Iterable[str]) -> None: ... + def set_fontfeatures(self, features: Sequence[str] | None) -> None: ... def set_fontvariant(self, variant: Literal["normal", "small-caps"]) -> None: ... def set_fontstyle( self, fontstyle: Literal["normal", "italic", "oblique"] diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 6f6f4daa4cfa..e7bb95159deb 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -67,7 +67,7 @@ def get_text_width_height_descent(self, s, prop, ismath): d /= 64.0 return w * scale, h * scale, d * scale - def get_text_path(self, prop, s, ismath=False, *, language=None): + def get_text_path(self, prop, s, ismath=False, *, features=None, language=None): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). @@ -110,8 +110,8 @@ def get_text_path(self, prop, s, ismath=False, *, language=None): glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) - glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s, - language=language) + glyph_info, glyph_map, rects = self.get_glyphs_with_font( + font, s, features=features, language=language) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) @@ -132,7 +132,8 @@ def get_text_path(self, prop, s, ismath=False, *, language=None): return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, - return_new_glyphs_only=False, *, language=None): + return_new_glyphs_only=False, *, features=None, + language=None): """ Convert string *s* to vertices and codes using the provided ttf font. """ @@ -147,7 +148,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_reprs = [] - for item in _text_helpers.layout(s, font, language=language): + for item in _text_helpers.layout(s, font, features=features, language=language): glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) glyph_reprs.append(glyph_repr) xpositions.append(item.x) diff --git a/lib/matplotlib/textpath.pyi b/lib/matplotlib/textpath.pyi index b83b337aa541..07f81598aa75 100644 --- a/lib/matplotlib/textpath.pyi +++ b/lib/matplotlib/textpath.pyi @@ -16,7 +16,12 @@ class TextToPath: self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"] ) -> tuple[float, float, float]: ... def get_text_path( - self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ..., *, + self, + prop: FontProperties, + s: str, + ismath: bool | Literal["TeX"] = ..., + *, + features: tuple[str] | None = ..., language: str | list[tuple[str, int, int]] | None = ..., ) -> list[np.ndarray]: ... def get_glyphs_with_font( @@ -26,6 +31,7 @@ class TextToPath: glyph_map: dict[str, tuple[np.ndarray, np.ndarray]] | None = ..., return_new_glyphs_only: bool = ..., *, + features: tuple[str] | None = ..., language: str | list[tuple[str, int, int]] | None = ..., ) -> tuple[ list[tuple[str, float, float, float]], diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 6f3db040f17d..8838f68ee5f8 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -320,7 +320,7 @@ void FT2Font::set_kerning_factor(int factor) std::vector FT2Font::layout( std::u32string_view text, FT_Int32 flags, - LanguageType languages, + std::optional> features, LanguageType languages, std::set& glyph_seen_fonts) { clear(); @@ -344,6 +344,13 @@ std::vector FT2Font::layout( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (features) { + for (auto const& feature : *features) { + if (!raqm_add_font_feature(rq, feature.c_str(), feature.size())) { + throw std::runtime_error("failed to set font feature {}"_s.format(feature)); + } + } + } if (languages) { for (auto & [lang_str, start, end] : *languages) { if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { @@ -417,6 +424,14 @@ std::vector FT2Font::layout( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (features) { + for (auto const& feature : *features) { + if (!raqm_add_font_feature(rq, feature.c_str(), feature.size())) { + throw std::runtime_error( + "failed to set font feature {}"_s.format(feature)); + } + } + } if (languages) { for (auto & [lang_str, start, end] : *languages) { if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { @@ -440,7 +455,7 @@ std::vector FT2Font::layout( void FT2Font::set_text( std::u32string_view text, double angle, FT_Int32 flags, - LanguageType languages, + std::optional> features, LanguageType languages, std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -457,7 +472,7 @@ void FT2Font::set_text( matrix.yy = (FT_Fixed)cosangle; std::set glyph_seen_fonts; - auto rq_glyphs = layout(text, flags, languages, glyph_seen_fonts); + auto rq_glyphs = layout(text, flags, features, languages, glyph_seen_fonts); bbox.xMin = bbox.yMin = 32000; bbox.xMax = bbox.yMax = -32000; diff --git a/src/ft2font.h b/src/ft2font.h index 841c66cfb5ee..b1458fe28ada 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -114,9 +114,11 @@ class FT2Font void set_charmap(int i); void select_charmap(unsigned long i); std::vector layout(std::u32string_view text, FT_Int32 flags, + std::optional> features, LanguageType languages, std::set& glyph_seen_fonts); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, + std::optional> features, LanguageType languages, std::vector &xys); int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); void set_kerning_factor(int factor); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index e0d5e0c23391..a348f0d312b6 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -687,6 +687,13 @@ const char *PyFT2Font_set_text__doc__ = R"""( .. versionchanged:: 3.10 This now takes an `.ft2font.LoadFlags` instead of an int. + features : tuple[str, ...] + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + + .. versionadded:: 3.11 Returns ------- @@ -697,6 +704,7 @@ const char *PyFT2Font_set_text__doc__ = R"""( static py::array_t PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT, + std::optional> features = std::nullopt, std::variant languages_or_str = nullptr) { std::vector xys; @@ -731,7 +739,7 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 throw py::type_error("languages must be str or list of tuple"); } - self->set_text(text, angle, static_cast(flags), languages, xys); + self->set_text(text, angle, static_cast(flags), features, languages, xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -1553,7 +1561,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_get_kerning__doc__) .def("set_text", &PyFT2Font_set_text, "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), - "language"_a=nullptr, + "features"_a=nullptr, "language"_a=nullptr, PyFT2Font_set_text__doc__) .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, PyFT2Font_get_fontmap__doc__) From 959799138e6b53bea788fb647f5315659f7c4e79 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 24 Sep 2025 01:13:22 -0400 Subject: [PATCH 47/58] ft2font: Add a wrapper around layouting for vector usage --- ci/mypy-stubtest-allowlist.txt | 1 + lib/matplotlib/ft2font.pyi | 23 ++++++ src/ft2font_wrapper.cpp | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 6e3e487d934f..1a9a888a1896 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -6,6 +6,7 @@ matplotlib\._.* matplotlib\.rcsetup\._listify_validator matplotlib\.rcsetup\._validate_linestyle matplotlib\.ft2font\.Glyph +matplotlib\.ft2font\.LayoutItem matplotlib\.testing\.jpl_units\..* matplotlib\.sphinxext(\..*)? diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index a4d1c77061be..88745e5e5cc9 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -191,6 +191,22 @@ class _SfntPcltDict(TypedDict): widthType: int serifStyle: int +@final +class LayoutItem: + @property + def ft_object(self) -> FT2Font: ... + @property + def char(self) -> str: ... + @property + def glyph_index(self) -> GlyphIndexType: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def prev_kern(self) -> float: ... + def __str__(self) -> str: ... + @final class FT2Font(Buffer): def __init__( @@ -204,6 +220,13 @@ class FT2Font(Buffer): if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... + def _layout( + self, + text: str, + flags: LoadFlags, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = ..., + ) -> list[LayoutItem]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ... diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index a348f0d312b6..6aa9188317fa 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1409,6 +1409,119 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self) return indices; } +/********************************************************************** + * Layout items + * */ + +struct LayoutItem { + PyFT2Font *ft_object; + std::u32string character; + int glyph_index; + double x; + double y; + double prev_kern; + + LayoutItem(PyFT2Font *f, std::u32string c, int i, double x, double y, double k) : + ft_object(f), character(c), glyph_index(i), x(x), y(y), prev_kern(k) {} +}; + +const char *PyFT2Font_layout__doc__ = R"""( + Layout a string and yield information about each used glyph. + + .. warning:: + This API uses the fallback list and is both private and provisional: do not use + it directly. + + .. versionadded:: 3.11 + + Parameters + ---------- + text : str + The characters for which to find fonts. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + Returns + ------- + list[LayoutItem] +)"""; + +static auto +PyFT2Font_layout(PyFT2Font *self, std::u32string text, LoadFlags flags, + std::optional> features = std::nullopt, + std::variant languages_or_str = nullptr) +{ + const auto hinting_factor = self->get_hinting_factor(); + const auto load_flags = static_cast(flags); + + FT2Font::LanguageType languages; + if (auto value = std::get_if(&languages_or_str)) { + languages = std::move(*value); + } else if (auto value = std::get_if(&languages_or_str)) { + languages = std::vector{ + FT2Font::LanguageRange{*value, 0, text.size()} + }; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("languages must be str or list of tuple"); + } + + std::set glyph_seen_fonts; + auto glyphs = self->layout(text, load_flags, features, languages, glyph_seen_fonts); + + std::set clusters; + for (auto &glyph : glyphs) { + clusters.emplace(glyph.cluster); + } + + std::vector items; + + double x = 0.0; + double y = 0.0; + std::optional prev_advance = std::nullopt; + double prev_x = 0.0; + for (auto &glyph : glyphs) { + auto ft_object = static_cast(glyph.ftface->generic.data); + + ft_object->load_glyph(glyph.index, load_flags); + + double prev_kern = 0.0; + if (prev_advance) { + double actual_advance = (x + glyph.x_offset) - prev_x; + prev_kern = actual_advance - *prev_advance; + } + + auto next = clusters.upper_bound(glyph.cluster); + auto end = (next != clusters.end()) ? *next : text.size(); + auto substr = text.substr(glyph.cluster, end - glyph.cluster); + + items.emplace_back(ft_object, substr, glyph.index, + (x + glyph.x_offset) / 64.0, (y + glyph.y_offset) / 64.0, + prev_kern / 64.0); + prev_x = x + glyph.x_offset; + x += glyph.x_advance; + y += glyph.y_advance; + // Note, linearHoriAdvance is a 16.16 instead of 26.6 fixed-point value. + prev_advance = ft_object->get_face()->glyph->linearHoriAdvance / 1024.0 / hinting_factor; + } + + return items; +} + +/********************************************************************** + * Deprecations + * */ + static py::object ft2font__getattr__(std::string name) { auto api = py::module_::import("matplotlib._api"); @@ -1543,6 +1656,32 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def_property_readonly("bbox", &PyGlyph_get_bbox, "The control box of the glyph."); + py::class_(m, "LayoutItem", py::is_final()) + .def(py::init<>([]() -> LayoutItem { + // LayoutItem is not useful from Python, so mark it as not constructible. + throw std::runtime_error("LayoutItem is not constructible"); + })) + .def_readonly("ft_object", &LayoutItem::ft_object, + "The FT_Face of the item.") + .def_readonly("char", &LayoutItem::character, + "The character code for the item.") + .def_readonly("glyph_index", &LayoutItem::glyph_index, + "The glyph index for the item.") + .def_readonly("x", &LayoutItem::x, + "The x position of the item.") + .def_readonly("y", &LayoutItem::y, + "The y position of the item.") + .def_readonly("prev_kern", &LayoutItem::prev_kern, + "The kerning between this item and the previous one.") + .def("__str__", + [](const LayoutItem& item) { + return + "LayoutItem(ft_object={}, char={!r}, glyph_index={}, "_s + "x={}, y={}, prev_kern={})"_s.format( + PyFT2Font_fname(item.ft_object), item.character, + item.glyph_index, item.x, item.y, item.prev_kern); + }); + auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), @@ -1559,6 +1698,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_select_charmap__doc__) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) + .def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(), + "features"_a=nullptr, "language"_a=nullptr, + PyFT2Font_layout__doc__) .def("set_text", &PyFT2Font_set_text, "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), "features"_a=nullptr, "language"_a=nullptr, From a47bd3fcf232f9deebf86ab066f2af06e7eabe31 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 17 Dec 2024 21:08:58 -0500 Subject: [PATCH 48/58] Remove forced fallback from FT2Font::load_char The only thing that expected this to work is Type 3 fonts in the PDF backend, but only to avoid an error when loading a range of characters (not all of which would be used.) This would introduce an odd behaviour in that `load_char` could never fail even if the glyph never existed. You would instead end up with the `null` glyph from the last font. --- src/ft2font.cpp | 9 ++++----- src/ft2font.h | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 8838f68ee5f8..0e98506536d0 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -552,7 +552,7 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, glyphs, char_to_font, charcode, flags, charcode_error, glyph_error, - glyph_seen_fonts, true); + glyph_seen_fonts); if (!was_found) { ft_glyph_warn(charcode, glyph_seen_fonts); if (charcode_error) { @@ -613,15 +613,14 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, - std::set &glyph_seen_fonts, - bool override = false) + std::set &glyph_seen_fonts) { FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); if (!warn_if_used) { glyph_seen_fonts.insert(face->family_name); } - if (glyph_index || override) { + if (glyph_index) { charcode_error = FT_Load_Glyph(face, glyph_index, flags); if (charcode_error) { return false; @@ -647,7 +646,7 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, bool was_found = fallback->load_char_with_fallback( ft_object_with_glyph, final_glyph_index, parent_glyphs, parent_char_to_font, charcode, flags, - charcode_error, glyph_error, glyph_seen_fonts, override); + charcode_error, glyph_error, glyph_seen_fonts); if (was_found) { return true; } diff --git a/src/ft2font.h b/src/ft2font.h index b1458fe28ada..b36bc4f02a76 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -131,8 +131,7 @@ class FT2Font FT_Int32 flags, FT_Error &charcode_error, FT_Error &glyph_error, - std::set &glyph_seen_fonts, - bool override); + std::set &glyph_seen_fonts); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); std::tuple get_width_height(); std::tuple get_bitmap_offset(); From bd17cd43dd71b879928828c899cb2f9087ce745e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 24 May 2025 05:02:40 -0400 Subject: [PATCH 49/58] Use libraqm for text in vector outputs --- lib/matplotlib/_text_helpers.py | 44 ++++++------------- lib/matplotlib/backends/_backend_pdf_ps.py | 18 ++++++-- lib/matplotlib/backends/backend_pdf.py | 23 +++++++--- lib/matplotlib/backends/backend_ps.py | 13 +++--- lib/matplotlib/backends/backend_svg.py | 8 +++- lib/matplotlib/ft2font.pyi | 1 - lib/matplotlib/tests/test_ft2font.py | 18 ++++---- lib/matplotlib/tests/test_text.py | 8 ++-- lib/matplotlib/textpath.py | 3 +- src/ft2font_wrapper.cpp | 50 ---------------------- 10 files changed, 72 insertions(+), 114 deletions(-) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index e4e6bb03a145..0ebbf3ac139d 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -4,29 +4,23 @@ from __future__ import annotations -import dataclasses +from collections.abc import Iterator from . import _api -from .ft2font import FT2Font, GlyphIndexType, Kerning, LoadFlags +from .ft2font import FT2Font, CharacterCodeType, LayoutItem, LoadFlags -@dataclasses.dataclass(frozen=True) -class LayoutItem: - ft_object: FT2Font - char: str - glyph_index: GlyphIndexType - x: float - prev_kern: float - - -def warn_on_missing_glyph(codepoint, fontnames): +def warn_on_missing_glyph(codepoint: CharacterCodeType, fontnames: str): _api.warn_external( f"Glyph {codepoint} " f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) " f"missing from font(s) {fontnames}.") -def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=None): +def layout(string: str, font: FT2Font, *, + features: tuple[str] | None = None, + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> Iterator[LayoutItem]: """ Render *string* with *font*. @@ -41,8 +35,6 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N The font. features : tuple of str, optional The font features to apply to the text. - kern_mode : Kerning - A FreeType kerning mode. language : str, optional The language of the text in a format accepted by libraqm, namely `a BCP47 language code `_. @@ -51,20 +43,8 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N ------ LayoutItem """ - x = 0 - prev_glyph_index = None - char_to_font = font._get_fontmap(string) # TODO: Pass in features and language. - base_font = font - for char in string: - # This has done the fallback logic - font = char_to_font.get(char, base_font) - glyph_index = font.get_char_index(ord(char)) - kern = ( - base_font.get_kerning(prev_glyph_index, glyph_index, kern_mode) / 64 - if prev_glyph_index is not None else 0. - ) - x += kern - glyph = font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) - yield LayoutItem(font, char, glyph_index, x, kern) - x += glyph.linearHoriAdvance / 65536 - prev_glyph_index = glyph_index + for raqm_item in font._layout(string, LoadFlags.NO_HINTING, + features=features, language=language): + raqm_item.ft_object.load_glyph(raqm_item.glyph_index, + flags=LoadFlags.NO_HINTING) + yield raqm_item diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 0ff17a105c20..b1a3f5c9f18b 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -199,7 +199,10 @@ def __init__(self, subset_size: int = 0): self.glyph_maps: dict[str, GlyphMap] = {} self.subset_size = subset_size - def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: + def track(self, font: FT2Font, s: str, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> list[tuple[int, CharacterCodeType]]: """ Record that string *s* is being typeset using font *font*. @@ -209,6 +212,14 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: A font that is being used for the provided string. s : str The string that should be marked as tracked by the provided font. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Returns ------- @@ -220,8 +231,9 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: and the character codes will be returned from the string unchanged. """ return [ - self.track_glyph(f, ord(c), f.get_char_index(ord(c))) - for c, f in font._get_fontmap(s).items() + self.track_glyph(raqm_item.ft_object, raqm_item.char, raqm_item.glyph_index) + for raqm_item in font._layout(s, ft2font.LoadFlags.NO_HINTING, + features=features, language=language) ] def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a5035d16e24f..613b00987730 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -34,7 +34,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags +from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -469,6 +469,7 @@ class Op(Enum): textpos = b'Td' selectfont = b'Tf' textmatrix = b'Tm' + textrise = b'Ts' show = b'Tj' showkern = b'TJ' setlinewidth = b'w' @@ -2285,6 +2286,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # If fonttype is neither 3 nor 42, emit the whole string at once # without manual kerning. if fonttype not in [3, 42]: + if not mpl.rcParams['pdf.use14corefonts']: + self.file._character_tracker.track(font, s, + features=features, language=language) self.file.output(Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) @@ -2305,6 +2309,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # kerning between chunks. else: def output_singlebyte_chunk(kerns_or_chars): + if not kerns_or_chars: + return self.file.output( # See pdf spec "Text space details" for the 1000/fontsize # (aka. 1000/T_fs) factor. @@ -2312,6 +2318,7 @@ def output_singlebyte_chunk(kerns_or_chars): else self._encode_glyphs(group, fonttype) for tp, group in itertools.groupby(kerns_or_chars, type)], Op.showkern) + kerns_or_chars.clear() # Do the rotation and global translation as a single matrix # concatenation up front self.file.output(Op.gsave) @@ -2326,24 +2333,26 @@ def output_singlebyte_chunk(kerns_or_chars): # Emit all the characters in a BT/ET group. self.file.output(Op.begin_text) for item in _text_helpers.layout(s, font, features=features, - kern_mode=Kerning.UNFITTED, language=language): subset, charcode = self.file._character_tracker.track_glyph( item.ft_object, item.char, item.glyph_index) if (item.ft_object, subset) != prev_font: - if singlebyte_chunk: - output_singlebyte_chunk(singlebyte_chunk) + output_singlebyte_chunk(singlebyte_chunk) ft_name = self.file.fontName(item.ft_object.fname, subset) self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) - singlebyte_chunk = [] prev_font = (item.ft_object, subset) prev_start_x = item.x + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(item.y, Op.textrise) if item.prev_kern: singlebyte_chunk.append(item.prev_kern) singlebyte_chunk.append(charcode) - if singlebyte_chunk: - output_singlebyte_chunk(singlebyte_chunk) + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(0, Op.textrise) + output_singlebyte_chunk(singlebyte_chunk) self.file.output(Op.end_text) self.file.output(Op.grestore) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 2743da13aec5..8ad31290a643 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -776,7 +776,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) - stream = [] # list of (ps_name, x, char_name) + stream = [] # list of (ps_name, x, y, char_name) if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) @@ -794,7 +794,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): kern = font.get_kern_dist_from_name(last_name, name) last_name = name thisx += kern * scale - stream.append((ps_name, thisx, name)) + stream.append((ps_name, thisx, 0, name)) thisx += width * scale else: @@ -814,14 +814,13 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) glyph_name = item.ft_object.get_glyph_name(item.glyph_index) - stream.append((f'{ps_name}-{subset}', item.x, glyph_name)) + stream.append((f'{ps_name}-{subset}', item.x, item.y, glyph_name)) self.set_color(*gc.get_rgb()) - for ps_name, group in itertools. \ - groupby(stream, lambda entry: entry[0]): + for ps_name, group in itertools.groupby(stream, lambda entry: entry[0]): self.set_font(ps_name, prop.get_size_in_points(), False) - thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow" - for _, x, name in group) + thetext = "\n".join(f"{x:g} {y:g} m /{name:s} glyphshow" + for _, x, y, name in group) self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 7b94a2b9ba2b..06d868bdab62 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1048,6 +1048,11 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None style = {} if color != '#000000': @@ -1068,7 +1073,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( - font, s, glyph_map=glyph_map, return_new_glyphs_only=True) + font, s, features=features, language=language, + glyph_map=glyph_map, return_new_glyphs_only=True) self._update_glyph_map_defs(glyph_map_new) for glyph_repr, xposition, yposition, scale in glyph_info: diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 88745e5e5cc9..9345c1c9057f 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -219,7 +219,6 @@ class FT2Font(Buffer): ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... - def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def _layout( self, text: str, diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 3c066a59e939..4a874deb5343 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -972,7 +972,7 @@ def test_fallback_last_resort(recwarn): "Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)") -def test__get_fontmap(): +def test__layout(): fonts, test_str = _gen_multi_font_text() # Add some glyphs that don't exist in either font to check the Last Resort fallback. missing_glyphs = '\n几个汉字' @@ -981,11 +981,11 @@ def test__get_fontmap(): ft = fm.get_font( fm.fontManager._find_fonts_by_props(fm.FontProperties(family=fonts)) ) - fontmap = ft._get_fontmap(test_str) - for char, font in fontmap.items(): - if char in missing_glyphs: - assert Path(font.fname).name == 'LastResortHE-Regular.ttf' - elif ord(char) > 127: - assert Path(font.fname).name == 'DejaVuSans.ttf' - else: - assert Path(font.fname).name == 'cmr10.ttf' + for substr in test_str.split('\n'): + for item in ft._layout(substr, ft2font.LoadFlags.DEFAULT): + if item.char in missing_glyphs: + assert Path(item.ft_object.fname).name == 'LastResortHE-Regular.ttf' + elif ord(item.char) > 127: + assert Path(item.ft_object.fname).name == 'DejaVuSans.ttf' + else: + assert Path(item.ft_object.fname).name == 'cmr10.ttf' diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 84d833a1a0ea..5ae606e413f0 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -115,7 +115,7 @@ def find_matplotlib_font(**kw): ax.set_yticks([]) -@image_comparison(['complex.png']) +@image_comparison(['complex'], extensions=['png', 'pdf', 'svg', 'eps']) def test_complex_shaping(): # Raqm is Arabic for writing; note that because Arabic is RTL, the characters here # may seem to be in a different order than expected, but libraqm will order them @@ -1240,7 +1240,8 @@ def test_ytick_rotation_mode(): plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) -@image_comparison(baseline_images=['features.png'], remove_text=False, style='mpl20') +@image_comparison(['features'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) def test_text_features(): fig = plt.figure(figsize=(5, 1.5)) t = fig.text(1, 0.7, 'Default: fi ffi fl st', @@ -1270,7 +1271,8 @@ def test_text_language_invalid(input, match): Text(0, 0, 'foo', language=input) -@image_comparison(baseline_images=['language.png'], remove_text=False, style='mpl20') +@image_comparison(['language'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) def test_text_language(): fig = plt.figure(figsize=(5, 3)) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index e7bb95159deb..d7c1cdf1622f 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -147,15 +147,16 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, glyph_map_new = glyph_map xpositions = [] + ypositions = [] glyph_reprs = [] for item in _text_helpers.layout(s, font, features=features, language=language): glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) glyph_reprs.append(glyph_repr) xpositions.append(item.x) + ypositions.append(item.y) if glyph_repr not in glyph_map: glyph_map_new[glyph_repr] = item.ft_object.get_path() - ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6aa9188317fa..21d8b01656b8 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -623,54 +623,6 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, return self->get_kerning(left, right, mode); } -const char *PyFT2Font_get_fontmap__doc__ = R"""( - Get a mapping between characters and the font that includes them. - - .. warning:: - This API uses the fallback list and is both private and provisional: do not use - it directly. - - Parameters - ---------- - text : str - The characters for which to find fonts. - - Returns - ------- - dict[str, FT2Font] - A dictionary mapping unicode characters to `.FT2Font` objects. -)"""; - -static py::dict -PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) -{ - std::set codepoints; - - py::dict char_to_font; - for (auto code : text) { - if (!codepoints.insert(code).second) { - continue; - } - - py::object target_font; - int index; - if (self->get_char_fallback_index(code, index)) { - if (index >= 0) { - target_font = self->fallbacks[index]; - } else { - target_font = py::cast(self); - } - } else { - // TODO Handle recursion! - target_font = py::cast(self); - } - - auto key = py::cast(std::u32string(1, code)); - char_to_font[key] = target_font; - } - return char_to_font; -} - const char *PyFT2Font_set_text__doc__ = R"""( Set the text *string* and *angle*. @@ -1705,8 +1657,6 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), "features"_a=nullptr, "language"_a=nullptr, PyFT2Font_set_text__doc__) - .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, - PyFT2Font_get_fontmap__doc__) .def("get_num_glyphs", &PyFT2Font::get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) .def("load_char", &PyFT2Font_load_char, From 4a99a83773ea6fd7bb5c65472f1928c4040394d3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 1 Nov 2025 23:09:37 +0100 Subject: [PATCH 50/58] Fix spacing in `r"$\max f$"`. Previously, in a mathtext string like `r"$\sin x$"`, a thin space would (correctly) be added between "sin" and "x", but that space would be missing in expressions like `r"$\max f$"`. The difference arose because of the slightly different handling of subscripts and superscripts after the `\sin` and `\max` operators: `\sin^n` puts the superscript as a normal exponent, but `\max_x` puts the subscript centered below the operator name ("overunder symbol). The previous code for inserting the thin space did not handle the "overunder" case; fix that. The new behavior is tested by the change in test_operator_space, as well as by mathtext1_dejavusans_06. The change in mathtext_foo_29 arises because the extra thin space now inserted after `\limsup` slightly shifts the centering of the whole string. Ideally that thin space should be suppressed if there's no token after the operator, but that's not something currently implemented either for e.g. `\sin` (compare e.g. the right-alignments in `text(.5, .9, r"$\sin$", ha="right"); text(.5, .8, r"$\mathrm{sin}$", ha="right"); axvline(.5)` where the extra thin space after `\sin` is visible), so this patch just makes things more consistent. --- lib/matplotlib/_mathtext.py | 10 +++++++--- lib/matplotlib/tests/test_mathtext.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index d628b18aebf4..4c3ad3c8d676 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2362,8 +2362,7 @@ def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any: next_char_loc += len('operatorname{}') next_char = next((c for c in s[next_char_loc:] if c != ' '), '') delimiters = self._delims | {'^', '_'} - if (next_char not in delimiters and - name not in self._overunder_functions): + if next_char not in delimiters: # Add thin space except when followed by parenthesis, bracket, etc. hlist_list += [self._make_space(self._space_widths[r'\,'])] self.pop_state() @@ -2483,7 +2482,12 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: shift = hlist.height + vgap + nucleus.depth vlt = Vlist(vlist) vlt.shift_amount = shift - result = Hlist([vlt]) + result = Hlist([ + vlt, + *([self._make_space(self._space_widths[r'\,'])] + if self._in_subscript_or_superscript else []), + ]) + self._in_subscript_or_superscript = False return [result] # We remove kerning on the last character for consistency (otherwise diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 5d0245bc5049..31b5d37ea041 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -400,7 +400,7 @@ def test_operator_space(fig_test, fig_ref): fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$") fig_test.text(0.1, 0.7, r"$\cos^2$") fig_test.text(0.1, 0.8, r"$\log_2$") - fig_test.text(0.1, 0.9, r"$\sin^2 \cos$") # GitHub issue #17852 + fig_test.text(0.1, 0.9, r"$\sin^2 \max \cos$") # GitHub issue #17852 fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$") fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$") @@ -410,7 +410,7 @@ def test_operator_space(fig_test, fig_ref): fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$") fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$") fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$") - fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,cos}$") + fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,max} \mathrm{\,cos}$") @check_figures_equal() From 17428e3e2f1ac21b2415be173c9eaed1043c176e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 1 Nov 2025 23:24:12 +0100 Subject: [PATCH 51/58] Tweak sub/superscript spacing implementation. Rename _in_subscript_or_superscript to the more descriptive _needs_space_after_subsuper; simplify its setting in operatorname(); avoid the need to introduce an extra explicitly-typed spaced_nucleus variable. --- lib/matplotlib/_mathtext.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 4c3ad3c8d676..ff2045a5b8b4 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2132,7 +2132,7 @@ def csnames(group: str, names: Iterable[str]) -> Regex: self._math_expression = p.math # To add space to nucleus operators after sub/superscripts - self._in_subscript_or_superscript = False + self._needs_space_after_subsuper = False def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hlist: """ @@ -2150,7 +2150,7 @@ def parse(self, s: str, fonts_object: Fonts, fontsize: float, dpi: float) -> Hli # explain becomes a plain method on pyparsing 3 (err.explain(0)). raise ValueError("\n" + ParseException.explain(err, 0)) from None self._state_stack = [] - self._in_subscript_or_superscript = False + self._needs_space_after_subsuper = False # prevent operator spacing from leaking into a new expression self._em_width_cache = {} ParserElement.reset_cache() @@ -2260,7 +2260,7 @@ def symbol(self, s: str, loc: int, prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') # Binary operators at start of string should not be spaced # Also, operators in sub- or superscripts should not be spaced - if (self._in_subscript_or_superscript or ( + if (self._needs_space_after_subsuper or ( c in self._binary_operators and ( len(s[:loc].split()) == 0 or prev_char in { '{', *self._left_delims, *self._relation_symbols}))): @@ -2366,13 +2366,9 @@ def operatorname(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Add thin space except when followed by parenthesis, bracket, etc. hlist_list += [self._make_space(self._space_widths[r'\,'])] self.pop_state() - # if followed by a super/subscript, set flag to true - # This flag tells subsuper to add space after this operator - if next_char in {'^', '_'}: - self._in_subscript_or_superscript = True - else: - self._in_subscript_or_superscript = False - + # If followed by a sub/superscript, set flag to true to tell subsuper + # to add space after this operator. + self._needs_space_after_subsuper = next_char in {'^', '_'} return Hlist(hlist_list) def start_group(self, toks: ParseResults) -> T.Any: @@ -2482,12 +2478,10 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: shift = hlist.height + vgap + nucleus.depth vlt = Vlist(vlist) vlt.shift_amount = shift - result = Hlist([ - vlt, - *([self._make_space(self._space_widths[r'\,'])] - if self._in_subscript_or_superscript else []), - ]) - self._in_subscript_or_superscript = False + optional_spacing = ([self._make_space(self._space_widths[r'\,'])] + if self._needs_space_after_subsuper else []) + self._needs_space_after_subsuper = False + result = Hlist([vlt, *optional_spacing]) return [result] # We remove kerning on the last character for consistency (otherwise @@ -2579,12 +2573,10 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Do we need to add a space after the nucleus? # To find out, check the flag set by operatorname - spaced_nucleus: list[Node] = [nucleus, x] - if self._in_subscript_or_superscript: - spaced_nucleus += [self._make_space(self._space_widths[r'\,'])] - self._in_subscript_or_superscript = False - - result = Hlist(spaced_nucleus) + optional_spacing = ([self._make_space(self._space_widths[r'\,'])] + if self._needs_space_after_subsuper else []) + self._needs_space_after_subsuper = False + result = Hlist([nucleus, x, *optional_spacing]) return [result] def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathStyle, From daae68a3686dcdc761291ff3b6192bf08736d033 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Jul 2025 03:50:41 -0400 Subject: [PATCH 52/58] Expose face index when loading fonts This enables loading a non-initial font from collections (`.ttc` files). Currently exposed for `FT2Font`, only. --- lib/matplotlib/ft2font.pyi | 3 +++ lib/matplotlib/tests/test_ft2font.py | 19 +++++++++++++++++++ src/ft2font.cpp | 4 ++-- src/ft2font.h | 2 +- src/ft2font_wrapper.cpp | 19 +++++++++++++++---- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 9345c1c9057f..71bd89f81561 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -214,6 +214,7 @@ class FT2Font(Buffer): filename: str | bytes | PathLike | BinaryIO, hinting_factor: int = ..., *, + face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., _kerning_factor: int | None = ... ) -> None: ... @@ -283,6 +284,8 @@ class FT2Font(Buffer): @property def face_flags(self) -> FaceFlags: ... @property + def face_index(self) -> int: ... + @property def family_name(self) -> str: ... @property def fname(self) -> str | bytes: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 4a874deb5343..17492e7690c0 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -199,6 +199,25 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _kerning_factor=123) +@pytest.mark.parametrize('name, size, skippable', + [('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)]) +def test_ft2font_face_index(name, size, skippable): + try: + file = fm.findfont(name, fallback_to_default=False) + except ValueError: + if skippable: + pytest.skip(r'Font {name} may be missing') + raise + for index in range(size): + font = ft2font.FT2Font(file, face_index=index) + assert font.num_faces >= size + assert font.face_index == index + with pytest.raises(ValueError, match='must be between'): # out of bounds for spec + ft2font.FT2Font(file, face_index=0x1ffff) + with pytest.raises(RuntimeError, match='invalid argument'): # invalid for this font + ft2font.FT2Font(file, face_index=0xff) + + def test_ft2font_clear(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 0e98506536d0..b70f3a29d469 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -222,9 +222,9 @@ FT2Font::~FT2Font() close(); } -void FT2Font::open(FT_Open_Args &open_args) +void FT2Font::open(FT_Open_Args &open_args, FT_Long face_index) { - FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face); + FT_CHECK(FT_Open_Face, _ft2Library, &open_args, face_index, &face); if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } diff --git a/src/ft2font.h b/src/ft2font.h index b36bc4f02a76..68d31bac9a41 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -107,7 +107,7 @@ class FT2Font FT2Font(long hinting_factor, std::vector &fallback_list, bool warn_if_used); virtual ~FT2Font(); - void open(FT_Open_Args &open_args); + void open(FT_Open_Args &open_args, FT_Long face_index); void close(); void clear(); void set_size(double ptsize, double dpi); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 21d8b01656b8..d5cf07e7762d 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -430,6 +430,9 @@ const char *PyFT2Font_init__doc__ = R"""( hinting_factor : int, optional Must be positive. Used to scale the hinting in the x-direction. + face_index : int, optional + The index of the face in the font file to load. + _fallback_list : list of FT2Font, optional A list of FT2Font objects used to find missing glyphs. @@ -444,7 +447,7 @@ const char *PyFT2Font_init__doc__ = R"""( )"""; static PyFT2Font * -PyFT2Font_init(py::object filename, long hinting_factor = 8, +PyFT2Font_init(py::object filename, long hinting_factor = 8, FT_Long face_index = 0, std::optional> fallback_list = std::nullopt, std::optional kerning_factor = std::nullopt, bool warn_if_used = false) @@ -460,6 +463,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, kerning_factor = 0; } + if (face_index < 0 || face_index > 0xffff) { + throw std::range_error("face_index must be between 0 and 65535, inclusive"); + } + std::vector fallback_fonts; if (fallback_list) { // go through fallbacks to add them to our lists @@ -509,7 +516,7 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->stream.close = nullptr; } - self->open(open_args); + self->open(open_args, face_index); return self; } @@ -1637,7 +1644,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), - "filename"_a, "hinting_factor"_a=8, py::kw_only(), + "filename"_a, "hinting_factor"_a=8, py::kw_only(), "face_index"_a=0, "_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(), "_warn_if_used"_a=false, PyFT2Font_init__doc__) @@ -1714,8 +1721,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "PostScript name of the font.") .def_property_readonly( "num_faces", [](PyFT2Font *self) { - return self->get_face()->num_faces; + return self->get_face()->num_faces & 0xffff; }, "Number of faces in file.") + .def_property_readonly( + "face_index", [](PyFT2Font *self) { + return self->get_face()->face_index; + }, "The index of the font in the file.") .def_property_readonly( "family_name", [](PyFT2Font *self) { if (const char *name = self->get_face()->family_name) { From b839f87b93f0a6447569768ff745de2ddefec5aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Jul 2025 04:51:05 -0400 Subject: [PATCH 53/58] Parse data from all fonts within a collection This should allow listing the metadata from the whole collection, which will also pick the right one if specified, though it will not load the specific index yet. --- lib/matplotlib/font_manager.py | 12 +++++++++--- lib/matplotlib/font_manager.pyi | 1 + lib/matplotlib/tests/test_font_manager.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d1a96826fbf6..a8427cdbaa30 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -319,6 +319,7 @@ class FontEntry: """ fname: str = '' + index: int = 0 name: str = '' style: str = 'normal' variant: str = 'normal' @@ -465,7 +466,8 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. raise NotImplementedError("Non-scalable fonts are not supported") size = 'scalable' - return FontEntry(font.fname, name, style, variant, weight, stretch, size) + return FontEntry(font.fname, font.face_index, name, + style, variant, weight, stretch, size) def afmFontProperty(fontpath, font): @@ -535,7 +537,7 @@ def afmFontProperty(fontpath, font): size = 'scalable' - return FontEntry(fontpath, name, style, variant, weight, stretch, size) + return FontEntry(fontpath, 0, name, style, variant, weight, stretch, size) def _cleanup_fontproperties_init(init_method): @@ -1069,7 +1071,7 @@ class FontManager: # Increment this version number whenever the font cache data # format or behavior has changed and requires an existing font # cache files to be rebuilt. - __version__ = '3.11.0a1' + __version__ = '3.11.0a2' def __init__(self, size=None, weight='normal'): self._version = self.__version__ @@ -1134,6 +1136,10 @@ def addfont(self, path): font = ft2font.FT2Font(path) prop = ttfFontProperty(font) self.ttflist.append(prop) + for face_index in range(1, font.num_faces): + subfont = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(subfont) + self.ttflist.append(prop) self._findfont_cached.cache_clear() @property diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index f5e3910e5f63..6b072f707f66 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -29,6 +29,7 @@ def findSystemFonts( @dataclass class FontEntry: fname: str = ... + index: int = ... name: str = ... style: str = ... variant: str = ... diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index d51eb8d9837f..6301163b4527 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -117,8 +117,13 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) - if Path(findfont(fp)).name != "wqy-zenhei.ttc": + fontpath = findfont(fp) + if Path(fontpath).name != "wqy-zenhei.ttc": pytest.skip("Font wqy-zenhei.ttc may be missing") + # All fonts from this collection should have loaded as well. + for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]: + assert findfont(FontProperties(family=[name]), + fallback_to_default=False) == fontpath fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: @@ -363,6 +368,10 @@ def test_get_font_names(): font = ft2font.FT2Font(path) prop = ttfFontProperty(font) ttf_fonts.append(prop.name) + for face_index in range(1, font.num_faces): + font = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(font) + ttf_fonts.append(prop.name) except Exception: pass available_fonts = sorted(list(set(ttf_fonts))) From 6d2ae678f44e0250e5ecbedccad5dc634d4cbd62 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 03:33:54 -0400 Subject: [PATCH 54/58] Implement loading of any font in a collection For backwards-compatibility, the path+index is passed around in a lightweight subclass of `str`. --- lib/matplotlib/font_manager.py | 97 ++++++++++++++++++++--- lib/matplotlib/font_manager.pyi | 22 ++++- lib/matplotlib/tests/test_font_manager.py | 44 +++++++++- 3 files changed, 144 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index a8427cdbaa30..5ca1c9aeafb7 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -310,6 +310,69 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): return [fname for fname in fontfiles if os.path.exists(fname)] +# To maintain backwards-compatibility with the current code we need to continue to +# return a str. However to support indexing into the file we need to return both the +# path and the index. Thus, we sub-class str to maintain compatibility and extend it to +# carry the index. +# +# The other alternative would be to create a completely new API and deprecate the +# existing one. In this case, sub-classing str is the simpler and less-disruptive +# option. +class FontPath(str): + """ + A class to describe a path to a font with a face index. + + Parameters + ---------- + path : str + The path to a font. + face_index : int + The face index in the font. + """ + + __match_args__ = ('path', 'face_index') + + def __new__(cls, path, face_index): + ret = super().__new__(cls, path) + ret._face_index = face_index + return ret + + @property + def path(self): + """The path to a font.""" + return str(self) + + @property + def face_index(self): + """The face index in a font.""" + return self._face_index + + def _as_tuple(self): + return (self.path, self.face_index) + + def __eq__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() == other._as_tuple() + return super().__eq__(other) + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() < other._as_tuple() + return super().__lt__(other) + + def __gt__(self, other): + return not (self == other or self < other) + + def __hash__(self): + return hash(self._as_tuple()) + + def __repr__(self): + return f'FontPath{self._as_tuple()}' + + @dataclasses.dataclass(frozen=True) class FontEntry: """ @@ -1326,7 +1389,7 @@ def findfont(self, prop, fontext='ttf', directory=None, Returns ------- - str + FontPath The filename of the best matching font. Notes @@ -1396,7 +1459,7 @@ def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, Returns ------- - list[str] + list[FontPath] The paths of the fonts found. Notes @@ -1542,7 +1605,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # actually raised. return cbook._ExceptionInfo(ValueError, "No valid font could be found") - return _cached_realpath(result) + return FontPath(_cached_realpath(result), best_font.index) @_api.deprecated("3.11") @@ -1562,15 +1625,16 @@ def is_opentype_cff_font(filename): @lru_cache(64) def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, enable_last_resort): - first_fontpath, *rest = font_filepaths + (first_fontpath, first_fontindex), *rest = font_filepaths fallback_list = [ - ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor) - for fpath in rest + ft2font.FT2Font(fpath, hinting_factor, face_index=index, + _kerning_factor=_kerning_factor) + for fpath, index in rest ] last_resort_path = _cached_realpath( cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf')) try: - last_resort_index = font_filepaths.index(last_resort_path) + last_resort_index = font_filepaths.index((last_resort_path, 0)) except ValueError: last_resort_index = -1 # Add Last Resort font so we always have glyphs regardless of font, unless we're @@ -1582,7 +1646,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, _warn_if_used=True)) last_resort_index = len(fallback_list) font = ft2font.FT2Font( - first_fontpath, hinting_factor, + first_fontpath, hinting_factor, face_index=first_fontindex, _fallback_list=fallback_list, _kerning_factor=_kerning_factor ) @@ -1617,7 +1681,8 @@ def get_font(font_filepaths, hinting_factor=None): Parameters ---------- - font_filepaths : Iterable[str, bytes, os.PathLike], str, bytes, os.PathLike + font_filepaths : Iterable[str, bytes, os.PathLike, FontPath], \ +str, bytes, os.PathLike, FontPath Relative or absolute paths to the font files to be used. If a single string, bytes, or `os.PathLike`, then it will be treated @@ -1632,10 +1697,16 @@ def get_font(font_filepaths, hinting_factor=None): `.ft2font.FT2Font` """ - if isinstance(font_filepaths, (str, bytes, os.PathLike)): - paths = (_cached_realpath(font_filepaths),) - else: - paths = tuple(_cached_realpath(fname) for fname in font_filepaths) + match font_filepaths: + case FontPath(path, index): + paths = ((_cached_realpath(path), index), ) + case str() | bytes() | os.PathLike() as path: + paths = ((_cached_realpath(path), 0), ) + case _: + paths = tuple( + (_cached_realpath(fname.path), fname.face_index) + if isinstance(fname, FontPath) else (_cached_realpath(fname), 0) + for fname in font_filepaths) hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor') diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index 6b072f707f66..936dad426522 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -3,7 +3,7 @@ from dataclasses import dataclass from numbers import Integral import os from pathlib import Path -from typing import Any, Literal +from typing import Any, Final, Literal from matplotlib._afm import AFM from matplotlib import ft2font @@ -26,6 +26,22 @@ def _get_fontconfig_fonts() -> list[Path]: ... def findSystemFonts( fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ... ) -> list[str]: ... + +class FontPath(str): + __match_args__: Final[tuple[str, ...]] + def __new__(cls: type[str], path: str, face_index: int) -> FontPath: ... + @property + def path(self) -> str: ... + @property + def face_index(self) -> int: ... + def _as_tuple(self) -> tuple[str, int]: ... + def __eq__(self, other: Any) -> bool: ... + def __ne__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + def __gt__(self, other: Any) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + @dataclass class FontEntry: fname: str = ... @@ -116,12 +132,12 @@ class FontManager: directory: str | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., - ) -> str: ... + ) -> FontPath: ... def get_font_names(self) -> list[str]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( - font_filepaths: Iterable[str | bytes | os.PathLike] | str | bytes | os.PathLike, + font_filepaths: Iterable[str | bytes | os.PathLike | FontPath] | str | bytes | os.PathLike | FontPath, hinting_factor: int | None = ..., ) -> ft2font.FT2Font: ... diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 6301163b4527..cc8ae03a9f97 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -13,7 +13,7 @@ import matplotlib as mpl from matplotlib.font_manager import ( - findfont, findSystemFonts, FontEntry, FontProperties, fontManager, + findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, MSUserFontDirectories, ttfFontProperty, _get_fontconfig_fonts, _normalize_weight) @@ -24,6 +24,38 @@ has_fclist = shutil.which('fc-list') is not None +def test_font_path(): + fp = FontPath('foo', 123) + fp2 = FontPath('foo', 321) + assert str(fp) == 'foo' + assert repr(fp) == "FontPath('foo', 123)" + assert fp.path == 'foo' + assert fp.face_index == 123 + # Should be immutable. + with pytest.raises(AttributeError, match='has no setter'): + fp.path = 'bar' + with pytest.raises(AttributeError, match='has no setter'): + fp.face_index = 321 + # Should be comparable with str and itself. + assert fp == 'foo' + assert fp == FontPath('foo', 123) + assert fp <= fp + assert fp >= fp + assert fp != fp2 + assert fp < fp2 + assert fp <= fp2 + assert fp2 > fp + assert fp2 >= fp + # Should be hashable, but not the same as str. + d = {fp: 1, 'bar': 2} + assert fp in d + assert d[fp] == 1 + assert d[FontPath('foo', 123)] == 1 + assert fp2 not in d + assert 'foo' not in d + assert FontPath('bar', 0) not in d + + def test_font_priority(): with rc_context(rc={ 'font.sans-serif': @@ -122,8 +154,12 @@ def test_find_ttc(): pytest.skip("Font wqy-zenhei.ttc may be missing") # All fonts from this collection should have loaded as well. for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]: - assert findfont(FontProperties(family=[name]), - fallback_to_default=False) == fontpath + subfontpath = findfont(FontProperties(family=[name]), fallback_to_default=False) + assert subfontpath.path == fontpath.path + assert subfontpath.face_index != fontpath.face_index + subfont = get_font(subfontpath) + assert subfont.fname == subfontpath.path + assert subfont.face_index == subfontpath.face_index fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: @@ -161,6 +197,8 @@ def __fspath__(self): assert font.fname == file_str font = get_font(PathLikeClass(file_bytes)) assert font.fname == file_bytes + font = get_font(FontPath(file_str, 0)) + assert font.fname == file_str # Note, fallbacks are not currently accessible. font = get_font([file_str, file_bytes, From ea80bbb58f6c349da05b3eb94c766059c0ac9bc8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 06:13:59 -0400 Subject: [PATCH 55/58] pdf/ps: Support any font in a collection --- lib/matplotlib/backends/_backend_pdf_ps.py | 10 +++--- lib/matplotlib/backends/backend_pdf.py | 29 +++++++++-------- lib/matplotlib/backends/backend_ps.py | 12 +++----- lib/matplotlib/dviread.py | 4 +++ lib/matplotlib/dviread.pyi | 2 ++ lib/matplotlib/tests/test_backend_pdf.py | 35 ++++++++++++++++++++- lib/matplotlib/tests/test_backend_ps.py | 36 +++++++++++++++++++++- 7 files changed, 101 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index b1a3f5c9f18b..83a8566517a7 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -42,7 +42,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: Parameters ---------- - fontfile : str + fontfile : FontPath Path to the font file glyphs : set[GlyphIndexType] Set of glyph indices to include in subset. @@ -80,8 +80,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: 'xref', # The cross-reference table (some Apple font tooling information). ] # if fontfile is a ttc, specify font number - if fontfile.endswith(".ttc"): - options.font_number = 0 + options.font_number = fontfile.face_index font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) @@ -267,11 +266,12 @@ def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, charcode = chars chars = chr(chars) - glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap()) + font_path = font_manager.FontPath(font.fname, font.face_index) + glyph_map = self.glyph_maps.setdefault(font_path, GlyphMap()) if result := glyph_map.get(chars, glyph): return result - subset_maps = self.used.setdefault(font.fname, [{}]) + subset_maps = self.used.setdefault(font_path, [{}]) use_next_charmap = ( # Multi-character glyphs always go in the non-0 subset. len(chars) > 1 or diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 613b00987730..a926cb41bb3b 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -32,7 +32,7 @@ RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure -from matplotlib.font_manager import get_font, fontManager as _fontManager +from matplotlib.font_manager import FontPath, get_font, fontManager as _fontManager from matplotlib._afm import AFM from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase @@ -894,8 +894,10 @@ def fontName(self, fontprop, subset=0): as the filename of the font. """ - if isinstance(fontprop, str): + if isinstance(fontprop, FontPath): filenames = [fontprop] + elif isinstance(fontprop, str): + filenames = [FontPath(fontprop, 0)] elif mpl.rcParams['pdf.use14corefonts']: filenames = _fontManager._find_fonts_by_props( fontprop, fontext='afm', directory=RendererPdf._afm_font_dir @@ -935,7 +937,7 @@ def writeFonts(self): _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) for (filename, subset), Fx in sorted(self._fontNames.items()): - _log.debug('Embedding font %s:%d.', filename, subset) + _log.debug('Embedding font %r:%d.', filename, subset) if filename.endswith('.afm'): # from pdf.use14corefonts _log.debug('Writing AFM font.') @@ -986,10 +988,11 @@ def _embedTeXFont(self, dvifont): # Reduce the font to only the glyphs used in the document, get the encoding # for that subset, and compute various properties based on the encoding. - charmap = self._character_tracker.used[dvifont.fname][0] + font_path = FontPath(dvifont.fname, dvifont.face_index) + charmap = self._character_tracker.used[font_path][0] chars = { # DVI type 1 fonts always map single glyph to single character. - ord(self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode)) + ord(self._character_tracker.subset_to_unicode(font_path, 0, ccode)) for ccode in charmap } t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) @@ -1241,12 +1244,12 @@ def embedTTFType42(font, subset_index, charmap, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - _log.debug("SUBSET %s:%d characters: %s", filename, subset_index, charmap) + _log.debug("SUBSET %r:%d characters: %s", filename, subset_index, charmap) with _backend_pdf_ps.get_glyphs_subset(filename, charmap.values()) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s:%d %d -> %d", filename, subset_index, + "SUBSET %r:%d %d -> %d", filename, subset_index, os.stat(filename).st_size, fontdata.getbuffer().nbytes ) @@ -2137,13 +2140,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): for font, fontsize, ccode, glyph_index, ox, oy in glyphs: subset_index, subset_charcode = self.file._character_tracker.track_glyph( font, ccode, glyph_index) - fontname = font.fname + font_path = FontPath(font.fname, font.face_index) self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy - if (fontname, subset_index, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname, subset_index), fontsize, + if (font_path, subset_index, fontsize) != prev_font: + self.file.output(self.file.fontName(font_path, subset_index), fontsize, Op.selectfont) - prev_font = fontname, subset_index, fontsize + prev_font = font_path, subset_index, fontsize self.file.output(self._encode_glyphs([subset_charcode], fonttype), Op.show) self.file.output(Op.end_text) @@ -2338,7 +2341,9 @@ def output_singlebyte_chunk(kerns_or_chars): item.ft_object, item.char, item.glyph_index) if (item.ft_object, subset) != prev_font: output_singlebyte_chunk(singlebyte_chunk) - ft_name = self.file.fontName(item.ft_object.fname, subset) + font_path = FontPath(item.ft_object.fname, + item.ft_object.face_index) + ft_name = self.file.fontName(font_path, subset) self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) prev_font = (item.ft_object, subset) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 8ad31290a643..3bdf7a0c514b 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -94,7 +94,7 @@ def _font_to_ps_type3(font_path, subset_index, glyph_indices): Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. subset_index : int The subset of the above font being created. @@ -176,7 +176,7 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. subset_index : int The subset of the above font being created. @@ -187,12 +187,8 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): """ _log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices) try: - kw = {} - # fix this once we support loading more fonts from a collection - # https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541 - if font_path.endswith('.ttc'): - kw['fontNumber'] = 0 - with (fontTools.ttLib.TTFont(font_path, **kw) as font, + with (fontTools.ttLib.TTFont(font_path.path, + fontNumber=font_path.face_index) as font, _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index f07157a63524..1a79e7277be8 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -719,6 +719,10 @@ def fname(self): """A fake filename""" return self.texname.decode('latin-1') + @property + def face_index(self): # For compatibility with FT2Font. + return 0 + def _get_fontmap(self, string): """Get the mapping from characters to the font that includes them. diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 1a3b3943d07b..de429bd0b7f1 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -78,6 +78,8 @@ class DviFont: def widths(self) -> list[int]: ... @property def fname(self) -> str: ... + @property + def face_index(self) -> int: ... def resolve_path(self) -> Path: ... @property def subfont(self) -> int: ... diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 2dc22fd9170e..2a9c3542e277 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -3,6 +3,7 @@ import io import os from pathlib import Path +import string import numpy as np import pytest @@ -365,7 +366,7 @@ def test_glyphs_subset(): # subsetted FT2Font glyph_indices = {nosubcmap[ord(c)] for c in chars} - with get_glyphs_subset(fpath, glyph_indices) as subset: + with get_glyphs_subset(fm.FontPath(fpath, 0), glyph_indices) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) subcmap = subfont.get_charmap() @@ -402,6 +403,38 @@ def test_multi_font_type42(): horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.pdf'], style='mpl20') +def test_ttc_type3(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.pdf'], style='mpl20') +def test_ttc_type42(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @pytest.mark.parametrize('family_name, file_name', [("Noto Sans", "NotoSans-Regular.otf"), ("FreeMono", "FreeMono.otf")]) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 9859a286e5fd..bb6b08d14a6d 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -1,12 +1,14 @@ from collections import Counter import io +from pathlib import Path import re +import string import tempfile import numpy as np import pytest -from matplotlib import cbook, path, patheffects +from matplotlib import cbook, font_manager, path, patheffects from matplotlib.figure import Figure from matplotlib.patches import Ellipse from matplotlib.testing import _gen_multi_font_text @@ -340,6 +342,38 @@ def test_multi_font_type42(): horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.eps'], style='mpl20') +def test_ttc_type3(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.eps'], style='mpl20') +def test_ttc_type42(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @image_comparison(["scatter.eps"]) def test_path_collection(): rng = np.random.default_rng(19680801) From e9917908b800db32f57acd3a8b3a58df54937dd0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 19:53:21 -0400 Subject: [PATCH 56/58] pgf: Support any font in a collection Note, this only has an effect if set as the global font. Otherwise, just the font name is recorded, and the TeX engine's normal lookup is performed. --- lib/matplotlib/backends/backend_pgf.py | 25 ++++++++++++------------ lib/matplotlib/tests/test_backend_pgf.py | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 48b6e8ac152c..ab9782b369a3 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -38,9 +38,17 @@ def _get_preamble(): """Prepare a LaTeX preamble based on the rcParams configuration.""" - font_size_pt = FontProperties( - size=mpl.rcParams["font.size"] - ).get_size_in_points() + def _to_fontspec(): + for command, family in [("setmainfont", "serif"), + ("setsansfont", "sans\\-serif"), + ("setmonofont", "monospace")]: + font_path = fm.findfont(family) + path = pathlib.Path(font_path) + yield r" \%s{%s}[Path=\detokenize{%s/}%s]" % ( + command, path.name, path.parent.as_posix(), + f',FontIndex={font_path.face_index:d}' if path.suffix == '.ttc' else '') + + font_size_pt = FontProperties(size=mpl.rcParams["font.size"]).get_size_in_points() return "\n".join([ # Remove Matplotlib's custom command \mathdefault. (Not using # \mathnormal instead since this looks odd with Computer Modern.) @@ -63,15 +71,8 @@ def _get_preamble(): *([ r"\ifdefined\pdftexversion\else % non-pdftex case.", r" \usepackage{fontspec}", - ] + [ - r" \%s{%s}[Path=\detokenize{%s/}]" - % (command, path.name, path.parent.as_posix()) - for command, path in zip( - ["setmainfont", "setsansfont", "setmonofont"], - [pathlib.Path(fm.findfont(family)) - for family in ["serif", "sans\\-serif", "monospace"]] - ) - ] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), + *_to_fontspec(), + r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), # Documented as "must come last". mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"), ]) diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e218a81cdceb..e5b73c9450f3 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -1,7 +1,9 @@ import datetime from io import BytesIO import os +from pathlib import Path import shutil +import string import numpy as np from packaging.version import parse as parse_version @@ -9,6 +11,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties, findfont from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images @@ -330,6 +333,23 @@ def test_png_transparency(): # Actually, also just testing that png works. assert (t[..., 3] == 0).all() # fully transparent. +@needs_pgf_xelatex +@pytest.mark.backend('pgf') +@image_comparison(['ttc_pgf.pdf'], style='mpl20') +def test_ttc_output(): + fp = FontProperties(family=['WenQuanYi Zen Hei']) + if Path(findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = {'sans-serif': 'WenQuanYi Zen Hei', 'monospace': 'WenQuanYi Zen Hei Mono'} + plt.rc('font', size=16, **fonts) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts.values(), figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @needs_pgf_xelatex def test_unknown_font(caplog): with caplog.at_level("WARNING"): From 9c20b0ce57938213201897e7c9cf3dc012e78cb2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 31 Oct 2025 04:33:26 -0400 Subject: [PATCH 57/58] DOC: Add what's new note for TTC loading --- doc/release/next_whats_new/ttc_fonts.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 doc/release/next_whats_new/ttc_fonts.rst diff --git a/doc/release/next_whats_new/ttc_fonts.rst b/doc/release/next_whats_new/ttc_fonts.rst new file mode 100644 index 000000000000..b80b1186707b --- /dev/null +++ b/doc/release/next_whats_new/ttc_fonts.rst @@ -0,0 +1,18 @@ +Support for loading TrueType Collection fonts +--------------------------------------------- + +TrueType Collection fonts (commonly found as files with a ``.ttc`` extension) are now +supported. Namely, Matplotlib will include these file extensions in its scan for system +fonts, and will add all sub-fonts to its list of available fonts (i.e., the list from +`~.font_manager.get_font_names`). + +From most high-level API, this means you should be able to specify the name of any +sub-font in a collection just as you would any other font. Note that at this time, there +is no way to specify the entire collection with any sort of automated selection of the +internal sub-fonts. + +In the low-level API, to ensure backwards-compatibility while facilitating this new +support, a `.FontPath` instance (comprised of a font path and a sub-font index, with +behaviour similar to a `str`) may be passed to the font management API in place of a +simple `os.PathLike` path. Any font management API that previously returned a string path +now returns a `.FontPath` instance instead. From b2aa1f238d355551d6cff5866c2feccb69f27cbd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 26 Sep 2025 00:26:26 -0400 Subject: [PATCH 58/58] Update test images for previous changes --- .../test_backend_pdf/ttc_type3.pdf | Bin 0 -> 21114 bytes .../test_backend_pdf/ttc_type42.pdf | Bin 0 -> 9696 bytes .../test_backend_pgf/ttc_pgf.pdf | Bin 0 -> 15526 bytes .../test_backend_ps/ttc_type3.eps | 1483 +++++++++++++++++ .../test_backend_ps/ttc_type42.eps | 1483 +++++++++++++++++ 5 files changed, 2966 insertions(+) create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type3.eps create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type42.eps diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a3fad017236451b768f01358368b601991bf5cf8 GIT binary patch literal 21114 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 Ch|0hK literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d43f233ef4e6bba524f535b400d38f7fe5a45bac GIT binary patch literal 9696 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 LJG-2QJn?@4aq!mr literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5d695e734577100ab881040016fbbdb0d76a016a GIT binary patch literal 15526 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 K