Skip to content

Transforms of BoundingBox objects wrong (when backend uses dpi != 72) #16641

Closed as not planned

Description

@griai

Bug report

Bug summary

When transforming BoundingBox objects their width and height are calculated wrong. This happens, supposedly, for backends that have a different dpi setting from 72. The issue is present, e.g., for the backends "svg", "pdf", "cairo" (pdf, ps, svg). It is not present, e.g., for the backends "ps", "png".

Code for reproduction

# Plot BoundingBox of Text object with PS backend

import matplotlib as mpl
mpl.use("PS")

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0, 1], [1, 0], "bo", ms=20)

text = ax.text(x=0.3, y=0.1, s="sample text", fontsize=20, family="Serif")

fig.savefig("bbox.ps")

bbox_display = text.get_window_extent()

bbox_data = bbox_display.inverse_transformed(ax.transData)
bbox_data_2 = ax.transData.inverted().transform_bbox(bbox_display)

print(f"bbox_display = {bbox_display}")
print(f"    width = {bbox_display.width}    height = {bbox_display.height}")
print(f"bbox_data = {bbox_data}")
print(f"bbox_data = {bbox_data_2}")
print(f"    width = {bbox_data.width}    height = {bbox_data.height}")

rect_data = mpl.patches.Rectangle(
    (bbox_data.x0, bbox_data.y0), bbox_data.width, bbox_data.height,
    fill=False, lw=0.2, color="g")
ax.add_patch(rect_data)

fig.savefig("bbox.ps")
# Try the same with PDF backend

import matplotlib as mpl
mpl.use("PDF")

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0, 1], [1, 0], "bo", ms=20)

text = ax.text(x=0.3, y=0.1, s="sample text", fontsize=20, family="Serif")

fig.savefig("bbox.pdf")

bbox_display = text.get_window_extent()

bbox_data = bbox_display.inverse_transformed(ax.transData)
bbox_data_2 = ax.transData.inverted().transform_bbox(bbox_display)

factor = 100 / 72

print(f"bbox_display = {bbox_display}")
print(f"    width = {bbox_display.width}    height = {bbox_display.height}")
print(f"bbox_data = {bbox_data}")
print(f"bbox_data = {bbox_data_2}")
print(f"    width = {bbox_data.width}    height = {bbox_data.height}")
print(f"   ~width = {bbox_data.width * factor}   ~height = {bbox_data.height * factor}")

rect_data = mpl.patches.Rectangle(
    (bbox_data.x0, bbox_data.y0), bbox_data.width, bbox_data.height,
    fill=False, lw=0.2, color="r")
ax.add_patch(rect_data)

rect_data_corrected = mpl.patches.Rectangle(
    (bbox_data.x0, bbox_data.y0), bbox_data.width * factor, bbox_data.height * factor,
    fill=False, lw=0.2, color="g", ls="--")
ax.add_patch(rect_data_corrected)

fig.savefig("bbox.pdf")

Actual outcome

PS output

bbox_ps

bbox_display = Bbox(x0=171.22909090909093, y0=70.14774999999999, x1=288.9634659090909, y1=89.50712499999999)
    width = 117.734375    height = 19.359375
bbox_data = Bbox(x0=0.30000000000000004, y0=0.08281973379629631, x1=0.6626450842853944, y1=0.1628436053240741)
bbox_data = Bbox(x0=0.30000000000000004, y0=0.08281973379629631, x1=0.6626450842853944, y1=0.1628436053240741)
    width = 0.3626450842853943    height = 0.08002387152777779

PDF output

bbox_pdf

bbox_display = Bbox(x0=237.8181818181818, y0=99.04374999999999, x1=355.5525568181818, y1=118.40312499999999)
    width = 117.734375    height = 19.359375
bbox_data = Bbox(x0=0.30000000000000004, y0=0.0876302083333334, x1=0.5611044606854839, y1=0.1452473958333334)
bbox_data = Bbox(x0=0.30000000000000004, y0=0.0876302083333334, x1=0.5611044606854839, y1=0.1452473958333334)
    width = 0.2611044606854839    height = 0.0576171875
   ~width = 0.36264508428539427   ~height = 0.08002387152777778

