26
26
GraphicsContext ,
27
27
GraphicsStyle ,
28
28
PaintedPath ,
29
+ ClippingPath ,
29
30
Transform ,
30
31
)
31
32
@@ -329,15 +330,17 @@ class ShapeBuilder:
329
330
"""A namespace within which methods for converting basic shapes can be looked up."""
330
331
331
332
@staticmethod
332
- def new_path (tag ):
333
+ def new_path (tag , clipping_path : bool = False ):
333
334
"""Create a new path with the appropriate styles."""
334
335
path = PaintedPath ()
336
+ if clipping_path :
337
+ path = ClippingPath ()
335
338
apply_styles (path , tag )
336
339
337
340
return path
338
341
339
342
@classmethod
340
- def rect (cls , tag ):
343
+ def rect (cls , tag , clipping_path : bool = False ):
341
344
"""Convert an SVG <rect> into a PDF path."""
342
345
# svg rect is wound clockwise
343
346
if "x" in tag .attrib :
@@ -387,33 +390,33 @@ def rect(cls, tag):
387
390
if ry > (height / 2 ):
388
391
ry = height / 2
389
392
390
- path = cls .new_path (tag )
393
+ path = cls .new_path (tag , clipping_path )
391
394
392
395
path .rectangle (x , y , width , height , rx , ry )
393
396
return path
394
397
395
398
@classmethod
396
- def circle (cls , tag ):
399
+ def circle (cls , tag , clipping_path : bool = False ):
397
400
"""Convert an SVG <circle> into a PDF path."""
398
401
cx = float (tag .attrib .get ("cx" , 0 ))
399
402
cy = float (tag .attrib .get ("cy" , 0 ))
400
403
r = float (tag .attrib ["r" ])
401
404
402
- path = cls .new_path (tag )
405
+ path = cls .new_path (tag , clipping_path )
403
406
404
407
path .circle (cx , cy , r )
405
408
return path
406
409
407
410
@classmethod
408
- def ellipse (cls , tag ):
411
+ def ellipse (cls , tag , clipping_path : bool = False ):
409
412
"""Convert an SVG <ellipse> into a PDF path."""
410
413
cx = float (tag .attrib .get ("cx" , 0 ))
411
414
cy = float (tag .attrib .get ("cy" , 0 ))
412
415
413
416
rx = tag .attrib .get ("rx" , "auto" )
414
417
ry = tag .attrib .get ("ry" , "auto" )
415
418
416
- path = cls .new_path (tag )
419
+ path = cls .new_path (tag , clipping_path )
417
420
418
421
if (rx == ry == "auto" ) or (rx == 0 ) or (ry == 0 ):
419
422
return path
@@ -457,11 +460,11 @@ def polyline(cls, tag):
457
460
return path
458
461
459
462
@classmethod
460
- def polygon (cls , tag ):
463
+ def polygon (cls , tag , clipping_path : bool = False ):
461
464
"""Convert an SVG <polygon> into a PDF path."""
462
465
points = tag .attrib ["points" ]
463
466
464
- path = cls .new_path (tag )
467
+ path = cls .new_path (tag , clipping_path )
465
468
466
469
points = "M" + points + "Z"
467
470
svg_path_converter (path , points )
@@ -665,6 +668,12 @@ def __init__(self, svg_text):
665
668
self .extract_shape_info (svg_tree )
666
669
self .convert_graphics (svg_tree )
667
670
671
+ @force_nodocument
672
+ def update_xref (self , key , referenced ):
673
+ if key :
674
+ key = "#" + key if not key .startswith ("#" ) else key
675
+ self .cross_references [key ] = referenced
676
+
668
677
@force_nodocument
669
678
def extract_shape_info (self , root_tag ):
670
679
"""Collect shape info from the given SVG."""
@@ -859,7 +868,15 @@ def handle_defs(self, defs):
859
868
self .build_group (child )
860
869
if child .tag in xmlns_lookup ("svg" , "path" ):
861
870
self .build_path (child )
862
- # We could/should also support <defs> that are rect, circle, ellipse, line, polyline, polygon...
871
+ elif child .tag in shape_tags :
872
+ self .build_shape (child )
873
+ if child .tag in xmlns_lookup ("svg" , "clipPath" ):
874
+ try :
875
+ clip_id = child .attrib ["id" ]
876
+ except KeyError :
877
+ clip_id = None
878
+ for child_ in child :
879
+ self .build_clipping_path (child_ , clip_id )
863
880
864
881
# this assumes xrefs only reference already-defined ids.
865
882
# I don't know if this is required by the SVG spec.
@@ -869,7 +886,7 @@ def build_xref(self, xref):
869
886
pdf_group = GraphicsContext ()
870
887
apply_styles (pdf_group , xref )
871
888
872
- for candidate in xmlns_lookup ("xlink" , "href" ):
889
+ for candidate in xmlns_lookup ("xlink" , "href" , "id" ):
873
890
try :
874
891
ref = xref .attrib [candidate ]
875
892
break
@@ -901,22 +918,23 @@ def build_group(self, group, pdf_group=None):
901
918
pdf_group = GraphicsContext ()
902
919
apply_styles (pdf_group , group )
903
920
921
+ # handle defs before anything else
922
+ for child in [
923
+ child for child in group if child .tag in xmlns_lookup ("svg" , "defs" )
924
+ ]:
925
+ self .handle_defs (child )
926
+
904
927
for child in group :
905
- if child .tag in xmlns_lookup ("svg" , "defs" ):
906
- self .handle_defs (child )
907
928
if child .tag in xmlns_lookup ("svg" , "g" ):
908
929
pdf_group .add_item (self .build_group (child ))
909
930
if child .tag in xmlns_lookup ("svg" , "path" ):
910
931
pdf_group .add_item (self .build_path (child ))
911
932
elif child .tag in shape_tags :
912
- pdf_group .add_item (getattr ( ShapeBuilder , shape_tags [ child . tag ]) (child ))
933
+ pdf_group .add_item (self . build_shape (child ))
913
934
if child .tag in xmlns_lookup ("svg" , "use" ):
914
935
pdf_group .add_item (self .build_xref (child ))
915
936
916
- try :
917
- self .cross_references ["#" + group .attrib ["id" ]] = pdf_group
918
- except KeyError :
919
- pass
937
+ self .update_xref (group .attrib .get ("id" ), pdf_group )
920
938
921
939
return pdf_group
922
940
@@ -925,15 +943,38 @@ def build_path(self, path):
925
943
"""Convert an SVG <path> tag into a PDF path object."""
926
944
pdf_path = PaintedPath ()
927
945
apply_styles (pdf_path , path )
946
+ self .apply_clipping_path (pdf_path , path )
928
947
929
- svg_path = path .attrib .get ("d" , None )
948
+ svg_path = path .attrib .get ("d" )
930
949
931
950
if svg_path is not None :
932
951
svg_path_converter (pdf_path , svg_path )
933
952
934
- try :
935
- self .cross_references ["#" + path .attrib ["id" ]] = pdf_path
936
- except KeyError :
937
- pass
953
+ self .update_xref (path .attrib .get ("id" ), pdf_path )
938
954
939
955
return pdf_path
956
+
957
+ @force_nodocument
958
+ def build_shape (self , shape ):
959
+ """Convert an SVG shape tag into a PDF path object. Necessary to make xref (because ShapeBuilder doesn't have access to this object.)"""
960
+ shape_path = getattr (ShapeBuilder , shape_tags [shape .tag ])(shape )
961
+ self .apply_clipping_path (shape_path , shape )
962
+
963
+ self .update_xref (shape .attrib .get ("id" ), shape_path )
964
+
965
+ return shape_path
966
+
967
+ @force_nodocument
968
+ def build_clipping_path (self , shape , clip_id ):
969
+ clipping_path_shape = getattr (ShapeBuilder , shape_tags [shape .tag ])(shape , True )
970
+
971
+ self .update_xref (clip_id , clipping_path_shape )
972
+
973
+ return clipping_path_shape
974
+
975
+ @force_nodocument
976
+ def apply_clipping_path (self , stylable , svg_element ):
977
+ clipping_path = svg_element .attrib .get ("clip-path" )
978
+ if clipping_path :
979
+ clipping_path_id = re .search (r"url\((\#\w+)\)" , clipping_path )
980
+ stylable .clipping_path = self .cross_references [clipping_path_id [1 ]]
0 commit comments