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

Commit4e8d111

Browse files
committed
Add URL support for images in PDF backend
1 parent51ce677 commit4e8d111

File tree

3 files changed

+174
-28
lines changed

3 files changed

+174
-28
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
URL Support for Images in PDF Backend
2+
-------------------------------------
3+
4+
The PDF backend can now generate clickable images if a URL is provided to the
5+
image. There are a few limitations worth noting though:
6+
7+
* If parts of the image are clipped, the non-visible parts are still clickable.
8+
* If there are transforms applied to the image, the whole enclosing rectangle
9+
is clickable. However, if you use ``interpolation='none'`` for the image,
10+
only the transformed image area is clickable (depending on viewer support).

‎lib/matplotlib/backends/backend_pdf.py

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -230,32 +230,66 @@ def _datetime_to_pdf(d):
230230
returnr
231231

232232

233-
def_calculate_quad_point_coordinates(x,y,width,height,angle=0):
233+
def_calculate_rotated_quad_point_coordinates(x,y,width,height,angle):
234234
"""
235235
Calculate the coordinates of rectangle when rotated by angle around x, y
236236
"""
237237

238-
angle=math.radians(-angle)
238+
angle=math.radians(angle)
239239
sin_angle=math.sin(angle)
240240
cos_angle=math.cos(angle)
241-
a=x+height*sin_angle
241+
width_cos=width*cos_angle
242+
width_sin=width*sin_angle
243+
a=x-height*sin_angle
242244
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
245+
c=a+width_cos
246+
d=b+width_sin
247+
e=x+width_cos
248+
f=y+width_sin
247249
return ((x,y), (e,f), (c,d), (a,b))
248250

249251

250-
def_get_coordinates_of_block(x,y,width,height,angle=0):
252+
def_calculate_transformed_quad_point_coordinates(x,y,trans):
251253
"""
252-
Get the coordinates of rotated rectangle and rectangle that covers the
253-
rotated rectangle.
254+
Calculate the coordinates of rectangle when transformed by trans
255+
positioned at x, y
256+
"""
257+
tr1,tr2,tr3,tr4,tr5,tr6=trans
258+
# Conceptual code
259+
# width = 1
260+
# height = 1
261+
# a = x + 0 * tr1 + height * tr3 + tr5
262+
# b = y + 0 * tr2 + height * tr4 + tr6
263+
# c = x + width * tr1 + height * tr3 + tr5
264+
# d = y + width * tr2 + height * tr4 + tr6
265+
# e = x + width * tr1 + 0 * tr3 + tr5
266+
# f = y + width * tr2 + 0 * tr4 + tr6
267+
# x = x + 0 * tr1 + 0 * tr3 + tr5
268+
# y = y + 0 * tr2 + 0 * tr4 + tr6
269+
x=x+tr5
270+
y=y+tr6
271+
a=x+tr3
272+
b=y+tr4
273+
c=a+tr1
274+
d=b+tr2
275+
e=x+tr1
276+
f=y+tr2
277+
return (((x,y), (e,f), (c,d), (a,b)),
278+
(0ifmath.isclose(tr2,0)else45))
279+
280+
281+
def_get_coordinates_of_block(x,y,width,height,angle,trans):
282+
"""
283+
Get the coordinates of rotated or transformed rectangle and rectangle
284+
that covers the rotated or transformed rectangle.
254285
"""
255286

256-
vertices=_calculate_quad_point_coordinates(x,y,width,
257-
height,angle)
258-
287+
iftransisNone:
288+
vertices=_calculate_rotated_quad_point_coordinates(x,y,width,
289+
height,angle)
290+
else:
291+
vertices,angle=_calculate_transformed_quad_point_coordinates(x,y,
292+
trans)
259293
# Find min and max values for rectangle
260294
# adjust so that QuadPoints is inside Rect
261295
# PDF docs says that QuadPoints should be ignored if any point lies
@@ -268,27 +302,29 @@ def _get_coordinates_of_block(x, y, width, height, angle=0):
268302
max_x=max(v[0]forvinvertices)+pad
269303
max_y=max(v[1]forvinvertices)+pad
270304
return (tuple(itertools.chain.from_iterable(vertices)),
271-
(min_x,min_y,max_x,max_y))
305+
(min_x,min_y,max_x,max_y),angle)
272306

273307

