diff --git a/CHANGELOG b/CHANGELOG index 38b1fe4e97aa..506d5ed307a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +2015-02-27 Added the rcParam 'image.composite_image' to permit users + to decide whether they want the vector graphics backends to combine + all images within a set of axes into a single composite image. + (If images do not get combined, users can open vector graphics files + in Adobe Illustrator or Inkscape and edit each image individually.) + 2015-02-19 Rewrite of C++ code that calculates contours to add support for corner masking. This is controlled by the 'corner_mask' keyword in plotting commands 'contour' and 'contourf'. - IMT diff --git a/doc/users/whats_new/rcparams.rst b/doc/users/whats_new/rcparams.rst index 3624fb7b11c4..d3b62bf40518 100644 --- a/doc/users/whats_new/rcparams.rst +++ b/doc/users/whats_new/rcparams.rst @@ -1,20 +1,21 @@ Added "legend.framealpha" key to rcParams ````````````````````````````````````````` - Added a key and the corresponding logic to control the default transparency of legend frames. This feature was written into the docstring of axes.legend(), but not yet implemented. - Added "figure.titlesize" and "figure.titleweight" keys to rcParams `````````````````````````````````````````````````````````````````` - Two new keys were added to rcParams to control the default font size and weight used by the figure title (as emitted by ``pyplot.suptitle()``). - - Added "legend.facecolor" and "legend.edgecolor" keys to rcParams ```````````````````````````````````````````````````````````````` - The new keys control colors (background and edge) of legend patches. + +``image.composite_image`` added to rcParams +``````````````````````````````````````````` +Controls whether vector graphics backends (i.e. PDF, PS, and SVG) combine +multiple images on a set of axes into a single composite image. Saving each +image individually can be useful if you generate vector graphics files in +matplotlib and then edit the files further in Inkscape or other programs. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index c8e5b3cbb955..e7b9696947c9 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -30,8 +30,6 @@ import matplotlib.text as mtext import matplotlib.image as mimage from matplotlib.artist import allow_rasterization - - from matplotlib.cbook import iterable rcParams = matplotlib.rcParams @@ -1241,7 +1239,9 @@ def apply_aspect(self, position=None): Xsize = ysize / data_ratio Xmarg = Xsize - xr Ymarg = Ysize - yr - xm = 0 # Setting these targets to, e.g., 0.05*xr does not seem to help + # Setting these targets to, e.g., 0.05*xr does not seem to + # help. + xm = 0 ym = 0 changex = (self in self._shared_y_axes and @@ -2028,8 +2028,8 @@ def draw(self, renderer=None, inframe=False): dsu = [(a.zorder, a) for a in artists if not a.get_animated()] - # add images to dsu if the backend support compositing. - # otherwise, does the manaul compositing without adding images to dsu. + # add images to dsu if the backend supports compositing. + # otherwise, does the manual compositing without adding images to dsu. if len(self.images) <= 1 or renderer.option_image_nocomposite(): dsu.extend([(im.zorder, im) for im in self.images]) _do_composite = False @@ -2055,7 +2055,7 @@ def draw(self, renderer=None, inframe=False): self.patch.draw(renderer) if _do_composite: - # make a composite image blending alpha + # make a composite image, blending alpha # list of (mimage.Image, ox, oy) zorder_images = [(im.zorder, im) for im in self.images diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index edcc81f95f89..8ef174652621 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -529,8 +529,8 @@ def draw_image(self, gc, x, y, im): def option_image_nocomposite(self): """ - override this method for renderers that do not necessarily - want to rescale and composite raster images. (like SVG) + override this method for renderers that do not necessarily always + want to rescale and composite raster images. (like SVG, PDF, or PS) """ return False diff --git a/lib/matplotlib/backends/backend_mixed.py b/lib/matplotlib/backends/backend_mixed.py index ee48f9274bbe..6de45ed2ed05 100644 --- a/lib/matplotlib/backends/backend_mixed.py +++ b/lib/matplotlib/backends/backend_mixed.py @@ -43,7 +43,6 @@ def __init__(self, figure, width, height, dpi, vector_renderer, self._height = height self.dpi = dpi - assert not vector_renderer.option_image_nocomposite() self._vector_renderer = vector_renderer self._raster_renderer = None diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index e03ab01ade93..3e23a758f2cb 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -462,8 +462,8 @@ def __init__(self, filename): self.fontNames = {} # maps filenames to internal font names self.nextFont = 1 # next free internal font name self.dviFontInfo = {} # information on dvi fonts - self.type1Descriptors = {} # differently encoded Type-1 fonts may - # share the same descriptor + # differently encoded Type-1 fonts may share the same descriptor + self.type1Descriptors = {} self.used_characters = {} self.alphaStates = {} # maps alpha values to graphics state objects @@ -1475,6 +1475,7 @@ def is_date(x): check_trapped = (lambda x: isinstance(x, Name) and x.name in ('True', 'False', 'Unknown')) + keywords = {'Title': is_string_like, 'Author': is_string_like, 'Subject': is_string_like, @@ -1576,6 +1577,13 @@ def option_scale_image(self): """ return True + def option_image_nocomposite(self): + """ + return whether to generate a composite image from multiple images on + a set of axes + """ + return not rcParams['image.composite_image'] + def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): self.check_gc(gc) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 1474d95d9c26..6aa54ad1de42 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -451,6 +451,13 @@ def option_scale_image(self): ps backend support arbitrary scaling of image. """ return True + + def option_image_nocomposite(self): + """ + return whether to generate a composite image from multiple images on + a set of axes + """ + return not rcParams['image.composite_image'] def _get_image_h_w_bits_command(self, im): if im.is_grayscale: diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 865acf66620e..160604ec95fe 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -529,9 +529,13 @@ def close_group(self, s): def option_image_nocomposite(self): """ - if svg.image_noscale is True, compositing multiple images into one is prohibited + return whether to generate a composite image from multiple images on + a set of axes """ - return rcParams['svg.image_noscale'] + if rcParams['svg.image_noscale']: + return True + else: + return not rcParams['image.composite_image'] def _convert_path(self, path, transform=None, clip=None, simplify=None): if clip: diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 51289832c6b1..9815d7547e54 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -595,6 +595,9 @@ def __call__(self, s): 'image.lut': [256, validate_int], # lookup table 'image.origin': ['upper', six.text_type], # lookup table 'image.resample': [False, validate_bool], + # Specify whether vector graphics backends will combine all images on a + # set of axes into a single composite image + 'image.composite_image': [True, validate_bool], # contour props 'contour.negative_linestyle': ['dashed', @@ -764,6 +767,7 @@ def __call__(self, s): # Maintain shell focus for TkAgg 'tk.window_focus': [False, validate_bool], 'tk.pythoninspect': [False, validate_tkpythoninspect], # obsolete + # Set the papersize/type 'ps.papersize': ['letter', validate_ps_papersize], 'ps.useafm': [False, validate_bool], # Set PYTHONINSPECT diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 0e693402ca16..6ba1e4a5c36e 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -9,8 +9,8 @@ import os import numpy as np - from matplotlib import cm, rcParams +from matplotlib.backends.backend_pdf import PdfPages from matplotlib import pyplot as plt from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) @@ -42,7 +42,6 @@ def test_type42(): @cleanup def test_multipage_pagecount(): - from matplotlib.backends.backend_pdf import PdfPages with PdfPages(io.BytesIO()) as pdf: assert pdf.get_pagecount() == 0 fig = plt.figure() @@ -58,7 +57,7 @@ def test_multipage_pagecount(): def test_multipage_keep_empty(): from matplotlib.backends.backend_pdf import PdfPages from tempfile import NamedTemporaryFile - ### test empty pdf files + # test empty pdf files # test that an empty pdf is left behind with keep_empty=True (default) with NamedTemporaryFile(delete=False) as tmp: with PdfPages(tmp) as pdf: @@ -69,7 +68,7 @@ def test_multipage_keep_empty(): with PdfPages(filename, keep_empty=False) as pdf: pass assert not os.path.exists(filename) - ### test pdf files with content, they should never be deleted + # test pdf files with content, they should never be deleted fig = plt.figure() ax = fig.add_subplot(111) ax.plot([1, 2, 3]) @@ -87,3 +86,24 @@ def test_multipage_keep_empty(): pdf.savefig() assert os.path.exists(filename) os.remove(filename) + + +@cleanup +def test_composite_image(): + #Test that figures can be saved with and without combining multiple images + #(on a single set of axes) into a single composite image. + X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1)) + Z = np.sin(Y ** 2) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0, 3) + ax.imshow(Z, extent=[0, 1, 0, 1]) + ax.imshow(Z[::-1], extent=[2, 3, 0, 1]) + plt.rcParams['image.composite_image'] = True + with PdfPages(io.BytesIO()) as pdf: + fig.savefig(pdf, format="pdf") + assert len(pdf._file.images.keys()) == 1 + plt.rcParams['image.composite_image'] = False + with PdfPages(io.BytesIO()) as pdf: + fig.savefig(pdf, format="pdf") + assert len(pdf._file.images.keys()) == 2 diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 4b92678d09e5..1f32c1fa2572 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -5,7 +5,7 @@ import io import re - +import numpy as np import six import matplotlib @@ -86,6 +86,30 @@ def test_savefig_to_stringio_with_usetex_eps(): _test_savefig_to_stringio(format='eps') +@cleanup +def test_composite_image(): + #Test that figures can be saved with and without combining multiple images + #(on a single set of axes) into a single composite image. + X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1)) + Z = np.sin(Y ** 2) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0, 3) + ax.imshow(Z, extent=[0, 1, 0, 1]) + ax.imshow(Z[::-1], extent=[2, 3, 0, 1]) + plt.rcParams['image.composite_image'] = True + with io.BytesIO() as ps: + fig.savefig(ps, format="ps") + ps.seek(0) + buff = ps.read() + assert buff.count(six.b(' colorimage')) == 1 + plt.rcParams['image.composite_image'] = False + with io.BytesIO() as ps: + fig.savefig(ps, format="ps") + ps.seek(0) + buff = ps.read() + assert buff.count(six.b(' colorimage')) == 2 + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 4d2632bc3afa..3dde9350bc5f 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -55,6 +55,31 @@ def test_noscale(): plt.rcParams['svg.image_noscale'] = True +@cleanup +def test_composite_images(): + #Test that figures can be saved with and without combining multiple images + #(on a single set of axes) into a single composite image. + X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1)) + Z = np.sin(Y ** 2) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0, 3) + ax.imshow(Z, extent=[0, 1, 0, 1]) + ax.imshow(Z[::-1], extent=[2, 3, 0, 1]) + plt.rcParams['image.composite_image'] = True + with BytesIO() as svg: + fig.savefig(svg, format="svg") + svg.seek(0) + buff = svg.read() + assert buff.count(six.b('