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

Skip to content

Bug when saving to vector format (pdf, svg, eps) #2831

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

Closed
wronk opened this issue Feb 21, 2014 · 25 comments · Fixed by #29910
Closed

Bug when saving to vector format (pdf, svg, eps) #2831

wronk opened this issue Feb 21, 2014 · 25 comments · Fixed by #29910

Comments

@wronk
Copy link

wronk commented Feb 21, 2014

There is a discrepancy between vector format (pdf, svg, eps) output and image output (png, jpg, etc) for the BboxImage object. I can't attach a pdf, but the this code creates and saves a pdf and png illustrating the BboxImage object translation. Just update the save folder path to run the code.

Thanks and let me know if I should include anything else.

'''
Minimum working example
version 1.3.1
Plots a descriptive flowchart showing error
@Author: wronk
'''

from os import path as op
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.image import BboxImage
from matplotlib.offsetbox import TextArea, AnnotationBbox
from matplotlib.transforms import Bbox
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.backends.backend_pdf import PdfPages

###############################################################################
#EDIT SAVE FOLDER TO AUTOSAVE
saveFig = True
save_fname = '/home/wronk/'

mpl.rcParams['mathtext.default'] = 'rm'

#Box properties
bboxProp = dict(boxstyle='round,pad=0.3', fill=False, ec='w', linewidth=8)

#Colors for annotation arrows
senCol = (0.35, 0.35, 0.35)
srcCol = (.8, .216, 0.0)

#Define color map
cm = 'autumn'
colRange = np.atleast_2d(np.arange(256)/256.)

###############################################################################
#Initialize Plot
plt.ion()
plt.close('all')
mpl.rcParams['pdf.fonttype'] = 42

figSize = (12, 6)
ftSize = 32
dpi = 80
rowYVals = [0.085, 0.55, 0.85]
rowXVals = [0, .075, .2, .34, .5, .65, .8, .875]

fig = plt.figure(figsize=figSize, facecolor='white', dpi=dpi)
#fig = plt.figure(figsize=figSize, facecolor='white')
#ax = fig.gca()
ax = plt.subplot(111)

###############################################################################
#Mid level of annotation boxes
#Text for boxes in mathematical font
flowMid = r'$j_{N-1}$'

midBox3 = ax.annotate(flowMid, xy=(0, .5), xycoords='axes fraction',
                      xytext=(rowXVals[3], rowYVals[1]),
                      textcoords='axes fraction',
                      size=ftSize, ha='left', va='center', bbox=bboxProp,
                      arrowprops=None, color='black')

#######################################
#HEREIN LIES THE PROBLEM
#Add color (gradients) behind the boxes

#Something about the bbox window extent property must get shifted when saving
#as a vector format (pdf, svg, eps) but not common image types. In my experience,
#every bboximage was translated and scaled uniformly.

#I tried changing DPI, image size, subplot parameters as well as sychronizing
#plotting and saving figures in matplotlibrc to no avail. DPI does seem to
#have some sort of effect in terms of how the bboximage gets shifted though.

gradient = BboxImage(midBox3.get_bbox_patch().get_window_extent,
                     data=np.zeros_like(colRange), cmap=cm, norm=None,
                     origin=None)
ax.add_artist(gradient)

#######################################
plt.draw()

#Save figure as pdf and png to highlight difference
#Same results when saving from GUI window
if saveFig:
    pdfFile = PdfPages(save_fname + 'flowchart_PDF.pdf')
    plt.savefig(pdfFile, format='pdf', dpi=fig.dpi)
    pdfFile.close()
    plt.savefig(save_fname + 'flowchart_PNG.png', dpi=fig.dpi)
plt.show()

(tacaswell edited for code markup)

@tacaswell
Copy link
Member

That is bad. This is maybe related to #2889 ?

Can confirm this on master.

If you set the dpi to 100 it more-or-less works correctly, you can see the corners of the red sequare outside of the edges of the annotation box, but it is in the right place.

@tacaswell tacaswell added this to the v1.4.0 milestone Mar 20, 2014
@pelson
Copy link
Member

pelson commented Mar 20, 2014

That is bad. This is maybe related to #2889 ?

I've not seen the picture, but it is worth noting that the issue there was highlighted by a change which was applied after v1.3.1. Though it could be the same underlying bug, none-the-less.

@pelson
Copy link
Member

pelson commented Mar 20, 2014

P.S. A screenshot of the problem:

bad_trans

So the red box is in the wrong place for the plot.

@pelson
Copy link
Member

pelson commented Mar 20, 2014

Interestingly, if you don't specify the correct DPI at savefig for PNG, the red box is also in the wrong place. Something is a little fishy there...

@tacaswell
Copy link
Member

@pelson The similarity is (I think) related to using get_extent in one context which creates values based on current screen-space transforms. The values are then saved and get re-used with different screen-space transforms which causes things to be the wrong size/in the wrong place.

@pelson
Copy link
Member

pelson commented Mar 21, 2014

The similarity is (I think) related to using get_extent in one context which creates values based on current screen-space transforms.

Agreed. The issue can be seen with all backends with a simple modification to @wronk 's code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.image import BboxImage

bboxProp = dict(boxstyle='round,pad=0.3', fill=False, ec='red', linewidth=8)

ax = plt.axes()
midBox3 = ax.annotate(r'$j_{N-1}$', xy=(0, .5), xycoords='axes fraction',
                      xytext=(0.7, 0.7),
                      textcoords='axes fraction',
                      size=50, ha='left', va='center', bbox=bboxProp,
                      arrowprops=None, color='black')

ax.add_artist(BboxImage(midBox3.get_bbox_patch().get_window_extent,
              data=np.zeros_like(np.atleast_2d(np.arange(256)/256.))))
ax.set_ylim(bottom=-1)
plt.show()

The key is that the red box only appears after the figure has been re-drawn. The change in x/y limit is significant here.

@tacaswell
Copy link
Member

@pelson @efiring @mdboom I am going to punt this and the related issues to 1.5 (at least).

My understanding of the problem here is that artists are being lined up in screen-space (through get_window_extent) and then when things get updated underneath the artists the changes don't propagate. I think any fix for this will be a major overhaul.

I will create an issue to add a note to get_window_extent docs warning that it can lead to this sort of thing.

@tacaswell tacaswell modified the milestones: v1.5.x, v1.4.0 May 9, 2014
@pelson
Copy link
Member

pelson commented May 9, 2014

@tacaswell - do you know if #3054 fixes this issue?

@tacaswell
Copy link
Member

I don't, but should probably check (I left this comment before I found that patch).

@efiring
Copy link
Member

efiring commented May 9, 2014

This does seem like quite a fundamental problem; a full solution might require a dependency-tracking system. This reminds me of the lazy evaluation system (implemented via a C++ extension) that JDH originally used in mpl.

Is it the case that get_window_extent can be used safely only inside a draw method? And that anything that changes what get_window_extent would yield needs to ensure that every draw method is executed after the change? Is the problem coming from caching the result of a draw, and not declaring the cached result invalid when a transform has changed?

@leejjoon
Copy link
Contributor

Using get_window_extent can be quite tricky as in the above case.
As it requires a renderer instance, it should be used inside a draw method.
But this is not all. For some artists (e.g., text, annotation), its extent is usually determined after the artist is drawn. Also, extent of some artist depends on extent of other artists. And a dependency-tracking is often manually implemented.

In the above case, gradient calls midBox3.get_bbox_patch().get_window_extent before it is drawn (because of the zorder), thus it ended up in a wrong location. It would have been better if midBox3.get_bbox_patch().get_window_extent somehow automatically update the extent of midBox, but I believe this is not easy to implement within the current framework.

And I agree with Eric that we need some sort of dependency-tracking.

For the current example, here is a work-around. It defines a function that update the extent of midBox3 and pass it to BboxImage.

def annoate_get_window_extent(renderer):
    midBox3.update_positions(renderer)
    Text.update_bbox_position_size(midBox3, renderer)
    rr = midBox3.get_bbox_patch().get_window_extent(renderer)
    return rr

gradient = BboxImage(annoate_get_window_extent,
                     data=np.zeros_like(np.atleast_2d(np.arange(256)/256.)))
ax.add_artist(gradient)

Looking at the code above, I strongly feel that it should be simpler than this though.

@tacaswell tacaswell modified the milestones: v1.5.x, v1.4.0 Jun 29, 2014
@tacaswell
Copy link
Member

I re-milestoned this as 1.5.x as it seems that nothing in behaving incorrectly, just inconveniently. Tacking automatic tracking of changing on to the frame work sounds like it will be a good deal of work (if it is possible) and there is a work-around (which I agree works...BboxImage needs documentation).

@wronk Does the work-around work for you?

@wronk
Copy link
Author

wronk commented Jul 1, 2014

That project is on the back burner for me at the moment, but I'll give it a
shot in a week or two and let you know how it goes. Thanks for identifying
the work around though.

On Sat, Jun 28, 2014 at 6:16 PM, Thomas A Caswell [email protected]
wrote:

I re-milestoned this as 1.5.x as it seems that nothing in behaving
incorrectly, just inconveniently. Tacking automatic tracking of
changing on to the frame work sounds like it will be a good deal of work
(if it is possible) and there is a a work-around (which I agree works...
BboxImage needs documentation).

@wronk https://github.com/wronk Does the work-around work for you?


Reply to this email directly or view it on GitHub
#2831 (comment)
.

@sirenayka
Copy link

(Encountered this issue while generating a figure for my publication and signed up on github to tell you this)

I tried this on Jupyter notebook and it generated wrong vector figure. When I copied the "same exact code" to IPython console, problem gone :) I don't know why.

@tacaswell
Copy link
Member

@sirenayka Can you be a bit more specific about what code you ran? I suspect the the effect was getting 'lucky' about what order things ran in.

@tacaswell tacaswell modified the milestones: 2.1 (next point release), 2.2 (next next feature release) Oct 3, 2017
@github-actions
Copy link