Expected outcome

I expect that the BoundingBoxes for both used backends have the same width and height in Axes coordinates. However, this is not the case. The BoundingBox in the PDF version can be transformed to the correct size via multiplication of its width and height by the factor 100 / 72. Therefore, I assume that the default dpi of the backends plays a role here and is not handled properly.

Matplotlib version

  • Operating system: Ubuntu 19.04
  • Matplotlib version: 3.1.1
  • Matplotlib backends: PDF (but several more are affected)
  • Python version: 3.7.4
  • Jupyter version (if applicable): 1.0.0
  • Other libraries:

Matplotlib was installed via conda.

Possibly related issues

Activity

  1. griai commented on Mar 3, 2020

    @griai
    Author

    One possible workaround (for the pdf backend) is to explicitly set the dpi of the figure, i.e., put this line of code directly after the figure creation with the plt.subplots() call:

    fig.dpi = 72
    

    So, this workaround confirms that the fig.dpi setting is not handled properly in the transformation of the BoundingBox.

    Note that this has nothing to do with the keyword argument dpi in the call to text.get_window_extent(). Specifying this with different values changes the anchor point of the BoundingBox, thus shifting it alltogether. But we need to only change width and height.

    Note further that changing the fig.dpi changes width and height of the BoundingBox for both, the PS and the PDF backend. But it does not do so for the AGG backend and PNG output. I think, I'm still missing something ...

  2. timhoffm commented on Mar 4, 2020

    @timhoffm
    Member

    Thanks for the clear and detailed report.

  3. ImportanceOfBeingErnest commented on Mar 4, 2020

    @ImportanceOfBeingErnest
    Member

    I think the real problem here is that you cannot get the extention of a text without rendering it first; but rendering requires a DPI, even for backends that are pixel-independent in their output.
    As you have seen you need to actually save the image first, in order to be able to use .get_window_extent(), because that requires a renderer. So with that renderer you get the bounding box of the text in pixel space. And pixel space for the pdf backend is always defined via a dpi of 72.

    A workaround which avoids the dpi coming out of sync would be to use the agg backend throughout and only saving the figure via the respective vector backend:

    import matplotlib as mpl
    mpl.use("agg")
    import matplotlib.pyplot as plt
    
    fig, ax = plt.subplots()
    ax.plot([0, 1], [1, 0], "bo", ms=20)
    
    text = ax.text(x=0.3, y=0.1, s="sample text", fontsize=20, family="Serif")
    
    fig.canvas.draw()
    bbox_data = text.get_window_extent().inverse_transformed(ax.transData)
    
    rect_data = mpl.patches.Rectangle(
        (bbox_data.x0, bbox_data.y0), bbox_data.width, bbox_data.height,
        fill=False, lw=1, color="r")
    ax.add_patch(rect_data)
    
    fig.savefig("bbox.png")
    fig.savefig("bbox.pdf")
    fig.savefig("bbox.ps")
    
  4. QuLogic commented on Mar 19, 2020

    @QuLogic
    Member

    I'm not sure what you're trying to do here, but if this is strictly what you're looking for (and not just some simplified case), a simpler workaround is to use the bbox parameter. That is:

    import matplotlib as mpl
    mpl.use("PDF")  # noqa
    
    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, ax = plt.subplots()
    ax.plot([0, 1], [1, 0], "bo", ms=20)
    
    text = ax.text(x=0.3, y=0.1, s="sample text", fontsize=20, family="Serif",
                   bbox={"fill": False, "lw": 0.2, "color": "r"})
    
    fig.savefig("bbox2.pdf")

    bbox2

    The default boxstyle is square,pad=0.3 so if you want it flush, then pass a style with an explicit padding:

    text = ax.text(x=0.3, y=0.1, s="sample text", fontsize=20, family="Serif",
                   bbox={"fill": False, "lw": 0.2, "color": "r",
                         "boxstyle": "square,pad=0"})

    bbox3

  5. github-actions commented on Jul 10, 2023

    @github-actions

    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!

  6. added
    status: closed as inactiveIssues closed by the "Stale" Github Action. Please comment on any you think should still be open.
    on Aug 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions