From 433b8996347dad2c144000a1d1e7b6b1e50f7cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sun, 15 May 2016 18:50:49 +0300 Subject: [PATCH 1/2] Separate pdf finalization from closing If an error is raised and the object state is inconsistent, we shouldn't try to finalize the file, just close all resources. The idea is to support the following idiom: try: figure.draw(file) # write pdf file, can raise file.finalize() # do this if everything went well finally: file.close() # do this in any case --- lib/matplotlib/backends/backend_pdf.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c73f0e867101..e4350586e5a1 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -552,9 +552,10 @@ def newTextnote(self, text, positionRect=[-100, -100, 0, 0]): self.writeObject(annotObject, theNote) self.pageAnnotations.append(annotObject) - def close(self): + def finalize(self): + "Write out the various deferred objects and the pdf end matter." + self.endStream() - # Write out the various deferred objects self.writeFonts() self.writeObject(self.alphaStateObject, dict([(val[0], val[1]) @@ -582,12 +583,16 @@ def close(self): # Finalize the file self.writeXref() self.writeTrailer() + + def close(self): + "Flush all buffers and free all resources." + + self.endStream() if self.passed_in_file_object: self.fh.flush() - elif self.original_file_like is not None: - self.original_file_like.write(self.fh.getvalue()) - self.fh.close() else: + if self.original_file_like is not None: + self.original_file_like.write(self.fh.getvalue()) self.fh.close() def write(self, data): @@ -2438,6 +2443,7 @@ def close(self): Finalize this object, making the underlying file a complete PDF file. """ + self._file.finalize() self._file.close() if (self.get_pagecount() == 0 and not self.keep_empty and not self._file.passed_in_file_object): @@ -2534,6 +2540,7 @@ def print_pdf(self, filename, **kwargs): bbox_inches_restore=_bbox_inches_restore) self.figure.draw(renderer) renderer.finalize() + file.finalize() finally: if isinstance(filename, PdfPages): # finish off this page file.endStream() From 4fcc0e704824b9cfc885036ede753899b2c8f6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sun, 15 May 2016 18:54:36 +0300 Subject: [PATCH 2/2] Give a better error message on missing PostScript fonts For #4167; does not fix the problem, as it would need supporting a whole different kind of font, but gives a more useful error message. --- lib/matplotlib/backends/backend_pdf.py | 6 +++++ lib/matplotlib/tests/test_backend_pdf.py | 30 +++++++++++++++++++++++- lib/matplotlib/tests/test_backend_svg.py | 27 +++++++++++++++++++++ lib/matplotlib/textpath.py | 6 +++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e4350586e5a1..31aab0e4074c 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1876,6 +1876,12 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): pdfname = self.file.fontName(dvifont.texname) if dvifont.texname not in self.file.dviFontInfo: psfont = self.tex_font_mapping(dvifont.texname) + if psfont.filename is None: + self.file.broken = True + raise ValueError( + ("No usable font file found for %s (%s). " + "The font may lack a Type-1 version.") + % (psfont.psname, dvifont.texname)) self.file.dviFontInfo[dvifont.texname] = Bunch( fontfile=psfont.filename, basefont=psfont.psname, diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index a52a95464491..e8f2b4e5743c 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -7,13 +7,21 @@ import io import os +import tempfile + +try: + from unittest.mock import patch +except ImportError: + from mock import patch +from nose.tools import raises import numpy as np -from matplotlib import cm, rcParams +from matplotlib import checkdep_tex, cm, rcParams from matplotlib.backends.backend_pdf import PdfPages from matplotlib import pyplot as plt from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) +from matplotlib import dviread if 'TRAVIS' not in os.environ: @image_comparison(baseline_images=['pdf_use14corefonts'], @@ -29,6 +37,10 @@ def test_use14corefonts(): and containing some French characters and the euro symbol: "Merci pépé pour les 10 €"''' +needs_tex = knownfailureif( + not checkdep_tex(), + "This test needs a TeX installation") + @cleanup def test_type42(): @@ -132,3 +144,19 @@ def test_grayscale_alpha(): ax.imshow(dd, interpolation='none', cmap='gray_r') ax.set_xticks([]) ax.set_yticks([]) + + +@cleanup +@needs_tex +@raises(ValueError) +@patch('matplotlib.dviread.PsfontsMap.__getitem__') +def test_missing_psfont(mock): + """An error is raised if a TeX font lacks a Type-1 equivalent""" + psfont = dviread.PsFont(texname='texfont', psname='Some Font', + effects=None, encoding=None, filename=None) + mock.configure_mock(return_value=psfont) + rcParams['text.usetex'] = True + fig, ax = plt.subplots() + ax.text(0.5, 0.5, 'hello') + with tempfile.TemporaryFile() as tmpfile: + fig.savefig(tmpfile, format='pdf') diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index fcbdefb9102f..6bae4f0d626c 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -5,12 +5,22 @@ import numpy as np from io import BytesIO +import os +import tempfile import xml.parsers.expat +try: + from unittest.mock import patch +except ImportError: + from mock import patch +from nose.tools import raises + import matplotlib.pyplot as plt from matplotlib.testing.decorators import cleanup from matplotlib.testing.decorators import image_comparison, knownfailureif import matplotlib +from matplotlib import dviread + needs_tex = knownfailureif( not matplotlib.checkdep_tex(), @@ -183,6 +193,23 @@ def test_determinism_tex(): _test_determinism('determinism_tex.svg', usetex=True) +@cleanup +@needs_tex +@raises(ValueError) +@patch('matplotlib.dviread.PsfontsMap.__getitem__') +def test_missing_psfont(mock): + """An error is raised if a TeX font lacks a Type-1 equivalent""" + from matplotlib import rc + psfont = dviread.PsFont(texname='texfont', psname='Some Font', + effects=None, encoding=None, filename=None) + mock.configure_mock(return_value=psfont) + rc('text', usetex=True) + fig, ax = plt.subplots() + ax.text(0.5, 0.5, 'hello') + with tempfile.NamedTemporaryFile(suffix='.svg') as tmpfile: + fig.savefig(tmpfile.name) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index bd0db3bc0bbe..17f59c09a7fb 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -335,6 +335,12 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, font_bunch = self.tex_font_map[dvifont.texname] if font_and_encoding is None: + if font_bunch.filename is None: + raise ValueError( + ("No usable font file found for %s (%s). " + "The font may lack a Type-1 version.") + % (font_bunch.psname, dvifont.texname)) + font = get_font(font_bunch.filename) for charmap_name, charmap_code in [("ADOBE_CUSTOM",