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

Skip to content

Commit 327cfcf

Browse files
authored
Merge pull request #17832 from QuLogic/pdf-url
ENH: Support setting URLs on Text objects in pdf
2 parents e5404d8 + b47fa78 commit 327cfcf

File tree

3 files changed

+110
-5
lines changed

3 files changed

+110
-5
lines changed

doc/users/next_whats_new/pdf_urls.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
PDF supports URLs on ``Text`` artists
2+
-------------------------------------
3+
4+
URLs on `.text.Text` artists (i.e., from `.Artist.set_url`) will now be saved
5+
in PDF files.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,13 @@ def __init__(self, filename, metadata=None):
694694

695695
self.paths = []
696696

697-
self.pageAnnotations = [] # A list of annotations for the current page
697+
# A list of annotations for each page. Each entry is a tuple of the
698+
# overall Annots object reference that's inserted into the page object,
699+
# followed by a list of the actual annotations.
700+
self._annotations = []
701+
# For annotations added before a page is created; mostly for the
702+
# purpose of newTextnote.
703+
self.pageAnnotations = []
698704

699705
# The PDF spec recommends to include every procset
700706
procsets = [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]
@@ -720,6 +726,7 @@ def newPage(self, width, height):
720726

721727
self.width, self.height = width, height
722728
contentObject = self.reserveObject('page contents')
729+
annotsObject = self.reserveObject('annotations')
723730
thePage = {'Type': Name('Page'),
724731
'Parent': self.pagesObject,
725732
'Resources': self.resourceObject,
@@ -728,11 +735,12 @@ def newPage(self, width, height):
728735
'Group': {'Type': Name('Group'),
729736
'S': Name('Transparency'),
730737
'CS': Name('DeviceRGB')},
731-
'Annots': self.pageAnnotations,
738+
'Annots': annotsObject,
732739
}
733740
pageObject = self.reserveObject('page')
734741
self.writeObject(pageObject, thePage)
735742
self.pageList.append(pageObject)
743+
self._annotations.append((annotsObject, self.pageAnnotations))
736744

737745
self.beginStream(contentObject.id,
738746
self.reserveObject('length of content stream'))
@@ -750,14 +758,13 @@ def newTextnote(self, text, positionRect=[-100, -100, 0, 0]):
750758
'Contents': text,
751759
'Rect': positionRect,
752760
}
753-
annotObject = self.reserveObject('annotation')
754-
self.writeObject(annotObject, theNote)
755-
self.pageAnnotations.append(annotObject)
761+
self.pageAnnotations.append(theNote)
756762

757763
def finalize(self):
758764
"""Write out the various deferred objects and the pdf end matter."""
759765

760766
self.endStream()
767+
self._write_annotations()
761768
self.writeFonts()
762769
self.writeExtGSTates()
763770
self._write_soft_mask_groups()
@@ -816,6 +823,10 @@ def endStream(self):
816823
self.currentstream.end()
817824
self.currentstream = None
818825

