savefig and Text.get_window_extent()

dpi settings are still a source of confusion. Suppose one wants to get the bounding boxes of strings in a png file, for use as clickable regions on a web site. Just use the get_window_extent() method of each text object after it has been drawn with savefig, right? Wrong! The gotcha is that get_window_extent() is always based on the "ordinary" dpi, not on the "savefig" dpi.

In [1]:import matplotlib as mpl
In [2]:mpl.use('agg')
In [4]:import matplotlib.pyplot as plt
In [5]:fig = plt.figure()
In [6]:t = plt.text(0.5, 0.6, 'testing')
In [7]:fig.savefig('/tmp/t50.png', dpi=50)
In [9]:t.get_window_extent().extents
Out[9]:array([ 328. , 278.4 , 356.046875, 287.4 ])
In [10]:fig.savefig('/tmp/t150.png', dpi=150)
In [11]:t.get_window_extent().extents
Out[11]:array([ 328. , 278.4 , 411.875, 302.4 ])

In [12]:t._renderer.dpi
Out[12]:150

I find this very confusing--the _renderer.dpi is not being used by get_window_extent(). Is this the intended behavior? If so, I would like to at least add a note to that effect to the get_window_extent docstring.

The obvious workaround is to always use
"fig.savefig('figname.png', dpi=rcParams['figure.dpi'])
in this sort of application.

Eric

No, this was definitely a bug. Because text layout is expensive, the
text module caches the layout based on a cache key (see
text.Text.get_prop_tup). We were using the renderer id, and lots of
other stuff, but not the dpi, in the cache key. I just added the dpi,
so it should work correctly now, though I have done no tests. Give
r6098 a whirl and let me know.

JDH

···

On Tue, Sep 16, 2008 at 5:09 PM, Eric Firing <efiring@...229...> wrote:

I find this very confusing--the _renderer.dpi is not being used by
get_window_extent(). Is this the intended behavior? If so, I would
like to at least add a note to that effect to the get_window_extent
docstring

Oops, wait, I answered too fast. The figure.dpi *was* already used in
the cache key and the renderer.dpi, which I just added, is not
guaranteed to exist (depending on the backend). I need to figure out
why the figure.dpi in the cache key is not sufficient ....

JDH

···

On Tue, Sep 16, 2008 at 8:57 PM, John Hunter <jdh2358@...149...> wrote:

On Tue, Sep 16, 2008 at 5:09 PM, Eric Firing <efiring@...229...> wrote:

I find this very confusing--the _renderer.dpi is not being used by
get_window_extent(). Is this the intended behavior? If so, I would
like to at least add a note to that effect to the get_window_extent
docstring

No, this was definitely a bug. Because text layout is expensive, the
text module caches the layout based on a cache key (see
text.Text.get_prop_tup). We were using the renderer id, and lots of
other stuff, but not the dpi, in the cache key. I just added the dpi,
so it should work correctly now, though I have done no tests. Give
r6098 a whirl and let me know.

John Hunter wrote:

I find this very confusing--the _renderer.dpi is not being used by
get_window_extent(). Is this the intended behavior? If so, I would
like to at least add a note to that effect to the get_window_extent
docstring

No, this was definitely a bug. Because text layout is expensive, the
text module caches the layout based on a cache key (see
text.Text.get_prop_tup). We were using the renderer id, and lots of
other stuff, but not the dpi, in the cache key. I just added the dpi,
so it should work correctly now, though I have done no tests. Give
r6098 a whirl and let me know.

Oops, wait, I answered too fast. The figure.dpi *was* already used in
the cache key and the renderer.dpi, which I just added, is not
guaranteed to exist (depending on the backend). I need to figure out
why the figure.dpi in the cache key is not sufficient ....

Maybe because backend_bases FigureCanvasBase.print_figure cleverly changes figure.dpi, prints, and then changes it back again?

         origDPI = self.figure.dpi
         origfacecolor = self.figure.get_facecolor()
         origedgecolor = self.figure.get_edgecolor()

         self.figure.dpi = dpi
         self.figure.set_facecolor(facecolor)
         self.figure.set_edgecolor(edgecolor)

         try:
             result = getattr(self, method_name)(
                 filename,
                 dpi=dpi,
                 facecolor=facecolor,
                 edgecolor=edgecolor,
                 orientation=orientation,
                 **kwargs)
         finally:
             self.figure.dpi = origDPI
             self.figure.set_facecolor(origfacecolor)
             self.figure.set_edgecolor(origedgecolor)
             self.figure.set_canvas(self)
             #self.figure.canvas.draw() ## seems superfluous
         return result

Eric

···

On Tue, Sep 16, 2008 at 8:57 PM, John Hunter <jdh2358@...149...> wrote:

On Tue, Sep 16, 2008 at 5:09 PM, Eric Firing <efiring@...229...> wrote:

OK, I see what is going on. Actually, nothing is broken (feature not
bug, though admittedly confusing). In savefig, the renderer saves the
display dpi, sets the savefig dpi on the figure and renderer (as
necessary) instances, draws the figure, and then restores the display
dpi. Outside of savefig therefore, all you see when calling
get_window_extent is the display dpi. What you need to do is hook
into the drawing so you can get the window extent when the canvas is
drawn by savefig, when the savefig dpi is set. The script below shows
that before and after savefig, you get the display dpi extent, but in
the draw event you get the savefig dpi. If this is too onerous and
you know you will be using an image backend, just set the rc figure
and savefig dpi to be the same.

    import matplotlib as mpl
    mpl.use('agg')
    import matplotlib.pyplot as plt
    fig = plt.figure()
    t = plt.text(0.5, 0.6, 'testing')

    print 'before save window extent', t.get_window_extent().extents

    def ondraw(event):
        print 'on draw window extent', t.get_window_extent().extents

    fig.canvas.mpl_connect('draw_event', ondraw)

    fig.savefig('/tmp/t150.png', dpi=150)
    print 'after save window extent', t.get_window_extent().extents

    plt.show()

JDH

···

On Tue, Sep 16, 2008 at 9:00 PM, John Hunter <jdh2358@...149...> wrote:

Oops, wait, I answered too fast. The figure.dpi *was* already used in
the cache key and the renderer.dpi, which I just added, is not
guaranteed to exist (depending on the backend). I need to figure out
why the figure.dpi in the cache key is not sufficient ....