274-
def_get_link_annotation(gc,x,y,width,height,angle=0):
308+
def_get_link_annotation(gc,x,y,width,height,angle=0,trans=None):
275309
"""
276310
Create a link annotation object for embedding URLs.
277311
"""
278-
quadpoints,rect=_get_coordinates_of_block(x,y,width,height,angle)
279312
link_annotation= {
280313
'Type':Name('Annot'),
281314
'Subtype':Name('Link'),
282-
'Rect':rect,
283315
'Border': [0,0,0],
284316
'A': {
285317
'S':Name('URI'),
286318
'URI':gc.get_url(),
287319
},
288320
}
321+
quadpoints,rect,angle=_get_coordinates_of_block(x,y,width,height,
322+
angle,trans)
323+
link_annotation['Rect']=rect
289324
ifangle%90:
290325
# Add QuadPoints
291326
link_annotation['QuadPoints']=quadpoints
327+
292328
returnlink_annotation
293329

294330

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

20132049
self.check_gc(gc)
20142050

2015-
w=72.0*w/self.image_dpi
2016-
h=72.0*h/self.image_dpi
2017-
20182051
imob=self.file.imageObject(im)
20192052

20202053
iftransformisNone:
2054+
w=72.0*w/self.image_dpi
2055+
h=72.0*h/self.image_dpi
2056+
ifgc.get_url()isnotNone:
2057+
self._add_link_annotation(gc,x,y,w,h)
20212058
self.file.output(Op.gsave,
20222059
w,0,0,h,x,y,Op.concat_matrix,
20232060
imob,Op.use_xobject,Op.grestore)
20242061
else:
2025-
tr1,tr2,tr3,tr4,tr5,tr6=transform.frozen().to_values()
2062+
trans=transform.frozen().to_values()
2063+
ifgc.get_url()isnotNone:
2064+
self._add_link_annotation(gc,x,y,w,h,trans=trans)
20262065

20272066
self.file.output(Op.gsave,
20282067
1,0,0,1,x,y,Op.concat_matrix,
2029-
tr1,tr2,tr3,tr4,tr5,tr6,Op.concat_matrix,
2068+
*trans,Op.concat_matrix,
20302069
imob,Op.use_xobject,Op.grestore)
20312070

20322071
defdraw_path(self,gc,path,transform,rgbFace=None):
@@ -2038,6 +2077,11 @@ def draw_path(self, gc, path, transform, rgbFace=None):
20382077
gc.get_sketch_params())
20392078
self.file.output(self.gc.paint())
20402079

