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

Skip to content

Unable to save figure as pdf while using latex package concmath #20469

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

Open
oorc06 opened this issue Jun 19, 2021 · 7 comments
Open

Unable to save figure as pdf while using latex package concmath #20469

oorc06 opened this issue Jun 19, 2021 · 7 comments
Labels
Difficulty: Hard https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues status: upstream fix required topic: text/fonts topic: text/usetex

Comments

@oorc06
Copy link

oorc06 commented Jun 19, 2021

Hello all,

I seem to run into a problem when I use the (latex) package concmath, and try saving the resultant plot as a pdf (png and jpeg work fine). As suggested in #10272, I've tried installing the "cm-super" package, but this does not solve the issue. I've also played around with the backends, but the problem still persists. A short code that generates this issue:

fig = plt.figure(figsize=(4, 4))
ax = fig.add_axes([0, 0, 1, 1])

plt.rcParams['font.family'] = 'monospace'
plt.rcParams['text.usetex'] = True
rc('text.latex', preamble=r'\usepackage[OT1]{fontenc}\usepackage{concmath}')

plt.plot([10**1,10**9], [1,10], label='Test M$_\star$')
plt.scatter([10**1,10**9], [1,10], label='Test1 M$_\star$ = 10M$_\odot$')
plt.xscale('log')
plt.yscale('log')

plt.xlabel('Test')
plt.ylabel('Test')

plt.xlim(10, 10**9)

plt.legend()

plt.savefig('test.pdf', bbox_inches='tight')

with the following traceback:

KeyError                                  Traceback (most recent call last)
<ipython-input-3-de7a1b0d622a> in <module>
     14 plt.legend()
     15 
---> 16 plt.savefig('test_sm.pdf', bbox_inches='tight')

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/pyplot.py in savefig(*args, **kwargs)
    964 def savefig(*args, **kwargs):
    965     fig = gcf()
--> 966     res = fig.savefig(*args, **kwargs)
    967     fig.canvas.draw_idle()   # need this if 'transparent=True' to reset colors
    968     return res

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/figure.py in savefig(self, fname, transparent, **kwargs)
   3003                 patch.set_edgecolor('none')
   3004 
-> 3005         self.canvas.print_figure(fname, **kwargs)
   3006 
   3007         if transparent:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2253                 # force the figure dpi to 72), so we need to set it again here.
   2254                 with cbook._setattr_cm(self.figure, dpi=dpi):
-> 2255                     result = print_method(
   2256                         filename,
   2257                         facecolor=facecolor,

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/backend_bases.py in wrapper(*args, **kwargs)
   1667             kwargs.pop(arg)
   1668 
-> 1669         return func(*args, **kwargs)
   1670 
   1671     return wrapper

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/_api/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    429                          else deprecation_addendum,
    430                 **kwargs)
--> 431         return func(*inner_args, **inner_kwargs)
    432 
    433     return wrapper

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/backends/backend_pdf.py in print_pdf(self, filename, dpi, bbox_inches_restore, metadata)
   2723                 RendererPdf(file, dpi, height, width),
   2724                 bbox_inches_restore=bbox_inches_restore)
-> 2725             self.figure.draw(renderer)
   2726             renderer.finalize()
   2727             if not isinstance(filename, PdfPages):

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     72     @wraps(draw)
     73     def draw_wrapper(artist, renderer, *args, **kwargs):
---> 74         result = draw(artist, renderer, *args, **kwargs)
     75         if renderer._rasterizing:
     76             renderer.stop_rasterizing()

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/figure.py in draw(self, renderer)
   2778 
   2779             self.patch.draw(renderer)
-> 2780             mimage._draw_list_compositing_images(
   2781                 renderer, self, artists, self.suppressComposite)
   2782 

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130     if not_composite or not has_images:
    131         for a in artists:
--> 132             a.draw(renderer)
    133     else:
    134         # Composite any adjacent images together

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/_api/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    429                          else deprecation_addendum,
    430                 **kwargs)
--> 431         return func(*inner_args, **inner_kwargs)
    432 
    433     return wrapper

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/axes/_base.py in draw(self, renderer, inframe)
   2919             renderer.stop_rasterizing()
   2920 
-> 2921         mimage._draw_list_compositing_images(renderer, self, artists)
   2922 
   2923         renderer.close_group('axes')

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130     if not_composite or not has_images:
    131         for a in artists:
--> 132             a.draw(renderer)
    133     else:
    134         # Composite any adjacent images together

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/axis.py in draw(self, renderer, *args, **kwargs)
   1139 
   1140         for tick in ticks_to_draw:
-> 1141             tick.draw(renderer)
   1142 
   1143         # scale up the axis label box to also find the neighbors, not

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/axis.py in draw(self, renderer)
    300         for artist in [self.gridline, self.tick1line, self.tick2line,
    301                        self.label1, self.label2]:
--> 302             artist.draw(renderer)
    303         renderer.close_group(self.__name__)
    304         self.stale = False

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/text.py in draw(self, renderer)
    721 
    722                 if textobj.get_usetex():
--> 723                     textrenderer.draw_tex(gc, x, y, clean_line,
    724                                           textobj._fontproperties, angle,
    725                                           mtext=mtext)

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/_api/deprecation.py in wrapper(*inner_args, **inner_kwargs)
    429                          else deprecation_addendum,
    430                 **kwargs)
--> 431         return func(*inner_args, **inner_kwargs)
    432 
    433     return wrapper

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/backends/backend_pdf.py in draw_tex(self, gc, x, y, s, prop, angle, ismath, mtext)
   2190         for x1, y1, dvifont, glyph, width in page.text:
   2191             if dvifont != oldfont:
