8000 Correct URL area with rotated texts in PDFs (#23288) · matplotlib/matplotlib@50aaa7c · GitHub
[go: up one dir, main page]

Skip to content

Commit 50aaa7c

Browse files
eindHoscarguseindH
authored
Correct URL area with rotated texts in PDFs (#23288)
Co-authored-by: Oscar Gustafsson <oscar.gustafsson@gmail.com> Co-authored-by: eindH <test@test.com>
1 parent aad38a0 commit 50aaa7c

File tree

3 files changed

+82
-5
lines changed

3 files changed

+82
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The active URL area rotates when link text is rotated
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
When link text is rotated in a matplotlib figure, the active URL
4+
area will now include the link area. Previously, the active area
5+
remained in the original, non-rotated, position.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,20 +230,65 @@ def _datetime_to_pdf(d):
230230
return r
231231

232232

233-
def _get_link_annotation(gc, x, y, width, height):
233+
def _calculate_quad_point_coordinates(x, y, width, height, angle=0):
234+
"""
235+
Calculate the coordinates of rectangle when rotated by angle around x, y
236+
"""
237+
238+
angle = math.radians(-angle)
239+
sin_angle = math.sin(angle)
240+
cos_angle = math.cos(angle)
241+
a = x + height * sin_angle
242+
b = y + height * cos_angle
243+
c = x + width * cos_angle + height * sin_angle
244+
d = y - width * sin_angle + height * cos_angle
245+
e = x + width * cos_angle
246+
f = y - width * sin_angle
247+
return ((x, y), (e, f), (c, d), (a, b))
248+
249+
250+
def _get_coordinates_of_block(x, y, width, height, angle=0):
251+
"""
252+
Get the coordinates of rotated rectangle and rectangle that covers the
253+
rotated rectangle.
254+
"""
255+
256+
vertices = _calculate_quad_point_coordinates(x, y, width,
257+
height, angle)
258+
259+
# Find min and max values for rectangle
260+
# adjust so that QuadPoints is inside Rect
261+
# PDF docs says that QuadPoints should be ignored if any point lies
262+
# outside Rect, but for Acrobat it is enough that QuadPoints is on the
263+
# border of Rect.
264+
265+
pad = 0.00001 if angle % 90 else 0
266+
min_x = min(v[0] for v in vertices) - pad
267+
min_y = min(v[1] for v in vertices) - pad
268+
max_x = max(v[0] for v in vertices) + pad
269+
max_y = max(v[1] for v in vertices) + pad
270+
return (tuple(itertools.chain.from_iterable(vertices)),
271+
(min_x, min_y, max_x, max_y))
272+
273+
274+
def _get_link_annotation(gc, x, y, width, height, angle=0):
234275
"""
235276
Create a link annotation object for embedding URLs.
236277
"""
278+
quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle)
237279
link_annotation = {
238280
'Type': Name('Annot'),
239281
'Subtype': Name('Link'),
240-
'Rect': (x, y, x + width, y + height),
282+
'Rect': rect,
241283
'Border': [0, 0, 0],
242284
'A': {
243285
'S': Name('URI'),
244286
'URI': gc.get_url(),
245287
},
246288
}
289+
if angle % 90:
290+
# Add QuadPoints
291+
link_annotation['QuadPoints'] = quadpoints
247292
return link_annotation
248293

249294

@@ -2162,7 +2207,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
21622207

21632208
if gc.get_url() is not None:
21642209
self.file._annotations[-1][1].append(_get_link_annotation(
2165-
gc, x, y, width, height))
2210+
gc, x, y, width, height, angle))
21662211

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

@@ -2219,7 +2264,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
22192264

22202265
if gc.get_url() is not None:
22212266
self.file._annotations[-1][1].append(_get_link_annotation(
2222-
gc, x, y, page.width, page.height))
2267+
gc, x, y, page.width, page.height, angle))
22232268

22242269
# Gather font information and do some setup for combining
22252270
# characters into strings. The variable seq will contain a
@@ -2320,7 +2365,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23202365
font.set_text(s)
23212366
width, height = font.get_width_height()
23222367
self.file._annotations[-1][1].append(_get_link_annotation(
2323-
gc, x, y, width / 64, height / 64))
2368+
gc, x, y, width / 64, height / 64, angle))
23242369

23252370
# If fonttype is neither 3 nor 42, emit the whole string at once
23262371
# without manual kerning.

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,37 @@ def test_text_urls():
243243
(a for a in annots if a.A.URI == f'{test_url}{fragment}'),
244244
None)
245245
assert annot is not None
246+
assert getattr(annot, 'QuadPoints', None) is None
246247
# Positions in points (72 per inch.)
247248
assert annot.Rect[1] == decimal.Decimal(y) * 72
248249

249250

251+
def test_text_rotated_urls():
252+
pikepdf = pytest.importorskip('pikepdf')
253+
254+
test_url = 'https://test_text_urls.matplotlib.org/'
255+
256+
fig = plt.figure(figsize=(1, 1))
257+
fig.text(0.1, 0.1, 'N', rotation=45, url=f'{test_url}')
258+
259+
with io.BytesIO() as fd:
260+
fig.savefig(fd, format='pdf')
261+
262+
with pikepdf.Pdf.open(fd) as pdf:
263+
annots = pdf.pages[0].Annots
264+
265+
# Iteration over Annots must occur within the context manager,
266+
# otherwise it may fail depending on the pdf structure.
267+
annot = next(
268+
(a for a in annots if a.A.URI == f'{test_url}'),
269+
None)
270+
assert annot is not None
271+
assert getattr(annot, 'QuadPoints', None) is not None
272+
# Positions in points (72 per inch)
273+
assert annot.Rect[0] == \
274+
annot.QuadPoints[6] - decimal.Decimal('0.00001')
275+
276+
250277
@needs_usetex
251278
def test_text_urls_tex():
252279
pikepdf = pytest.importorskip('pikepdf')

0 commit comments

Comments
 (0)
0