github-actions bot commented Mar 4, 2023

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Mar 4, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 4, 2023
@QuLogic
Copy link
Member

QuLogic commented Apr 7, 2023

This is a strange one. If I run it as-is, I see nothing in the PDF but the text. If I change the code to save the PNG first, then the PDF starts showing the image, but it disappears from the PNG. If I then change figure DPI to 72 (which matches PDF's internal DPI), then the image moves under the text.

I guess this has to do with #2831 (comment)

@QuLogic QuLogic reopened this Apr 7, 2023
@github-actions github-actions bot removed the status: inactive Marked by the “Stale” Github Action label Apr 8, 2023
Copy link

github-actions bot commented Apr 8, 2024

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Apr 8, 2024
@tacaswell
Copy link
Member

I still see the same as reported by @QuLogic in that there is just the text, no box, in the PDF

@tacaswell
Copy link
Member

This "fixes" the problem

   ...: if saveFig:
   ...:     pdfFile = PdfPages(save_fname + 'flowchart_PDF.pdf')
   ...:     plt.savefig(BytesIO(), format='pdf', dpi=fig.dpi)
   ...:     plt.savefig(pdfFile, format='pdf', dpi=fig.dpi)
   ...:     pdfFile.close()
   ...:     plt.savefig(save_fname + 'flowchart_PNG.png', dpi=fig.dpi)
   ...: plt.show()

@github-actions github-actions bot removed the status: inactive Marked by the “Stale” Github Action label Apr 10, 2024
Copy link

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Apr 11, 2025
@tacaswell
Copy link
Member

Re-evaluating this, I think @leejjoon is completely correct. The source of the problem is that for zorder reasons we have to draw the background before the text, but the text does not know how big it is before has been drawn.

Short of implementing a system to automatically detect when we need to render multiple times there are two fixes:

  1. do a manually "dummy" save to a BytesIO with the exact same settings as you want to save the output as.
  2. use layuot='constrained' or layout='tight' which internally do a dummy render to get all the text sizes so that they can then update the layout.

@timhoffm
Copy link
Member

1. do a manually "dummy" save to a `BytesIO` with the exact same settings as you want to save the output as.

Does this work? I believe not reliably just with a savefig, because it only replaces the canvas temporarily.

If I do:


In [6]: print(ax.transData)
CompositeGenericTransform(
    [...]
        BboxTransformTo(
            TransformedBbox(
                Bbox(x0=0.125, y0=0.10999999999999999, x1=0.9, y1=0.88),
                BboxTransformTo(
                    TransformedBbox(
                        Bbox(x0=0.0, y0=0.0, x1=6.4, y1=4.8),
                        Affine2D().scale(100.0)))))))

In [7]: fig.savefig('test.png', dpi=50)

In [8]: print(ax.transData)
CompositeGenericTransform(
    [...]
        BboxTransformTo(
            TransformedBbox(
                Bbox(x0=0.125, y0=0.10999999999999999, x1=0.9, y1=0.88),
                BboxTransformTo(
                    TransformedBbox(
                        Bbox(x0=0.0, y0=0.0, x1=6.4, y1=4.8),
                        Affine2D().scale(100.0)))))))

the Affine2D().scale(100.0) is the dpi scaling so I won't get my dpi=50 into the window extent ?!?

@github-actions github-actions bot removed the status: inactive Marked by the “Stale” Github Action label Apr 14, 2025
@leejjoon
Copy link
Contributor

Another approach would be to use a callable object that explicitly update the position of the text and the patch. This does not require rendering the figure twice

def get_bbox(renderer):
    midBox3.update_positions(renderer)
    midBox3.update_bbox_position_size(renderer)
    return midBox3.get_bbox_patch().get_window_extent(renderer)

arr = np.atleast_2d(np.arange(256)/256.)
bbox_image = BboxImage(get_bbox, data=arr)
ax.add_artist(bbox_image)

Yet another option is to use patheffects. The example code below uses mpl_visual_context module which I created.

from matplotlib.patheffects import Normal
from mpl_visual_context.patheffects import FillImage

bbox_image = BboxImage(midBox4.get_bbox_patch().get_window_extent,
                       data=arr)

midBox3.get_bbox_patch().set_path_effects([FillImage(bbox_image, ax=ax),
                                           Normal()])

@tacaswell
Copy link
Member

Does this work?

My understanding is that in similar cases (passing arg.get_window_extent as a callable referring to something later in the draw order, that artist may move/resize itself as part of the draw, and saving to a vector format) this will work reliably. Because vector backends always set the dpi to 72 (because we need all of the "screen" positions that come out to be in inches) so doing draw_without_render without doing the same won't work. The fist save will get the wrong position for the box, correct the position of the text which is preserved in some state (I'm not entirely sure which state) between draws so in the second draw the box is in the right place and the text recomputes to the same place.

@leejjoon's suggestion will work 100% with only one save, but requires the user to know which methods to call to update the correct state.

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

Successfully merging a pull request may close this issue.

8 participants