Skip to content

Commit be24fc9

Browse files
committed
Merge pull request matplotlib#2588 from pwuertz/print_refactor
Refactor mechanism for saving files.
2 parents 0b1820d + 522fbe7 commit be24fc9

23 files changed

+214
-157
lines changed

lib/matplotlib/backend_bases.py

+97-136
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from six.moves import xrange
3535

3636
import os
37+
import sys
3738
import warnings
3839
import time
3940
import io
@@ -56,17 +57,76 @@
5657
from matplotlib.path import Path
5758
from matplotlib.cbook import mplDeprecation
5859

60+
try:
61+
from importlib import import_module
62+
except:
63+
# simple python 2.6 implementation (no relative imports)
64+
def import_module(name):
65+
__import__(name)
66+
return sys.modules[name]
67+
5968
try:
6069
from PIL import Image
6170
_has_pil = True
6271
except ImportError:
6372
_has_pil = False
6473

65-
_backend_d = {}
74+
75+
_default_filetypes = {
76+
'ps': 'Postscript',
77+
'eps': 'Encapsulated Postscript',
78+
'pdf': 'Portable Document Format',
79+
'pgf': 'PGF code for LaTeX',
80+
'png': 'Portable Network Graphics',
81+
'raw': 'Raw RGBA bitmap',
82+
'rgba': 'Raw RGBA bitmap',
83+
'svg': 'Scalable Vector Graphics',
84+
'svgz': 'Scalable Vector Graphics'
85+
}
86+
87+
88+
_default_backends = {
89+
'ps': 'matplotlib.backends.backend_ps',
90+
'eps': 'matplotlib.backends.backend_ps',
91+
'pdf': 'matplotlib.backends.backend_pdf',
92+
'pgf': 'matplotlib.backends.backend_pgf',
93+
'png': 'matplotlib.backends.backend_agg',
94+
'raw': 'matplotlib.backends.backend_agg',
95+
'rgba': 'matplotlib.backends.backend_agg',
96+
'svg': 'matplotlib.backends.backend_svg',
97+
'svgz': 'matplotlib.backends.backend_svg',
98+
}
99+
100+
101+
def register_backend(format, backend, description):
102+
"""
103+
Register a backend for saving to a given file format.
104+
105+
*format*
106+
File extention
107+
108+
*backend*
109+
Backend for handling file output (module string or canvas class)
110+
111+
*description*
112+
Description of the file type
113+
"""
114+
_default_backends[format] = backend
115+
_default_filetypes[format] = description
66116

67117

68-
def register_backend(format, backend_class):
69-
_backend_d[format] = backend_class
118+
def get_registered_canvas_class(format):
119+
"""
120+
Return the registered default canvas for given file format.
121+
Handles deferred import of required backend.
122+
"""
123+
if format not in _default_backends:
124+
return None
125+
backend_class = _default_backends[format]
126+
if cbook.is_string_like(backend_class):
127+
backend_class = import_module(backend_class).FigureCanvas
128+
_default_backends[format] = backend_class
129+
return backend_class
70130

71131

72132
class ShowBase(object):
@@ -1566,6 +1626,20 @@ class FigureCanvasBase(object):
15661626

15671627
supports_blit = True
15681628