-> 2192                 pdfname = self.file.dviFontName(dvifont)
   2193                 seq += [['font', pdfname, dvifont.size]]
   2194                 oldfont = dvifont

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/backends/backend_pdf.py in dviFontName(self, dvifont)
    863 
    864         tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
--> 865         psfont = tex_font_map[dvifont.texname]
    866         if psfont.filename is None:
    867             raise ValueError(

~/opt/anaconda3/lib/python3.8/site-packages/matplotlib/dviread.py in __getitem__(self, texname)
    856         assert isinstance(texname, bytes)
    857         try:
--> 858             result = self._font[texname]
    859         except KeyError:
    860             fmt = ('A PostScript file for the font whose TeX name is "{0}" '

KeyError: b'ccr10'

Please let me know if there's a way to circumvent this issue. Thank you all in advance!

  • Operating system: macOS Big Sur 11.2.3

  • Matplotlib version: 3.4.2

  • Matplotlib backend: module://ipykernel.pylab.backend_inline

  • Python version: 3.8.8

  • Matploblib came pre-installed with anaconda, but upgraded through pip.

@anntzer
Copy link
Contributor

anntzer commented Jun 19, 2021

From some research, it looks like this arises because concmath uses fonts in the Metafont format, which is not supported by Matplotlib. If you run either latex+dvips or pdflatex on a single tex file that uses such fonts, you'll see in the log an invocation of mktexpk and mf-nowin to convert the Metafont font to a bitmap (pk) format (you may need to clear your user-level texlive cache to see that, though). So we'd need to either look into supporting pk fonts as well (likely this will run into FreeType's lack of support, see the last entry at https://www.freetype.org/gsoc.html), or into auto-conversion of Metafont to e.g. Type1 (the usual font supported for usetex), which is not trivial (https://en.wikipedia.org/wiki/Metafont#Producing_PostScript_Type_1_fonts).

(I'm tempted to close as "upstream fix required" due to the situation with FreeType.)

@anntzer anntzer added Difficulty: Hard https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues topic: text/fonts labels Jun 19, 2021
@oorc06
Copy link
Author

oorc06 commented Jun 19, 2021

Sorry if this is a naive question, but I'm confused about this statement: "..., which is not supported by Matplotlib.": Everything works fine as long as one doesn't try saving the plot as a pdf. For example, as mentioned earlier, there are no problems if one tries to save the figure as a png or jpeg file (and there are, of course, no issues encountered if one doesn't try to save the file at all). So wouldn't this mean that Matplotlib does support the font, and rather the problem is with saving into a pdf?

@anntzer
Copy link
Contributor

anntzer commented Jun 19, 2021

No, it's not a naive question at all!

That's because for png/jpeg output, we rely on dvipng to rasterize the text (i.e. transform the text into an image). For pdf we don't do that, as the point of pdf is normally to have selectable text, but then we need to actually know how to embed the font and the glyph infos into the pdf file (which is what's not supported here). I guess yet another solution would be to give up on embedding the font/glyphs in that case and just embed a dvipng-generated image...

@aitikgupta I guess this is actually relevant for your gsoc: switching to parsing dvi even for raster output would mean a small loss of functionality (for agg) in this case, and conversely, perhaps falling back on embedding dvipng rasters would be a relatively simple workaround for vector output?

@aitikgupta
Copy link
Contributor

aitikgupta commented Jun 20, 2021

hmm, what if instead we let users decide this?
A parameter while exporting a PDF: editable (=True by default), but if the user decides that no, they don't want editable PDFs, they could switch the flag (and we will end up using the dvipng route to just embed the text image in the PDF).
(edit: This could be a much simpler way of getting around user-end issues, such as this one)


On a different note, non-editable PDFs hardly make sense? If users want to use just 'images' in their PDFs (and not actual text), they could just convert PNG outputs. (or we could provide a flag like mentioned above)
But as a library, I think we should stick to editable PDFs only, and possibly fix the issue of PK fonts.

@anntzer
Copy link
Contributor

anntzer commented Jun 20, 2021

Just a side point: I realized we don't actually need to figure out the correct mktexpk/mf invocations ourselves, at least that part can be handled transparently by kpathsea (https://tug.org/texinfohtml/kpathsea.html#mktex-scripts). (We need to call something like kpsewhich -mktex pk ccr10.600pk (where 600 should be replaced by some dpi-like value).)

Making things user-configurable seems fine, although as always that means that both branches need to be implemented :-)


After some more investigation, it looks like pdflatex embeds such fonts as type3, taking advantage of the possibility to just specify glyphs as raster images (after all, it can use arbitrary postscript operators to define glyphs). (We'd still need to somehow get the bitmap out of the pk file, though.)

@anntzer
Copy link
Contributor

anntzer commented Sep 11, 2021

Just for future reference, here's a minimal repro:
Install the concmath and concrete ctan packages, then

from pylab import *
rcParams["text.latex.preamble"] = r"\usepackage{concmath}"
figtext(.5, .5, r"\textrm{hello, world}", usetex=True)
savefig("/tmp/test.pdf")'

@anntzer
Copy link
Contributor

anntzer commented Sep 19, 2024

So we'd need to either look into supporting pk fonts as well

I guess if we want to work on this, the first part would be to implement a parser for the pk format, following the description at https://ctan.math.illinois.edu/info/knuth-pdf/mfware/gftopk.pdf (starting at section 21). Probably a good programming exercise ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Difficulty: Hard https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues status: upstream fix required topic: text/fonts topic: text/usetex
Projects
None yet
Development

No branches or pull requests

3 participants