Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement Font-Fallback in Matplotlib #20740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 4, 2022

Conversation

aitikgupta
Copy link
Contributor

@aitikgupta aitikgupta commented Jul 25, 2021

PR Summary

This PR modifies the internal structure of FT2Font (the interface between fonts and Matplotlib) in favor of implementing Font Fallback for Matplotlib, and allow Agg backend to use the new codepath.
It builds on the previous PR: #20549, which was the 'first-step', i.e., "parsing multiple families".. this PR implements using those families for font fallback.

This would help us in multi-language support, for example (Previous / After):

^the fonts are chosen such that the difference is visually noticeable.

A flowchart explaining the text rendering algorithm with font fallback:
FontFallback

Here's the script:

import matplotlib.pyplot as plt
# "Authentic" is a fancy font, whereas "SimHei" is a CJK font
plt.rcParams['font.family'] = ['Authentic', 'SimHei']
plt.rcParams['font.size'] = 30

plt.figtext(0.18, 0.45, "There are 多个汉字 in between!")
plt.show()

Fixes #18883, #15260

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).

@aitikgupta
Copy link
Contributor Author

aitikgupta commented Jul 25, 2021

To actually trigger this workflow, I'll push similar commits similar to #20549..
Note: this is only tested against Agg backend (since agg is pretty straightforward in terms of font usage)

Also, there's a lot of print/debug statements throughout, I'll remove them once this moves ahead..

@aitikgupta aitikgupta marked this pull request as draft July 26, 2021 06:32
@aitikgupta aitikgupta force-pushed the fallback-revamp branch 2 times, most recently from 9aee34b to 9fabbc7 Compare August 6, 2021 23:47
@aitikgupta aitikgupta changed the title Implement Font-Fallback in Matplotlib Implement Font-Fallback for Agg backend Aug 6, 2021
@aitikgupta
Copy link
Contributor Author

aitikgupta commented Aug 7, 2021

Note: There's some structural changes to how FT2Font is handling stuff in #20804, which builds on this PR..

EDIT: I've merged the FT2Font changes together for both PRs

@aitikgupta aitikgupta force-pushed the fallback-revamp branch 2 times, most recently from 0fa0625 to e868fbc Compare August 7, 2021 00:08
@aitikgupta aitikgupta marked this pull request as ready for review August 7, 2021 08:22
@aitikgupta aitikgupta changed the title Implement Font-Fallback for Agg backend Implement Font-Fallback in Matplotlib Aug 13, 2021
jkseppan added a commit to jkseppan/matplotlib that referenced this pull request Aug 20, 2021
On Linux and Mac. Add a font_manager test that loads the font and
uses it. This could be useful in testing the multi-font support
(matplotlib#20740 etc).

I couldn't get the font installed on Windows. There is a Chocolatey
installer that installs all of the Noto fonts, which takes a really
long time to run.
@jklymak
Copy link
Member

jklymak commented Oct 4, 2021

@aitikgupta Do these have a review team looking at them?

@aitikgupta
Copy link
Contributor Author

@jklymak I don't think anybody is actively reviewing these font handling PRs at the moment.. but they're ready for review and not drafts (I'll resolve the conflicts)

@aitikgupta
Copy link
Contributor Author

aitikgupta commented Oct 22, 2021

Going ahead and removing the test, since font distributions for different Linux versions across CI are different.. and thus their kerning information is different -- different visual outputs.

We can test it with a self-shipped CJK font, which is guaranteed to be present across all platforms.. but that'd be another problem because their file sizes are huge.

@anntzer
Copy link
Contributor

anntzer commented Oct 27, 2021

(Unfortunately, I will not have the bandwidth to review this in the coming days.)

@jklymak
Copy link
Member

jklymak commented Jan 14, 2022

Hi @aitikgupta we were all discussing this PR yesterday - did you have an opportunity to rebase and address the review comments?

@aitikgupta
Copy link
Contributor Author

Hey @jklymak, I'm currently out sick. Will get back to this as soon as I can, probably a week or two. Apologies!

@jklymak
Copy link
Member

jklymak commented Jan 16, 2022

No rush, get better!

@aitikgupta
Copy link
Contributor Author

Hey @QuLogic, addressed all the comments (added comments for further discussions), and brought the PR up to speed!

@anntzer
Copy link
Contributor

anntzer commented Jul 7, 2022

The general design looks reasonable and a skim through the actual implementation only revealed the small issues listed above, which should be fixed, but once that's done I am happy with merging this as is and iterating over it later.

@QuLogic
Copy link
Member

QuLogic commented Jul 7, 2022

The new code is 2-space indent instead of 4.

@@ -272,7 +272,9 @@ def _prepare_font(self, font_prop):
"""
Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
"""
font = get_font(findfont(font_prop))
fname = find_fonts_by_props(font_prop)
Copy link
Contributor

@anntzer anntzer Jul 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fname should be renamed to filename_dict or at least filenames (plural) for clarity.
Given that get_font only uses .values(), I would suggest making it clearer at this stage too (filenames = list(find_fonts_by_props(font_prop).values()); font = get_font(filenames)) (and modify get_font to take a list instead of a dict). This would also make it easier for third-parties to directly call get_font with a list of font paths rather having to invent keys to create a dict, if they don't already have a dict at hand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not remember why we used a dictionary instead of a list

@jklymak
Copy link
Member

jklymak commented Jul 8, 2022

As a meta comment, this seems like a pretty big change to squeeze in right before a 3.6 release, versus getting it in main and letting the devs live with it, and anyone else who is testing our dev tree, for a few months. However, maybe I am misunderstanding its potential to break existing users who don't typically need the fallback.

@oscargus
Copy link
Member

oscargus commented Jul 8, 2022

right before a 3.6 release

Talking about that: what is the time line? I sort of expected 3.5.3 to be released "soon" and then probably a few months until 3.6? (Really OT, but still...)

@tacaswell
Copy link
Member

As a meta comment, this seems like a pretty big change to squeeze in right before a 3.6 release, ...

Fair, however you do not pass a list of fallbacks then there should be no behavior change, we hammer these code paths in the tests, and I do not think of the core devs do enough mixed glyph work (I could be wrong about that) to meaning stress it.

@anntzer
Copy link
Contributor

anntzer commented Jul 9, 2022

An additional point I just thought about: the actual low-level API (including passing a list of fallback FT2Font to the FT2Font constructor) should probably be marked as provisional. While I haven't actually checked, I believe that the current API may be slightly inefficient, because (if I understood it correctly) we will always immediately parse all font files (a costly operation) in the fallback list, regardless of whether we actually need them or not, whereas we could imagine an API where we only store a list of fallback paths that get parsed one at a time when needed. (This is also relevant for mplcairo, which doesn't use FT2Font at all and simply gets the paths of the fonts to do its own interfacing with freetype.)
I certainly don't want to hold up merging by requesting a large API design change at the last minute, but marking the low-level parts as provisional would certainly simplify possible improvements in the future.

@tacaswell
Copy link
Member

An additional point I just thought about: the actual low-level API (including passing a list of fallback FT2Font to the FT2Font constructor) should probably be marked as provisional.

That will require documenting ft2font at all, but a reasonable request. The keyword already starts with _ so I think documenting it at provisional and/or private is consistent with the original intent.

@anntzer
Copy link
Contributor

anntzer commented Jul 10, 2022

I wasn't actually asking for FT2Font to be documented (a blurb in the changelog without linking API entries would have worked too), but sure, that's always a good thing to add.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good, though needs the rebase for added/removed files.

Co-authored-by: Aitik Gupta <[email protected]>
Co-authored-by: Elliott Sales de Andrade <[email protected]>
@tacaswell
Copy link
Member

Squashed this all to 1 commit:

(dd310) ~/s/p/m/matplotlib fallback-revamp↑·1↓·63|…3⚑5
✔ 17:45:01 $ git describe
v3.5.2-2641-g85bacdccc5
(dd310) ~/s/p/m/matplotlib fallback-revamp↑·1↓·63|…3⚑5
✔ 17:45:05 $ git diff 14251eb
(dd310) ~/s/p/m/matplotlib fallback-revamp↑·1↓·63|…3⚑5

@efiring efiring merged commit 535c953 into matplotlib:main Aug 4, 2022
@tacaswell
Copy link
Member

Thank you @aitikgupta ! This would not have been possible without your work.

@aitikgupta
Copy link
Contributor Author

Super glad this finally made it! Thanks everyone for pushing this 🙇🏼‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend: agg Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: text/fonts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Matplotlib would not try to apply all the font in font list to draw all characters in the given string.
8 participants