1629+
filetypes = _default_filetypes
1630+
if _has_pil:
1631+
# JPEG support
1632+
register_backend('jpg', 'matplotlib.backends.backend_agg',
1633+
'Joint Photographic Experts Group')
1634+
register_backend('jpeg', 'matplotlib.backends.backend_agg',
1635+
'Joint Photographic Experts Group')
1636+
1637+
# TIFF support
1638+
register_backend('tif', 'matplotlib.backends.backend_agg',
1639+
'Tagged Image File Format')
1640+
register_backend('tiff', 'matplotlib.backends.backend_agg',
1641+
'Tagged Image File Format')
1642+
15691643
def __init__(self, figure):
15701644
figure.set_canvas(self)
15711645
self.figure = figure
@@ -1927,119 +2001,6 @@ def get_width_height(self):
19272001
"""
19282002
return int(self.figure.bbox.width), int(self.figure.bbox.height)
19292003

1930-
filetypes = {
1931-
'eps': 'Encapsulated Postscript',
1932-
'pdf': 'Portable Document Format',
1933-
'pgf': 'LaTeX PGF Figure',
1934-
'png': 'Portable Network Graphics',
1935-
'ps': 'Postscript',
1936-
'raw': 'Raw RGBA bitmap',
1937-
'rgba': 'Raw RGBA bitmap',
1938-
'svg': 'Scalable Vector Graphics',
1939-
'svgz': 'Scalable Vector Graphics'}
1940-
1941-
# All of these print_* functions do a lazy import because
1942-
# a) otherwise we'd have cyclical imports, since all of these
1943-
# classes inherit from FigureCanvasBase
1944-
# b) so we don't import a bunch of stuff the user may never use
1945-
1946-
# TODO: these print_* throw ImportErrror when called from
1947-
# compare_images_decorator (decorators.py line 112)
1948-
# if the backend has not already been loaded earlier on. Simple trigger:
1949-
# >>> import matplotlib.tests.test_spines
1950-
# >>> list(matplotlib.tests.test_spines.test_spines_axes_positions())[0][0]()
1951-
1952-
def print_eps(self, *args, **kwargs):
1953-
from .backends.backend_ps import FigureCanvasPS # lazy import
1954-
ps = self.switch_backends(FigureCanvasPS)
1955-
return ps.print_eps(*args, **kwargs)
1956-
1957-
def print_pdf(self, *args, **kwargs):
1958-
from .backends.backend_pdf import FigureCanvasPdf # lazy import
1959-
pdf = self.switch_backends(FigureCanvasPdf)
1960-
return pdf.print_pdf(*args, **kwargs)
1961-
1962-
def print_pgf(self, *args, **kwargs):
1963-
from .backends.backend_pgf import FigureCanvasPgf # lazy import
1964-
pgf = self.switch_backends(FigureCanvasPgf)
1965-
return pgf.print_pgf(*args, **kwargs)
1966-
1967-
def print_png(self, *args, **kwargs):
1968-
from .backends.backend_agg import FigureCanvasAgg # lazy import
1969-
agg = self.switch_backends(FigureCanvasAgg)
1970-
return agg.print_png(*args, **kwargs)
1971-
1972-
def print_ps(self, *args, **kwargs):
1973-
from .backends.backend_ps import FigureCanvasPS # lazy import
1974-
ps = self.switch_backends(FigureCanvasPS)
1975-
return ps.print_ps(*args, **kwargs)
1976-
1977-
def print_raw(self, *args, **kwargs):
1978-
from .backends.backend_agg import FigureCanvasAgg # lazy import
1979-
agg = self.switch_backends(FigureCanvasAgg)
1980-
return agg.print_raw(*args, **kwargs)
1981-
print_bmp = print_rgba = print_raw
1982-
1983-
def print_svg(self, *args, **kwargs):
1984-
from .backends.backend_svg import FigureCanvasSVG # lazy import
1985-
svg = self.switch_backends(FigureCanvasSVG)
1986-
return svg.print_svg(*args, **kwargs)
1987-
1988-
def print_svgz(self, *args, **kwargs):
1989-
from .backends.backend_svg import FigureCanvasSVG # lazy import
1990-
svg = self.switch_backends(FigureCanvasSVG)
1991-
return svg.print_svgz(*args, **kwargs)
1992-
1993-
if _has_pil:
1994-
filetypes['jpg'] = 'Joint Photographic Experts Group'
1995-
filetypes['jpeg'] = filetypes['jpg']
1996-
1997-
def print_jpg(self, filename_or_obj, *args, **kwargs):
1998-
"""
1999-
Supported kwargs:
2000-
2001-
*quality*: The image quality, on a scale from 1 (worst) to
2002-
95 (best). The default is 95, if not given in the
2003-
matplotlibrc file in the savefig.jpeg_quality parameter.
2004-
Values above 95 should be avoided; 100 completely
2005-
disables the JPEG quantization stage.
2006-
2007-
*optimize*: If present, indicates that the encoder should
2008-
make an extra pass over the image in order to select
2009-
optimal encoder settings.
2010-
2011-
*progressive*: If present, indicates that this image
2012-
should be stored as a progressive JPEG file.
2013-
"""
2014-
from .backends.backend_agg import FigureCanvasAgg # lazy import
2015-
agg = self.switch_backends(FigureCanvasAgg)
2016-
buf, size = agg.print_to_buffer()
2017-
if kwargs.pop("dryrun", False):
2018-
return
2019-
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
2020-
options = cbook.restrict_dict(kwargs, ['quality', 'optimize',
2021-
'progressive'])
2022-
2023-
if 'quality' not in options:
2024-
options['quality'] = rcParams['savefig.jpeg_quality']
2025-
2026-
return image.save(filename_or_obj, format='jpeg', **options)
2027-
print_jpeg = print_jpg
2028-
2029-
filetypes['tif'] = filetypes['tiff'] = 'Tagged Image File Format'
2030-
2031-
def print_tif(self, filename_or_obj, *args, **kwargs):
2032-
from .backends.backend_agg import FigureCanvasAgg # lazy import
2033-
agg = self.switch_backends(FigureCanvasAgg)
2034-
buf, size = agg.print_to_buffer()
2035-
if kwargs.pop("dryrun", False):
2036-
return
2037-
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
2038-
dpi = (self.figure.dpi, self.figure.dpi)
2039-
return image.save(filename_or_obj, format='tiff',
2040-
dpi=dpi)
2041-
print_tiff = print_tif
2042-
20432004
@classmethod
20442005
def get_supported_filetypes(cls):
20452006
"""Return dict of savefig file formats supported by this backend"""
@@ -2057,29 +2018,27 @@ def get_supported_filetypes_grouped(cls):
20572018
groupings[name].sort()
20582019
return groupings
20592020

2060-
def _get_print_method(self, format):
2021+
def _get_output_canvas(self, format):
2022+
"""Return a canvas that is suitable for saving figures to a specified
2023+
file format. If necessary, this function will switch to a registered
2024+
backend that supports the format.
2025+
"""
20612026
method_name = 'print_%s' % format
20622027

2063-
# check for registered backends
2064-
if format in _backend_d:
2065-
backend_class = _backend_d[format]
2066-
2067-
def _print_method(*args, **kwargs):
2068-
backend = self.switch_backends(backend_class)
2069-
print_method = getattr(backend, method_name)
2070-
return print_method(*args, **kwargs)
2071-
2072-
return _print_method
2028+
# check if this canvas supports the requested format
2029+
if hasattr(self, method_name):
2030+
return self
20732031

2074-
formats = self.get_supported_filetypes()
2075-
if (format not in formats or not hasattr(self, method_name)):
2076-
formats = sorted(formats)
2077-
raise ValueError(
2078-
'Format "%s" is not supported.\n'
2079-
'Supported formats: '
2080-
'%s.' % (format, ', '.join(formats)))
2032+
# check if there is a default canvas for the requested format
2033+
canvas_class = get_registered_canvas_class(format)
2034+
if canvas_class:
2035+
return self.switch_backends(canvas_class)
20812036

2082-
return getattr(self, method_name)
2037+
# else report error for unsupported format
2038+
formats = sorted(self.get_supported_filetypes())
2039+
raise ValueError('Format "%s" is not supported.\n'
2040+
'Supported formats: '
2041+
'%s.' % (format, ', '.join(formats)))
20832042

20842043
def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
20852044
orientation='portrait', format=None, **kwargs):
@@ -2136,7 +2095,9 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
21362095
filename = filename.rstrip('.') + '.' + format
21372096
format = format.lower()
21382097

2139-
print_method = self._get_print_method(format)
2098+
# get canvas object and print method for format
2099+
canvas = self._get_output_canvas(format)
2100+
print_method = getattr(canvas, 'print_%s' % format)
21402101

21412102
if dpi is None:
21422103
dpi = rcParams['savefig.dpi']

lib/matplotlib/backends/backend_agg.py

+55-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* linewidth
99
* lines, rectangles, ellipses
1010
* clipping to a rectangle
11-
* output to RGBA and PNG
11+
* output to RGBA and PNG, optionally JPEG and TIFF
1212
* alpha blending
1313
* DPI scaling properly - everything scales properly (dashes, linewidths, etc)
1414
* draw polygon
@@ -30,7 +30,7 @@
3030
from matplotlib import verbose, rcParams
3131
from matplotlib.backend_bases import RendererBase,\
3232
FigureManagerBase, FigureCanvasBase
33-
from matplotlib.cbook import is_string_like, maxdict
33+
from matplotlib.cbook import is_string_like, maxdict, restrict_dict
3434
from matplotlib.figure import Figure
3535
from matplotlib.font_manager import findfont
3636
from matplotlib.ft2font import FT2Font, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, \
@@ -42,6 +42,12 @@
4242
from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
4343
from matplotlib import _png
4444

45+
try:
46+
from PIL import Image
47+
_has_pil = True
48+
except ImportError:
49+
_has_pil = False
50+
4551
backend_version = 'v2.2'
4652

4753
def get_hinting_flag():
@@ -456,8 +462,6 @@ def draw(self):
456462
finally:
457463
RendererAgg.lock.release()
458464

459-
460-
461465
def get_renderer(self, cleared=False):
462466
l, b, w, h = self.figure.bbox.bounds
463467
key = w, h, self.figure.dpi
@@ -533,3 +537,50 @@ def print_to_buffer(self):
533537
(int(renderer.width), int(renderer.height)))
534538
renderer.dpi = original_dpi
535539
return result
540+
541+
if _has_pil:
542+
543+
# add JPEG support
544+
def print_jpg(self, filename_or_obj, *args, **kwargs):
545+
"""
546+
Supported kwargs:
547+
548+
*quality*: The image quality, on a scale from 1 (worst) to
549+
95 (best). The default is 95, if not given in the
550+
matplotlibrc file in the savefig.jpeg_quality parameter.
551+
Values above 95 should be avoided; 100 completely
552+
disables the JPEG quantization stage.
553+
554+
*optimize*: If present, indicates that the encoder should
555+
make an extra pass over the image in order to select
556+
optimal encoder settings.
557+
558+
*progressive*: If present, indicates that this image
559+
should be stored as a progressive JPEG file.
560+
"""
561+
buf, size = self.print_to_buffer()
562+
if kwargs.pop("dryrun", False):
563+
return
564+
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
565+
options = restrict_dict(kwargs, ['quality', 'optimize',
566+
'progressive'])
567+
568+
if 'quality' not in options:
569+
options['quality'] = rcParams['savefig.jpeg_quality']
570+
571+
return image.save(filename_or_obj, format='jpeg', **options)
572+
print_jpeg = print_jpg
573+
574+
# add TIFF support
575+
def print_tif(self, filename_or_obj, *args, **kwargs):
576+
buf, size = self.print_to_buffer()
577+
if kwargs.pop("dryrun", False):
578+
return
579+
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
580+
dpi = (self.figure.dpi, self.figure.dpi)
581+
return image.save(filename_or_obj, format='tiff',
582+
dpi=dpi)
583+
print_tiff = print_tif
584+
585+
586+
FigureCanvas = FigureCanvasAgg

lib/matplotlib/backends/backend_cairo.py

+3
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,6 @@ def _save (self, fo, format, **kwargs):
514514

515515
ctx.show_page()
516516
surface.finish()
517+
518+
519+
FigureCanvas = FigureCanvasCairo

0 commit comments

Comments
 (0)