2080+
def_add_link_annotation(self,gc,x,y,width,height,angle=0,
2081+
trans=None):
2082+
self.file._annotations[-1][1].append(
2083+
_get_link_annotation(gc,x,y,width,height,angle,trans))
2084+
20412085
defdraw_path_collection(self,gc,master_transform,paths,all_transforms,
20422086
offsets,offsetTrans,facecolors,edgecolors,
20432087
linewidths,linestyles,antialiaseds,urls,
@@ -2206,8 +2250,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
22062250
self._text2path.mathtext_parser.parse(s,72,prop)
22072251

22082252
ifgc.get_url()isnotNone:
2209-
self.file._annotations[-1][1].append(_get_link_annotation(
2210-
gc,x,y,width,height,angle))
2253+
self._add_link_annotation(gc,x,y,width,height,angle)
22112254

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

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

22652308
ifgc.get_url()isnotNone:
2266-
self.file._annotations[-1][1].append(_get_link_annotation(
2267-
gc,x,y,page.width,page.height,angle))
2309+
self._add_link_annotation(gc,x,y,page.width,page.height,angle)
22682310

22692311
# Gather font information and do some setup for combining
22702312
# characters into strings. The variable seq will contain a
@@ -2364,8 +2406,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23642406
ifgc.get_url()isnotNone:
23652407
font.set_text(s)
23662408
width,height=font.get_width_height()
2367-
self.file._annotations[-1][1].append(_get_link_annotation(
2368-
gc,x,y,width/64,height/64,angle))
2409+
self._add_link_annotation(gc,x,y,width/64,height/64,angle)
23692410

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

‎lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,101 @@ def test_kerning():
360360
fig.text(0,.75,s,size=20)
361361

362362

363+
deftest_image_url():
364+
pikepdf=pytest.importorskip('pikepdf')
365+
366+
image_url='https://test_image_urls.matplotlib.org/'
367+
image_url2='https://test_image_urls2.matplotlib.org/'
368+
369+
X,Y=np.meshgrid(np.arange(-5,5,1),np.arange(-5,5,1))
370+
Z=np.sin(Y**2)
371+
fig,ax=plt.subplots()
372+
ax.imshow(Z,extent=[0,1,0,1],url=image_url)
373+
withio.BytesIO()asfd:
374+
fig.savefig(fd,format="pdf")
375+
withpikepdf.Pdf.open(fd)aspdf:
376+
annots=pdf.pages[0].Annots
377+
378+
# Iteration over Annots must occur within the context manager,
379+
# otherwise it may fail depending on the pdf structure.
380+
annot=next(
381+
(aforainannotsifa.A.URI==image_url),
382+
None)
383+
assertannotisnotNone
384+
# Positions in points (72 per inch.)
385+
assertannot.Rect== [decimal.Decimal('122.4'),
386+
decimal.Decimal('43.2'),
387+
468,
388+
decimal.Decimal('388.8')]
389+
ax.set_xlim(0,3)
390+
ax.imshow(Z[::-1],extent=[2,3,0,1],url=image_url2)
391+
# Must save as separate images
392+
plt.rcParams['image.composite_image']=False
393+
withio.BytesIO()asfd:
394+
fig.savefig(fd,format="pdf")
395+
withpikepdf.Pdf.open(fd)aspdf:
396+
annots=pdf.pages[0].Annots
397+
398+
# Iteration over Annots must occur within the context manager,
399+
# otherwise it may fail depending on the pdf structure.
400+
annot=next(
401+
(aforainannotsifa.A.URI==image_url2),
402+
None)
403+
assertannotisnotNone
404+
# Positions in points (72 per inch.)
405+
assertannot.Rect== [decimal.Decimal('369.6'),
406+
decimal.Decimal('141.6'),
407+
decimal.Decimal('518.64'),
408+
decimal.Decimal('290.64')]
409+
410+
411+
deftest_transformed_image_url():
412+
pikepdf=pytest.importorskip('pikepdf')
413+
414+
image_url='https://test_image_urls.matplotlib.org/'
415+
416+
X,Y=np.meshgrid(np.arange(-5,5,1),np.arange(-5,5,1))
417+
Z=np.sin(Y**2)
418+
fig,ax=plt.subplots()
419+
im=ax.imshow(Z,interpolation='none',url=image_url)
420+
withio.BytesIO()asfd:
421+
fig.savefig(fd,format="pdf")
422+
withpikepdf.Pdf.open(fd)aspdf:
423+
annots=pdf.pages[0].Annots
424+
425+
# Iteration over Annots must occur within the context manager,
426+
# otherwise it may fail depending on the pdf structure.
427+
annot=next(
428+
(aforainannotsifa.A.URI==image_url),
429+
None)
430+
assertannotisnotNone
431+
# Positions in points (72 per inch.)
432+
assertannot.Rect== [decimal.Decimal('122.4'),
433+
decimal.Decimal('43.2'),
434+
decimal.Decimal('468.4'),
435+
decimal.Decimal('389.2')]
436+
assertgetattr(annot,'QuadPoints',None)isNone
437+
# Transform
438+
im.set_transform(mpl.transforms.Affine2D().skew_deg(30,15)+ax.transData)
439+
withio.BytesIO()asfd:
440+
fig.savefig(fd,format="pdf")
441+
withpikepdf.Pdf.open(fd)aspdf:
442+
annots=pdf.pages[0].Annots
443+
444+
# Iteration over Annots must occur within the context manager,
445+
# otherwise it may fail depending on the pdf structure.
446+
annot=next(
447+
(aforainannotsifa.A.URI==image_url),
448+
None)
449+
assertannotisnotNone
450+
# Positions in points (72 per inch.)
451+
assertannot.Rect[0]==decimal.Decimal('112.411830343')
452+
assertgetattr(annot,'QuadPoints',None)isnotNone
453+
# Positions in points (72 per inch)
454+
assertannot.Rect[0]== \
455+
annot.QuadPoints[0]-decimal.Decimal('0.00001')
456+
457+
363458
deftest_glyphs_subset():
364459
fpath=str(_get_data_path("fonts/ttf/DejaVuSerif.ttf"))
365460
chars="these should be subsetted! 1234567890"

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp