Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add URL support for images in PDF backend#23454

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
oscargus wants to merge1 commit intomatplotlib:main
base:main
Choose a base branch
Loading
fromoscargus:pdfimageurl
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletionsdoc/users/next_whats_new/url_in_images_for_pdfs.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
URL Support for Images in PDF Backend
-------------------------------------

The PDF backend can now generate clickable images if a URL is provided to the
image. There are a few limitations worth noting though:

* If parts of the image are clipped, the non-visible parts are still clickable.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Does this mean, if you zoom in on the image, parts outside the Axes will be clickable?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes. It is sort of easy to fix for non-transformed images (if there is a way to get the Axes extents from the gc(?). But for transformed it is much harder.

(I think the same holds for texts.)

* If there are transforms applied to the image, the whole enclosing rectangle
is clickable. However, if you use ``interpolation='none'`` for the image,
only the transformed image area is clickable (depending on viewer support).
Comment on lines +8 to +10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Sorry, I'm confused about what this means.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

First sentence: Basically, the image with the hand drawn "rectangle" above is clickable. Probably there is a better wording for it.

Second sentence: if you rely on the PDF transform (i.e. set interpolation='none' and possibly a few more conditions) the clickable area is the image. Bracket part: not all PDF viewers support that and will fall back on the red rectangle.

97 changes: 69 additions & 28 deletionslib/matplotlib/backends/backend_pdf.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -230,32 +230,66 @@ def _datetime_to_pdf(d):
return r


def_calculate_quad_point_coordinates(x, y, width, height, angle=0):
def_calculate_rotated_quad_point_coordinates(x, y, width, height, angle):
"""
Calculate the coordinates of rectangle when rotated by angle around x, y
"""

angle = math.radians(-angle)
angle = math.radians(angle)
sin_angle = math.sin(angle)
cos_angle = math.cos(angle)
a = x + height * sin_angle
width_cos = width * cos_angle
width_sin = width * sin_angle
a = x - height * sin_angle
b = y + height * cos_angle
c =x +width * cos_angle + height * sin_angle
d =y - width * sin_angle + height * cos_angle
e = x +width * cos_angle
f = y- width * sin_angle
c =a +width_cos
d =b + width_sin
e = x +width_cos
f = y+ width_sin
return ((x, y), (e, f), (c, d), (a, b))


def_get_coordinates_of_block(x, y,width, height, angle=0):
def_calculate_transformed_quad_point_coordinates(x, y,trans):
"""
Get the coordinates of rotated rectangle and rectangle that covers the
rotated rectangle.
Calculate the coordinates of rectangle when transformed by trans
positioned at x, y
"""
tr1, tr2, tr3, tr4, tr5, tr6 = trans
# Conceptual code
# width = 1
# height = 1
# a = x + 0 * tr1 + height * tr3 + tr5
# b = y + 0 * tr2 + height * tr4 + tr6
# c = x + width * tr1 + height * tr3 + tr5
# d = y + width * tr2 + height * tr4 + tr6
# e = x + width * tr1 + 0 * tr3 + tr5
# f = y + width * tr2 + 0 * tr4 + tr6
# x = x + 0 * tr1 + 0 * tr3 + tr5
# y = y + 0 * tr2 + 0 * tr4 + tr6
x = x + tr5
y = y + tr6
a = x + tr3
b = y + tr4
c = a + tr1
d = b + tr2
e = x + tr1
f = y + tr2
return (((x, y), (e, f), (c, d), (a, b)),
(0 if math.isclose(tr2, 0) else 45))


def _get_coordinates_of_block(x, y, width, height, angle, trans):
"""
Get the coordinates of rotated or transformed rectangle and rectangle
that covers the rotated or transformed rectangle.
"""

vertices = _calculate_quad_point_coordinates(x, y, width,
height, angle)

if trans is None:
vertices = _calculate_rotated_quad_point_coordinates(x, y, width,
height, angle)
else:
vertices, angle = _calculate_transformed_quad_point_coordinates(x, y,
trans)
# Find min and max values for rectangle
# adjust so that QuadPoints is inside Rect
# PDF docs says that QuadPoints should be ignored if any point lies
Expand All@@ -268,27 +302,29 @@ def _get_coordinates_of_block(x, y, width, height, angle=0):
max_x = max(v[0] for v in vertices) + pad
max_y = max(v[1] for v in vertices) + pad
return (tuple(itertools.chain.from_iterable(vertices)),
(min_x, min_y, max_x, max_y))
(min_x, min_y, max_x, max_y), angle)


def _get_link_annotation(gc, x, y, width, height, angle=0):
def _get_link_annotation(gc, x, y, width, height, angle=0, trans=None):
"""
Create a link annotation object for embedding URLs.
"""
quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle)
link_annotation = {
'Type': Name('Annot'),
'Subtype': Name('Link'),
'Rect': rect,
'Border': [0, 0, 0],
'A': {
'S': Name('URI'),
'URI': gc.get_url(),
},
}
quadpoints, rect, angle = _get_coordinates_of_block(x, y, width, height,
angle, trans)
link_annotation['Rect'] = rect
if angle % 90:
# Add QuadPoints
link_annotation['QuadPoints'] = quadpoints

return link_annotation


Expand DownExpand Up@@ -2012,21 +2048,24 @@ def draw_image(self, gc, x, y, im, transform=None):

self.check_gc(gc)

w = 72.0 * w / self.image_dpi
h = 72.0 * h / self.image_dpi

imob = self.file.imageObject(im)

if transform is None:
w = 72.0 * w / self.image_dpi
h = 72.0 * h / self.image_dpi
if gc.get_url() is not None:
self._add_link_annotation(gc, x, y, w, h)
self.file.output(Op.gsave,
w, 0, 0, h, x, y, Op.concat_matrix,
imob, Op.use_xobject, Op.grestore)
else:
tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values()
trans = transform.frozen().to_values()
if gc.get_url() is not None:
self._add_link_annotation(gc, x, y, w, h, trans=trans)

self.file.output(Op.gsave,
1, 0, 0, 1, x, y, Op.concat_matrix,
tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix,
*trans, Op.concat_matrix,
imob, Op.use_xobject, Op.grestore)

def draw_path(self, gc, path, transform, rgbFace=None):
Expand All@@ -2038,6 +2077,11 @@ def draw_path(self, gc, path, transform, rgbFace=None):
gc.get_sketch_params())
self.file.output(self.gc.paint())

def _add_link_annotation(self, gc, x, y, width, height, angle=0,
trans=None):
self.file._annotations[-1][1].append(
_get_link_annotation(gc, x, y, width, height, angle, trans))

def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
Expand DownExpand Up@@ -2206,8 +2250,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
self._text2path.mathtext_parser.parse(s, 72, prop)

if gc.get_url() is not None:
self.file._annotations[-1][1].append(_get_link_annotation(
gc, x, y, width, height, angle))
self._add_link_annotation(gc, x, y, width, height, angle)

fonttype = mpl.rcParams['pdf.fonttype']

Expand DownExpand Up@@ -2263,8 +2306,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
page, = dvi

if gc.get_url() is not None:
self.file._annotations[-1][1].append(_get_link_annotation(
gc, x, y, page.width, page.height, angle))
self._add_link_annotation(gc, x, y, page.width, page.height, angle)

# Gather font information and do some setup for combining
# characters into strings. The variable seq will contain a
Expand DownExpand Up@@ -2364,8 +2406,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
if gc.get_url() is not None:
font.set_text(s)
width, height = font.get_width_height()
self.file._annotations[-1][1].append(_get_link_annotation(
gc, x, y, width / 64, height / 64, angle))
self._add_link_annotation(gc, x, y, width/64, height/64, angle)

# If fonttype is neither 3 nor 42, emit the whole string at once
# without manual kerning.
Expand Down
95 changes: 95 additions & 0 deletionslib/matplotlib/tests/test_backend_pdf.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -360,6 +360,101 @@ def test_kerning():
fig.text(0, .75, s, size=20)


def test_image_url():
pikepdf = pytest.importorskip('pikepdf')

image_url = 'https://test_image_urls.matplotlib.org/'
image_url2 = 'https://test_image_urls2.matplotlib.org/'

X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig, ax = plt.subplots()
ax.imshow(Z, extent=[0, 1, 0, 1], url=image_url)
with io.BytesIO() as fd:
fig.savefig(fd, format="pdf")
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots

# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == image_url),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect == [decimal.Decimal('122.4'),
decimal.Decimal('43.2'),
468,
decimal.Decimal('388.8')]
ax.set_xlim(0, 3)
ax.imshow(Z[::-1], extent=[2, 3, 0, 1], url=image_url2)
# Must save as separate images
plt.rcParams['image.composite_image'] = False
with io.BytesIO() as fd:
fig.savefig(fd, format="pdf")
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots

# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == image_url2),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect == [decimal.Decimal('369.6'),
decimal.Decimal('141.6'),
decimal.Decimal('518.64'),
decimal.Decimal('290.64')]


def test_transformed_image_url():
pikepdf = pytest.importorskip('pikepdf')

image_url = 'https://test_image_urls.matplotlib.org/'

X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig, ax = plt.subplots()
im = ax.imshow(Z, interpolation='none', url=image_url)
with io.BytesIO() as fd:
fig.savefig(fd, format="pdf")
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots

# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == image_url),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect == [decimal.Decimal('122.4'),
decimal.Decimal('43.2'),
decimal.Decimal('468.4'),
decimal.Decimal('389.2')]
assert getattr(annot, 'QuadPoints', None) is None
# Transform
im.set_transform(mpl.transforms.Affine2D().skew_deg(30, 15) + ax.transData)
with io.BytesIO() as fd:
fig.savefig(fd, format="pdf")
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots

# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == image_url),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect[0] == decimal.Decimal('112.411830343')
assert getattr(annot, 'QuadPoints', None) is not None
# Positions in points (72 per inch)
assert annot.Rect[0] == \
annot.QuadPoints[0] - decimal.Decimal('0.00001')


def test_glyphs_subset():
fpath = str(_get_data_path("fonts/ttf/DejaVuSerif.ttf"))
chars = "these should be subsetted! 1234567890"
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp