-
Notifications
You must be signed in to change notification settings - Fork 6
/
svgparser.py
1839 lines (1641 loc) · 73.6 KB
/
svgparser.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#
# Based on the official Blender addon.
# "Scalable Vector Graphics (SVG) 1.1 format" by J. M. Soler, Sergey Sharybin
# Additions and modifications:
# Copyright (C) 2020, 2021, 2022 Jens Zamanian, https://github.com/JezuzStardust
# TODO (longterm):
# - Add more features of SVG.
# - Opacity.
# - Stroke: Do this with Blenders curve bevel and use a rectangle with zero height.
# The end-caps can be added as circles.
# In case the end is a square, we simply extend the curve with a straight line
# in the tangent direction a length equal to the stroke distance.
# The end-caps should be parented to vertices the end vertices (in the case of a circle).
# We should also set the end-cap as non-selectable.
# I think this will give the best experience for the user since they can
# change the shape of the object without affecting the result.
# The only part left to figure out is how to do miter and miterlimit.
# But that can probably be done using simple geometries like triangles.
# The only problem is that this cannot be recalculated once the geometry is
# imported into Blender.
# Miter is only relevant for handles that are not on a straight line.
# Make sure vector handles are only used for such cases!
# - Gradients: Could be a fun problem to solve with materials. Must learn about texture spaces.
# - Import as grease pencil: Could be use as an alternative or as an addition for doing strokes.
# - Preserve hierarchy: The name of the object should also read off inkscape:label
# (extra love to Inkscape).
# SVG G's should be grouped together, but Blender does not support
# groups so we should create an empty and link the geometries.
# TODO: Idea for refactor.
# 1. Parse the geometries as before.
# 2. At the stage where Blender objects are created, we should instead store all
# transformed points in the different classes.
# 2a. Alternatively: Instead we can store curves. Bezier, curves.Spline, etc in the classes.
# 2b. Probably: Create a separate data class that stores the hierarchy of curves.
# 3. This should create a new hierarchy where, e.g., all USE nodes are replaced
# in place with the corresponding geometry.
# 4. Equipped with this new hierarchy, we can either create Blender curves directly,
# or do something else, like create the Phovie classes.
# 5. Think about if this can be done in a reasonable manner so that this is also
# useful for the standalone SVG-parser.
# ALGORITHM AND STRUCTURE
# The goal of this program is to:
# - Read and interpret (parse) an .svg-file.
# - Create curves in Blender that looks like the .svg-file.
#
# Since svg-files have a tree structure, we use a component programming pattern, where the main class,
# SVGLoader, is a subclass of SVGGeometryContainer which is a subclass of SVGGeometry, and it contains
# the leaves, which are either subclasses of SVGGeometryContainer or SVGGeometry.
#
# Since the svg specification contains a lot of different quirks, we need to pass over the hierarchy twice.
# In the first pass, the file is read and the different classes, which reflects the different svg nodes
# are created.
# The coordinates and style of each object is read as they are in the svg file.
# References to objects that are inserted by use nodes are stored.
# In the second pass, we calculate the exact dimensions needed for each class based on where in the hierarchy
# they sit and which view ports and view boxes that determines their dimensions.
# We also replace the use nodes by their corresponding geometry objects.
# In the second pass over we also create the Blender spines and insert them into Blender.
import bpy
from math import tan, sin, cos, acos, sqrt, pi
from mathutils import Matrix, Vector
import os
# import numpy as np
import xml.dom.minidom
from . import svgcolors
from . import svgutils
from . import svgtransforms
class SVGGeometry:
"""Geometry base class.
PARAMETERS
----------
node : :class:`xml.dom.minidom.Document` || class:`xml.dom.minidom.Element`
context : :class:`dict[]`
"""
__slots__ = (
"_node",
"_transform",
"_style",
"_context",
"_name",
)
def __init__(self, node, context):
self._node = node
self._transform = Matrix()
self._style = svgutils.SVG_EMPTY_STYLE
self._context = context
self._name = None
def parse(self):
"""Parse the style and transformations of xml.dom.minidom.Element."""
if type(self._node) is xml.dom.minidom.Element:
self._style = self._parse_style()
self._transform = self._parse_transform()
# If the node has an id or class store reference to the instance
for attr in ("id", "class"):
id_or_class = self._node.getAttribute(attr)
if id_or_class:
print("SVGGeometry parse id_or_class: ", id_or_class)
# For elements with both: keep only id.
if self._context["defs"].get("#" + id_or_class) is None:
self._context["defs"]["#" + id_or_class] = self
if not self._name:
self._name = id_or_class
def _parse_style(self):
"""Parse the style attributes (fill="red", stroke-width=".1cm", ...),
and/or (style='fill:blue;stroke:green...').
The latter should take precedence according to the SVG specification
Overwrites the first type if both are present.
"""
style = svgutils.SVG_EMPTY_STYLE.copy()
for attr in svgutils.SVG_EMPTY_STYLE.keys():
val = self._node.getAttribute(attr)
if val:
style[attr] = val.strip().lower()
style_attr = self._node.getAttribute("style")
if style_attr:
elems = style_attr.split(";")
for elem in elems:
s = elem.split(":")
if len(s) != 2:
continue
name = s[0].strip().lower()
val = s[1].strip()
if name in svgutils.SVG_EMPTY_STYLE.keys():
style[name] = val
return style
def _parse_transform(self):
"""Parse the transform attribute on the xml.dom.minidom.Element."""
transform = self._node.getAttribute("transform")
m = Matrix()
if transform:
for match in svgutils.re_match_transform.finditer(transform):
trans = match.group(1)
params = match.group(2)
params = params.replace(",", " ").split()
transform_function = svgtransforms.SVG_TRANSFORMS.get(trans)
if transform_function is None:
raise Exception("Unknown transform function: " + trans)
m = m @ transform_function(params)
return m
def _push_transform(self, transform):
"""Push the transformation matrix onto the stack."""
m = self._context["current_transform"]
self._context["current_transform"] = m @ transform
def _pop_transform(self, transform):
"""Pop the transformation matrix from the current_transform stack.
Does not return the popped value."""
m = self._context["current_transform"]
self._context["current_transform"] = m @ transform.inverted()
def _transform_coord(self, co):
"""Transform the vector co with the current transform.
Return the resulting vector."""
m = self._context["current_transform"]
v = Vector((co[0], co[1], 0))
return m @ v
def _new_blender_curve_object(self, name):
"""Create new curve object and add it to the Blender collection."""
# Create a new curve in the data collection.
# Create a new (Blender) object with the curve as data.
# Link the object to the scene collection.
# Set the dimension of the object data to "2D".
# Set fill_mode of the object data to "NONE" or "BOTH" depending
# on whether the curve is filled or not.
# Create a material with the correct color.
# Add the material to the object data.
# TODO: Eliminate one of this and new_blender_curve.
curve = bpy.data.curves.new(name, "CURVE")
obj = bpy.data.objects.new(name, curve)
self._context["blender_collection"].objects.link(obj)
obj.data.dimensions = "2D"
# TODO: Code repetition in new_blender_curve.
style = self._calculate_style_in_context()
if style["fill"] == "none":
obj.data.fill_mode = "NONE"
else:
obj.data.fill_mode = "BOTH"
material = self._get_material_with_color(style["fill"])
obj.data.materials.append(material)
# TODO: Come up with a good way of using this.
#Test by translating the curves a bit in the z-direction for every layer.
# m = Matrix.Translation((0, 0, 0.000015))
# self._push_transform(m)
return obj.data
def _new_blender_curve(self, name, is_cyclic):
"""Create new curve object and link it to the Blender collection.
Then add a spline to the given curve."""
# TODO: Keep only one of this and _new_blender_curve_object.
curve = bpy.data.curves.new(name, "CURVE")
obj = bpy.data.objects.new(name, curve)
self._context["blender_collection"].objects.link(obj)
obj.data.dimensions = "2D"
style = self._calculate_style_in_context()
if style["fill"] == "none":
obj.data.fill_mode = "NONE"
else:
obj.data.fill_mode = "BOTH"
material = self._get_material_with_color(style["fill"])
obj.data.materials.append(material)
obj.data.splines.new("BEZIER")
spline = obj.data.splines[-1]
spline.use_cyclic_u = is_cyclic
return spline
def _new_spline_to_blender_curve(self, curve_object_data, is_cyclic):
"""
Adds a new spline to an existing Blender curve object and returns
a reference to the spline.
"""
style = self._calculate_style_in_context()
if style["fill"] != "none":
is_cyclic = True
curve_object_data.splines.new("BEZIER")
spline = curve_object_data.splines[-1]
spline.use_cyclic_u = is_cyclic
return spline
def _add_points_to_blender(self, coords, spline):
"""Add coordinate points and handles to a given spline.
coords = list of coordinates (point, handle_left, handle_right, ...).
spline = a reference to bpy.objects[<current curve>].data.splines[-1].
"""
# TODO: It might be better to create the spline within the loop (for point...).
# In this way, we use the first point of the spline directly when it is
# created and the remaining points are added.
# No need for 'first_point'.
# In that case we might be able to get rid of the
# function _new_blender_curve completely.
# Alternatively, call it from here.
first_point = True
for co in coords:
if not first_point:
spline.bezier_points.add(1)
bezt = spline.bezier_points[-1]
bezt.co = self._transform_coord(co[0])
if co[1]:
bezt.handle_left = self._transform_coord(co[1])
else:
bezt.handle_left_type = "VECTOR"
if co[2]:
bezt.handle_right = self._transform_coord(co[2])
else:
bezt.handle_right_type = "VECTOR"
first_point = False
def _get_material_with_color(self, color):
"""
Parse a color, creates and add a corresponding material
in Blender.
"""
# Parse the color according to the specification.
# If the material already exists, return it.
# Create a new Blender material with this color (using nodes).
# Add the material to the material list in context.
if color in self._context["materials"]:
return self._context["materials"][color]
# TODO: Consider using a unique name for each different SVG-file instead.
if "SVG_" + color in bpy.data.materials:
return bpy.data.materials["SVG_" + color]
if color.startswith("#"):
# According the SVG 1.1 specification, if only three hexdigits
# are given, they should each be repeated twice.
if len(color) == 4:
diff = color[0] + color[1] * 2 + color[2] * 2 + color[3] * 2
else:
diff = color
diff = (int(diff[1:3], 16), int(diff[3:5], 16), int(diff[5:7], 16))
diffuse_color = [x / 255 for x in diff]
elif color in svgcolors.SVG_COLORS:
name = color
diff = svgcolors.SVG_COLORS[color]
diffuse_color = [x / 255 for x in diff]
elif svgutils.re_match_rgb.match(color):
diff = svgutils.re_match_rgb.findall(color)[0]
# If given as % we have diff[1] == diff[3] == diff[5] == %
if diff[1] == '%':
diffuse_color = [
int(diff[0])/100, int(diff[2])/100, int(diff[4])/100]
else:
diffuse_color = [
int(diff[0])/255, int(diff[2])/255, int(diff[4])/255]
else:
return None
if self._context["do_colormanage"]:
diffuse_color = [svgutils.srgb_to_linear(x) for x in diffuse_color]
mat = bpy.data.materials.new(name="SVG_" + color)
# Set material both in Blender default material and node based material.
# Otherwise switching to node tree eliminates the color.
mat.diffuse_color = (*diffuse_color, 1.0)
mat.use_nodes = True
mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (
*diffuse_color,
1.0,
)
# Add the material to the materials stack.
self._context["materials"][color] = mat
return mat
def _calculate_style_in_context(self):
"""
Starts from default material and successively overwrites
the different attributes for each of the parent.
In the end, if an attribute e.g. fill is still None,
the default value is used.
"""
style = self._style
for sty in reversed(self._context["style_stack"]):
for key in svgutils.SVG_EMPTY_STYLE.keys():
if style[key] == None:
style[key] = sty[key]
for key in svgutils.SVG_DEFAULT_STYLE:
if style[key] == None:
style[key] = svgutils.SVG_DEFAULT_STYLE[key]
return style
def _push_style(self, style):
"""
Pushes the 'style' onto the style stack.
"""
self._context["style_stack"].append(style)
def _pop_style(self):
"""
Pops the last style from the style stack.
"""
self._context["style_stack"].pop()
def _get_name_from_node(self):
"""
Gets the id or class name from the node if present.
"""
name = None
name = self._node.getAttribute(
"id") or self._node.getAttribute("class")
return name
def print_hierarchy(self, level=0):
if self._name:
print(level * "\t" + str(self.__class__) + " " + self._name)
else:
print(level * "\t" + str(self.__class__))
class SVGGeometryContainer(SVGGeometry):
"""Container class for SVGGeometry.
Since a container has attributes such as style, and transformations,
it inherits from SVGGeometry.
"""
__slots__ = ("_geometries")
def __init__(self, node, context):
"""Initializes the container
"""
self._geometries = []
super().__init__(node, context)
def parse(self):
"""Initializes and parses all the children elements and add them
to _geometries.
"""
super().parse() # Parse style and transform of the container.
for node in self._node.childNodes:
if type(node) is not xml.dom.minidom.Element:
continue
name = node.tagName
# Sometimes an SVG namespace (xmlns) is used.
if name.startswith("svg:"):
name = name[4:]
geometry_class = SVG_GEOMETRY_CLASSES.get(name)
if geometry_class is not None:
geometry_instance = geometry_class(node, self._context)
geometry_instance.parse()
self._geometries.append(geometry_instance)
def create_blender_splines(self):
"""Make all children elements create splines in Blender.
Does not call the creation for SYMBOLS and DEFS, instead
they will be created via a USE element.
"""
self._push_style(self._style)
for geom in self._geometries:
if geom.__class__ not in (SVGGeometrySYMBOL, SVGGeometryDEFS):
geom.create_blender_splines()
self._pop_style()
def create_phovie_objects(self):
"""Create Phovie objects based on the geometries.
"""
# TODO: This has nothing to do with the Blender SVG Parser.
# Instead we should move this externally?
self._push_style(self._style)
for geom in self._geometries:
if geom.__class__ not in (SVGGeometrySYMBOL, SVGGeometryDEFS):
geom.create_phovie_objects()
self._pop_style()
def __repr__(self):
string = f"{self._node.__repr__()}"
for geo in self._geometries:
if geo is not None:
string += f"\n\t{geo.__repr__()}"
return string
def print_hierarchy(self, level=0):
if self._name:
print(level * "\t" + str(self.__class__) + " " + self._name)
else:
print(level * "\t" + str(self.__class__))
for geo in self._geometries:
geo.print_hierarchy(level=level+1)
class SVGGeometrySVG(SVGGeometryContainer):
"""Corresponds to the <svg> elements.
"""
__slots__ = ("_viewBox",
"viewport",
"_preserveAspectRatio",
)
def __init__(self, node, context):
super().__init__(node, context)
if not self._context["outermost_SVG"]: # Store outermost SVG.
self._context["outermost_SVG"] = self
# TODO: Check that outermost viewport always have viewBox defined.
# Otherwise set it to default values.
def parse(self):
"""Parse the attributes of the SVG element.
The viewport (x, y, width, height) cannot actually be calculate at this time
since they require knowledge of the ancestor/parent viewBox in case the values
are given in percentages.
This method simply read the values and units of the element in the svg-file.
"""
self.viewport = self._parse_viewport()
self._viewBox = self._parse_viewBox()
self._preserveAspectRatio = self._parse_preserveAspectRatio()
super().parse()
def create_blender_splines(self):
"""Adds geometry to Blender.
"""
viewport_transform = self._view_to_transform()
self._push_transform(viewport_transform)
self._push_transform(self._transform)
# If there is no viewBox we inherit it from the current viewport.
# Since the viewport is context dependent, this viewBox may change
# each time the container is used (if referenced multiple times).
# It is therefore not possible to store the viewBox in self._viewBox.
# Instead it is pushed onto a stack and remove it later.
viewBox = self._viewBox
if not viewBox:
viewBox = self._calculate_viewBox_from_viewport()
self._push_viewBox(viewBox)
super().create_blender_splines()
self._pop_viewBox()
self._pop_transform(self._transform)
self._pop_transform(viewport_transform)
def _parse_viewport(self):
"""Parse the x, y, width, and height attributes."""
vp_x = self._node.getAttribute("x") or "0"
vp_y = self._node.getAttribute("y") or "0"
vp_width = self._node.getAttribute("width") or "100%"
vp_height = self._node.getAttribute("height") or "100%"
return (vp_x, vp_y, vp_width, vp_height)
def _parse_viewBox(self):
"""Parse the viewBox attribute.
"""
# TODO: Should we check for outermost viewBox here?
viewBox = self._node.getAttribute("viewBox")
if viewBox:
min_x, min_y, width, height = viewBox.replace(",", " ").split()
return (min_x, min_y, width, height)
else:
return None
def _push_viewBox(self, viewBox):
"""Push the viewBox onto the stack."""
if viewBox:
self._context["current_viewBox"] = viewBox
self._context["viewBox_stack"].append(viewBox)
def _pop_viewBox(self):
""""""
if self._viewBox:
self._context["viewBox_stack"].pop()
self._context["current_viewBox"] = self._context["viewBox_stack"][-1]
def _parse_preserveAspectRatio(self):
# TODO: Handle cases where it starts with 'defer' (and ignore this case).
# TODO: Handle 'none'. However, see _view_to_transform. Might be OK as is.
preserveAspectRatio = self._node.getAttribute("preserveAspectRatio")
if preserveAspectRatio:
for match in svgutils.re_match_align_meet_or_slice.finditer(preserveAspectRatio):
align = match.group(1)
meetOrSlice = match.group(4)
else:
align = "xMidYMid"
meetOrSlice = "meet"
align_x = align[:4]
align_y = align[4:]
return (align_x, align_y, meetOrSlice)
def _calculate_viewBox_from_viewport(self):
"""
Inherit the viewBox from viewport, i.e. use standard coordinates.
Used when there is not viewBox present.
"""
current_viewBox = self._context["current_viewBox"]
viewport = self.viewport
viewBox_width = svgutils.svg_parse_coord(
viewport[2], current_viewBox[2])
viewBox_height = svgutils.svg_parse_coord(
viewport[3], current_viewBox[3])
return (0, 0, viewBox_width, viewBox_height)
def _view_to_transform(self):
"""
Resolves the viewport and viewBox and converts them into
an equivalent transform.
"""
viewBox = self._viewBox
viewport = self.viewport
preserveAspectRatio = self._preserveAspectRatio
current_viewBox = self._context["current_viewBox"] # Parent's viewBox
# First parse the viewport and (if necessary) resolve percentages
# using the parent's viewBox.
# Then parse the viewBox. In case there is no viewBox,
# then use the values from the rect.
# Parse the SVG viewport.
# Resolve percentages to parent viewport.
# If viewport missing, use parent viewBox.
# TODO: ALL SVG has a viewport!
e_x = svgutils.svg_parse_coord(viewport[0], current_viewBox[0])
e_y = svgutils.svg_parse_coord(viewport[1], current_viewBox[1])
e_width = svgutils.svg_parse_coord(viewport[2], current_viewBox[2])
e_height = svgutils.svg_parse_coord(viewport[3], current_viewBox[3])
if viewBox:
vb_x = svgutils.svg_parse_coord(viewBox[0])
vb_y = svgutils.svg_parse_coord(viewBox[1])
vb_width = svgutils.svg_parse_coord(viewBox[2])
vb_height = svgutils.svg_parse_coord(viewBox[3])
else:
# TODO: This is the same as is done in calculate_viewBox_from_viewport.
# However, faster to do it here instead of calling that function.
vb_x = 0
vb_y = 0
vb_width = e_width
vb_height = e_height
scale_x = e_width / vb_width
scale_y = e_height / vb_height
# TODO: Handle preserveAspectRatio='none', and 'defer'.
# This might actually handled 'by accident' by the code below.
pARx = preserveAspectRatio[0]
pARy = preserveAspectRatio[1]
meetOrSlice = preserveAspectRatio[2]
if meetOrSlice == "meet": # Must also check that align is not none.
# TODO: Check how none affects this value.
scale_x = scale_y = min(scale_x, scale_y)
elif meetOrSlice == "slice":
scale_x = scale_y = max(scale_x, scale_y)
translate_x = e_x - vb_x * scale_x
translate_y = e_y - vb_y * scale_y
if pARx == "xMid":
translate_x += (e_width - vb_width * scale_x) / 2
if pARx == "xMax":
translate_x += e_width - vb_width * scale_x
if pARy == "YMid":
translate_y += (e_height - vb_height * scale_y) / 2
if pARy == "YMax":
translate_y += e_height - vb_height * scale_y
m = Matrix()
# Position the origin in the correct place.
# TODO: Update the interface to include also verbose words.
# TODO: How do I handle two separate words, e.g. top bottom or Top Bottom, etc.
if self._context["outermost_SVG"] is self:
position = self._context["origin"]
pos_y = position[0]
o_pos_y = 0 # Default
if pos_y == "T": # Not needed but keep to remember.
o_pos_y = 0
elif pos_y == "M":
o_pos_y = -e_height / 2
elif pos_y == "B":
o_pos_y = -e_height
elif pos_y == "D": # Baseline of the text from LaTeX.
o_pos_y = -e_height + svgutils.svg_parse_coord(self._context["depth"])
pos_x = position[1]
o_pos_x = 0 # Default
if pos_x == "L": # Not really needed, but keep it to remember.
o_pos_x = 0
elif pos_x == "C":
o_pos_x = -e_width / 2
elif pos_x == "R":
o_pos_x = -e_width
m = m @ Matrix.Translation(Vector((o_pos_x, o_pos_y, 0)))
m = m @ Matrix.Translation(Vector((translate_x, translate_y, 0)))
m = m @ Matrix.Scale(scale_x, 4, Vector((1, 0, 0)))
m = m @ Matrix.Scale(scale_y, 4, Vector((0, 1, 0)))
return m
class SVGGeometryG(SVGGeometryContainer):
"""
Same as SVGGeometryContainer, but can also have transform.
"""
def create_blender_splines(self):
self._push_transform(self._transform)
super().create_blender_splines()
self._pop_transform(self._transform)
class SVGGeometryRECT(SVGGeometry):
"""
SVG <rect>.
"""
__slots__ = ("_x", "_y", "_width", "_height", "_rx", "_ry")
def __init__(self, node, context):
"""
Initialize a new rectangle with default values.
"""
super().__init__(node, context)
self._x = "0"
self._y = "0"
self._width = "0"
self._height = "0"
self._rx = "0"
self._ry = "0"
def parse(self):
"""
Parse the data from the node and store in the local variables.
Reads x, y, width, height, rx, ry from the node.
Also reads in the style.
Should it also read the transformation?
"""
super().parse()
self._x = self._node.getAttribute("x") or "0"
self._y = self._node.getAttribute("y") or "0"
self._width = self._node.getAttribute("width") or "0"
self._height = self._node.getAttribute("height") or "0"
self._rx = self._node.getAttribute("rx") or "0"
self._ry = self._node.getAttribute("ry") or "0"
def create_blender_splines(self):
"""
Create Blender geometry.
"""
vB = self._context["current_viewBox"][2:] # width and height of viewBox.
x = svgutils.svg_parse_coord(self._x, vB[0])
y = svgutils.svg_parse_coord(self._y, vB[1])
w = svgutils.svg_parse_coord(self._width, vB[0])
h = svgutils.svg_parse_coord(self._height, vB[1])
rx = ry = 0
rad_x = self._rx
rad_y = self._ry
# For radii rx and ry, resolve % values against half the width and height,
# respectively. It is not clear from the specification which width
# and height are considered.
# In SVG 2.0 it seems to indicate that it should be the width and height
# of the rectangle. However, to be consistent with other % it is
# most likely the width and height of the current viewBox.
# If only one is given then the other one should be the same.
# Clamp the values to width/2 respectively height/2 if larger than
# this.
# 100% means half the width or height of the viewBox (or viewport).
# https://www.w3.org/TR/SVG11/shapes.html#RectElement
rounded = True
if rad_x != "0" and rad_y != "0":
rx = min(svgutils.svg_parse_coord(rad_x, vB[0]), w / 2)
ry = min(svgutils.svg_parse_coord(rad_y, vB[1]), h / 2)
elif rad_x != "0":
rx = min(svgutils.svg_parse_coord(rad_x, vB[0]), w / 2)
ry = min(rx, h / 2)
elif rad_y != "0":
ry = min(svgutils.svg_parse_coord(rad_y, vB[1]), h / 2)
rx = min(ry, w / 2)
else:
rounded = False
# Approximation of elliptic curve for corner.
# Put the handles semi minor(or semi major) axis radius times
# factor = (sqrt(7) - 1)/3 away from Bezier point.
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
factor_x = rx * (sqrt(7) - 1) / 3
factor_y = ry * (sqrt(7) - 1) / 3
if rounded:
coords = [
((x + rx, y), (x + rx - factor_x, y), None),
((x + w - rx, y), None, (x + w - rx + factor_x, y)),
((x + w, y + ry), (x + w, y + ry - factor_y), None),
((x + w, y + h - ry), None, (x + w, y + h - ry + factor_y)),
((x + w - rx, y + h), (x + w - rx + factor_x, y + h), None),
((x + rx, y + h), None, (x + rx - factor_x, y + h)),
((x, y + h - ry), (x, y + h - ry + factor_y), None),
((x, y + ry), None, (x, y + ry - factor_y)),
]
else:
coords = [
((x, y), None, None),
((x + w, y), None, None),
((x + w, y + h), None, None),
((x, y + h), None, None),
]
# TODO: Move this to a general purpose function.
# Perhaps name can be defined in SVGGeometry even, since
# all elements can have names.
if not self._name:
self._name = "Rect"
spline = self._new_blender_curve(self._name, True)
self._push_transform(self._transform)
self._add_points_to_blender(coords, spline)
self._pop_transform(self._transform)
def create_phovie_objects(self):
"""
Create Blender geometry.
"""
vB = self._context["current_viewBox"][2:] # width and height of viewBox.
x = svgutils.svg_parse_coord(self._x, vB[0])
y = svgutils.svg_parse_coord(self._y, vB[1])
w = svgutils.svg_parse_coord(self._width, vB[0])
h = svgutils.svg_parse_coord(self._height, vB[1])
rx = ry = 0
rad_x = self._rx
rad_y = self._ry
# For radii rx and ry, resolve % values against half the width and height,
# respectively. It is not clear from the specification which width
# and height are considered.
# In SVG 2.0 it seems to indicate that it should be the width and height
# of the rectangle. However, to be consistent with other % it is
# most likely the width and height of the current viewBox.
# If only one is given then the other one should be the same.
# Then clamp the values to width/2 respectively height/2.
# 100% means half the width or height of the viewBox (or viewport).
# https://www.w3.org/TR/SVG11/shapes.html#RectElement
rounded = True
if rad_x != "0" and rad_y != "0":
rx = min(svgutils.svg_parse_coord(rad_x, vB[0]), w / 2)
ry = min(svgutils.svg_parse_coord(rad_y, vB[1]), h / 2)
elif rad_x != "0":
rx = min(svgutils.svg_parse_coord(rad_x, vB[0]), w / 2)
ry = min(rx, h / 2)
elif rad_y != "0":
ry = min(svgutils.svg_parse_coord(rad_y, vB[1]), h / 2)
rx = min(ry, w / 2)
else:
rounded = False
# Approximation of elliptic curve for corner.
# Put the handles semi minor(or major) axis radius times
# factor = (sqrt(7) - 1)/3 away from Bezier point.
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
factor_x = rx * (sqrt(7) - 1) / 3
factor_y = ry * (sqrt(7) - 1) / 3
# TODO: Probably better to use a specific class for all Bezier curves.
if rounded:
# (coordinate, handle_left, handle_right)
# If a handle is None, it means that it will be a straight line
# (vector handle).
# o (HR8) o (HL3)
# . .
# (HL1) . (P1) (P2) . (HR2)
# o.....o------------------------------o.....o
# ./ \.
# o (P8) o (P3)
# | |
# | |
# o (P7) o (P4)
# .\ /.
# (HR6) o.....o------------------------------o.....o (HL5)
# . (P6) (P5) .
# . .
# o o
# (HL7) (HR4)
#
# coords = [ ( (P1), (HL1), (HR1) ), ( (P2), (HL2), (HR2) ), ... ]
coords = [
((x + rx, y), (x + rx - factor_x, y), None),
((x + w - rx, y), None, (x + w - rx + factor_x, y)),
((x + w, y + ry), (x + w, y + ry - factor_y), None),
((x + w, y + h - ry), None, (x + w, y + h - ry + factor_y)),
((x + w - rx, y + h), (x + w - rx + factor_x, y + h), None),
((x + rx, y + h), None, (x + rx - factor_x, y + h)),
((x, y + h - ry), (x, y + h - ry + factor_y), None),
((x, y + ry), None, (x, y + ry - factor_y)),
]
else:
coords = [
((x, y), None, None),
((x + w, y), None, None),
((x + w, y + h), None, None),
((x, y + h), None, None),
]
# TODO: Move this to a general purpose function.
# Perhaps name can be defined in SVGGeometry even, since
# all elements can have names.
# YES! Do this!
if not self._name:
self._name = "Rect"
spline = self._new_blender_curve(self._name, True)
self._push_transform(self._transform)
self._add_points_to_blender(coords, spline)
self._pop_transform(self._transform)
@staticmethod
def _new_point(coordinate, handle_left=None, handle_right=None, in_type=None, out_type=None):
"""Not currently used."""
# TODO: Remove this?
return {
"coordinates": coordinate,
"handle_left": handle_right,
"handle_right": handle_left,
"in_type": in_type,
"out_type": out_type,
}
@staticmethod
def _new_path(is_closed=False):
"""Not currently used."""
return {"points": [], "is_closed": is_closed}
class SVGGeometryELLIPSE(SVGGeometry):
"""
SVG <ellipse>.
"""
__slots__ = ("_cx", "_cy", "_rx", "_ry", "_is_circle")
def __init__(self, node, context):
"""
Initialize the ellipse with default values (all zero).
"""
super().__init__(node, context)
self._is_circle = False
self._cx = "0"
self._cy = "0"
self._rx = "0"
self._ry = "0"
def parse(self):
"""
Parses the data from the <ellipse> element.
"""
super().parse()
self._cx = self._node.getAttribute("cx") or "0"
self._cy = self._node.getAttribute("cy") or "0"
self._rx = self._node.getAttribute("rx") or "0"
self._ry = self._node.getAttribute("ry") or "0"
r = self._node.getAttribute("r") or "0"
if r != "0":
self._is_circle = True
self._rx = r
def create_blender_splines(self):
"""Create Blender geometry.
"""
vB = self._context["current_viewBox"][2:] # width and height of viewBox.
cx = svgutils.svg_parse_coord(self._cx, vB[0])
cy = svgutils.svg_parse_coord(self._cy, vB[1])
if self._is_circle:
weighted_diagonal = sqrt(
float(vB[0]) ** 2 + float(vB[1]) ** 2) / sqrt(2)
rx = ry = svgutils.svg_parse_coord(self._rx, weighted_diagonal)
else:
rx = svgutils.svg_parse_coord(self._rx, vB[0])
ry = svgutils.svg_parse_coord(self._ry, vB[1])
# Approximation of elliptic curve for corner.
# Put the handles semi minor(or major) axis radius times
# factor = (sqrt(7) - 1)/3 away from Bezier point.
# http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
factor_x = rx * (sqrt(7) - 1) / 3
factor_y = ry * (sqrt(7) - 1) / 3
# Coordinate, first handle, second handle
coords = [
((cx - rx, cy), (cx - rx, cy + factor_y), (cx - rx, cy - factor_y)),
((cx, cy - ry), (cx - factor_x, cy - ry), (cx + factor_x, cy - ry)),
((cx + rx, cy), (cx + rx, cy - factor_y), (cx + rx, cy + factor_y)),
((cx, cy + ry), (cx + factor_x, cy + ry), (cx - factor_x, cy + ry)),
]
if not self._name:
if self._is_circle:
self._name = "Circle"
else:
self._name = "Ellipse"
spline = self._new_blender_curve(self._name, True)
self._push_transform(self._transform)
self._add_points_to_blender(coords, spline)
self._pop_transform(self._transform)
class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
"""
A <circle> element with a lot of reuse of ellipse code.
"""
pass # Handled completely by ELLIPSE.
class SVGGeometryLINE(SVGGeometry):
"""
SVG <line>.
"""
__slots__ = ("_x1", "_y1", "_x2", "_y2")
def __init__(self, node, context):
"""
Initialize the ellipse with default values (all zero).
"""
super().__init__(node, context)
self._x1 = "0"
self._y1 = "0"
self._x2 = "0"
self._y2 = "0"
def parse(self):
"""
Parses the data from the <ellipse> element.
"""
super().parse()
self._x1 = self._node.getAttribute("x1") or "0"
self._y1 = self._node.getAttribute("y1") or "0"