826+
def _write_annotations(self):
827+
for annotsObject, annotations in self._annotations:
828+
self.writeObject(annotsObject, annotations)
829+
819830
def fontName(self, fontprop):
820831
"""
821832
Select a font based on fontprop and return a name suitable for
@@ -2095,6 +2106,19 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
20952106
width, height, descent, glyphs, rects = \
20962107
self._text2path.mathtext_parser.parse(s, 72, prop)
20972108

2109+
if gc.get_url() is not None:
2110+
link_annotation = {
2111+
'Type': Name('Annot'),
2112+
'Subtype': Name('Link'),
2113+
'Rect': (x, y, x + width, y + height),
2114+
'Border': [0, 0, 0],
2115+
'A': {
2116+
'S': Name('URI'),
2117+
'URI': gc.get_url(),
2118+
},
2119+
}
2120+
self.file._annotations[-1][1].append(link_annotation)
2121+
20982122
global_fonttype = mpl.rcParams['pdf.fonttype']
20992123

21002124
# Set up a global transformation matrix for the whole math expression
@@ -2151,6 +2175,19 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
21512175
with dviread.Dvi(dvifile, 72) as dvi:
21522176
page, = dvi
21532177

2178+
if gc.get_url() is not None:
2179+
link_annotation = {
2180+
'Type': Name('Annot'),
2181+
'Subtype': Name('Link'),
2182+
'Rect': (x, y, x + page.width, y + page.height),
2183+
'Border': [0, 0, 0],
2184+
'A': {
2185+
'S': Name('URI'),
2186+
'URI': gc.get_url(),
2187+
},
2188+
}
2189+
self.file._annotations[-1][1].append(link_annotation)
2190+
21542191
# Gather font information and do some setup for combining
21552192
# characters into strings. The variable seq will contain a
21562193
# sequence of font and text entries. A font entry is a list
@@ -2250,6 +2287,21 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
22502287
if is_opentype_cff_font(font.fname):
22512288
fonttype = 42
22522289

2290+
if gc.get_url() is not None:
2291+
font.set_text(s)
2292+
width, height = font.get_width_height()
2293+
link_annotation = {
2294+
'Type': Name('Annot'),
2295+
'Subtype': Name('Link'),
2296+
'Rect': (x, y, x + width / 64, y + height / 64),
2297+
'Border': [0, 0, 0],
2298+
'A': {
2299+
'S': Name('URI'),
2300+
'URI': gc.get_url(),
2301+
},
2302+
}
2303+
self.file._annotations[-1][1].append(link_annotation)
2304+
22532305
# If fonttype != 3 or there are no multibyte characters, emit the whole
22542306
# string at once.
22552307
if fonttype != 3 or all(ord(char) <= 255 for char in s):

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import decimal
23
import io
34
import os
45
from pathlib import Path
@@ -212,6 +213,53 @@ def test_multipage_metadata(monkeypatch):
212213
}
213214

214215

216+
def test_text_urls():
217+
pikepdf = pytest.importorskip('pikepdf')
218+
219+
test_url = 'https://test_text_urls.matplotlib.org/'
220+
221+
fig = plt.figure(figsize=(2, 1))
222+
fig.text(0.1, 0.1, 'test plain 123', url=f'{test_url}plain')
223+
fig.text(0.1, 0.4, 'test mathtext $123$', url=f'{test_url}mathtext')
224+
225+
with io.BytesIO() as fd:
226+
fig.savefig(fd, format='pdf')
227+
228+
with pikepdf.Pdf.open(fd) as pdf:
229+
annots = pdf.pages[0].Annots
230+
231+
for y, fragment in [('0.1', 'plain'), ('0.4', 'mathtext')]:
232+
annot = next(
233+
(a for a in annots if a.A.URI == f'{test_url}{fragment}'),
234+
None)
235+
assert annot is not None
236+
# Positions in points (72 per inch.)
237+
assert annot.Rect[1] == decimal.Decimal(y) * 72
238+
239+
240+
@needs_usetex
241+
def test_text_urls_tex():
242+
pikepdf = pytest.importorskip('pikepdf')
243+
244+
test_url = 'https://test_text_urls.matplotlib.org/'
245+
246+
fig = plt.figure(figsize=(2, 1))
247+
fig.text(0.1, 0.7, 'test tex $123$', usetex=True, url=f'{test_url}tex')
248+
249+
with io.BytesIO() as fd:
250+
fig.savefig(fd, format='pdf')
251+
252+
with pikepdf.Pdf.open(fd) as pdf:
253+
annots = pdf.pages[0].Annots
254+
255+
annot = next(
256+
(a for a in annots if a.A.URI == f'{test_url}tex'),
257+
None)
258+
assert annot is not None
259+
# Positions in points (72 per inch.)
260+
assert annot.Rect[1] == decimal.Decimal('0.7') * 72
261+
262+
215263
def test_pdfpages_fspath():
216264
with PdfPages(Path(os.devnull)) as pdf:
217265
pdf.savefig(plt.figure())

0 commit comments

Comments
 (0)