Expect input coordinates for the mesh projection function to be in world
[blender-addons-contrib.git] / io_vector / svg.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 """Reading SVG file format.
22 """
23
24 __author__ = "howard.trickey@gmail.com"
25
26 import re
27 import xml.dom.minidom
28 from . import geom
29
30 TOL = 1e-5
31
32
33 def ParseSVGFile(filename):
34     """Parse an SVG file name and return an Art object for it.
35
36     Args:
37       filename: string - name of file to read and parse
38     Returns:
39       geom.Art
40     """
41
42     dom = xml.dom.minidom.parse(filename)
43     return _SVGDomToArt(dom)
44
45
46 def ParseSVGString(s):
47     """Parse an SVG string and return an Art object for it.
48
49     Args:
50       s: string - contains svg
51     Returns:
52       geom.Art
53     """
54
55     dom = xml.dom.minidom.parseString(s)
56     return _SVGDomToArg(dom)
57
58
59 class _SState(object):
60     """Holds state that affects the conversion.
61     """
62
63     def __init__(self):
64         self.ctm = geom.TransformMatrix()
65         self.fill = "black"
66         self.fillrule = "nonzero"
67         self.stroke = "none"
68         self.dpi = 90  # default Inkscape DPI
69
70
71 def _SVGDomToArt(dom):
72     """Convert an svg file in dom form into an Art object.
73
74     Args:
75       dom: xml.dom.minidom.Document
76     Returns:
77       geom.Art
78     """
79
80     art = geom.Art()
81     svgs = dom.getElementsByTagName('svg')
82     if len(svgs) == 0:
83         return art
84     gs = _SState()
85     gs.ctm.d = -1.0
86     _ProcessChildren(svgs[0], art, gs)
87     return art
88
89
90 def _ProcessChildren(nodes, art, gs):
91     """Process a list of SVG nodes, updating art.
92
93     Args:
94       nodes: list of xml.dom.Node
95       art: geom.Art
96       gs: _SState
97     Side effects:
98       Maybe adds paths to art.
99     """
100
101     for node in nodes.childNodes:
102         _ProcessNode(node, art, gs)
103
104
105 def _ProcessNode(node, art, gs):
106     """Process an SVG node, updating art.
107
108     Args:
109       node: xml.dom.Node
110       art: geom.Art
111       gs: _SState
112     Side effects:
113       Maybe adds paths to art.
114     """
115
116     if node.nodeType != node.ELEMENT_NODE:
117         return
118     tag = node.tagName
119     if tag == 'g':
120         _ProcessChildren(node, art, gs)
121     elif tag == 'defs':
122         pass  # TODO
123     elif tag == 'path':
124         _ProcessPath(node, art, gs)
125     elif tag == 'polygon':
126         _ProcessPolygon(node, art, gs)
127     elif tag == 'rect':
128         _ProcessRect(node, art, gs)
129     elif tag == 'ellipse':
130         _ProcessEllipse(node, art, gs)
131     elif tag == 'circle':
132         _ProcessCircle(node, art, gs)
133
134
135 def _ProcessPolygon(node, art, gs):
136     """Process a 'polygon' SVG node, updating art.
137
138     Args:
139       node: xml.dom.Node - a 'polygon' node
140       arg: geom.Art
141       gs: _SState
142     Side effects:
143       Adds path for polygon to art
144     """
145
146     if node.hasAttribute('points'):
147         coords = _ParseCoordPairList(node.getAttribute('points'))
148         n = len(coords)
149         if n > 0:
150             c = [gs.ctm.Apply(coords[i]) for i in range(n)]
151             sp = geom.Subpath()
152             sp.segments = [('L', c[i], c[i % n]) for i in range(n)]
153             sp.closed = True
154             path = geom.Path()
155             _SetPathAttributes(path, node, gs)
156             path.subpaths = [sp]
157             art.paths.append(path)
158
159
160 def _ProcessPath(node, art, gs):
161     """Process a 'polygon' SVG node, updating art.
162
163     Args:
164       node: xml.dom.Node - a 'polygon' node
165       arg: geom.Art
166       gs: _SState
167     Side effects:
168       Adds path for polygon to art
169     """
170
171     if not node.hasAttribute('d'):
172         return
173     s = node.getAttribute('d')
174     i = 0
175     n = len(s)
176     path = geom.Path()
177     _SetPathAttributes(path, node, gs)
178     initpt = (0.0, 0.0)
179     subpath = None
180     while i < len(s):
181         (i, subpath, initpt) = _ParseSubpath(s, i, initpt, gs)
182         if subpath:
183             if not subpath.Empty():
184                 path.AddSubpath(subpath)
185         else:
186             break
187     if path.subpaths:
188         art.paths.append(path)
189
190
191 def _ParseSubpath(s, i, initpt, gs):
192     """Parse a moveto-drawto-command-group starting at s[i] and return Subpath.
193
194     Args:
195       s: string - should be the 'd' attribute of a 'path' element
196       i: int - index in s to start parsing
197       initpt: (float, float) - coordinates of initial point
198       gs: _SState - used to transform coordinates
199     Returns:
200       (int, geom.Subpath, (float, float)) -
201           (index after subpath and subsequent whitespace,
202           the Subpath itself or Non if there was an error, final point)
203     """
204
205     subpath = geom.Subpath()
206     i = _SkipWS(s, i)
207     n = len(s)
208     if i >= n:
209         return (i, None, initpt)
210     if s[i] == 'M':
211         move_cmd = 'M'
212     elif s[i] == 'm':
213         move_cmd = 'm'
214     else:
215         return (i, None, initpt)
216     (i, cur) = _ParseCoordPair(s, _SkipWS(s, i + 1))
217     if not cur:
218         return (i, None, initpt)
219     prev_cmd = 'L'  # implicit cmd if coords follow directly
220     if move_cmd == 'm':
221         cur = geom.VecAdd(initpt, cur)
222         prev_cmd = 'l'
223     while True:
224         implicit_cmd = False
225         if i < n:
226             cmd = s[i]
227             if _PeekCoord(s, i):
228                 cmd = prev_cmd
229                 implicit_cmd = True
230         else:
231             cmd = None
232         if cmd == 'z' or cmd == 'Z' or cmd == None:
233             if cmd:
234                 i = _SkipWS(s, i + 1)
235                 subpath.closed = True
236             return (i, subpath, cur)
237         if not implicit_cmd:
238             i = _SkipWS(s, i + 1)
239         if cmd == 'l' or cmd == 'L':
240             (i, p1) = _ParseCoordPair(s, i)
241             if not p1:
242                 break
243             if cmd == 'l':
244                 p1 = geom.VecAdd(cur, p1)
245             subpath.AddSegment(_LineSeg(cur, p1, gs))
246             cur = p1
247         elif cmd == 'c' or cmd == 'C':
248             (i, p1, p2, p3) = _ParseThreeCoordPairs(s, i)
249             if not p1:
250                 break
251             if cmd == 'c':
252                 p1 = geom.VecAdd(cur, p1)
253                 p2 = geom.VecAdd(cur, p2)
254                 p3 = geom.VecAdd(cur, p3)
255             subpath.AddSegment(_Bezier3Seg(cur, p3, p1, p2, gs))
256             cur = p3
257         elif cmd == 'a' or cmd == 'A':
258             (i, p1, rad, rot, la, ccw) = _ParseArc(s, i)
259             if not p1:
260                 break
261             if cmd == 'a':
262                 p1 = geom.VecAdd(cur, p1)
263             subpath.AddSegment(_ArcSeg(cur, p1, rad, rot, la, ccw, gs))
264             cur = p1
265         elif cmd == 'h' or cmd == 'H':
266             (i, x) = _ParseCoord(s, i)
267             if x is None:
268                 break
269             if cmd == 'h':
270                 x += cur[0]
271             subpath.AddSegment(_LineSeg(cur, (x, cur[1]), gs))
272             cur = (x, cur[1])
273         elif cmd == 'v' or cmd == 'V':
274             (i, y) = _ParseCoord(s, i)
275             if y is None:
276                 break
277             if cmd == 'v':
278                 y += cur[1]
279             subpath.AddSegment(_LineSeg(cur, (cur[0], y), gs))
280             cur = (cur[0], y)
281         elif cmd == 's' or cmd == 'S':
282             (i, p2, p3) = _ParseTwoCoordPairs(s, i)
283             if not p2:
284                 break
285             if cmd == 's':
286                 p2 = geom.VecAdd(cur, p2)
287                 p3 = geom.VecAdd(cur, p3)
288             # p1 is reflection of cp2 of previous command
289             # through current point (but p1 is cur if no previous)
290             if len(subpath.segments) > 0 and subpath.segments[-1][0] == 'B':
291                 p4 = subpath.segments[-1][4]
292             else:
293                 p4 = cur
294             p1 = geom.VecAdd(cur, geom.VecSub(cur, p4))
295             subpath.AddSegment(_Bezier3Seg(cur, p3, p1, p2, gs))
296             cur = p3
297         else:
298             # TODO: quadratic beziers, 'q', and 't'
299             break
300         i = _SkipCommaSpace(s, i)
301         prev_cmd = cmd
302     return (i, None, cur)
303
304
305 def _ProcessRect(node, art, gs):
306     """Process a 'rect' SVG node, updating art.
307
308     Args:
309       node: xml.dom.Node - a 'polygon' node
310       arg: geom.Art
311       gs: _SState
312     Side effects:
313       Adds path for rectangle to art
314     """
315
316     if not (node.hasAttribute('width') and node.hasAttribute('height')):
317         return
318     w = _ParseLengthAttrOrDefault(node, 'width', gs, 0.0)
319     h = _ParseLengthAttrOrDefault(node, 'height', gs, 0.0)
320     if w <= 0.0 or h <= 0.0:
321         return
322     x = _ParseCoordAttrOrDefault(node, 'x', 0.0)
323     y = _ParseCoordAttrOrDefault(node, 'y', 0.0)
324     rx = _ParseLengthAttrOrDefault(node, 'rx', gs, 0.0)
325     ry = _ParseLengthAttrOrDefault(node, 'ry', gs, 0.0)
326     if rx == 0.0 and ry > 0.0:
327         rx = ry
328     elif rx > 0.0 and ry == 0.0:
329         ry = rx
330     if rx > w / 2.0:
331         rx = w / 2.0
332     if ry > h / 2.0:
333         ry = h / 2.0
334     subpath = geom.Subpath()
335     subpath.closed = True
336     if rx == 0.0 and ry == 0.0:
337         subpath.AddSegment(_LineSeg((x, y), (x + w, y), gs))
338         subpath.AddSegment(_LineSeg((x + w, y), (x + w, y + h), gs))
339         subpath.AddSegment(_LineSeg((x + w, y + h), (x, y + h), gs))
340         subpath.AddSegment(_LineSeg((x, y + h), (x, y), gs))
341     else:
342         wmid = w - 2 * rx
343         hmid = h - 2 * ry
344         # top line
345         if wmid > TOL:
346             subpath.AddSegment(_LineSeg((x + rx, y), (x + rx + wmid, y), gs))
347         # top right corner: remember, y positive downward, so this clockwise
348         subpath.AddSegment(_ArcSeg((x + rx + wmid, y), (x + w, y + ry),
349             (rx, ry), 0.0, False, False, gs))
350         # right line
351         if hmid > TOL:
352             subpath.AddSegment(_LineSeg((x + w, y + ry),
353                 (x + w, y + ry + hmid), gs))
354         # bottom right corner
355         subpath.AddSegment(_ArcSeg((x + w, y + ry + hmid),
356             (x + rx + wmid, y + h),
357             (rx, ry), 0.0, False, False, gs))
358         # bottom line
359         if wmid > TOL:
360             subpath.AddSegment(_LineSeg((x + rx + wmid, y + h),
361                 (x + rx, y + h), gs))
362         # bottom left corner
363         subpath.AddSegment(_ArcSeg((x + rx, y + h), (x, y + ry + hmid),
364             (rx, ry), 0.0, False, False, gs))
365         # left line
366         if hmid > TOL:
367             subpath.AddSegment(_LineSeg((x, y + ry + hmid), (x, y + ry), gs))
368         # top left corner
369         subpath.AddSegment(_ArcSeg((x, y + ry), (x + rx, y),
370             (rx, ry), 0.0, False, False, gs))
371     path = geom.Path()
372     _SetPathAttributes(path, node, gs)
373     path.subpaths = [subpath]
374     art.paths.append(path)
375
376
377 def _ProcessEllipse(node, art, gs):
378     """Process an 'ellipse' SVG node, updating art.
379
380     Args:
381       node: xml.dom.Node - a 'polygon' node
382       arg: geom.Art
383       gs: _SState
384     Side effects:
385       Adds path for ellipse to art
386     """
387
388     if not (node.hasAttribute('rx') and node.hasAttribute('ry')):
389         return
390     rx = _ParseLengthAttrOrDefault(node, 'rx', gs, 0.0)
391     ry = _ParseLengthAttrOrDefault(node, 'ry', gs, 0.0)
392     if rx < TOL or ry < TOL:
393         return
394     cx = _ParseCoordAttrOrDefault(node, 'cx', 0.0)
395     cy = _ParseCoordAttrOrDefault(node, 'cy', 0.0)
396     subpath = _FullEllipseSubpath(cx, cy, rx, ry, gs)
397     path = geom.Path()
398     path.subpaths = [subpath]
399     _SetPathAttributes(path, node, gs)
400     art.paths.append(path)
401
402
403 def _ProcessCircle(node, art, gs):
404     """Process a 'circle' SVG node, updating art.
405
406     Args:
407       node: xml.dom.Node - a 'polygon' node
408       arg: geom.Art
409       gs: _SState
410     Side effects:
411       Adds path for circle to art
412     """
413
414     if not node.hasAttribute('r'):
415         return
416     r = _ParseLengthAttrOrDefault(node, 'r', gs, 0.0)
417     if r < TOL:
418         return
419     cx = _ParseCoordAttrOrDefault(node, 'cx', 0.0)
420     cy = _ParseCoordAttrOrDefault(node, 'cy', 0.0)
421     subpath = _FullEllipseSubpath(cx, cy, r, r, gs)
422     path = geom.Path()
423     path.subpaths = [subpath]
424     _SetPathAttributes(path, node, gs)
425     art.paths.append(path)
426
427
428 def _FullEllipseSubpath(cx, cy, rx, ry, gs):
429     """Return a Subpath for a full ellipse.
430
431     Args:
432       cx: float - center x
433       cy: float - center y
434       rx: float - x radius
435       ry: float - y radius
436       gs: _SState - for transform
437     Returns:
438       geom.Subpath
439     """
440
441     # arc starts at 3 o'clock
442     # TODO: if gs has rotate transform, figure that out
443     # and use that as angle for arc x-rotation
444     subpath = geom.Subpath()
445     subpath.closed = True
446     subpath.AddSegment(_ArcSeg((cx + rx, cy), (cx, cy + ry),
447         (rx, ry), 0.0, False, False, gs))
448     subpath.AddSegment(_ArcSeg((cx, cy + ry), (cx - rx, cy),
449         (rx, ry), 0.0, False, False, gs))
450     subpath.AddSegment(_ArcSeg((cx - rx, cy), (cx, cy - ry),
451         (rx, ry), 0.0, False, False, gs))
452     subpath.AddSegment(_ArcSeg((cx, cy - ry), (cx + rx, cy),
453         (rx, ry), 0.0, False, False, gs))
454     return subpath
455
456
457 def _LineSeg(p1, p2, gs):
458     """Return an 'L' segment, transforming coordinates.
459
460     Args:
461       p1: (float, float) - start point
462       p2: (float, float) - end point
463       gs: _SState - used to transform coordinates
464     Returns:
465       tuple - an 'L' type geom.Subpath segment
466     """
467
468     return ('L', gs.ctm.Apply(p1), gs.ctm.Apply(p2))
469
470
471 def _Bezier3Seg(p1, p2, c1, c2, gs):
472     """Return a 'B' segment, transforming coordinates.
473
474     Args:
475       p1: (float, float) - start point
476       p2: (float, float) - end point
477       c1: (float, float) - first control point
478       c2: (float, float) - second control point
479       gs: _SState - used to transform coordinates
480     Returns:
481       tuple - an 'L' type geom.Subpath segment
482     """
483
484     return ('B', gs.ctm.Apply(p1), gs.ctm.Apply(p2),
485         gs.ctm.Apply(c1), gs.ctm.Apply(c2))
486
487
488 def _ArcSeg(p1, p2, rad, rot, la, ccw, gs):
489     """Return an 'A' segment, with attempt to transform.
490
491     Our A segments don't allow modeling the effect of
492     arbitrary transforms, but we can handle translation
493     and scaling.
494
495     Args:
496       p1: (float, float) - start point
497       p2: (float, float) - end point
498       rad: (float, float) - (x radius, y radius)
499       rot: float - x axis rotation, in degrees
500       la: bool - large arc if True
501       ccw: bool - counter-clockwise if True
502       gs: _SState - used to transform
503     Returns:
504       tuple - an 'A' type geom.Subpath segment
505     """
506
507     tp1 = gs.ctm.Apply(p1)
508     tp2 = gs.ctm.Apply(p2)
509     rx = rad[0] * gs.ctm.a
510     ry = rad[1] * gs.ctm.d
511     # if one of axes is mirrored, invert the ccw flag
512     if rx * ry < 0.0:
513         ccw = not ccw
514     trad = (abs(rx), abs(ry))
515     # TODO: abs(gs.ctm.a) != abs(ts.ctm.d), adjust xrot
516     return ('A', tp1, tp2, trad, rot, la, ccw)
517
518
519 def _SetPathAttributes(path, node, gs):
520     """Set the attributes related to filling/stroking in path.
521
522     Use attribute settings in node, if there, else those in the
523     current graphics state, gs.
524
525     Arguments:
526       path: geom.Path
527       node: xml.dom.Node
528       gs: _SState
529     Side effects:
530       May set filled, fillevenodd, stroked, fillpaint, strokepaint in path.
531     """
532
533     fill = gs.fill
534     stroke = gs.stroke
535     fillrule = gs.fillrule
536     if node.hasAttribute('style'):
537         style = _CSSInlineDict(node.getAttribute('style'))
538         if 'fill' in style:
539             fill = style['fill']
540         if 'stroke' in style:
541             stroke = style['stroke']
542         if 'fill-rule' in style:
543             fillrule = style['fill-rule']
544     if node.hasAttribute('fill'):
545         fill = node.getAttribute('fill')
546     if fill != 'none':
547         paint = _ParsePaint(fill)
548         if paint is not None:
549             path.fillpaint = paint
550             path.filled = True
551     if node.hasAttribute('stroke'):
552         stroke = node.getAttribute('stroke')
553     if stroke != 'none':
554         paint = _ParsePaint(stroke)
555         if stroke is not None:
556             path.strokepaint = paint
557             path.stroked = True
558     if node.hasAttribute('fill-rule'):
559         fillrule = node.getAttribute('fill-rule')
560     path.fillevenodd = (fillrule == 'evenodd')
561
562
563 # Some useful regular expressions
564 _re_float = re.compile(r"(\+|-)?(([0-9]+\.[0-9]*)|(\.[0-9]+)|([0-9]+))")
565 _re_int = re.compile(r"(\+|-)?[0-9]+")
566 _re_wsopt = re.compile(r"\s*")
567 _re_wscommaopt = re.compile(r"(\s*,\s*)|(\s*)")
568 _re_namevalue = re.compile(r"\s*(\S+)\s*:\s*(\S+)\s*(?:;|$)")
569
570
571 def _CSSInlineDict(s):
572     """Parse string s as CSS inline spec, and return a dictionary for it.
573
574     An inline CSS spec is semi-colon separated list of prop : value pairs,
575     such as: "fill:none;fill-rule : evenodd"
576
577     Args:
578       s: string - inline CSS spec
579     Returns:
580       dict : maps string (prop name) -> string (value)
581     """
582
583     pairs = _re_namevalue.findall(s)
584     return dict(pairs)
585
586
587 def _ParsePaint(s):
588     """Parse an SVG paint definition and return our version of Paint.
589
590     If is 'none', return None.
591     If fail to parse (e.g., a TODO syntax), return black_paint.
592
593     Args:
594       s: string - should contain an SVG paint spec
595     Returns:
596       geom.Paint or None
597     """
598
599     if len(s) == 0 or s == 'none':
600         return None
601     if s[0] == '#':
602         if len(s) == 7:
603             # 6 hex digits
604             return geom.Paint( \
605               int(s[1:3], 16) / 255.0,
606               int(s[3:5], 16) / 255.0,
607               int(s[5:7], 16) / 255.0)
608         elif len(s) == 4:
609             # 3 hex digits
610             return geom.Paint( \
611               int(s[1], 16) * 17 / 255.0,
612               int(s[2], 16) * 17 / 255.0,
613               int(s[3], 16) * 17 / 255.0)
614     else:
615         if s in geom.ColorDict:
616             return geom.ColorDict[s]
617     return geom.black_paint
618
619
620 def _ParseLengthAttrOrDefault(node, attr, gs, default):
621     """Parse the given attribute as a length, else return default.
622
623     Args:
624       node: xml.dom.Node
625       attr: string - the attribute name
626       gs: _SState - for dots-per-inch, for units conversion
627       default: float - to return if no attr or error parsing it
628     Returns:
629       float - the length
630     """
631
632     if not node.hasAttribute(attr):
633         return default
634     (_, v) = _ParseLength(node.getAttribute(attr), gs, 0)
635     if v is None:
636         return default
637     else:
638         return v
639
640
641 def _ParseCoordAttrOrDefault(node, attr, default):
642     """Parse the given attribute as a coordinate, else return default.
643
644     Args:
645       node: xml.dom.Node
646       attr: string - the attribute name
647       default: float - to return if no attr or error parsing it
648     Returns:
649       float - the coordinate
650     """
651
652     if not node.hasAttribute(attr):
653         return default
654     (_, v) = _ParseCoord(node.getAttribute(attr), 0)
655     if v is None:
656         return default
657     else:
658         return v
659
660
661 def _ParseCoord(s, i):
662     """Parse a coordinate (floating point number).
663
664     Args:
665       s: string
666       i: int - where to start parsing
667     Returns:
668       (int, float or None) - int is index after the coordinate
669         and subsequent white space
670     """
671
672     m = _re_float.match(s, i)
673     if m:
674         return (_SkipWS(s, m.end()), float(m.group()))
675     else:
676         return (i, None)
677
678
679 def _PeekCoord(s, i):
680     """Return True if s[i] starts a coordinate.
681
682     Args:
683       s: string
684       i: int - place in s to start looking
685     Returns:
686       bool - True if s[i] starts a coordinate, perhaps after comma / space
687     """
688
689     i = _SkipCommaSpace(s, i)
690     m = _re_float.match(s, i)
691     return True if m else False
692
693
694 def _ParseCoordPair(s, i):
695     """Parse pair of coordinates, with optional comma between.
696
697     Args:
698       s: string
699       i: int - where to start parsing
700     Returns:
701       (int, (float, float) or None) - int is index after the coordinate
702         and subsequent white space
703     """
704
705     (j, x) = _ParseCoord(s, i)
706     if x is not None:
707         j = _SkipCommaSpace(s, j)
708         (j, y) = _ParseCoord(s, j)
709         if y is not None:
710             return (_SkipWS(s, j), (x, y))
711     return (i, None)
712
713
714 def _ParseTwoCoordPairs(s, i):
715     """Parse two coordinate pairs, optionally separated by commas.
716
717     Args:
718       s: string
719       i: int - where to start parsing
720     Returns:
721       (int, (float, float) or None, (float, float) or None) -
722         int is index after the coordinate and subsequent white space
723     """
724
725     (j, pair1) = _ParseCoordPair(s, i)
726     if pair1:
727         j = _SkipCommaSpace(s, j)
728         (j, pair2) = _ParseCoordPair(s, j)
729         if pair2:
730             return (j, pair1, pair2)
731     return (i, None, None)
732
733
734 def _ParseThreeCoordPairs(s, i):
735     """Parse three coordinate pairs, optionally separated by commas.
736
737     Args:
738       s: string
739       i: int - where to start parsing
740     Returns:
741       (int, (float, float) or None, (float, float) or None,
742           (float, float) or None) -
743         int is index after the coordinateand subsequent white space
744     """
745
746     (j, pair1) = _ParseCoordPair(s, i)
747     if pair1:
748         j = _SkipCommaSpace(s, j)
749         (j, pair2) = _ParseCoordPair(s, j)
750         if pair2:
751             j = _SkipCommaSpace(s, j)
752             (j, pair3) = _ParseCoordPair(s, j)
753             if pair3:
754                 return (j, pair1, pair2, pair3)
755     return (i, None, None, None)
756
757
758 def _ParseCoordPairList(s):
759     """Parse a list of coordinate pairs.
760
761     The numbers should be separated by whitespace
762     or a comma with optional whitespace around it.
763
764     Args:
765       s: string - should contain coordinate pairs
766     Returns:
767       list of (float, float)
768     """
769
770     ans = []
771     i = _SkipWS(s, 0)
772     while i < len(s):
773         (i, pair) = _ParseCoordPair(s, i)
774         if not pair:
775             break
776         ans.append(pair)
777     return ans
778
779
780 # units to be scaled by 'dots-per-inch' with these factors
781 _UnitDict = {
782   'in': 1.0, 'mm': 0.0393700787,
783   'cm': 0.393700787, 'pt': 0.0138888889, 'pc': 0.166666667,
784   # assume 10pt font, 5pt font x-height
785   'em': 0.138888889, 'ex': 0.0138888889 * 5}
786
787
788 def _ParseLength(s, gs, i):
789     """Parse a length (floating point number, with possible units).
790
791     Args:
792       s: string
793       gs: _SState, for dpi if needed for units conversion
794       i: int - where to start parsing
795     Returns:
796       (int, float or None) - int is index after the coordinate
797         and subsequent white space; float is converted to user coords
798     """
799
800     (i, v) = _ParseCoord(s, i)
801     if v is None:
802         return (i, None)
803     upi = 1.0
804     if i < len(s):
805         if s[i] == '%':
806             # supposed to be percentage of nearest enclosing
807             # viewport in appropriate direction.
808             # for now, assume viewport is 10in in each dir
809             upi = dpi * 10.0 / 100.0
810         elif i < len(s) - 1:
811             cc = s[i:i + 2]
812             if cc == 'px':
813                 upi = 1.0
814                 i += 2
815             elif cc in _UnitDict:
816                 upi = gs.dpi * _UnitDict[cc]
817                 i += 2
818     return (i, v * upi)
819
820
821 def _ParseArc(s, i):
822     """Parse an elliptical arc specification.
823
824     Args:
825       s: string
826       i: int - where to start parsing
827     Returns:
828       (int, (float, float) or None, (float, float), float, bool, bool) -
829         int is index after spec and subsequent white space,
830         first (float, float) is end point of arc
831         second (float, float) is (x-radius, y-radius)
832         float is x-axis rotation, in degrees
833         first bool is True if larger arc is to be used
834         second bool is True if arc follows ccw direction
835     """
836
837     (j, rad) = _ParseCoordPair(s, i)
838     if rad:
839         j = _SkipCommaSpace(s, j)
840         (j, rot) = _ParseCoord(s, j)
841         if rot is not None:
842             j = _SkipCommaSpace(s, j)
843             (j, f) = _ParseCoord(s, j)  # should really just look for 0 or 1
844             if f is not None:
845                 laf = (f != 0.0)
846                 j = _SkipCommaSpace(s, j)
847                 (j, f) = _ParseCoord(s, j)
848                 if f is not None:
849                     ccw = (f != 0.0)
850                     j = _SkipCommaSpace(s, j)
851                     (j, pt) = _ParseCoordPair(s, j)
852                     if pt:
853                         return (j, pt, rad, rot, laf, ccw)
854     return (i, None, None, None, None, None)
855
856
857 def _SkipWS(s, i):
858     """Skip optional whitespace at s[i]... and return new i.
859
860     Args:
861       s: string
862       i: int - index into s
863     Returns:
864       int - index of first none-whitespace character from s[i], or len(s)
865     """
866
867     m = _re_wsopt.match(s, i)
868     if m:
869         return m.end()
870     else:
871         return i
872
873
874 def _SkipCommaSpace(s, i):
875     """Skip optional space with optional comma in it.
876
877     Args:
878       s: string
879       i: int - index into s
880     Returns:
881       int - index after optional space with optional comma
882     """
883
884     m = _re_wscommaopt.match(s, i)
885     if m:
886         return m.end()
887     else:
888         return i