Skip to content

imsave gives blank pdf/eps  #9203

Closed
Closed

Description

@zimmerrol

Bug report

Bug summary

the plt.imsave(..) method creates a blank file using the .pdf/.eps format.

Code for reproduction

import matplotlib.pyplot as plt
import numpy as np
data = np.arange(100*100).reshape(100, 100).astype(float)
plt.imsave("test.pdf", data, origin="lower", format="pdf") #same behavior also with eps instead of pdf

Actual outcome

A blank .pdf/.eps is created (see test.pdf)

Expected outcome
A non-blank .pdf which shows the same plot as the .png version of the code:

Matplotlib version

  • Operating System: Windows 10 Pro x64
  • Matplotlib Version: 2.0.2
  • Python Version: 3.6
  • Other Libraries: numpy 1.12.1

I installed matplotlib via anaconda.

Activity

  1. dstansby commented on Sep 19, 2017

    @dstansby
    Member

    I can reproduce this on the master branch and OSX. Seems to be present in 2.0 but not an issue in 1.5.3

  2. ImportanceOfBeingErnest commented on Sep 19, 2017

    @ImportanceOfBeingErnest
    Member

    Maybe it is worth noting that the output is different for different backends, while still being completely undesired. Using python 2.7 on windows 8.1 (64bit) and matplotlib 2.0.2,

    • with the Agg or pdf backend, the output is a completely white pdf
      image

    • while with Qt4Agg the output shows exactly one small pixel in the lower left corner
      image

  3. added this to the milestone on Sep 19, 2017
  4. added
    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
    on Sep 19, 2017
  5. tacaswell commented on Sep 19, 2017

    @tacaswell
    Member

    This is a pretty major regression, I am inclined to block 2.1 on this.

    imsave uses Figure.figimage and mimage.FigureImage which I am not sure are used anywhere else. I have a vague memory of fixing a similar bug for imshow (which uses AxesImage) that probably just needs to be applied here.

  6. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    So, FigureImage clips the image to the size of the figure, but was using renderer.width and renderer.height. If I change this to

           width, height = renderer.get_canvas_width_height()
            clip = Bbox([[0, 0], [width, height]])

    I think it gives the desired result

  7. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    Possible fix? #9204

  8. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    Ooops, no. The pdf canvas is hardwired at dpi=72 for some reason, so renderer.get_canvas_width_height() doesn't gove the "correct" width/height either in the user-defined dpi. So relying on the pdf canvas size doesn't work for the clipping.

    Frustratingly, for renderer.width,RenderAgg returns fig.width * fig.dpi

    For MixedModeRenderer, renderer.width simply returns fig.width. This is the root of the bug since clip in the above is in pixels. I don't know how to fix the root problem without breaking lots of other things.

    I guess the fundamental issue is that pdf and eps are not rasterized backends, so having their size defined by a number of pixels doesn't make sense to them fundamentally. Yes, I understand you can put a 100 dpi image on a 2"x2" pdf and it should be an image with 200x200 pixels, but I can understand why the design of the backends doesn't give you access to that information, and hence why FigureImage fails.

  9. WeatherGod commented on Sep 19, 2017

    @WeatherGod
    Member
  10. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    Not sure what you mean. I can place a 200 dpi raster into a pdf w/o a problem, can't I?

  11. dopplershift commented on Sep 19, 2017

    @dopplershift
    Contributor

    @tacaswell If it's broken on 2.x already, I'm not sure you need to block 2.1 on this. That said, I think it depends heavily on how long it takes to fix. I wouldn't delay 2.1 past the end of the month for a bug that already exists in several releases.

  12. WeatherGod commented on Sep 19, 2017

    @WeatherGod
    Member
  13. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    OK, my apologies, maybe I'm not being clear because we don't have the right example...

    I got onto this by trying to make the tests work. I think the example above works fine w/ my previous fix. However in general this example from the tests clips the 200x200 pixel image off at 144x144 if I render in pdf...

    import matplotlib
    matplotlib.use('pdf')
    import matplotlib.pyplot as plt
    import numpy as np
    for suppressComposite in [False]:
        fig = plt.figure(figsize=(2,2), dpi=100)
        fig.suppressComposite = suppressComposite
        x,y = np.ix_(np.arange(100.0)/100.0, np.arange(100.0)/100.0)
        z = np.sin(x**2 + y**2 - x*y)
        c = np.sin(20*x**2 + 50*y**2)
        img = z + c/5
    
        fig.figimage(img, xo=0, yo=0, origin='lower')
        fig.figimage(img[::-1,:], xo=0, yo=100, origin='lower')
        fig.figimage(img[:,::-1], xo=100, yo=0, origin='lower')
        fig.figimage(img[::-1,::-1], xo=100, yo=100, origin='lower')
    
        if suppressComposite:
            fig.savefig('Boo.pdf', dpi=100)
            fig.savefig('Boo.png', dpi=100)
        else:
            fig.savefig('Boo2.pdf', dpi=100)
            #fig.savefig('Boo2.png', dpi=100)

    What happens is that figimage ignores the dpi call in figure and clips the figure to 72dpi when the MixedModeRenderer (pdf) is used.

    But maybe thats OK? The docs for figimage say:

    figimage complements the axes image
            (:meth:`~matplotlib.axes.Axes.imshow`) which will be resampled
            to fit the current axes.  If you want a resampled image to
            fill the entire figure, you can define an
            :class:`~matplotlib.axes.Axes` with extent [0,0,1,1].
    

    ...so does the PDF backend simply always resample and fig.figimage is poorly defined for pdf? I guess I'd be OK w/ that logic.

  14. zimmerrol commented on Sep 19, 2017

    @zimmerrol
    Author

    @dopplershift If the 2.1 update will not be blocked until this issue is solved, I think it would at least be useful to show a warning when using the function for pdf files. Otherwise, a user can spend a long time in which he doesn't know, if he's doing something wrong, or matplotlib is not working properly.

  15. WeatherGod commented on Sep 19, 2017

    @WeatherGod
    Member
  16. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    @WeatherGod Test.pdf and Test2.pdf look very different! FWIW, this is the number-one reason I switched to matplotlib!

    fig, ax = plt.subplots(figsize=(2,2))
    ax.pcolormesh(np.random.rand(100,100), rasterized=True)
    fig.savefig('Test.pdf', dpi=300)
    fig.savefig('Test2.pdf', dpi=30)

    Test.pdf
    Test2.pdf

  17. jklymak commented on Sep 19, 2017

    @jklymak
    Member

    OK, re-opened #9204. New approach seems OK.

    I will still argue that hardwiring the pdf backend to have dpi=72 is not practically useful, and makes it hard to deal with rasterized objects which need not have pixels-per-inch=72, which is how dpi is used in savefig (appropriately in my opinion).

  18. jklymak commented on Sep 20, 2017

    @jklymak
    Member

    The PDF spec makes lots of reference to 1/72" as the default size of UserUnit, but I don't think that has anything fundamentally to do with pixels-per-inch of rasterized objects.

    To me, the fig.savefig(fname, dpi=100., fortmat='pdf') has always meant save any rasterized components at 100 pixels-per-inch. Which it does just fine as is. But, I'm not familiar enough with the backend machinery to understand why it needs to set the figure dpi to 72.

  19. tacaswell commented on Sep 20, 2017

    @tacaswell
    Member

    The reason that all of the the render has the dpi set to 72 is that it gets fed into the transform stack that converts data space coordinates to 'screen' space coordinates. In the case of Agg we then feed those coordinates to the resterizer which colors the pixels, in the case of pdf we just write the values out.

    Individual artists have a 'rasterized' flag that determines if they get output to the vector backends as vector data or if it should be rasterized and then embedded into the vector output as an image. This can be useful to reduce ex 1M points in a scatter to something more manageable in the output.

  20. jklymak commented on Sep 20, 2017

    @jklymak
    Member

    Right, but what is confusing is that I can do

    matplotlib.use('pdf')
    
    fig, ax = plt.subplots(figsize=(2, 2), dpi=200)
    ax.pcolormesh( np.random.rand(400,400), rasterized=True)
    fig.savefig('Boo.pdf', dpi=300)

    and have the figure return different values for dpi depending on where I query fig.dpi. I guess I can see why the dpi gets set by the pdf canvas, so that the figure transform gives the right distances, but it means that the original dpi=200 gets ignored. As a user, I expect that this dpi will get carried through to the savefig statement so that I don't need to set the dpi at that stage. For instance:

    matplotlib.use('pdf')
    
    fig, ax = plt.subplots(figsize=(2,2), dpi=10)
    print(fig.dpi)
    ax.pcolormesh( np.random.rand(400,400), rasterized=True)
    fig.savefig('Boo10.pdf')

    completely ignores dpi=10 and saves the raster at dpi=100 (my default). But the print statement returns 10.

    Even more surprising, if I do savefig('Boo10.png') it still ignores the dpi=10. I find this pretty confusing, but maybe its fine.

  21. tacaswell commented on Sep 20, 2017

    @tacaswell
    Member

    If you pass 'dpi='figure'' to save fig (or set it in you rcparams) the dpi on the figure will be respected in savefig. As of 2.0 that is the default (via 016406a).

    The reason for having a separate configuration for the savefig dpi (as far as I know) is lost to the sands of time. Changing the default to 'figure' is the next best thing to deprecating it.

  22. jklymak commented on Sep 20, 2017

    @jklymak
    Member

    dpi='figure' is certainly not the default on master using the PDF backend. Or rather, perhaps it is saving at 72 dpi, which is what the pdf backend sets the figure dpi to, but its certainly not respecting the dpi=10 that I used in the subplots call.

    import matplotlib
    matplotlib.use('pdf')
    import matplotlib.pyplot as plt
    import numpy as np
    
    X = np.random.rand(400,400)
    
    fig, ax = plt.subplots(figsize=(2,2), dpi=10)
    print(fig.dpi)
    ax.pcolormesh(X, rasterized=True)
    fig.savefig('Boo10.pdf')
    
    fig, ax = plt.subplots(figsize=(2,2), dpi=72)
    print(fig.dpi)
    ax.pcolormesh(X, rasterized=True)
    fig.savefig('Boo72.pdf')
    
    fig, ax = plt.subplots(figsize=(2,2))
    print(fig.dpi)
    ax.pcolormesh(X, rasterized=True)
    fig.savefig('BooNone.pdf')

    yield exactly the same files.... diff Boo10.pdf Boo72.pdf returns no differences, nor does diff Boo10.pdf BooNone.pdf.

    Boo10.pdf
    Boo72.pdf
    BooNone.pdf

    The fundamental issue is that FigureCanvasPdf.print_pdf does:

        def print_pdf(self, filename, **kwargs):
            image_dpi = kwargs.get('dpi', 72)  # dpi to use for images
            self.figure.set_dpi(72)            # there are 72 pdf points to an inch

    So maybe a simple change of image_dpi = kwargs.get('dpi', self.figure.dpi) would do the trick?

  23. tacaswell commented on Sep 24, 2017

    @tacaswell
    Member

    Closed by #9204

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.backend: pdf

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions