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

Skip to content

Add custom math fallback #16509

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
Mar 3, 2020
Merged

Conversation

andrzejnovak
Copy link
Contributor

@andrzejnovak andrzejnovak commented Feb 14, 2020

PR Summary

Follow-up on #11644

I've switched the logic so it's more transparent when mathtext.fallback is by default 'cm' . The original mathtext.fallback_to_cm defaults to None, but either True or False will raise a warning and override the mathtext.fallback setting.

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@andrzejnovak
Copy link
Contributor Author

@anntzer This should about do it. Seems mpl ships only with DejaVu Sans/STIX so I couldn't test a real figure against an incomplete font that would have to get substituted.

Unsure what's failing the docs though

@anntzer
Copy link
Contributor

anntzer commented Feb 15, 2020

@geib7592 Can you please comment on this PR? Thanks!

@anntzer
Copy link
Contributor

anntzer commented Feb 15, 2020

Looking a bit below, it appears that DejaVuFonts (the actual default) already implements its own fallback solution, which goes to stix, not not cm. If you set mathtext.fallback, does that also override the fallback for dejavu?

I guess this suggests that the proper solution may instead to make mathtext.fontset accept a list of fonts, falling back from one to the next (so e.g. mathtext.fontset: dejavusans, stix or custom, cm or whatnot -- where custom is again defined by mathtext.{cal,rm,tt,it,bf,sf}).

@andrzejnovak
Copy link
Contributor Author

Looking a bit below, it appears that DejaVuFonts (the actual default) already implements its own fallback solution, which goes to stix, not not cm. If you set mathtext.fallback, does that also override the fallback for dejavu?

I guess this suggests that the proper solution may instead to make mathtext.fontset accept a list of fonts, falling back from one to the next (so e.g. mathtext.fontset: dejavusans, stix or custom, cm or whatnot -- where custom is again defined by mathtext.{cal,rm,tt,it,bf,sf}).

IIUC fallback_to_cm only came to effect when using a math.text = custom and the behaviour should not be changed in this PR.

I agree it would be nicer to have a fallback list, but that seems like it would require major code changes, meanwhile the functionality in this PR (or the original PR indeed) is good enough.

Though it could be easily extended to also so StixSansFonts.

@geib7592
Copy link
Contributor

Hello, it's me from #8619. I'm excited to see someone working on this issue. It seems that since matplotlib ships with stix and dejavu sans, adding a fallback to dajavu sans (and stix sans) could be another (very similar) PR, and in my opinion, it would be worthwhile for flexibility in using custom sans-serif fonts.

Allowing a custom fallback font is likely more work, but would be useful for cases I pointed out in #8619. (@anntzer, your suggestion for having mathtext.fontset be a list seems to preclude the future possibility of having a custom fallback font)

I think @anntzer also suggested I add my two cents about why this PR is important. Since posting the feature request in 2017, I've thought a fair amount about how to typeset publication-quality scientific figures. Currently, I don't think any free (or even non-free) software does it perfectly, and it's an area where, with a few well chosen tweaks, matplotlib could really excel.

For example, major scientific publishing companies like Nature and Science explicitly prefer Helvetica for the text of their figures. For Greek letters and mathematical symbols, they use a similarly weighted serif font used in their main text. This PR as it stands really helps move in the right direction; the example in #11644 shows how to use Helvetica with a fallback to stix, which gets closer to Nature's guideline.

I'd love to see more development toward industry standard design principles in the future with similar tweaks to the mathtext and latex engines. If the matplotlib devs are interested, I'd be happy to discuss and get more involved.

Thanks for all your work!

@anntzer
Copy link
Contributor

anntzer commented Feb 15, 2020

I'm fine with not doing the most general approach; I just want the various fallback options and their interaction to be properly documented (e.g. whether mathtext.fallback affects dejavu fallback as well).

your suggestion for having mathtext.fontset be a list seems to preclude the future possibility of having a custom fallback font

why?

@geib7592
Copy link
Contributor

Maybe I misunderstood, but right now custom mathfont requires mathtext.fontset=custom, so if you also wanted a custom fallback, you'd need a second custom setting, like allowing mathtext.{cal,rm,tt,it,bf,sf} to be lists too, where missing characters are sourced down the list.

