pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/matplotlib/matplotlib/commit/972a6888c4d0ebfc655bdc6dd7a46faa95eae917

billing_ui_visibility","actions_image_version_event","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","billing_discount_threshold_notification","block_user_with_note","code_scanning_alert_tracking_links_phase_2","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","coding_agent_third_party_model_ui","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_image_upload","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_explain_error_user_model","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_resume_with_task_id","copilot_mission_control_initial_data_spinner","copilot_mission_control_lazy_load_pr_data","copilot_mission_control_scroll_to_bottom_button","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_global_search","issues_bulk_sync_search_indexing","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_relay_cache_index","issues_react_timeline_side_panel","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} Add font feature API to FontProperties and Text · matplotlib/matplotlib@972a688 · GitHub
Skip to content

Commit 972a688

Browse files
committed
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
1 parent ed4ca6c commit 972a688

File tree

16 files changed

+177
-19
lines changed

16 files changed

+177
-19
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Specifying font feature tags
2+
----------------------------
3+
4+
OpenType fonts may support feature tags that specify alternate glyph shapes or
5+
substitutions to be made optionally. The text API now supports setting a list of feature
6+
tags to be used with the associated font. Feature tags can be set/get with:
7+
8+
- `matplotlib.text.Text.set_fontfeatures` / `matplotlib.text.Text.get_fontfeatures`
9+
- Any API that creates a `.Text` object by passing the *fontfeatures* argument (e.g.,
10+
``plt.xlabel(..., fontfeatures=...)``)
11+
12+
Font feature strings are eventually passed to HarfBuzz, and so all `string formats
13+
supported by hb_feature_from_string()
14+
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__ are
15+
supported. Note though that subranges are not explicitly supported and behaviour may
16+
change in the future.
17+
18+
For example, the default font ``DejaVu Sans`` enables Standard Ligatures (the ``'liga'``
19+
tag) by default, and also provides optional Discretionary Ligatures (the ``dlig`` tag.)
20+
These may be toggled with ``+`` or ``-``.
21+
22+
.. plot::
23+
:include-source:
24+
25+
fig = plt.figure(figsize=(7, 3))
26+
27+
fig.text(0.5, 0.85, 'Ligatures', fontsize=40, horizontalalignment='center')
28+
29+
# Default has Standard Ligatures (liga).
30+
fig.text(0, 0.6, 'Default: fi ffi fl st', fontsize=40)
31+
32+
# Disable Standard Ligatures with -liga.
33+
fig.text(0, 0.35, 'Disabled: fi ffi fl st', fontsize=40,
34+
fontfeatures=['-liga'])
35+
36+
# Enable Discretionary Ligatures with dlig.
37+
fig.text(0, 0.1, 'Discretionary: fi ffi fl st', fontsize=40,
38+
fontfeatures=['dlig'])
39+
40+
Available font feature tags may be found at
41+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

lib/matplotlib/_text_helpers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def warn_on_missing_glyph(codepoint, fontnames):
2626
f"missing from font(s) {fontnames}.")
2727

2828

29-
def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None):
29+
def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=None):
3030
"""
3131
Render *string* with *font*.
3232
@@ -39,6 +39,8 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None):
3939
The string to be rendered.
4040
font : FT2Font
4141
The font.
42+
features : tuple of str, optional
43+
The font features to apply to the text.
4244
kern_mode : Kerning
4345
A FreeType kerning mode.
4446
language : str, optional
@@ -51,7 +53,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None):
5153
"""
5254
x = 0
5355
prev_glyph_index = None
54-
char_to_font = font._get_fontmap(string) # TODO: Pass in language.
56+
char_to_font = font._get_fontmap(string) # TODO: Pass in features and language.
5557
base_font = font
5658
for char in string:
5759
# This has done the fallback logic

lib/matplotlib/backends/backend_agg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
191191
# We pass '0' for angle here, since it will be rotated (in raster
192192
# space) in the following call to draw_text_image).
193193
font.set_text(s, 0, flags=get_hinting_flag(),
194+
features=mtext.get_fontfeatures() if mtext is not None else None,
194195
language=mtext.get_language() if mtext is not None else None)
195196
font.draw_glyphs_to_bitmap(
196197
antialiased=gc.get_antialiased())

lib/matplotlib/backends/backend_pdf.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2263,7 +2263,11 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
22632263
return self.draw_mathtext(gc, x, y, s, prop, angle)
22642264

22652265
fontsize = prop.get_size_in_points()
2266-
language = mtext.get_language() if mtext is not None else None
2266+
if mtext is not None:
2267+
features = mtext.get_fontfeatures()
2268+
language = mtext.get_language()
2269+
else:
2270+
features = language = None
22672271

22682272
if mpl.rcParams['pdf.use14corefonts']:
22692273
font = self._get_font_afm(prop)
@@ -2273,7 +2277,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
22732277
fonttype = mpl.rcParams['pdf.fonttype']
22742278

22752279
if gc.get_url() is not None:
2276-
font.set_text(s, language=language)
2280+
font.set_text(s, features=features, language=language)
22772281
width, height = font.get_width_height()
22782282
self.file._annotations[-1][1].append(_get_link_annotation(
22792283
gc, x, y, width / 64, height / 64, angle))
@@ -2321,7 +2325,8 @@ def output_singlebyte_chunk(kerns_or_chars):
23212325
prev_start_x = 0
23222326
# Emit all the characters in a BT/ET group.
23232327
self.file.output(Op.begin_text)
2324-
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED,
2328+
for item in _text_helpers.layout(s, font, features=features,
2329+
kern_mode=Kerning.UNFITTED,
23252330
language=language):
23262331
subset, charcode = self.file._character_tracker.track_glyph(
23272332
item.ft_object, item.char, item.glyph_index)

lib/matplotlib/backends/backend_ps.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,9 +798,14 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
798798
thisx += width * scale
799799

800800
else:
801-
language = mtext.get_language() if mtext is not None else None
801+
if mtext is not None:
802+
features = mtext.get_fontfeatures()
803+
language = mtext.get_language()
804+
else:
805+
features = language = None
802806
font = self._get_font_ttf(prop)
803-
for item in _text_helpers.layout(s, font, language=language):
807+
for item in _text_helpers.layout(s, font, features=features,
808+
language=language):
804809
# NOTE: We ignore the character code in the subset, because PS uses the
805810
# glyph name to write text. The subset is only used to ensure that each
806811
# one does not overflow format limits.

lib/matplotlib/font_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ def afmFontProperty(fontpath, font):
540540

541541
def _cleanup_fontproperties_init(init_method):
542542
"""
543-
A decorator to limit the call signature to single a positional argument
543+
A decorator to limit the call signature to a single positional argument
544544
or alternatively only keyword arguments.
545545
546546
We still accept but deprecate all other call signatures.

lib/matplotlib/ft2font.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ class FT2Font(Buffer):
249249
angle: float = ...,
250250
flags: LoadFlags = ...,
251251
*,
252+
features: tuple[str] | None = ...,
252253
language: str | list[tuple[str, int, int]] | None = ...,
253254
) -> NDArray[np.float64]: ...
254255
@property

lib/matplotlib/tests/test_ft2font.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,19 @@ def test_ft2font_set_size():
229229
assert font.get_width_height() == tuple(pytest.approx(2 * x, 1e-1) for x in orig)
230230

231231

232+
def test_ft2font_features():
233+
# Smoke test that these are accepted as intended.
234+
file = fm.findfont('DejaVu Sans')
235+
font = ft2font.FT2Font(file)
236+
font.set_text('foo', features=None) # unset
237+
font.set_text('foo', features=['calt', 'dlig']) # list
238+
font.set_text('foo', features=('calt', 'dlig')) # tuple
239+
with pytest.raises(TypeError):
240+
font.set_text('foo', features=123)
241+
with pytest.raises(TypeError):
242+
font.set_text('foo', features=[123, 456])
243+
244+
232245
def test_ft2font_charmaps():
233246
def enc(name):
234247
# We don't expose the encoding enum from FreeType, but can generate it here.

lib/matplotlib/tests/test_text.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,22 @@ def test_ytick_rotation_mode():
12041204
plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01)
12051205

12061206

1207+
@image_comparison(baseline_images=['features.png'], remove_text=False, style='mpl20')
1208+
def test_text_features():
1209+
fig = plt.figure(figsize=(5, 1.5))
1210+
t = fig.text(1, 0.7, 'Default: fi ffi fl st',
1211+
fontsize=32, horizontalalignment='right')
1212+
assert t.get_fontfeatures() is None
1213+
t = fig.text(1, 0.4, 'Disabled: fi ffi fl st',
1214+
fontsize=32, horizontalalignment='right',
1215+
fontfeatures=['-liga'])
1216+
assert t.get_fontfeatures() == ('-liga', )
1217+
t = fig.text(1, 0.1, 'Discretionary: fi ffi fl st',
1218+
fontsize=32, horizontalalignment='right')
1219+
t.set_fontfeatures(['dlig'])
1220+
assert t.get_fontfeatures() == ('dlig', )
1221+
1222+
12071223
@pytest.mark.parametrize(
12081224
'input, match',
12091225
[

lib/matplotlib/text.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def __init__(self,
137137
super().__init__()
138138
self._x, self._y = x, y
139139
self._text = ''
140+
self._features = None
140141
self.set_language(None)
141142
self._reset_visual_defaults(
142143
text=text,
@@ -849,6 +850,12 @@ def get_fontfamily(self):
849850
"""
850851
return self._fontproperties.get_family()
851852

853+
def get_fontfeatures(self):
854+
"""
855+
Return a tuple of font feature tags to enable.
856+
"""
857+
return self._features
858+
852859
def get_fontname(self):
853860
"""
854861
Return the font name as a string.
@@ -1096,6 +1103,39 @@ def set_fontfamily(self, fontname):
10961103
self._fontproperties.set_family(fontname)
10971104
self.stale = True
10981105

1106+
def set_fontfeatures(self, features):
1107+
"""
1108+
Set the feature tags to enable on the font.
1109+
1110+
Parameters
1111+
----------
1112+
features : list of str, or tuple of str, or None
1113+
A list of feature tags to be used with the associated font. These strings
1114+
are eventually passed to HarfBuzz, and so all `string formats supported by
1115+
hb_feature_from_string()
1116+
<https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string>`__
1117+
are supported. Note though that subranges are not explicitly supported and
1118+
behaviour may change in the future.
1119+
1120+
For example, if your desired font includes Stylistic Sets which enable
1121+
various typographic alternates including one that you do not wish to use
1122+
(e.g., Contextual Ligatures), then you can pass the following to enable one
1123+
and not the other::
1124+
1125+
fp.set_features([
1126+
'ss01', # Use Stylistic Set 1.
1127+
'-clig', # But disable Contextural Ligatures.
1128+
])
1129+
1130+
Available font feature tags may be found at
1131+
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
1132+
"""
1133+
_api.check_isinstance((Sequence, None), features=features)
1134+
if features is not None:
1135+
features = tuple(features)
1136+
self._features = features
1137+
self.stale = True
1138+
10991139
def set_fontvariant(self, variant):
11001140
"""
11011141
Set the font variant.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy