1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 class SVGState
30 {
31 public:
32     //==============================================================================
SVGState(const XmlElement * topLevel,const File & svgFile={})33     explicit SVGState (const XmlElement* topLevel, const File& svgFile = {})
34        : originalFile (svgFile), topLevelXml (topLevel, nullptr)
35     {
36     }
37 
38     struct XmlPath
39     {
XmlPathjuce::SVGState::XmlPath40         XmlPath (const XmlElement* e, const XmlPath* p) noexcept : xml (e), parent (p)  {}
41 
operator *juce::SVGState::XmlPath42         const XmlElement& operator*() const noexcept            { jassert (xml != nullptr); return *xml; }
operator ->juce::SVGState::XmlPath43         const XmlElement* operator->() const noexcept           { return xml; }
getChildjuce::SVGState::XmlPath44         XmlPath getChild (const XmlElement* e) const noexcept   { return XmlPath (e, this); }
45 
46         template <typename OperationType>
applyOperationToChildWithIDjuce::SVGState::XmlPath47         bool applyOperationToChildWithID (const String& id, OperationType& op) const
48         {
49             forEachXmlChildElement (*xml, e)
50             {
51                 XmlPath child (e, this);
52 
53                 if (e->compareAttribute ("id", id)
54                       && ! child->hasTagName ("defs"))
55                     return op (child);
56 
57                 if (child.applyOperationToChildWithID (id, op))
58                     return true;
59             }
60 
61             return false;
62         }
63 
64         const XmlElement* xml;
65         const XmlPath* parent;
66     };
67 
68     //==============================================================================
69     struct UsePathOp
70     {
71         const SVGState* state;
72         Path* targetPath;
73 
operator ()juce::SVGState::UsePathOp74         bool operator() (const XmlPath& xmlPath) const
75         {
76             return state->parsePathElement (xmlPath, *targetPath);
77         }
78     };
79 
80     struct UseTextOp
81     {
82         const SVGState* state;
83         AffineTransform* transform;
84         Drawable* target;
85 
operator ()juce::SVGState::UseTextOp86         bool operator() (const XmlPath& xmlPath)
87         {
88             target = state->parseText (xmlPath, true, transform);
89             return target != nullptr;
90         }
91     };
92 
93     struct UseImageOp
94     {
95         const SVGState* state;
96         AffineTransform* transform;
97         Drawable* target;
98 
operator ()juce::SVGState::UseImageOp99         bool operator() (const XmlPath& xmlPath)
100         {
101             target = state->parseImage (xmlPath, true, transform);
102             return target != nullptr;
103         }
104     };
105 
106     struct GetClipPathOp
107     {
108         SVGState* state;
109         Drawable* target;
110 
operator ()juce::SVGState::GetClipPathOp111         bool operator() (const XmlPath& xmlPath)
112         {
113             return state->applyClipPath (*target, xmlPath);
114         }
115     };
116 
117     struct SetGradientStopsOp
118     {
119         const SVGState* state;
120         ColourGradient* gradient;
121 
operator ()juce::SVGState::SetGradientStopsOp122         bool operator() (const XmlPath& xml) const
123         {
124             return state->addGradientStopsIn (*gradient, xml);
125         }
126     };
127 
128     struct GetFillTypeOp
129     {
130         const SVGState* state;
131         const Path* path;
132         float opacity;
133         FillType fillType;
134 
operator ()juce::SVGState::GetFillTypeOp135         bool operator() (const XmlPath& xml)
136         {
137             if (xml->hasTagNameIgnoringNamespace ("linearGradient")
138                  || xml->hasTagNameIgnoringNamespace ("radialGradient"))
139             {
140                 fillType = state->getGradientFillType (xml, *path, opacity);
141                 return true;
142             }
143 
144             return false;
145         }
146     };
147 
148     //==============================================================================
parseSVGElement(const XmlPath & xml)149     Drawable* parseSVGElement (const XmlPath& xml)
150     {
151         auto drawable = new DrawableComposite();
152         setCommonAttributes (*drawable, xml);
153 
154         SVGState newState (*this);
155 
156         if (xml->hasAttribute ("transform"))
157             newState.addTransform (xml);
158 
159         newState.width  = getCoordLength (xml->getStringAttribute ("width",  String (newState.width)),  viewBoxW);
160         newState.height = getCoordLength (xml->getStringAttribute ("height", String (newState.height)), viewBoxH);
161 
162         if (newState.width  <= 0) newState.width  = 100;
163         if (newState.height <= 0) newState.height = 100;
164 
165         Point<float> viewboxXY;
166 
167         if (xml->hasAttribute ("viewBox"))
168         {
169             auto viewBoxAtt = xml->getStringAttribute ("viewBox");
170             auto viewParams = viewBoxAtt.getCharPointer();
171             Point<float> vwh;
172 
173             if (parseCoords (viewParams, viewboxXY, true)
174                  && parseCoords (viewParams, vwh, true)
175                  && vwh.x > 0
176                  && vwh.y > 0)
177             {
178                 newState.viewBoxW = vwh.x;
179                 newState.viewBoxH = vwh.y;
180 
181                 auto placementFlags = parsePlacementFlags (xml->getStringAttribute ("preserveAspectRatio").trim());
182 
183                 if (placementFlags != 0)
184                     newState.transform = RectanglePlacement (placementFlags)
185                                             .getTransformToFit (Rectangle<float> (viewboxXY.x, viewboxXY.y, vwh.x, vwh.y),
186                                                                 Rectangle<float> (newState.width, newState.height))
187                                             .followedBy (newState.transform);
188             }
189         }
190         else
191         {
192             if (viewBoxW == 0.0f)  newState.viewBoxW = newState.width;
193             if (viewBoxH == 0.0f)  newState.viewBoxH = newState.height;
194         }
195 
196         newState.parseSubElements (xml, *drawable);
197 
198         drawable->setContentArea ({ viewboxXY.x, viewboxXY.y, newState.viewBoxW, newState.viewBoxH });
199         drawable->resetBoundingBoxToContentArea();
200 
201         return drawable;
202     }
203 
204     //==============================================================================
parsePathString(Path & path,const String & pathString) const205     void parsePathString (Path& path, const String& pathString) const
206     {
207         auto d = pathString.getCharPointer().findEndOfWhitespace();
208 
209         Point<float> subpathStart, last, last2, p1, p2, p3;
210         juce_wchar currentCommand = 0, previousCommand = 0;
211         bool isRelative = true;
212         bool carryOn = true;
213 
214         while (! d.isEmpty())
215         {
216             if (CharPointer_ASCII ("MmLlHhVvCcSsQqTtAaZz").indexOf (*d) >= 0)
217             {
218                 currentCommand = d.getAndAdvance();
219                 isRelative = currentCommand >= 'a';
220             }
221 
222             switch (currentCommand)
223             {
224             case 'M':
225             case 'm':
226             case 'L':
227             case 'l':
228                 if (parseCoordsOrSkip (d, p1, false))
229                 {
230                     if (isRelative)
231                         p1 += last;
232 
233                     if (currentCommand == 'M' || currentCommand == 'm')
234                     {
235                         subpathStart = p1;
236                         path.startNewSubPath (p1);
237                         currentCommand = 'l';
238                     }
239                     else
240                         path.lineTo (p1);
241 
242                     last2 = last = p1;
243                 }
244                 break;
245 
246             case 'H':
247             case 'h':
248                 if (parseCoord (d, p1.x, false, true))
249                 {
250                     if (isRelative)
251                         p1.x += last.x;
252 
253                     path.lineTo (p1.x, last.y);
254 
255                     last2.x = last.x;
256                     last.x = p1.x;
257                 }
258                 else
259                 {
260                     ++d;
261                 }
262                 break;
263 
264             case 'V':
265             case 'v':
266                 if (parseCoord (d, p1.y, false, false))
267                 {
268                     if (isRelative)
269                         p1.y += last.y;
270 
271                     path.lineTo (last.x, p1.y);
272 
273                     last2.y = last.y;
274                     last.y = p1.y;
275                 }
276                 else
277                 {
278                     ++d;
279                 }
280                 break;
281 
282             case 'C':
283             case 'c':
284                 if (parseCoordsOrSkip (d, p1, false)
285                      && parseCoordsOrSkip (d, p2, false)
286                      && parseCoordsOrSkip (d, p3, false))
287                 {
288                     if (isRelative)
289                     {
290                         p1 += last;
291                         p2 += last;
292                         p3 += last;
293                     }
294 
295                     path.cubicTo (p1, p2, p3);
296 
297                     last2 = p2;
298                     last = p3;
299                 }
300                 break;
301 
302             case 'S':
303             case 's':
304                 if (parseCoordsOrSkip (d, p1, false)
305                      && parseCoordsOrSkip (d, p3, false))
306                 {
307                     if (isRelative)
308                     {
309                         p1 += last;
310                         p3 += last;
311                     }
312 
313                     p2 = last;
314 
315                     if (CharPointer_ASCII ("CcSs").indexOf (previousCommand) >= 0)
316                         p2 += (last - last2);
317 
318                     path.cubicTo (p2, p1, p3);
319 
320                     last2 = p1;
321                     last = p3;
322                 }
323                 break;
324 
325             case 'Q':
326             case 'q':
327                 if (parseCoordsOrSkip (d, p1, false)
328                      && parseCoordsOrSkip (d, p2, false))
329                 {
330                     if (isRelative)
331                     {
332                         p1 += last;
333                         p2 += last;
334                     }
335 
336                     path.quadraticTo (p1, p2);
337 
338                     last2 = p1;
339                     last = p2;
340                 }
341                 break;
342 
343             case 'T':
344             case 't':
345                 if (parseCoordsOrSkip (d, p1, false))
346                 {
347                     if (isRelative)
348                         p1 += last;
349 
350                     p2 = last;
351 
352                     if (CharPointer_ASCII ("QqTt").indexOf (previousCommand) >= 0)
353                         p2 += (last - last2);
354 
355                     path.quadraticTo (p2, p1);
356 
357                     last2 = p2;
358                     last = p1;
359                 }
360                 break;
361 
362             case 'A':
363             case 'a':
364                 if (parseCoordsOrSkip (d, p1, false))
365                 {
366                     String num;
367                     bool flagValue = false;
368 
369                     if (parseNextNumber (d, num, false))
370                     {
371                         auto angle = degreesToRadians (parseSafeFloat (num));
372 
373                         if (parseNextFlag (d, flagValue))
374                         {
375                             auto largeArc = flagValue;
376 
377                             if (parseNextFlag (d, flagValue))
378                             {
379                                 auto sweep = flagValue;
380 
381                                 if (parseCoordsOrSkip (d, p2, false))
382                                 {
383                                     if (isRelative)
384                                         p2 += last;
385 
386                                     if (last != p2)
387                                     {
388                                         double centreX, centreY, startAngle, deltaAngle;
389                                         double rx = p1.x, ry = p1.y;
390 
391                                         endpointToCentreParameters (last.x, last.y, p2.x, p2.y,
392                                                                     angle, largeArc, sweep,
393                                                                     rx, ry, centreX, centreY,
394                                                                     startAngle, deltaAngle);
395 
396                                         path.addCentredArc ((float) centreX, (float) centreY,
397                                                             (float) rx, (float) ry,
398                                                             angle, (float) startAngle, (float) (startAngle + deltaAngle),
399                                                             false);
400 
401                                         path.lineTo (p2);
402                                     }
403 
404                                     last2 = last;
405                                     last = p2;
406                                 }
407                             }
408                         }
409                     }
410                 }
411 
412                 break;
413 
414             case 'Z':
415             case 'z':
416                 path.closeSubPath();
417                 last = last2 = subpathStart;
418                 d = d.findEndOfWhitespace();
419                 currentCommand = 'M';
420                 break;
421 
422             default:
423                 carryOn = false;
424                 break;
425             }
426 
427             if (! carryOn)
428                 break;
429 
430             previousCommand = currentCommand;
431         }
432 
433         // paths that finish back at their start position often seem to be
434         // left without a 'z', so need to be closed explicitly..
435         if (path.getCurrentPosition() == subpathStart)
436             path.closeSubPath();
437     }
438 
439 private:
440     //==============================================================================
441     const File originalFile;
442     const XmlPath topLevelXml;
443     float width = 512, height = 512, viewBoxW = 0, viewBoxH = 0;
444     AffineTransform transform;
445     String cssStyleText;
446 
isNone(const String & s)447     static bool isNone (const String& s) noexcept
448     {
449         return s.equalsIgnoreCase ("none");
450     }
451 
setCommonAttributes(Drawable & d,const XmlPath & xml)452     static void setCommonAttributes (Drawable& d, const XmlPath& xml)
453     {
454         auto compID = xml->getStringAttribute ("id");
455         d.setName (compID);
456         d.setComponentID (compID);
457 
458         if (isNone (xml->getStringAttribute ("display")))
459             d.setVisible (false);
460     }
461 
462     //==============================================================================
parseSubElements(const XmlPath & xml,DrawableComposite & parentDrawable,bool shouldParseClip=true)463     void parseSubElements (const XmlPath& xml, DrawableComposite& parentDrawable, bool shouldParseClip = true)
464     {
465         forEachXmlChildElement (*xml, e)
466         {
467             const XmlPath child (xml.getChild (e));
468 
469             if (auto* drawable = parseSubElement (child))
470             {
471                 parentDrawable.addChildComponent (drawable);
472 
473                 if (! isNone (getStyleAttribute (child, "display")))
474                     drawable->setVisible (true);
475 
476                 if (shouldParseClip)
477                     parseClipPath (child, *drawable);
478             }
479         }
480     }
481 
parseSubElement(const XmlPath & xml)482     Drawable* parseSubElement (const XmlPath& xml)
483     {
484         {
485             Path path;
486             if (parsePathElement (xml, path))
487                 return parseShape (xml, path);
488         }
489 
490         auto tag = xml->getTagNameWithoutNamespace();
491 
492         if (tag == "g")         return parseGroupElement (xml, true);
493         if (tag == "svg")       return parseSVGElement (xml);
494         if (tag == "text")      return parseText (xml, true);
495         if (tag == "image")     return parseImage (xml, true);
496         if (tag == "switch")    return parseSwitch (xml);
497         if (tag == "a")         return parseLinkElement (xml);
498         if (tag == "use")       return parseUseOther (xml);
499         if (tag == "style")     parseCSSStyle (xml);
500         if (tag == "defs")      parseDefs (xml);
501 
502         return nullptr;
503     }
504 
parsePathElement(const XmlPath & xml,Path & path) const505     bool parsePathElement (const XmlPath& xml, Path& path) const
506     {
507         auto tag = xml->getTagNameWithoutNamespace();
508 
509         if (tag == "path")      { parsePath (xml, path);           return true; }
510         if (tag == "rect")      { parseRect (xml, path);           return true; }
511         if (tag == "circle")    { parseCircle (xml, path);         return true; }
512         if (tag == "ellipse")   { parseEllipse (xml, path);        return true; }
513         if (tag == "line")      { parseLine (xml, path);           return true; }
514         if (tag == "polyline")  { parsePolygon (xml, true, path);  return true; }
515         if (tag == "polygon")   { parsePolygon (xml, false, path); return true; }
516         if (tag == "use")       { return parseUsePath (xml, path); }
517 
518         return false;
519     }
520 
parseSwitch(const XmlPath & xml)521     DrawableComposite* parseSwitch (const XmlPath& xml)
522     {
523         if (auto* group = xml->getChildByName ("g"))
524             return parseGroupElement (xml.getChild (group), true);
525 
526         return nullptr;
527     }
528 
parseGroupElement(const XmlPath & xml,bool shouldParseTransform)529     DrawableComposite* parseGroupElement (const XmlPath& xml, bool shouldParseTransform)
530     {
531         if (shouldParseTransform && xml->hasAttribute ("transform"))
532         {
533             SVGState newState (*this);
534             newState.addTransform (xml);
535 
536             return newState.parseGroupElement (xml, false);
537         }
538 
539         auto* drawable = new DrawableComposite();
540         setCommonAttributes (*drawable, xml);
541         parseSubElements (xml, *drawable);
542 
543         drawable->resetContentAreaAndBoundingBoxToFitChildren();
544         return drawable;
545     }
546 
parseLinkElement(const XmlPath & xml)547     DrawableComposite* parseLinkElement (const XmlPath& xml)
548     {
549         return parseGroupElement (xml, true); // TODO: support for making this clickable
550     }
551 
552     //==============================================================================
parsePath(const XmlPath & xml,Path & path) const553     void parsePath (const XmlPath& xml, Path& path) const
554     {
555         parsePathString (path, xml->getStringAttribute ("d"));
556 
557         if (getStyleAttribute (xml, "fill-rule").trim().equalsIgnoreCase ("evenodd"))
558             path.setUsingNonZeroWinding (false);
559     }
560 
parseRect(const XmlPath & xml,Path & rect) const561     void parseRect (const XmlPath& xml, Path& rect) const
562     {
563         const bool hasRX = xml->hasAttribute ("rx");
564         const bool hasRY = xml->hasAttribute ("ry");
565 
566         if (hasRX || hasRY)
567         {
568             float rx = getCoordLength (xml, "rx", viewBoxW);
569             float ry = getCoordLength (xml, "ry", viewBoxH);
570 
571             if (! hasRX)
572                 rx = ry;
573             else if (! hasRY)
574                 ry = rx;
575 
576             rect.addRoundedRectangle (getCoordLength (xml, "x", viewBoxW),
577                                       getCoordLength (xml, "y", viewBoxH),
578                                       getCoordLength (xml, "width", viewBoxW),
579                                       getCoordLength (xml, "height", viewBoxH),
580                                       rx, ry);
581         }
582         else
583         {
584             rect.addRectangle (getCoordLength (xml, "x", viewBoxW),
585                                getCoordLength (xml, "y", viewBoxH),
586                                getCoordLength (xml, "width", viewBoxW),
587                                getCoordLength (xml, "height", viewBoxH));
588         }
589     }
590 
parseCircle(const XmlPath & xml,Path & circle) const591     void parseCircle (const XmlPath& xml, Path& circle) const
592     {
593         auto cx = getCoordLength (xml, "cx", viewBoxW);
594         auto cy = getCoordLength (xml, "cy", viewBoxH);
595         auto radius = getCoordLength (xml, "r", viewBoxW);
596 
597         circle.addEllipse (cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
598     }
599 
parseEllipse(const XmlPath & xml,Path & ellipse) const600     void parseEllipse (const XmlPath& xml, Path& ellipse) const
601     {
602         auto cx      = getCoordLength (xml, "cx", viewBoxW);
603         auto cy      = getCoordLength (xml, "cy", viewBoxH);
604         auto radiusX = getCoordLength (xml, "rx", viewBoxW);
605         auto radiusY = getCoordLength (xml, "ry", viewBoxH);
606 
607         ellipse.addEllipse (cx - radiusX, cy - radiusY, radiusX * 2.0f, radiusY * 2.0f);
608     }
609 
parseLine(const XmlPath & xml,Path & line) const610     void parseLine (const XmlPath& xml, Path& line) const
611     {
612         auto x1 = getCoordLength (xml, "x1", viewBoxW);
613         auto y1 = getCoordLength (xml, "y1", viewBoxH);
614         auto x2 = getCoordLength (xml, "x2", viewBoxW);
615         auto y2 = getCoordLength (xml, "y2", viewBoxH);
616 
617         line.startNewSubPath (x1, y1);
618         line.lineTo (x2, y2);
619     }
620 
parsePolygon(const XmlPath & xml,bool isPolyline,Path & path) const621     void parsePolygon (const XmlPath& xml, bool isPolyline, Path& path) const
622     {
623         auto pointsAtt = xml->getStringAttribute ("points");
624         auto points = pointsAtt.getCharPointer();
625         Point<float> p;
626 
627         if (parseCoords (points, p, true))
628         {
629             Point<float> first (p), last;
630 
631             path.startNewSubPath (first);
632 
633             while (parseCoords (points, p, true))
634             {
635                 last = p;
636                 path.lineTo (p);
637             }
638 
639             if ((! isPolyline) || first == last)
640                 path.closeSubPath();
641         }
642     }
643 
getLinkedID(const XmlPath & xml)644     static String getLinkedID (const XmlPath& xml)
645     {
646         auto link = xml->getStringAttribute ("xlink:href");
647 
648         if (link.startsWithChar ('#'))
649             return link.substring (1);
650 
651         return {};
652     }
653 
parseUsePath(const XmlPath & xml,Path & path) const654     bool parseUsePath (const XmlPath& xml, Path& path) const
655     {
656         auto linkedID = getLinkedID (xml);
657 
658         if (linkedID.isNotEmpty())
659         {
660             UsePathOp op = { this, &path };
661             return topLevelXml.applyOperationToChildWithID (linkedID, op);
662         }
663 
664         return false;
665     }
666 
parseUseOther(const XmlPath & xml) const667     Drawable* parseUseOther (const XmlPath& xml) const
668     {
669         if (auto* drawableText  = parseText (xml, false))    return drawableText;
670         if (auto* drawableImage = parseImage (xml, false))   return drawableImage;
671 
672         return nullptr;
673     }
674 
parseURL(const String & str)675     static String parseURL (const String& str)
676     {
677         if (str.startsWithIgnoreCase ("url"))
678             return str.fromFirstOccurrenceOf ("#", false, false)
679                       .upToLastOccurrenceOf (")", false, false).trim();
680 
681         return {};
682     }
683 
684     //==============================================================================
parseShape(const XmlPath & xml,Path & path,bool shouldParseTransform=true,AffineTransform * additonalTransform=nullptr) const685     Drawable* parseShape (const XmlPath& xml, Path& path,
686                           bool shouldParseTransform = true,
687                           AffineTransform* additonalTransform = nullptr) const
688     {
689         if (shouldParseTransform && xml->hasAttribute ("transform"))
690         {
691             SVGState newState (*this);
692             newState.addTransform (xml);
693 
694             return newState.parseShape (xml, path, false, additonalTransform);
695         }
696 
697         auto dp = new DrawablePath();
698         setCommonAttributes (*dp, xml);
699         dp->setFill (Colours::transparentBlack);
700 
701         path.applyTransform (transform);
702 
703         if (additonalTransform != nullptr)
704             path.applyTransform (*additonalTransform);
705 
706         dp->setPath (path);
707 
708         dp->setFill (getPathFillType (path, xml, "fill",
709                                       getStyleAttribute (xml, "fill-opacity"),
710                                       getStyleAttribute (xml, "opacity"),
711                                       pathContainsClosedSubPath (path) ? Colours::black
712                                                                        : Colours::transparentBlack));
713 
714         auto strokeType = getStyleAttribute (xml, "stroke");
715 
716         if (strokeType.isNotEmpty() && ! isNone (strokeType))
717         {
718             dp->setStrokeFill (getPathFillType (path, xml, "stroke",
719                                                 getStyleAttribute (xml, "stroke-opacity"),
720                                                 getStyleAttribute (xml, "opacity"),
721                                                 Colours::transparentBlack));
722 
723             dp->setStrokeType (getStrokeFor (xml));
724         }
725 
726         auto strokeDashArray = getStyleAttribute (xml, "stroke-dasharray");
727 
728         if (strokeDashArray.isNotEmpty())
729             parseDashArray (strokeDashArray, *dp);
730 
731         return dp;
732     }
733 
pathContainsClosedSubPath(const Path & path)734     static bool pathContainsClosedSubPath (const Path& path) noexcept
735     {
736         for (Path::Iterator iter (path); iter.next();)
737             if (iter.elementType == Path::Iterator::closePath)
738                 return true;
739 
740         return false;
741     }
742 
parseDashArray(const String & dashList,DrawablePath & dp) const743     void parseDashArray (const String& dashList, DrawablePath& dp) const
744     {
745         if (dashList.equalsIgnoreCase ("null") || isNone (dashList))
746             return;
747 
748         Array<float> dashLengths;
749 
750         for (auto t = dashList.getCharPointer();;)
751         {
752             float value;
753             if (! parseCoord (t, value, true, true))
754                 break;
755 
756             dashLengths.add (value);
757 
758             t = t.findEndOfWhitespace();
759 
760             if (*t == ',')
761                 ++t;
762         }
763 
764         if (dashLengths.size() > 0)
765         {
766             auto* dashes = dashLengths.getRawDataPointer();
767 
768             for (int i = 0; i < dashLengths.size(); ++i)
769             {
770                 if (dashes[i] <= 0)  // SVG uses zero-length dashes to mean a dotted line
771                 {
772                     if (dashLengths.size() == 1)
773                         return;
774 
775                     const float nonZeroLength = 0.001f;
776                     dashes[i] = nonZeroLength;
777 
778                     const int pairedIndex = i ^ 1;
779 
780                     if (isPositiveAndBelow (pairedIndex, dashLengths.size())
781                           && dashes[pairedIndex] > nonZeroLength)
782                         dashes[pairedIndex] -= nonZeroLength;
783                 }
784             }
785 
786             dp.setDashLengths (dashLengths);
787         }
788     }
789 
parseClipPath(const XmlPath & xml,Drawable & d)790     bool parseClipPath (const XmlPath& xml, Drawable& d)
791     {
792         const String clipPath (getStyleAttribute (xml, "clip-path"));
793 
794         if (clipPath.isNotEmpty())
795         {
796             auto urlID = parseURL (clipPath);
797 
798             if (urlID.isNotEmpty())
799             {
800                 GetClipPathOp op = { this, &d };
801                 return topLevelXml.applyOperationToChildWithID (urlID, op);
802             }
803         }
804 
805         return false;
806     }
807 
applyClipPath(Drawable & target,const XmlPath & xmlPath)808     bool applyClipPath (Drawable& target, const XmlPath& xmlPath)
809     {
810         if (xmlPath->hasTagNameIgnoringNamespace ("clipPath"))
811         {
812             std::unique_ptr<DrawableComposite> drawableClipPath (new DrawableComposite());
813 
814             parseSubElements (xmlPath, *drawableClipPath, false);
815 
816             if (drawableClipPath->getNumChildComponents() > 0)
817             {
818                 setCommonAttributes (*drawableClipPath, xmlPath);
819                 target.setClipPath (std::move (drawableClipPath));
820                 return true;
821             }
822         }
823 
824         return false;
825     }
826 
addGradientStopsIn(ColourGradient & cg,const XmlPath & fillXml) const827     bool addGradientStopsIn (ColourGradient& cg, const XmlPath& fillXml) const
828     {
829         bool result = false;
830 
831         if (fillXml.xml != nullptr)
832         {
833             forEachXmlChildElementWithTagName (*fillXml, e, "stop")
834             {
835                 auto col = parseColour (fillXml.getChild (e), "stop-color", Colours::black);
836 
837                 auto opacity = getStyleAttribute (fillXml.getChild (e), "stop-opacity", "1");
838                 col = col.withMultipliedAlpha (jlimit (0.0f, 1.0f, parseSafeFloat (opacity)));
839 
840                 auto offset = parseSafeFloat (e->getStringAttribute ("offset"));
841 
842                 if (e->getStringAttribute ("offset").containsChar ('%'))
843                     offset *= 0.01f;
844 
845                 cg.addColour (jlimit (0.0f, 1.0f, offset), col);
846                 result = true;
847             }
848         }
849 
850         return result;
851     }
852 
getGradientFillType(const XmlPath & fillXml,const Path & path,const float opacity) const853     FillType getGradientFillType (const XmlPath& fillXml,
854                                   const Path& path,
855                                   const float opacity) const
856     {
857         ColourGradient gradient;
858 
859         {
860             auto linkedID = getLinkedID (fillXml);
861 
862             if (linkedID.isNotEmpty())
863             {
864                 SetGradientStopsOp op = { this, &gradient, };
865                 topLevelXml.applyOperationToChildWithID (linkedID, op);
866             }
867         }
868 
869         addGradientStopsIn (gradient, fillXml);
870 
871         if (int numColours = gradient.getNumColours())
872         {
873             if (gradient.getColourPosition (0) > 0)
874                 gradient.addColour (0.0, gradient.getColour (0));
875 
876             if (gradient.getColourPosition (numColours - 1) < 1.0)
877                 gradient.addColour (1.0, gradient.getColour (numColours - 1));
878         }
879         else
880         {
881             gradient.addColour (0.0, Colours::black);
882             gradient.addColour (1.0, Colours::black);
883         }
884 
885         if (opacity < 1.0f)
886             gradient.multiplyOpacity (opacity);
887 
888         jassert (gradient.getNumColours() > 0);
889 
890         gradient.isRadial = fillXml->hasTagNameIgnoringNamespace ("radialGradient");
891 
892         float gradientWidth = viewBoxW;
893         float gradientHeight = viewBoxH;
894         float dx = 0.0f;
895         float dy = 0.0f;
896 
897         const bool userSpace = fillXml->getStringAttribute ("gradientUnits").equalsIgnoreCase ("userSpaceOnUse");
898 
899         if (! userSpace)
900         {
901             auto bounds = path.getBounds();
902             dx = bounds.getX();
903             dy = bounds.getY();
904             gradientWidth = bounds.getWidth();
905             gradientHeight = bounds.getHeight();
906         }
907 
908         if (gradient.isRadial)
909         {
910             if (userSpace)
911                 gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute ("cx", "50%"), gradientWidth),
912                                        dy + getCoordLength (fillXml->getStringAttribute ("cy", "50%"), gradientHeight));
913             else
914                 gradient.point1.setXY (dx + gradientWidth  * getCoordLength (fillXml->getStringAttribute ("cx", "50%"), 1.0f),
915                                        dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("cy", "50%"), 1.0f));
916 
917             auto radius = getCoordLength (fillXml->getStringAttribute ("r", "50%"), gradientWidth);
918             gradient.point2 = gradient.point1 + Point<float> (radius, 0.0f);
919 
920             //xxx (the fx, fy focal point isn't handled properly here..)
921         }
922         else
923         {
924             if (userSpace)
925             {
926                 gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute ("x1", "0%"), gradientWidth),
927                                        dy + getCoordLength (fillXml->getStringAttribute ("y1", "0%"), gradientHeight));
928 
929                 gradient.point2.setXY (dx + getCoordLength (fillXml->getStringAttribute ("x2", "100%"), gradientWidth),
930                                        dy + getCoordLength (fillXml->getStringAttribute ("y2", "0%"), gradientHeight));
931             }
932             else
933             {
934                 gradient.point1.setXY (dx + gradientWidth  * getCoordLength (fillXml->getStringAttribute ("x1", "0%"), 1.0f),
935                                        dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("y1", "0%"), 1.0f));
936 
937                 gradient.point2.setXY (dx + gradientWidth  * getCoordLength (fillXml->getStringAttribute ("x2", "100%"), 1.0f),
938                                        dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("y2", "0%"), 1.0f));
939             }
940 
941             if (gradient.point1 == gradient.point2)
942                 return Colour (gradient.getColour (gradient.getNumColours() - 1));
943         }
944 
945         FillType type (gradient);
946 
947         auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform"));
948 
949         if (gradient.isRadial)
950         {
951             type.transform = gradientTransform;
952         }
953         else
954         {
955             // Transform the perpendicular vector into the new coordinate space for the gradient.
956             // This vector is now the slope of the linear gradient as it should appear in the new coord space
957             auto perpendicular = Point<float> (gradient.point2.y - gradient.point1.y,
958                                                gradient.point1.x - gradient.point2.x)
959                                     .transformedBy (gradientTransform.withAbsoluteTranslation (0, 0));
960 
961             auto newGradPoint1 = gradient.point1.transformedBy (gradientTransform);
962             auto newGradPoint2 = gradient.point2.transformedBy (gradientTransform);
963 
964             // Project the transformed gradient vector onto the transformed slope of the linear
965             // gradient as it should appear in the new coordinate space
966             const float scale = perpendicular.getDotProduct (newGradPoint2 - newGradPoint1)
967                                   / perpendicular.getDotProduct (perpendicular);
968 
969             type.gradient->point1 = newGradPoint1;
970             type.gradient->point2 = newGradPoint2 - perpendicular * scale;
971         }
972 
973         return type;
974     }
975 
getPathFillType(const Path & path,const XmlPath & xml,StringRef fillAttribute,const String & fillOpacity,const String & overallOpacity,const Colour defaultColour) const976     FillType getPathFillType (const Path& path,
977                               const XmlPath& xml,
978                               StringRef fillAttribute,
979                               const String& fillOpacity,
980                               const String& overallOpacity,
981                               const Colour defaultColour) const
982     {
983         float opacity = 1.0f;
984 
985         if (overallOpacity.isNotEmpty())
986             opacity = jlimit (0.0f, 1.0f, parseSafeFloat (overallOpacity));
987 
988         if (fillOpacity.isNotEmpty())
989             opacity *= jlimit (0.0f, 1.0f, parseSafeFloat (fillOpacity));
990 
991         String fill (getStyleAttribute (xml, fillAttribute));
992         String urlID = parseURL (fill);
993 
994         if (urlID.isNotEmpty())
995         {
996             GetFillTypeOp op = { this, &path, opacity, FillType() };
997 
998             if (topLevelXml.applyOperationToChildWithID (urlID, op))
999                 return op.fillType;
1000         }
1001 
1002         if (isNone (fill))
1003             return Colours::transparentBlack;
1004 
1005         return parseColour (xml, fillAttribute, defaultColour).withMultipliedAlpha (opacity);
1006     }
1007 
getJointStyle(const String & join)1008     static PathStrokeType::JointStyle getJointStyle (const String& join) noexcept
1009     {
1010         if (join.equalsIgnoreCase ("round"))  return PathStrokeType::curved;
1011         if (join.equalsIgnoreCase ("bevel"))  return PathStrokeType::beveled;
1012 
1013         return PathStrokeType::mitered;
1014     }
1015 
getEndCapStyle(const String & cap)1016     static PathStrokeType::EndCapStyle getEndCapStyle (const String& cap) noexcept
1017     {
1018         if (cap.equalsIgnoreCase ("round"))   return PathStrokeType::rounded;
1019         if (cap.equalsIgnoreCase ("square"))  return PathStrokeType::square;
1020 
1021         return PathStrokeType::butt;
1022     }
1023 
getStrokeWidth(const String & strokeWidth) const1024     float getStrokeWidth (const String& strokeWidth) const noexcept
1025     {
1026         auto transformScale = std::sqrt (std::abs (transform.getDeterminant()));
1027         return transformScale * getCoordLength (strokeWidth, viewBoxW);
1028     }
1029 
getStrokeFor(const XmlPath & xml) const1030     PathStrokeType getStrokeFor (const XmlPath& xml) const
1031     {
1032         return PathStrokeType (getStrokeWidth (getStyleAttribute (xml, "stroke-width", "1")),
1033                                getJointStyle  (getStyleAttribute (xml, "stroke-linejoin")),
1034                                getEndCapStyle (getStyleAttribute (xml, "stroke-linecap")));
1035     }
1036 
1037     //==============================================================================
useText(const XmlPath & xml) const1038     Drawable* useText (const XmlPath& xml) const
1039     {
1040         auto translation = AffineTransform::translation (parseSafeFloat (xml->getStringAttribute ("x")),
1041                                                          parseSafeFloat (xml->getStringAttribute ("y")));
1042 
1043         UseTextOp op = { this, &translation, nullptr };
1044 
1045         auto linkedID = getLinkedID (xml);
1046 
1047         if (linkedID.isNotEmpty())
1048             topLevelXml.applyOperationToChildWithID (linkedID, op);
1049 
1050         return op.target;
1051     }
1052 
parseText(const XmlPath & xml,bool shouldParseTransform,AffineTransform * additonalTransform=nullptr) const1053     Drawable* parseText (const XmlPath& xml, bool shouldParseTransform,
1054                          AffineTransform* additonalTransform = nullptr) const
1055     {
1056         if (shouldParseTransform && xml->hasAttribute ("transform"))
1057         {
1058             SVGState newState (*this);
1059             newState.addTransform (xml);
1060 
1061             return newState.parseText (xml, false, additonalTransform);
1062         }
1063 
1064         if (xml->hasTagName ("use"))
1065             return useText (xml);
1066 
1067         if (! xml->hasTagName ("text") && ! xml->hasTagNameIgnoringNamespace ("tspan"))
1068             return nullptr;
1069 
1070         Array<float> xCoords, yCoords, dxCoords, dyCoords;
1071 
1072         getCoordList (xCoords,  getInheritedAttribute (xml, "x"),  true, true);
1073         getCoordList (yCoords,  getInheritedAttribute (xml, "y"),  true, false);
1074         getCoordList (dxCoords, getInheritedAttribute (xml, "dx"), true, true);
1075         getCoordList (dyCoords, getInheritedAttribute (xml, "dy"), true, false);
1076 
1077         auto font = getFont (xml);
1078         auto anchorStr = getStyleAttribute (xml, "text-anchor");
1079 
1080         auto dc = new DrawableComposite();
1081         setCommonAttributes (*dc, xml);
1082 
1083         forEachXmlChildElement (*xml, e)
1084         {
1085             if (e->isTextElement())
1086             {
1087                 auto text = e->getText().trim();
1088 
1089                 auto dt = new DrawableText();
1090                 dc->addAndMakeVisible (dt);
1091 
1092                 dt->setText (text);
1093                 dt->setFont (font, true);
1094 
1095                 if (additonalTransform != nullptr)
1096                     dt->setTransform (transform.followedBy (*additonalTransform));
1097                 else
1098                     dt->setTransform (transform);
1099 
1100                 dt->setColour (parseColour (xml, "fill", Colours::black)
1101                                  .withMultipliedAlpha (parseSafeFloat (getStyleAttribute (xml, "fill-opacity", "1"))));
1102 
1103                 Rectangle<float> bounds (xCoords[0], yCoords[0] - font.getAscent(),
1104                                          font.getStringWidthFloat (text), font.getHeight());
1105 
1106                 if (anchorStr == "middle")   bounds.setX (bounds.getX() - bounds.getWidth() / 2.0f);
1107                 else if (anchorStr == "end") bounds.setX (bounds.getX() - bounds.getWidth());
1108 
1109                 dt->setBoundingBox (bounds);
1110             }
1111             else if (e->hasTagNameIgnoringNamespace ("tspan"))
1112             {
1113                 dc->addAndMakeVisible (parseText (xml.getChild (e), true));
1114             }
1115         }
1116 
1117         return dc;
1118     }
1119 
getFont(const XmlPath & xml) const1120     Font getFont (const XmlPath& xml) const
1121     {
1122         Font f;
1123         auto family = getStyleAttribute (xml, "font-family").unquoted();
1124 
1125         if (family.isNotEmpty())
1126             f.setTypefaceName (family);
1127 
1128         if (getStyleAttribute (xml, "font-style").containsIgnoreCase ("italic"))
1129             f.setItalic (true);
1130 
1131         if (getStyleAttribute (xml, "font-weight").containsIgnoreCase ("bold"))
1132             f.setBold (true);
1133 
1134         return f.withPointHeight (getCoordLength (getStyleAttribute (xml, "font-size", "15"), 1.0f));
1135     }
1136 
1137     //==============================================================================
useImage(const XmlPath & xml) const1138     Drawable* useImage (const XmlPath& xml) const
1139     {
1140         auto translation = AffineTransform::translation (parseSafeFloat (xml->getStringAttribute ("x")),
1141                                                          parseSafeFloat (xml->getStringAttribute ("y")));
1142 
1143         UseImageOp op = { this, &translation, nullptr };
1144 
1145         auto linkedID = getLinkedID (xml);
1146 
1147         if (linkedID.isNotEmpty())
1148             topLevelXml.applyOperationToChildWithID (linkedID, op);
1149 
1150         return op.target;
1151     }
1152 
parseImage(const XmlPath & xml,bool shouldParseTransform,AffineTransform * additionalTransform=nullptr) const1153     Drawable* parseImage (const XmlPath& xml, bool shouldParseTransform,
1154                           AffineTransform* additionalTransform = nullptr) const
1155     {
1156         if (shouldParseTransform && xml->hasAttribute ("transform"))
1157         {
1158             SVGState newState (*this);
1159             newState.addTransform (xml);
1160 
1161             return newState.parseImage (xml, false, additionalTransform);
1162         }
1163 
1164         if (xml->hasTagName ("use"))
1165             return useImage (xml);
1166 
1167         if (! xml->hasTagName ("image"))
1168             return nullptr;
1169 
1170         auto link = xml->getStringAttribute ("xlink:href");
1171 
1172         std::unique_ptr<InputStream> inputStream;
1173         MemoryOutputStream imageStream;
1174 
1175         if (link.startsWith ("data:"))
1176         {
1177             const auto indexOfComma = link.indexOf (",");
1178             auto format = link.substring (5, indexOfComma).trim();
1179             auto indexOfSemi = format.indexOf (";");
1180 
1181             if (format.substring (indexOfSemi + 1).trim().equalsIgnoreCase ("base64"))
1182             {
1183                 auto mime = format.substring (0, indexOfSemi).trim();
1184 
1185                 if (mime.equalsIgnoreCase ("image/png") || mime.equalsIgnoreCase ("image/jpeg"))
1186                 {
1187                     auto base64text = link.substring (indexOfComma + 1).removeCharacters ("\t\n\r ");
1188 
1189                     if (Base64::convertFromBase64 (imageStream, base64text))
1190                         inputStream.reset (new MemoryInputStream (imageStream.getData(), imageStream.getDataSize(), false));
1191                 }
1192             }
1193         }
1194         else
1195         {
1196             auto linkedFile = originalFile.getParentDirectory().getChildFile (link);
1197 
1198             if (linkedFile.existsAsFile())
1199                 inputStream = linkedFile.createInputStream();
1200         }
1201 
1202         if (inputStream != nullptr)
1203         {
1204             auto image = ImageFileFormat::loadFrom (*inputStream);
1205 
1206             if (image.isValid())
1207             {
1208                 auto* di = new DrawableImage();
1209 
1210                 setCommonAttributes (*di, xml);
1211 
1212                 Rectangle<float> imageBounds (parseSafeFloat (xml->getStringAttribute ("x")),
1213                                               parseSafeFloat (xml->getStringAttribute ("y")),
1214                                               parseSafeFloat (xml->getStringAttribute ("width",  String (image.getWidth()))),
1215                                               parseSafeFloat (xml->getStringAttribute ("height", String (image.getHeight()))));
1216 
1217                 di->setImage (image.rescaled ((int) imageBounds.getWidth(),
1218                                               (int) imageBounds.getHeight()));
1219 
1220                 di->setTransformToFit (imageBounds, RectanglePlacement (parsePlacementFlags (xml->getStringAttribute ("preserveAspectRatio").trim())));
1221 
1222                 if (additionalTransform != nullptr)
1223                     di->setTransform (di->getTransform().followedBy (transform).followedBy (*additionalTransform));
1224                 else
1225                     di->setTransform (di->getTransform().followedBy (transform));
1226 
1227                 return di;
1228             }
1229         }
1230 
1231         return nullptr;
1232     }
1233 
1234     //==============================================================================
addTransform(const XmlPath & xml)1235     void addTransform (const XmlPath& xml)
1236     {
1237         transform = parseTransform (xml->getStringAttribute ("transform"))
1238                         .followedBy (transform);
1239     }
1240 
1241     //==============================================================================
parseCoord(String::CharPointerType & s,float & value,bool allowUnits,bool isX) const1242     bool parseCoord (String::CharPointerType& s, float& value, bool allowUnits, bool isX) const
1243     {
1244         String number;
1245 
1246         if (! parseNextNumber (s, number, allowUnits))
1247         {
1248             value = 0;
1249             return false;
1250         }
1251 
1252         value = getCoordLength (number, isX ? viewBoxW : viewBoxH);
1253         return true;
1254     }
1255 
parseCoords(String::CharPointerType & s,Point<float> & p,bool allowUnits) const1256     bool parseCoords (String::CharPointerType& s, Point<float>& p, bool allowUnits) const
1257     {
1258         return parseCoord (s, p.x, allowUnits, true)
1259             && parseCoord (s, p.y, allowUnits, false);
1260     }
1261 
parseCoordsOrSkip(String::CharPointerType & s,Point<float> & p,bool allowUnits) const1262     bool parseCoordsOrSkip (String::CharPointerType& s, Point<float>& p, bool allowUnits) const
1263     {
1264         if (parseCoords (s, p, allowUnits))
1265             return true;
1266 
1267         if (! s.isEmpty()) ++s;
1268         return false;
1269     }
1270 
getCoordLength(const String & s,const float sizeForProportions) const1271     float getCoordLength (const String& s, const float sizeForProportions) const noexcept
1272     {
1273         auto n = parseSafeFloat (s);
1274         auto len = s.length();
1275 
1276         if (len > 2)
1277         {
1278             auto dpi = 96.0f;
1279 
1280             auto n1 = s[len - 2];
1281             auto n2 = s[len - 1];
1282 
1283             if (n1 == 'i' && n2 == 'n')         n *= dpi;
1284             else if (n1 == 'm' && n2 == 'm')    n *= dpi / 25.4f;
1285             else if (n1 == 'c' && n2 == 'm')    n *= dpi / 2.54f;
1286             else if (n1 == 'p' && n2 == 'c')    n *= 15.0f;
1287             else if (n2 == '%')                 n *= 0.01f * sizeForProportions;
1288         }
1289 
1290         return n;
1291     }
1292 
getCoordLength(const XmlPath & xml,const char * attName,const float sizeForProportions) const1293     float getCoordLength (const XmlPath& xml, const char* attName, const float sizeForProportions) const noexcept
1294     {
1295         return getCoordLength (xml->getStringAttribute (attName), sizeForProportions);
1296     }
1297 
getCoordList(Array<float> & coords,const String & list,bool allowUnits,bool isX) const1298     void getCoordList (Array<float>& coords, const String& list, bool allowUnits, bool isX) const
1299     {
1300         auto text = list.getCharPointer();
1301         float value;
1302 
1303         while (parseCoord (text, value, allowUnits, isX))
1304             coords.add (value);
1305     }
1306 
parseSafeFloat(const String & s)1307     static float parseSafeFloat (const String& s)
1308     {
1309         auto n = s.getFloatValue();
1310         return (std::isnan (n) || std::isinf (n)) ? 0.0f : n;
1311     }
1312 
1313     //==============================================================================
parseCSSStyle(const XmlPath & xml)1314     void parseCSSStyle (const XmlPath& xml)
1315     {
1316         cssStyleText = xml->getAllSubText() + "\n" + cssStyleText;
1317     }
1318 
parseDefs(const XmlPath & xml)1319     void parseDefs (const XmlPath& xml)
1320     {
1321         if (auto* style = xml->getChildByName ("style"))
1322             parseCSSStyle (xml.getChild (style));
1323     }
1324 
findStyleItem(String::CharPointerType source,String::CharPointerType name)1325     static String::CharPointerType findStyleItem (String::CharPointerType source, String::CharPointerType name)
1326     {
1327         auto nameLength = (int) name.length();
1328 
1329         while (! source.isEmpty())
1330         {
1331             if (source.getAndAdvance() == '.'
1332                  && CharacterFunctions::compareIgnoreCaseUpTo (source, name, nameLength) == 0)
1333             {
1334                 auto endOfName = (source + nameLength).findEndOfWhitespace();
1335 
1336                 if (*endOfName == '{')
1337                     return endOfName;
1338 
1339                 if (*endOfName == ',')
1340                     return CharacterFunctions::find (endOfName, (juce_wchar) '{');
1341             }
1342         }
1343 
1344         return source;
1345     }
1346 
getStyleAttribute(const XmlPath & xml,StringRef attributeName,const String & defaultValue=String ()) const1347     String getStyleAttribute (const XmlPath& xml, StringRef attributeName, const String& defaultValue = String()) const
1348     {
1349         if (xml->hasAttribute (attributeName))
1350             return xml->getStringAttribute (attributeName, defaultValue);
1351 
1352         auto styleAtt = xml->getStringAttribute ("style");
1353 
1354         if (styleAtt.isNotEmpty())
1355         {
1356             auto value = getAttributeFromStyleList (styleAtt, attributeName, {});
1357 
1358             if (value.isNotEmpty())
1359                 return value;
1360         }
1361         else if (xml->hasAttribute ("class"))
1362         {
1363             for (auto i = cssStyleText.getCharPointer();;)
1364             {
1365                 auto openBrace = findStyleItem (i, xml->getStringAttribute ("class").getCharPointer());
1366 
1367                 if (openBrace.isEmpty())
1368                     break;
1369 
1370                 auto closeBrace = CharacterFunctions::find (openBrace, (juce_wchar) '}');
1371 
1372                 if (closeBrace.isEmpty())
1373                     break;
1374 
1375                 auto value = getAttributeFromStyleList (String (openBrace + 1, closeBrace),
1376                                                         attributeName, defaultValue);
1377                 if (value.isNotEmpty())
1378                     return value;
1379 
1380                 i = closeBrace + 1;
1381             }
1382         }
1383 
1384         if (xml.parent != nullptr)
1385             return getStyleAttribute (*xml.parent, attributeName, defaultValue);
1386 
1387         return defaultValue;
1388     }
1389 
getInheritedAttribute(const XmlPath & xml,StringRef attributeName) const1390     String getInheritedAttribute (const XmlPath& xml, StringRef attributeName) const
1391     {
1392         if (xml->hasAttribute (attributeName))
1393             return xml->getStringAttribute (attributeName);
1394 
1395         if (xml.parent != nullptr)
1396             return getInheritedAttribute (*xml.parent, attributeName);
1397 
1398         return {};
1399     }
1400 
parsePlacementFlags(const String & align)1401     static int parsePlacementFlags (const String& align) noexcept
1402     {
1403         if (align.isEmpty())
1404             return 0;
1405 
1406         if (isNone (align))
1407             return RectanglePlacement::stretchToFit;
1408 
1409         return (align.containsIgnoreCase ("slice") ? RectanglePlacement::fillDestination : 0)
1410              | (align.containsIgnoreCase ("xMin")  ? RectanglePlacement::xLeft
1411                                                    : (align.containsIgnoreCase ("xMax") ? RectanglePlacement::xRight
1412                                                                                         : RectanglePlacement::xMid))
1413              | (align.containsIgnoreCase ("yMin")  ? RectanglePlacement::yTop
1414                                                    : (align.containsIgnoreCase ("yMax") ? RectanglePlacement::yBottom
1415                                                                                         : RectanglePlacement::yMid));
1416     }
1417 
1418     //==============================================================================
isIdentifierChar(juce_wchar c)1419     static bool isIdentifierChar (juce_wchar c)
1420     {
1421         return CharacterFunctions::isLetter (c) || c == '-';
1422     }
1423 
getAttributeFromStyleList(const String & list,StringRef attributeName,const String & defaultValue)1424     static String getAttributeFromStyleList (const String& list, StringRef attributeName, const String& defaultValue)
1425     {
1426         int i = 0;
1427 
1428         for (;;)
1429         {
1430             i = list.indexOf (i, attributeName);
1431 
1432             if (i < 0)
1433                 break;
1434 
1435             if ((i == 0 || (i > 0 && ! isIdentifierChar (list [i - 1])))
1436                  && ! isIdentifierChar (list [i + attributeName.length()]))
1437             {
1438                 i = list.indexOfChar (i, ':');
1439 
1440                 if (i < 0)
1441                     break;
1442 
1443                 int end = list.indexOfChar (i, ';');
1444 
1445                 if (end < 0)
1446                     end = 0x7ffff;
1447 
1448                 return list.substring (i + 1, end).trim();
1449             }
1450 
1451             ++i;
1452         }
1453 
1454         return defaultValue;
1455     }
1456 
1457     //==============================================================================
isStartOfNumber(juce_wchar c)1458     static bool isStartOfNumber (juce_wchar c) noexcept
1459     {
1460         return CharacterFunctions::isDigit (c) || c == '-' || c == '+';
1461     }
1462 
parseNextNumber(String::CharPointerType & text,String & value,bool allowUnits)1463     static bool parseNextNumber (String::CharPointerType& text, String& value, bool allowUnits)
1464     {
1465         auto s = text;
1466 
1467         while (s.isWhitespace() || *s == ',')
1468             ++s;
1469 
1470         auto start = s;
1471 
1472         if (isStartOfNumber (*s))
1473             ++s;
1474 
1475         while (s.isDigit())
1476             ++s;
1477 
1478         if (*s == '.')
1479         {
1480             ++s;
1481 
1482             while (s.isDigit())
1483                 ++s;
1484         }
1485 
1486         if ((*s == 'e' || *s == 'E') && isStartOfNumber (s[1]))
1487         {
1488             s += 2;
1489 
1490             while (s.isDigit())
1491                 ++s;
1492         }
1493 
1494         if (allowUnits)
1495             while (s.isLetter())
1496                 ++s;
1497 
1498         if (s == start)
1499         {
1500             text = s;
1501             return false;
1502         }
1503 
1504         value = String (start, s);
1505 
1506         while (s.isWhitespace() || *s == ',')
1507             ++s;
1508 
1509         text = s;
1510         return true;
1511     }
1512 
parseNextFlag(String::CharPointerType & text,bool & value)1513     static bool parseNextFlag (String::CharPointerType& text, bool& value)
1514     {
1515         while (text.isWhitespace() || *text == ',')
1516             ++text;
1517 
1518         if (*text != '0' && *text != '1')
1519             return false;
1520 
1521         value = *(text++) != '0';
1522 
1523         while (text.isWhitespace() || *text == ',')
1524              ++text;
1525 
1526         return true;
1527     }
1528 
1529     //==============================================================================
parseColour(const XmlPath & xml,StringRef attributeName,const Colour defaultColour) const1530     Colour parseColour (const XmlPath& xml, StringRef attributeName, const Colour defaultColour) const
1531     {
1532         auto text = getStyleAttribute (xml, attributeName);
1533 
1534         if (text.startsWithChar ('#'))
1535         {
1536             uint32 hex[8] = { 0 };
1537             hex[6] = hex[7] = 15;
1538 
1539             int numChars = 0;
1540             auto s = text.getCharPointer();
1541 
1542             while (numChars < 8)
1543             {
1544                 auto hexValue = CharacterFunctions::getHexDigitValue (*++s);
1545 
1546                 if (hexValue >= 0)
1547                     hex[numChars++] = (uint32) hexValue;
1548                 else
1549                     break;
1550             }
1551 
1552             if (numChars <= 3)
1553                 return Colour ((uint8) (hex[0] * 0x11),
1554                                (uint8) (hex[1] * 0x11),
1555                                (uint8) (hex[2] * 0x11));
1556 
1557             return Colour ((uint8) ((hex[0] << 4) + hex[1]),
1558                            (uint8) ((hex[2] << 4) + hex[3]),
1559                            (uint8) ((hex[4] << 4) + hex[5]),
1560                            (uint8) ((hex[6] << 4) + hex[7]));
1561         }
1562 
1563         if (text.startsWith ("rgb") || text.startsWith ("hsl"))
1564         {
1565             auto tokens = [&text]
1566             {
1567                 auto openBracket = text.indexOfChar ('(');
1568                 auto closeBracket = text.indexOfChar (openBracket, ')');
1569 
1570                 StringArray arr;
1571 
1572                 if (openBracket >= 3 && closeBracket > openBracket)
1573                 {
1574                     arr.addTokens (text.substring (openBracket + 1, closeBracket), ",", "");
1575                     arr.trim();
1576                     arr.removeEmptyStrings();
1577                 }
1578 
1579                 return arr;
1580             }();
1581 
1582             auto alpha = [&tokens, &text]
1583             {
1584                 if ((text.startsWith ("rgba") || text.startsWith ("hsla")) && tokens.size() == 4)
1585                     return parseSafeFloat (tokens[3]);
1586 
1587                 return 1.0f;
1588             }();
1589 
1590             if (text.startsWith ("hsl"))
1591                 return Colour::fromHSL (parseSafeFloat (tokens[0]) / 360.0f,
1592                                         parseSafeFloat (tokens[1]) / 100.0f,
1593                                         parseSafeFloat (tokens[2]) / 100.0f,
1594                                         alpha);
1595 
1596             if (tokens[0].containsChar ('%'))
1597                 return Colour ((uint8) roundToInt (2.55f * parseSafeFloat (tokens[0])),
1598                                (uint8) roundToInt (2.55f * parseSafeFloat (tokens[1])),
1599                                (uint8) roundToInt (2.55f * parseSafeFloat (tokens[2])),
1600                                alpha);
1601 
1602             return Colour ((uint8) tokens[0].getIntValue(),
1603                            (uint8) tokens[1].getIntValue(),
1604                            (uint8) tokens[2].getIntValue(),
1605                            alpha);
1606         }
1607 
1608         if (text == "inherit")
1609         {
1610             for (const XmlPath* p = xml.parent; p != nullptr; p = p->parent)
1611                 if (getStyleAttribute (*p, attributeName).isNotEmpty())
1612                     return parseColour (*p, attributeName, defaultColour);
1613         }
1614 
1615         return Colours::findColourForName (text, defaultColour);
1616     }
1617 
parseTransform(String t)1618     static AffineTransform parseTransform (String t)
1619     {
1620         AffineTransform result;
1621 
1622         while (t.isNotEmpty())
1623         {
1624             StringArray tokens;
1625             tokens.addTokens (t.fromFirstOccurrenceOf ("(", false, false)
1626                                .upToFirstOccurrenceOf (")", false, false),
1627                               ", ", "");
1628 
1629             tokens.removeEmptyStrings (true);
1630 
1631             float numbers[6];
1632 
1633             for (int i = 0; i < numElementsInArray (numbers); ++i)
1634                 numbers[i] = parseSafeFloat (tokens[i]);
1635 
1636             AffineTransform trans;
1637 
1638             if (t.startsWithIgnoreCase ("matrix"))
1639             {
1640                 trans = AffineTransform (numbers[0], numbers[2], numbers[4],
1641                                          numbers[1], numbers[3], numbers[5]);
1642             }
1643             else if (t.startsWithIgnoreCase ("translate"))
1644             {
1645                 trans = AffineTransform::translation (numbers[0], numbers[1]);
1646             }
1647             else if (t.startsWithIgnoreCase ("scale"))
1648             {
1649                 trans = AffineTransform::scale (numbers[0], numbers[tokens.size() > 1 ? 1 : 0]);
1650             }
1651             else if (t.startsWithIgnoreCase ("rotate"))
1652             {
1653                 trans = AffineTransform::rotation (degreesToRadians (numbers[0]), numbers[1], numbers[2]);
1654             }
1655             else if (t.startsWithIgnoreCase ("skewX"))
1656             {
1657                 trans = AffineTransform::shear (std::tan (degreesToRadians (numbers[0])), 0.0f);
1658             }
1659             else if (t.startsWithIgnoreCase ("skewY"))
1660             {
1661                 trans = AffineTransform::shear (0.0f, std::tan (degreesToRadians (numbers[0])));
1662             }
1663 
1664             result = trans.followedBy (result);
1665             t = t.fromFirstOccurrenceOf (")", false, false).trimStart();
1666         }
1667 
1668         return result;
1669     }
1670 
endpointToCentreParameters(double x1,double y1,double x2,double y2,double angle,bool largeArc,bool sweep,double & rx,double & ry,double & centreX,double & centreY,double & startAngle,double & deltaAngle)1671     static void endpointToCentreParameters (double x1, double y1,
1672                                             double x2, double y2,
1673                                             double angle,
1674                                             bool largeArc, bool sweep,
1675                                             double& rx, double& ry,
1676                                             double& centreX, double& centreY,
1677                                             double& startAngle, double& deltaAngle) noexcept
1678     {
1679         const double midX = (x1 - x2) * 0.5;
1680         const double midY = (y1 - y2) * 0.5;
1681 
1682         const double cosAngle = std::cos (angle);
1683         const double sinAngle = std::sin (angle);
1684         const double xp = cosAngle * midX + sinAngle * midY;
1685         const double yp = cosAngle * midY - sinAngle * midX;
1686         const double xp2 = xp * xp;
1687         const double yp2 = yp * yp;
1688 
1689         double rx2 = rx * rx;
1690         double ry2 = ry * ry;
1691 
1692         const double s = (xp2 / rx2) + (yp2 / ry2);
1693         double c;
1694 
1695         if (s <= 1.0)
1696         {
1697             c = std::sqrt (jmax (0.0, ((rx2 * ry2) - (rx2 * yp2) - (ry2 * xp2))
1698                                          / (( rx2 * yp2) + (ry2 * xp2))));
1699 
1700             if (largeArc == sweep)
1701                 c = -c;
1702         }
1703         else
1704         {
1705             const double s2 = std::sqrt (s);
1706             rx *= s2;
1707             ry *= s2;
1708             c = 0;
1709         }
1710 
1711         const double cpx = ((rx * yp) / ry) * c;
1712         const double cpy = ((-ry * xp) / rx) * c;
1713 
1714         centreX = ((x1 + x2) * 0.5) + (cosAngle * cpx) - (sinAngle * cpy);
1715         centreY = ((y1 + y2) * 0.5) + (sinAngle * cpx) + (cosAngle * cpy);
1716 
1717         const double ux = (xp - cpx) / rx;
1718         const double uy = (yp - cpy) / ry;
1719         const double vx = (-xp - cpx) / rx;
1720         const double vy = (-yp - cpy) / ry;
1721 
1722         const double length = juce_hypot (ux, uy);
1723 
1724         startAngle = acos (jlimit (-1.0, 1.0, ux / length));
1725 
1726         if (uy < 0)
1727             startAngle = -startAngle;
1728 
1729         startAngle += MathConstants<double>::halfPi;
1730 
1731         deltaAngle = acos (jlimit (-1.0, 1.0, ((ux * vx) + (uy * vy))
1732                                                 / (length * juce_hypot (vx, vy))));
1733 
1734         if ((ux * vy) - (uy * vx) < 0)
1735             deltaAngle = -deltaAngle;
1736 
1737         if (sweep)
1738         {
1739             if (deltaAngle < 0)
1740                 deltaAngle += MathConstants<double>::twoPi;
1741         }
1742         else
1743         {
1744             if (deltaAngle > 0)
1745                 deltaAngle -= MathConstants<double>::twoPi;
1746         }
1747 
1748         deltaAngle = fmod (deltaAngle, MathConstants<double>::twoPi);
1749     }
1750 
1751     SVGState& operator= (const SVGState&) = delete;
1752 };
1753 
1754 
1755 //==============================================================================
createFromSVG(const XmlElement & svgDocument)1756 std::unique_ptr<Drawable> Drawable::createFromSVG (const XmlElement& svgDocument)
1757 {
1758     if (! svgDocument.hasTagNameIgnoringNamespace ("svg"))
1759         return {};
1760 
1761     SVGState state (&svgDocument);
1762     return std::unique_ptr<Drawable> (state.parseSVGElement (SVGState::XmlPath (&svgDocument, {})));
1763 }
1764 
createFromSVGFile(const File & svgFile)1765 std::unique_ptr<Drawable> Drawable::createFromSVGFile (const File& svgFile)
1766 {
1767     if (auto xml = parseXMLIfTagMatches (svgFile, "svg"))
1768         return createFromSVG (*xml);
1769 
1770     return {};
1771 }
1772 
parseSVGPath(const String & svgPath)1773 Path Drawable::parseSVGPath (const String& svgPath)
1774 {
1775     SVGState state (nullptr);
1776     Path p;
1777     state.parsePathString (p, svgPath);
1778     return p;
1779 }
1780 
1781 } // namespace juce
1782