@anntzer
Copy link
Contributor

anntzer commented Feb 15, 2020

Ah yes, you want to fallback from one custom font to another one...
I guess making all of mathtext.cal/rm/... lists too would work indeed? (not that it has to be done in this PR)

@anntzer
Copy link
Contributor

anntzer commented Feb 16, 2020

If you look a bit below, class DejaVuFonts has

class DejaVuFonts(UnicodeFonts):
    use_cmex = False

    def __init__(self, *args, **kwargs):
        # This must come first so the backend's owner is set correctly
        if isinstance(self, DejaVuSerifFonts):
            self.cm_fallback = StixFonts(*args, **kwargs)
        else:
            self.cm_fallback = StixSansFonts(*args, **kwargs)
        self.bakoma = BakomaFonts(*args, **kwargs)
        TruetypeFonts.__init__(self, *args, **kwargs)

(the point I was mentioning above)

I'm actually now a bit confused by why DejaVuFonts inherits from UnicodeFonts but calls TruetypeFonts.__init__ in its __init__, and am not even sure anymore that DejaVuFonts.cm_fallback is ever used?

@andrzejnovak
Copy link
Contributor Author

I think we all agree that what we want is a fallback list for math as well. However, since that's major surgery, especially since the default fonts have a bunch of no-warnings/no-docs fallback which should likely be overhauled, we should open this as a new issue for a new PR.

This PR bring major improvement in just being able to have a sans-serif math fallback for custom fonts. Fulfilling most of what was requested in the 3 separate issues #8619, #11644 and #16492

