Repository: matplotlib/matplotlib

Issue:
[Bug]: Style flag errors trying to save figures as PDF with font Inter
### Bug summary

I have installed the font Inter with `brew install font-inter` and successfully imported it into matplotlib such that the figure from the plot below displays correctly with the Inter font as specified; however, when it comes to saving, I get the error below in "actual outcome". 

### Code for reproduction

```Python
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

matplotlib.use("QtAgg")
# Ensure 'Inter' is available
available_fonts = [f.name for f in fm.fontManager.ttflist]
if 'Inter' in available_fonts:
    plt.rcParams['font.family'] = 'Inter'
else:
    print("Inter font is not available. Please ensure it is installed.")

# generate a test plot and save it
fig, ax = plt.subplots()
ax.plot([0, 1], [0, 1])
ax.set_title("Example Plot with Inter Font")
plt.show()
fig.savefig("example_plot.pdf", format='pdf')
```


### Actual outcome

```
Traceback (most recent call last):
  File "/Users/atom/hemanpro/HeMan/misc_tools/addfont.py", line 18, in <module>
    fig.savefig("example_plot.pdf", format='pdf')
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/figure.py", line 3490, in savefig
    self.canvas.print_figure(fname, **kwargs)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_qtagg.py", line 75, in print_figure
    super().print_figure(*args, **kwargs)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py", line 2184, in print_figure
    result = print_method(
        filename,
    ...<3 lines>...
        bbox_inches_restore=_bbox_inches_restore,
        **kwargs)
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py", line 2040, in <lambda>
    print_method = functools.wraps(meth)(lambda *args, **kwargs: meth(
                                                                 ~~~~^
        *args, **{k: v for k, v in kwargs.items() if k not in skip}))
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_pdf.py", line 2789, in print_pdf
    file.finalize()
    ~~~~~~~~~~~~~^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_pdf.py", line 827, in finalize
    self.writeFonts()
    ~~~~~~~~~~~~~~~^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_pdf.py", line 973, in writeFonts
    fonts[Fx] = self.embedTTF(filename, chars)
                ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_pdf.py", line 1416, in embedTTF
    sf = font.style_flags
         ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py", line 726, in __call__
    return cls.__new__(cls, value)
           ~~~~~~~~~~~^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py", line 1207, in __new__
    raise exc
  File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py", line 1184, in __new__
    result = cls._missing_(value)
  File "/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py", line 1480, in _missing_
    raise ValueError(
    ...<2 lines>...
                ))
ValueError: <flag 'StyleFlags'> invalid value 589824
    given 0b0 10010000000000000000
  allowed 0b0 00000000000000000011
.venvFAIL
```

### Expected outcome

The figure is saved as a PDF with the Inter font.

### Additional information

This only occurs on my macOS installation of Python 3.13.1 with matplotlib 3.10.0.

### Operating system

macOS Sequoia 15.2

### Matplotlib Version

3.10.0

### Matplotlib Backend

QtAgg, cairo, macosx

### Python version

3.13.1

### Jupyter version

_No response_

### Installation

pip


Discussion:
Note for repro: to trigger the bug it may be necessary to use the explicit path to the "bad" (actually, variable) font, i.e. something like
```python
from pylab import *; from pathlib import Path
figtext(.5, .5, "foo", font=Path("/path/to/InterVariable.ttf"))  # or font="Inter Variable" also seems to "work" here
savefig("/tmp/test.pdf")
```
but the bug is valid.

It basically arises because of the conversion of FT_STYLE_FLAG_XXX to enum.Flag (regression in #28842), which means that any unknown value in style_flags raises the error seen above.  Note that such values are explicitly allowed by the FreeType docs (https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_facerec): "[Since 2.6.1] Bits 16-30 hold the number of named instances available for the current face if we have a GX or OpenType variation (sub)font. Bit 31 is always zero (that is, style_flags is always a positive value). Note that a variation font has always at least one named instance, namely the default instance."

There's a few fixes possible there:
- switch the use of enum.Flag to enum.IntFlag, which allows "unregistered" high bits; probably the easiest fix by far.
- or, on Py3.11+ only, use `boundary=KEEP` when declaring the enum (the easiest way to do so with the current pybind11 enum machinery is probably `p11x::enums["StyleFlags"].attr("_boundary_") = py::module::import("enum").attr("KEEP");` at the same place as where the enum docs are set, but one can probably also adjust the P11X_DECLARE_ENUM macro to avoid touching at semi-internal APIs if desired).  However I don't think there's an equivalent available on Py3.10.
- or, split out bits 16-30 of style_flags (per the documentation) and store them in a separate attribute, as they are semantically separate.

Note that face_flags likely suffers from the same issue; at least, the commented out FaceFlag values in ft2font_wrapper.cpp's `P11X_DECLARE_ENUM("FaceFlags", ...)` should probably be ifdef'd as done in `DECLARE_FLAG` just above.
> Note that face_flags likely suffers from the same issue; at least, the commented out FaceFlag values in ft2font_wrapper.cpp's `P11X_DECLARE_ENUM("FaceFlags", ...)` should probably be ifdef'd as done in `DECLARE_FLAG` just above.

These can't be `#ifdef` because `P11X_DECLARE_ENUM` is a macro invocation. If we could rewrite that as some kind of template, then it might be possible, but not as a macro.
Good catch.  The following appears to work, though:
```cpp
#ifdef FT_FACE_FLAG_VARIATION
    #define FACE_FLAG_VARIATION {"VARIATION", FaceFlags::VARIATION},
#else
    #define FACE_FLAG_VARIATION
#endif
P11X_DECLARE_ENUM(
    "FaceFlags", "Flag",
    {"SCALABLE", FaceFlags::SCALABLE},
    {"FIXED_SIZES", FaceFlags::FIXED_SIZES},
    {"FIXED_WIDTH", FaceFlags::FIXED_WIDTH},
    {"SFNT", FaceFlags::SFNT},
    {"HORIZONTAL", FaceFlags::HORIZONTAL},
    {"VERTICAL", FaceFlags::VERTICAL},
    {"KERNING", FaceFlags::KERNING},
    {"FAST_GLYPHS", FaceFlags::FAST_GLYPHS},
    {"MULTIPLE_MASTERS", FaceFlags::MULTIPLE_MASTERS},
    {"GLYPH_NAMES", FaceFlags::GLYPH_NAMES},
    {"EXTERNAL_STREAM", FaceFlags::EXTERNAL_STREAM},
    {"HINTER", FaceFlags::HINTER},
    {"CID_KEYED", FaceFlags::CID_KEYED},
    {"TRICKY", FaceFlags::TRICKY},
    {"COLOR", FaceFlags::COLOR},
    FACE_FLAG_VARIATION  // backcompat: ft 2.9.0.
    // ditto for the others
);
```



Patch:
diff --git a/galleries/examples/misc/ftface_props.py b/galleries/examples/misc/ftface_props.py
index 8306e142c144..ec26dff5bf6a 100644
--- a/galleries/examples/misc/ftface_props.py
+++ b/galleries/examples/misc/ftface_props.py
@@ -18,12 +18,13 @@
     os.path.join(matplotlib.get_data_path(),
                  'fonts/ttf/DejaVuSans-Oblique.ttf'))
 
-print('Num faces:  ', font.num_faces)        # number of faces in file
-print('Num glyphs: ', font.num_glyphs)       # number of glyphs in the face
-print('Family name:', font.family_name)      # face family name
-print('Style name: ', font.style_name)       # face style name
-print('PS name:    ', font.postscript_name)  # the postscript name
-print('Num fixed:  ', font.num_fixed_sizes)  # number of embedded bitmaps
+print('Num instances:  ', font.num_named_instances)  # number of named instances in file
+print('Num faces:      ', font.num_faces)            # number of faces in file
+print('Num glyphs:     ', font.num_glyphs)           # number of glyphs in the face
+print('Family name:    ', font.family_name)          # face family name
+print('Style name:     ', font.style_name)           # face style name
+print('PS name:        ', font.postscript_name)      # the postscript name
+print('Num fixed:      ', font.num_fixed_sizes)      # number of embedded bitmaps
 
 # the following are only available if face.scalable
 if font.scalable:
diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi
index 811f82f95963..37281afeaafa 100644
--- a/lib/matplotlib/ft2font.pyi
+++ b/lib/matplotlib/ft2font.pyi
@@ -265,6 +265,8 @@ class FT2Font(Buffer):
     @property
     def num_glyphs(self) -> int: ...
     @property
+    def num_named_instances(self) -> int: ...
+    @property
     def postscript_name(self) -> str: ...
     @property
     def scalable(self) -> bool: ...
diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp
index 7db8421f80d6..ac2c6d46f984 100644
--- a/src/ft2font_wrapper.cpp
+++ b/src/ft2font_wrapper.cpp
@@ -1475,7 +1475,13 @@ PyFT2Font_face_flags(PyFT2Font *self)
 static StyleFlags
 PyFT2Font_style_flags(PyFT2Font *self)
 {
-    return static_cast<StyleFlags>(self->x->get_face()->style_flags);
+    return static_cast<StyleFlags>(self->x->get_face()->style_flags & 0xffff);
+}
+
+static FT_Long
+PyFT2Font_num_named_instances(PyFT2Font *self)
+{
+    return (self->x->get_face()->style_flags & 0x7fff0000) >> 16;
 }
 
 static FT_Long
@@ -1766,6 +1772,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
                                "Face flags; see `.FaceFlags`.")
         .def_property_readonly("style_flags", &PyFT2Font_style_flags,
                                "Style flags; see `.StyleFlags`.")
+        .def_property_readonly("num_named_instances", &PyFT2Font_num_named_instances,
+                               "Number of named instances in the face.")
         .def_property_readonly("num_glyphs", &PyFT2Font_num_glyphs,
                                "Number of glyphs in the face.")
         .def_property_readonly("num_fixed_sizes", &PyFT2Font_num_fixed_sizes,


Test patch:
diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py
index 7dc851b2c9cf..6ba23bab347e 100644
--- a/lib/matplotlib/tests/test_ft2font.py
+++ b/lib/matplotlib/tests/test_ft2font.py
@@ -40,6 +40,7 @@ def test_ft2font_dejavu_attrs():
     assert font.family_name == 'DejaVu Sans'
     assert font.style_name == 'Book'
     assert font.num_faces == 1  # Single TTF.
+    assert font.num_named_instances == 0  # Not a variable font.
     assert font.num_glyphs == 6241  # From compact encoding view in FontForge.
     assert font.num_fixed_sizes == 0  # All glyphs are scalable.
     assert font.num_charmaps == 5
@@ -73,6 +74,7 @@ def test_ft2font_cm_attrs():
     assert font.family_name == 'cmtt10'
     assert font.style_name == 'Regular'
     assert font.num_faces == 1  # Single TTF.
+    assert font.num_named_instances == 0  # Not a variable font.
     assert font.num_glyphs == 133  # From compact encoding view in FontForge.
     assert font.num_fixed_sizes == 0  # All glyphs are scalable.
     assert font.num_charmaps == 2
@@ -105,6 +107,7 @@ def test_ft2font_stix_bold_attrs():
     assert font.family_name == 'STIXSizeTwoSym'
     assert font.style_name == 'Bold'
     assert font.num_faces == 1  # Single TTF.
+    assert font.num_named_instances == 0  # Not a variable font.
     assert font.num_glyphs == 20  # From compact encoding view in FontForge.
     assert font.num_fixed_sizes == 0  # All glyphs are scalable.
     assert font.num_charmaps == 3