Re some details, I added the option to explicitly ask for stixsans but actually have trouble finding characters that would be missing in a default font (say Arial) and would be different between stix and stixsans, which seem to differ for regular alphabet chars, but not so much for math symbols. (Basically I can't test is)

Otherwise, if we can agree on this functionality I'll update the tut/whats_new etc...

@anntzer
Copy link
Contributor

anntzer commented Feb 18, 2020

I think you can add a test by picking some math strings that include a character that's not present in e.g. dejavusans but present in cm/stix/stixsans, and then saving a svg file with that string and rcParams["svg.fonttype"] = None, with fallback configured to cm/stix/stixsans/nofallback, which will saves the individual glyphs; you can then check that fallback correctly occurred (or not, if not falling back).

@andrzejnovak
Copy link
Contributor Author

Ok, I did the SVG check and the fallbacks work fine, the issue is just that some of the math characters are the same for both stix/stixsans e.g.("#STIXGeneral-Regular-10938") In reality it looks like it won't matter if the math fallback is stix or stixsans

@anntzer
Copy link
Contributor

anntzer commented Feb 19, 2020

It's fine if the test doesn't cover all cases, it would still be an improvement over the current situation.

@@ -660,6 +660,7 @@ class BakomaFonts(TruetypeFonts):

def __init__(self, *args, **kwargs):
self._stix_fallback = StixFonts(*args, **kwargs)
self.name = "Computer Modern"
Copy link
Contributor

Choose a reason for hiding this comment

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

Well, the hope was that you would not have to hardcode this but could instead infer it from the FT2Font object, but I guess that somehow didn't work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I'm doing it wrong, but the only reasonable way I found was self.cm_fallback.default_font_prop.get_family() but that just returns ['sans-serif'] which doesn't help here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or fontmap which returns sth like {'cal': '/home/anovak/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmsy10.ttf', ... 'cmsy10': '/home/anovak/matplotlib/lib/matplotlib/mpl- data/fonts/ttf/cmex10.ttf', 'cmex10': '/home/anovak/matplotlib/lib/matplotlib/mpl-data/fonts/ttf/cmex10.ttf'} which would need some extra matching overhead to spit out a useful message.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was basically considering something like

                if (fontname in ('it', 'regular')
                        and isinstance(self.cm_fallback, StixFonts)):
                    fontname = 'rm'

                g = self.cm_fallback._get_glyph(
                    fontname, font_class, sym, fontsize)
                _log.warning("Substituting with a symbol from {}".format(g[0].family_name))
                # or .postscript_name
                return g

As it turns out, while this would work for stix fallback and most other fonts (I guess), this would fail specifically for computer modern fallback because cmmi10.ttf has poor metadata (it lists its family as "cmmi10"...), but I guess you can always special-case cm; at least this should work for "all" other fallbacks...

Copy link
Contributor Author

@andrzejnovak andrzejnovak Feb 20, 2020

Choose a reason for hiding this comment

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

Ah nice, that's quite fun, because you can report per symbol. Checkout:

import matplotlib as mpl
test_str = r'a$b\AA\Finv\breve\gimel\succnapprox$'

mpl.rcParams['mathtext.fontset'] = 'custom'
mpl.rcParams['mathtext.rm'] = 'Courier New'
mpl.rcParams['mathtext.it'] = 'Courier New:italic'
mpl.rcParams['mathtext.bf'] = 'Courier New:bold'

fig, ax = plt.subplots(figsize=(8, 8))

mpl.rcParams['mathtext.fallback'] = 'cm'
txt = fig.text(.5, .5, test_str, fontsize=40, ha='center')

Output:

Substituting with a \Finv from STIXGeneral
Substituting with a \gimel from STIXGeneral
Substituting with a \combiningbreve from Computer Modern
Substituting with a \succnapprox from STIXGeneral
Substituting with a \Finv from STIXGeneral
Substituting with a \gimel from STIXGeneral
Substituting with a \combiningbreve from Computer Modern
Substituting with a \succnapprox from STIXGeneral

Can't figure out why is it doing two loops though. It will do yet another loop if a giure is saved, which is probably too many warnings to show.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@anntzer yeah, warnings fail tests

Copy link
Contributor

@anntzer anntzer Feb 20, 2020

Choose a reason for hiding this comment

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

You can use recwarn or pytest.warns (https://docs.pytest.org/en/5.3.5/warnings.html#assertwarnings) to catch them.

Copy link
Contributor

Choose a reason for hiding this comment

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

My point was that you actually want to emit a test here, but in the tests you can check that the warning is correctly thrown using pytest.warns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

better just go back to logging, than changing all those tests

Copy link
Contributor

Choose a reason for hiding this comment

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

that's fine with me too, I don't think the extra logging is too bad(?)

@andrzejnovak
Copy link
Contributor Author

@anntzer that should be it

@anntzer
Copy link
Contributor

anntzer commented Feb 24, 2020

A test (per #16509 (comment)) would be nice.

@anntzer
Copy link
Contributor

anntzer commented Feb 25, 2020

lgtm, can you please squash?

@andrzejnovak
Copy link
Contributor Author

andrzejnovak commented Feb 25, 2020

phew I'm just gonna drop this here when I inevitably need it again https://blog.appsignal.com/2016/09/27/git-rebasing-strategies.html

@anntzer
Copy link
Contributor

anntzer commented Feb 25, 2020

looks like you got it to work :)

@andrzejnovak
Copy link
Contributor Author

what's failing the codecov?

@anntzer
Copy link
Contributor

anntzer commented Feb 25, 2020

It's not failing?

@anntzer
Copy link
Contributor

anntzer commented Feb 25, 2020

flake8 is unhappy though

./lib/matplotlib/rcsetup.py:443:25: E128 continuation line under-indented for visual indent
2390./lib/matplotlib/rcsetup.py:444:25: E128 continuation line under-indented for visual indent
23912     E128 continuation line under-indented for visual indent

@andrzejnovak
Copy link
Contributor Author

It's not failing?

image

somehow it's transient?

@anntzer
Copy link
Contributor

anntzer commented Feb 25, 2020

Now it's back.
It's pretty flaky anyways (they likely have some race conditions against when the CI run finishes), so we don't overly worry about it.

@andrzejnovak andrzejnovak force-pushed the fallbackto branch 3 times, most recently from 8c6734f to 5424ff5 Compare February 26, 2020 09:40
Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

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

conditional on CI

@andrzejnovak
Copy link
Contributor Author

@anntzer what about the codecov

@anntzer
Copy link
Contributor

anntzer commented Mar 2, 2020

it's fine.

@timhoffm timhoffm merged commit 09436b4 into matplotlib:master Mar 3, 2020
@QuLogic QuLogic changed the title Add custom math fallback [WIP] Add custom math fallback Mar 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants