JUCE-7.0.12-0-g4f43011b96 JUCE-7.0.12-0-g4f43011b96
JUCE — C++ application framework with suport for VST, VST3, LV2 audio plug-ins

« « « Anklang Documentation
Loading...
Searching...
No Matches
juce_SVGParser.cpp
Go to the documentation of this file.
1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - 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 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-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
26namespace juce
27{
28
30{
31public:
32 //==============================================================================
33 explicit SVGState (const XmlElement* topLevel, const File& svgFile = {})
34 : originalFile (svgFile), topLevelXml (topLevel, nullptr)
35 {
36 }
37
38 struct XmlPath
39 {
40 XmlPath (const XmlElement* e, const XmlPath* p) noexcept : xml (e), parent (p) {}
41
42 const XmlElement& operator*() const noexcept { jassert (xml != nullptr); return *xml; }
43 const XmlElement* operator->() const noexcept { return xml; }
44 XmlPath getChild (const XmlElement* e) const noexcept { return XmlPath (e, this); }
45
46 template <typename OperationType>
47 bool applyOperationToChildWithID (const String& id, OperationType& op) const
48 {
49 for (auto* e : xml->getChildIterator())
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
74 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
86 bool operator() (const XmlPath& xmlPath)
87 {
88 target = state->parseText (xmlPath, true, transform);
89 return target != nullptr;
90 }
91 };
92
94 {
95 const SVGState* state;
96 AffineTransform* transform;
97 Drawable* target;
98
99 bool operator() (const XmlPath& xmlPath)
100 {
101 target = state->parseImage (xmlPath, true, transform);
102 return target != nullptr;
103 }
104 };
105
107 {
108 SVGState* state;
109 Drawable* target;
110
111 bool operator() (const XmlPath& xmlPath)
112 {
113 return state->applyClipPath (*target, xmlPath);
114 }
115 };
116
118 {
119 const SVGState* state;
120 ColourGradient* gradient;
121
122 bool operator() (const XmlPath& xml) const
123 {
124 return state->addGradientStopsIn (*gradient, xml);
125 }
126 };
127
129 {
130 const SVGState* state;
131 const Path* path;
132 float opacity;
133 FillType fillType;
134
135 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 //==============================================================================
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
166
167 if (xml->hasAttribute ("viewBox"))
168 {
169 auto viewBoxAtt = xml->getStringAttribute ("viewBox");
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)
186 Rectangle<float> (newState.width, newState.height))
187 .followedBy (newState.transform);
188 }
189 }
190 else
191 {
192 if (approximatelyEqual (viewBoxW, 0.0f)) newState.viewBoxW = newState.width;
193 if (approximatelyEqual (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 //==============================================================================
205 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;
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 {
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, Axis::x))
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, Axis::y))
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 {
389 double rx = p1.x, ry = p1.y;
390
391 endpointToCentreParameters (last.x, last.y, p2.x, p2.y,
393 rx, ry, centreX, centreY,
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.incrementToEndOfWhitespace();
419 currentCommand = 'M';
420 break;
421
422 default:
423 carryOn = false;
424 break;
425 }
426
427 if (! carryOn)
428 break;
429
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
439private:
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
447 static bool isNone (const String& s) noexcept
448 {
449 return s.equalsIgnoreCase ("none");
450 }
451
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 //==============================================================================
463 void parseSubElements (const XmlPath& xml, DrawableComposite& parentDrawable, bool shouldParseClip = true)
464 {
465 for (auto* e : xml->getChildIterator())
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
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, nullptr);
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
505 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
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
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
547 DrawableComposite* parseLinkElement (const XmlPath& xml)
548 {
549 return parseGroupElement (xml, true); // TODO: support for making this clickable
550 }
551
552 //==============================================================================
553 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
561 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
591 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
600 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
610 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
621 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
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
654 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
667 Drawable* parseUseOther (const XmlPath& xml) const
668 {
669 if (auto* drawableText = parseText (xml, false, nullptr)) return drawableText;
670 if (auto* drawableImage = parseImage (xml, false)) return drawableImage;
671
672 return nullptr;
673 }
674
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 //==============================================================================
685 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
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
743 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, Axis::x))
754 break;
755
756 dashLengths.add (value);
757
758 t.incrementToEndOfWhitespace();
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;
777
778 const int pairedIndex = i ^ 1;
779
780 if (isPositiveAndBelow (pairedIndex, dashLengths.size())
783 }
784 }
785
786 dp.setDashLengths (dashLengths);
787 }
788 }
789
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
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
827 bool addGradientStopsIn (ColourGradient& cg, const XmlPath& fillXml) const
828 {
829 bool result = false;
830
831 if (fillXml.xml != nullptr)
832 {
833 for (auto* e : fillXml->getChildWithTagNameIterator ("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
853 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
976 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
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
1014 }
1015
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
1024 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
1030 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 //==============================================================================
1038 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
1053 /* Handling the stateful consumption of x and y coordinates added to <text> and <tspan> elements.
1054
1055 <text> elements must have their own x and y attributes, or be positioned at (0, 0) since groups
1056 enclosing <text> elements can't have x and y attributes.
1057
1058 <tspan> elements can be embedded inside <text> elements, and <tspan> elements. <text> elements
1059 can't be embedded inside <text> or <tspan> elements.
1060
1061 A <tspan> element can have its own x, y attributes, which it will consume at the same time as
1062 it consumes its parent's attributes. Its own elements will take precedence, but parent elements
1063 will be consumed regardless.
1064 */
1065 class StringLayoutState
1066 {
1067 public:
1068 StringLayoutState (StringLayoutState* parentIn, Array<float> xIn, Array<float> yIn)
1069 : parent (parentIn),
1070 xCoords (std::move (xIn)),
1071 yCoords (std::move (yIn))
1072 {
1073 }
1074
1075 Point<float> getNextStartingPos() const
1076 {
1077 if (parent != nullptr)
1078 return parent->getNextStartingPos();
1079
1080 return nextStartingPos;
1081 }
1082
1083 void setNextStartingPos (Point<float> newPos)
1084 {
1085 nextStartingPos = newPos;
1086
1087 if (parent != nullptr)
1088 parent->setNextStartingPos (newPos);
1089 }
1090
1092 {
1093 auto x = xCoords.isEmpty() ? std::optional<float>{} : std::make_optional (xCoords.removeAndReturn (0));
1094 auto y = yCoords.isEmpty() ? std::optional<float>{} : std::make_optional (yCoords.removeAndReturn (0));
1095
1096 if (parent != nullptr)
1097 {
1098 auto [parentX, parentY] = parent->popCoords();
1099
1100 if (! x.has_value())
1101 x = parentX;
1102
1103 if (! y.has_value())
1104 y = parentY;
1105 }
1106
1107 return { x, y };
1108 }
1109
1110 bool hasMoreCoords() const
1111 {
1112 if (! xCoords.isEmpty() || ! yCoords.isEmpty())
1113 return true;
1114
1115 if (parent != nullptr)
1116 return parent->hasMoreCoords();
1117
1118 return false;
1119 }
1120
1121 private:
1122 StringLayoutState* parent = nullptr;
1123 Point<float> nextStartingPos;
1124 Array<float> xCoords, yCoords;
1125 };
1126
1127 Drawable* parseText (const XmlPath& xml, bool shouldParseTransform,
1128 AffineTransform* additonalTransform,
1129 StringLayoutState* parentLayoutState = nullptr) const
1130 {
1131 if (shouldParseTransform && xml->hasAttribute ("transform"))
1132 {
1133 SVGState newState (*this);
1134 newState.addTransform (xml);
1135
1136 return newState.parseText (xml, false, additonalTransform);
1137 }
1138
1139 if (xml->hasTagName ("use"))
1140 return useText (xml);
1141
1142 if (! xml->hasTagName ("text") && ! xml->hasTagNameIgnoringNamespace ("tspan"))
1143 return nullptr;
1144
1145 // If a <tspan> element has no x, or y attributes of its own, it can still use the
1146 // parent's yet unconsumed such attributes.
1147 StringLayoutState layoutState { parentLayoutState,
1148 getCoordList (*xml, Axis::x),
1149 getCoordList (*xml, Axis::y) };
1150
1151 auto font = getFont (xml);
1152 auto anchorStr = getStyleAttribute (xml, "text-anchor");
1153
1154 auto dc = new DrawableComposite();
1155 setCommonAttributes (*dc, xml);
1156
1157 for (auto* e : xml->getChildIterator())
1158 {
1159 if (e->isTextElement())
1160 {
1161 auto fullText = e->getText();
1162
1163 const auto subtextElements = [&]
1164 {
1166
1167 for (auto it = fullText.begin(), end = fullText.end(); it != end;)
1168 {
1169 const auto pos = layoutState.popCoords();
1170 const auto next = layoutState.hasMoreCoords() ? it + 1 : end;
1171 result.emplace_back (String (it, next), pos.first, pos.second);
1172 it = next;
1173 }
1174
1175 return result;
1176 }();
1177
1178 for (const auto& [text, optX, optY] : subtextElements)
1179 {
1180 auto dt = new DrawableText();
1181 dc->addAndMakeVisible (dt);
1182
1183 dt->setText (text);
1184 dt->setFont (font, true);
1185
1186 if (additonalTransform != nullptr)
1187 dt->setDrawableTransform (transform.followedBy (*additonalTransform));
1188 else
1189 dt->setDrawableTransform (transform);
1190
1191 dt->setColour (parseColour (xml, "fill", Colours::black)
1192 .withMultipliedAlpha (parseSafeFloat (getStyleAttribute (xml, "fill-opacity", "1"))));
1193
1194 const auto x = optX.value_or (layoutState.getNextStartingPos().getX());
1195 const auto y = optY.value_or (layoutState.getNextStartingPos().getY());
1196
1197 Rectangle<float> bounds (x, y - font.getAscent(),
1198 font.getStringWidthFloat (text), font.getHeight());
1199
1200 if (anchorStr == "middle") bounds.setX (bounds.getX() - bounds.getWidth() / 2.0f);
1201 else if (anchorStr == "end") bounds.setX (bounds.getX() - bounds.getWidth());
1202
1203 dt->setBoundingBox (bounds);
1204
1205 layoutState.setNextStartingPos ({ bounds.getRight(), y });
1206 }
1207 }
1208 else if (e->hasTagNameIgnoringNamespace ("tspan"))
1209 {
1210 dc->addAndMakeVisible (parseText (xml.getChild (e), true, nullptr, &layoutState));
1211 }
1212 }
1213
1214 return dc;
1215 }
1216
1217 Font getFont (const XmlPath& xml) const
1218 {
1219 Font f;
1220 auto family = getStyleAttribute (xml, "font-family").unquoted();
1221
1222 if (family.isNotEmpty())
1223 f.setTypefaceName (family);
1224
1225 if (getStyleAttribute (xml, "font-style").containsIgnoreCase ("italic"))
1226 f.setItalic (true);
1227
1228 if (getStyleAttribute (xml, "font-weight").containsIgnoreCase ("bold"))
1229 f.setBold (true);
1230
1231 return f.withPointHeight (getCoordLength (getStyleAttribute (xml, "font-size", "15"), 1.0f));
1232 }
1233
1234 //==============================================================================
1235 Drawable* useImage (const XmlPath& xml) const
1236 {
1237 auto translation = AffineTransform::translation (parseSafeFloat (xml->getStringAttribute ("x")),
1238 parseSafeFloat (xml->getStringAttribute ("y")));
1239
1240 UseImageOp op = { this, &translation, nullptr };
1241
1242 auto linkedID = getLinkedID (xml);
1243
1244 if (linkedID.isNotEmpty())
1245 topLevelXml.applyOperationToChildWithID (linkedID, op);
1246
1247 return op.target;
1248 }
1249
1250 Drawable* parseImage (const XmlPath& xml, bool shouldParseTransform,
1251 AffineTransform* additionalTransform = nullptr) const
1252 {
1253 if (shouldParseTransform && xml->hasAttribute ("transform"))
1254 {
1255 SVGState newState (*this);
1256 newState.addTransform (xml);
1257
1258 return newState.parseImage (xml, false, additionalTransform);
1259 }
1260
1261 if (xml->hasTagName ("use"))
1262 return useImage (xml);
1263
1264 if (! xml->hasTagName ("image"))
1265 return nullptr;
1266
1267 auto link = xml->getStringAttribute ("xlink:href");
1268
1269 std::unique_ptr<InputStream> inputStream;
1270 MemoryOutputStream imageStream;
1271
1272 if (link.startsWith ("data:"))
1273 {
1274 const auto indexOfComma = link.indexOf (",");
1275 auto format = link.substring (5, indexOfComma).trim();
1276 auto indexOfSemi = format.indexOf (";");
1277
1278 if (format.substring (indexOfSemi + 1).trim().equalsIgnoreCase ("base64"))
1279 {
1280 auto mime = format.substring (0, indexOfSemi).trim();
1281
1282 if (mime.equalsIgnoreCase ("image/png") || mime.equalsIgnoreCase ("image/jpeg"))
1283 {
1284 auto base64text = link.substring (indexOfComma + 1).removeCharacters ("\t\n\r ");
1285
1287 inputStream.reset (new MemoryInputStream (imageStream.getData(), imageStream.getDataSize(), false));
1288 }
1289 }
1290 }
1291 else
1292 {
1293 auto linkedFile = originalFile.getParentDirectory().getChildFile (link);
1294
1295 if (linkedFile.existsAsFile())
1296 inputStream = linkedFile.createInputStream();
1297 }
1298
1299 if (inputStream != nullptr)
1300 {
1301 auto image = ImageFileFormat::loadFrom (*inputStream);
1302
1303 if (image.isValid())
1304 {
1305 auto* di = new DrawableImage();
1306
1307 setCommonAttributes (*di, xml);
1308
1309 Rectangle<float> imageBounds (parseSafeFloat (xml->getStringAttribute ("x")),
1310 parseSafeFloat (xml->getStringAttribute ("y")),
1311 parseSafeFloat (xml->getStringAttribute ("width", String (image.getWidth()))),
1312 parseSafeFloat (xml->getStringAttribute ("height", String (image.getHeight()))));
1313
1314 di->setImage (image.rescaled ((int) imageBounds.getWidth(),
1315 (int) imageBounds.getHeight()));
1316
1317 di->setTransformToFit (imageBounds, RectanglePlacement (parsePlacementFlags (xml->getStringAttribute ("preserveAspectRatio").trim())));
1318
1319 if (additionalTransform != nullptr)
1320 di->setTransform (di->getTransform().followedBy (transform).followedBy (*additionalTransform));
1321 else
1322 di->setTransform (di->getTransform().followedBy (transform));
1323
1324 return di;
1325 }
1326 }
1327
1328 return nullptr;
1329 }
1330
1331 //==============================================================================
1332 void addTransform (const XmlPath& xml)
1333 {
1334 transform = parseTransform (xml->getStringAttribute ("transform"))
1335 .followedBy (transform);
1336 }
1337
1338 //==============================================================================
1339 enum class Axis { x, y };
1340
1341 bool parseCoord (String::CharPointerType& s, float& value, bool allowUnits, Axis axis) const
1342 {
1343 String number;
1344
1345 if (! parseNextNumber (s, number, allowUnits))
1346 {
1347 value = 0;
1348 return false;
1349 }
1350
1351 value = getCoordLength (number, axis == Axis::x ? viewBoxW : viewBoxH);
1352 return true;
1353 }
1354
1355 bool parseCoords (String::CharPointerType& s, Point<float>& p, bool allowUnits) const
1356 {
1357 return parseCoord (s, p.x, allowUnits, Axis::x)
1358 && parseCoord (s, p.y, allowUnits, Axis::y);
1359 }
1360
1361 bool parseCoordsOrSkip (String::CharPointerType& s, Point<float>& p, bool allowUnits) const
1362 {
1363 if (parseCoords (s, p, allowUnits))
1364 return true;
1365
1366 if (! s.isEmpty()) ++s;
1367 return false;
1368 }
1369
1370 float getCoordLength (const String& s, const float sizeForProportions) const noexcept
1371 {
1372 auto n = parseSafeFloat (s);
1373 auto len = s.length();
1374
1375 if (len > 2)
1376 {
1377 auto dpi = 96.0f;
1378
1379 auto n1 = s[len - 2];
1380 auto n2 = s[len - 1];
1381
1382 if (n1 == 'i' && n2 == 'n') n *= dpi;
1383 else if (n1 == 'm' && n2 == 'm') n *= dpi / 25.4f;
1384 else if (n1 == 'c' && n2 == 'm') n *= dpi / 2.54f;
1385 else if (n1 == 'p' && n2 == 'c') n *= 15.0f;
1386 else if (n2 == '%') n *= 0.01f * sizeForProportions;
1387 }
1388
1389 return n;
1390 }
1391
1392 float getCoordLength (const XmlPath& xml, const char* attName, const float sizeForProportions) const noexcept
1393 {
1394 return getCoordLength (xml->getStringAttribute (attName), sizeForProportions);
1395 }
1396
1397 Array<float> getCoordList (const XmlElement& xml, Axis axis) const
1398 {
1399 const String attributeName { axis == Axis::x ? "x" : "y" };
1400
1401 if (! xml.hasAttribute (attributeName))
1402 return {};
1403
1404 return getCoordList (xml.getStringAttribute (attributeName), true, axis);
1405 }
1406
1407 Array<float> getCoordList (const String& list, bool allowUnits, Axis axis) const
1408 {
1409 auto text = list.getCharPointer();
1410 float value;
1411 Array<float> coords;
1412
1413 while (parseCoord (text, value, allowUnits, axis))
1414 coords.add (value);
1415
1416 return coords;
1417 }
1418
1419 static float parseSafeFloat (const String& s)
1420 {
1421 auto n = s.getFloatValue();
1422 return (std::isnan (n) || std::isinf (n)) ? 0.0f : n;
1423 }
1424
1425 //==============================================================================
1426 void parseCSSStyle (const XmlPath& xml)
1427 {
1428 cssStyleText = xml->getAllSubText() + "\n" + cssStyleText;
1429 }
1430
1431 void parseDefs (const XmlPath& xml)
1432 {
1433 if (auto* style = xml->getChildByName ("style"))
1434 parseCSSStyle (xml.getChild (style));
1435 }
1436
1438 {
1439 auto nameLength = (int) name.length();
1440
1441 while (! source.isEmpty())
1442 {
1443 if (source.getAndAdvance() == '.'
1445 {
1446 auto endOfName = (source + nameLength).findEndOfWhitespace();
1447
1448 if (*endOfName == '{')
1449 return endOfName;
1450
1451 if (*endOfName == ',')
1453 }
1454 }
1455
1456 return source;
1457 }
1458
1459 String getStyleAttribute (const XmlPath& xml, StringRef attributeName, const String& defaultValue = String()) const
1460 {
1461 if (xml->hasAttribute (attributeName))
1462 return xml->getStringAttribute (attributeName, defaultValue);
1463
1464 auto styleAtt = xml->getStringAttribute ("style");
1465
1466 if (styleAtt.isNotEmpty())
1467 {
1468 auto value = getAttributeFromStyleList (styleAtt, attributeName, {});
1469
1470 if (value.isNotEmpty())
1471 return value;
1472 }
1473 else if (xml->hasAttribute ("class"))
1474 {
1475 for (auto i = cssStyleText.getCharPointer();;)
1476 {
1477 auto openBrace = findStyleItem (i, xml->getStringAttribute ("class").getCharPointer());
1478
1479 if (openBrace.isEmpty())
1480 break;
1481
1482 auto closeBrace = CharacterFunctions::find (openBrace, (juce_wchar) '}');
1483
1484 if (closeBrace.isEmpty())
1485 break;
1486
1487 auto value = getAttributeFromStyleList (String (openBrace + 1, closeBrace),
1488 attributeName, defaultValue);
1489 if (value.isNotEmpty())
1490 return value;
1491
1492 i = closeBrace + 1;
1493 }
1494 }
1495
1496 if (xml.parent != nullptr)
1497 return getStyleAttribute (*xml.parent, attributeName, defaultValue);
1498
1499 return defaultValue;
1500 }
1501
1502 String getInheritedAttribute (const XmlPath& xml, StringRef attributeName) const
1503 {
1504 if (xml->hasAttribute (attributeName))
1505 return xml->getStringAttribute (attributeName);
1506
1507 if (xml.parent != nullptr)
1508 return getInheritedAttribute (*xml.parent, attributeName);
1509
1510 return {};
1511 }
1512
1513 static int parsePlacementFlags (const String& align) noexcept
1514 {
1515 if (align.isEmpty())
1516 return 0;
1517
1518 if (isNone (align))
1520
1521 return (align.containsIgnoreCase ("slice") ? RectanglePlacement::fillDestination : 0)
1522 | (align.containsIgnoreCase ("xMin") ? RectanglePlacement::xLeft
1523 : (align.containsIgnoreCase ("xMax") ? RectanglePlacement::xRight
1524 : RectanglePlacement::xMid))
1525 | (align.containsIgnoreCase ("yMin") ? RectanglePlacement::yTop
1526 : (align.containsIgnoreCase ("yMax") ? RectanglePlacement::yBottom
1527 : RectanglePlacement::yMid));
1528 }
1529
1530 //==============================================================================
1531 static bool isIdentifierChar (juce_wchar c)
1532 {
1533 return CharacterFunctions::isLetter (c) || c == '-';
1534 }
1535
1536 static String getAttributeFromStyleList (const String& list, StringRef attributeName, const String& defaultValue)
1537 {
1538 int i = 0;
1539
1540 for (;;)
1541 {
1542 i = list.indexOf (i, attributeName);
1543
1544 if (i < 0)
1545 break;
1546
1547 if ((i == 0 || (i > 0 && ! isIdentifierChar (list [i - 1])))
1548 && ! isIdentifierChar (list [i + attributeName.length()]))
1549 {
1550 i = list.indexOfChar (i, ':');
1551
1552 if (i < 0)
1553 break;
1554
1555 int end = list.indexOfChar (i, ';');
1556
1557 if (end < 0)
1558 end = 0x7ffff;
1559
1560 return list.substring (i + 1, end).trim();
1561 }
1562
1563 ++i;
1564 }
1565
1566 return defaultValue;
1567 }
1568
1569 //==============================================================================
1570 static bool isStartOfNumber (juce_wchar c) noexcept
1571 {
1572 return CharacterFunctions::isDigit (c) || c == '-' || c == '+';
1573 }
1574
1575 static bool parseNextNumber (String::CharPointerType& text, String& value, bool allowUnits)
1576 {
1577 auto s = text;
1578
1579 while (s.isWhitespace() || *s == ',')
1580 ++s;
1581
1582 auto start = s;
1583
1584 if (isStartOfNumber (*s))
1585 ++s;
1586
1587 while (s.isDigit())
1588 ++s;
1589
1590 if (*s == '.')
1591 {
1592 ++s;
1593
1594 while (s.isDigit())
1595 ++s;
1596 }
1597
1598 if ((*s == 'e' || *s == 'E') && isStartOfNumber (s[1]))
1599 {
1600 s += 2;
1601
1602 while (s.isDigit())
1603 ++s;
1604 }
1605
1606 if (allowUnits)
1607 while (s.isLetter())
1608 ++s;
1609
1610 if (s == start)
1611 {
1612 text = s;
1613 return false;
1614 }
1615
1616 value = String (start, s);
1617
1618 while (s.isWhitespace() || *s == ',')
1619 ++s;
1620
1621 text = s;
1622 return true;
1623 }
1624
1625 static bool parseNextFlag (String::CharPointerType& text, bool& value)
1626 {
1627 while (text.isWhitespace() || *text == ',')
1628 ++text;
1629
1630 if (*text != '0' && *text != '1')
1631 return false;
1632
1633 value = *(text++) != '0';
1634
1635 while (text.isWhitespace() || *text == ',')
1636 ++text;
1637
1638 return true;
1639 }
1640
1641 //==============================================================================
1642 Colour parseColour (const XmlPath& xml, StringRef attributeName, const Colour defaultColour) const
1643 {
1644 auto text = getStyleAttribute (xml, attributeName);
1645
1646 if (text.startsWithChar ('#'))
1647 {
1648 uint32 hex[8] = { 0 };
1649 hex[6] = hex[7] = 15;
1650
1651 int numChars = 0;
1652 auto s = text.getCharPointer();
1653
1654 while (numChars < 8)
1655 {
1657
1658 if (hexValue >= 0)
1659 hex[numChars++] = (uint32) hexValue;
1660 else
1661 break;
1662 }
1663
1664 if (numChars <= 3)
1665 return Colour ((uint8) (hex[0] * 0x11),
1666 (uint8) (hex[1] * 0x11),
1667 (uint8) (hex[2] * 0x11));
1668
1669 return Colour ((uint8) ((hex[0] << 4) + hex[1]),
1670 (uint8) ((hex[2] << 4) + hex[3]),
1671 (uint8) ((hex[4] << 4) + hex[5]),
1672 (uint8) ((hex[6] << 4) + hex[7]));
1673 }
1674
1675 if (text.startsWith ("rgb") || text.startsWith ("hsl"))
1676 {
1677 auto tokens = [&text]
1678 {
1679 auto openBracket = text.indexOfChar ('(');
1680 auto closeBracket = text.indexOfChar (openBracket, ')');
1681
1682 StringArray arr;
1683
1684 if (openBracket >= 3 && closeBracket > openBracket)
1685 {
1686 arr.addTokens (text.substring (openBracket + 1, closeBracket), ",", "");
1687 arr.trim();
1688 arr.removeEmptyStrings();
1689 }
1690
1691 return arr;
1692 }();
1693
1694 auto alpha = [&tokens, &text]
1695 {
1696 if ((text.startsWith ("rgba") || text.startsWith ("hsla")) && tokens.size() == 4)
1697 return parseSafeFloat (tokens[3]);
1698
1699 return 1.0f;
1700 }();
1701
1702 if (text.startsWith ("hsl"))
1703 return Colour::fromHSL (parseSafeFloat (tokens[0]) / 360.0f,
1704 parseSafeFloat (tokens[1]) / 100.0f,
1705 parseSafeFloat (tokens[2]) / 100.0f,
1706 alpha);
1707
1708 if (tokens[0].containsChar ('%'))
1709 return Colour ((uint8) roundToInt (2.55f * parseSafeFloat (tokens[0])),
1710 (uint8) roundToInt (2.55f * parseSafeFloat (tokens[1])),
1711 (uint8) roundToInt (2.55f * parseSafeFloat (tokens[2])),
1712 alpha);
1713
1714 return Colour ((uint8) tokens[0].getIntValue(),
1715 (uint8) tokens[1].getIntValue(),
1716 (uint8) tokens[2].getIntValue(),
1717 alpha);
1718 }
1719
1720 if (text == "inherit")
1721 {
1722 for (const XmlPath* p = xml.parent; p != nullptr; p = p->parent)
1723 if (getStyleAttribute (*p, attributeName).isNotEmpty())
1724 return parseColour (*p, attributeName, defaultColour);
1725 }
1726
1728 }
1729
1730 static AffineTransform parseTransform (String t)
1731 {
1732 AffineTransform result;
1733
1734 while (t.isNotEmpty())
1735 {
1736 StringArray tokens;
1737 tokens.addTokens (t.fromFirstOccurrenceOf ("(", false, false)
1738 .upToFirstOccurrenceOf (")", false, false),
1739 ", ", "");
1740
1741 tokens.removeEmptyStrings (true);
1742
1743 float numbers[6];
1744
1745 for (int i = 0; i < numElementsInArray (numbers); ++i)
1746 numbers[i] = parseSafeFloat (tokens[i]);
1747
1748 AffineTransform trans;
1749
1750 if (t.startsWithIgnoreCase ("matrix"))
1751 {
1752 trans = AffineTransform (numbers[0], numbers[2], numbers[4],
1753 numbers[1], numbers[3], numbers[5]);
1754 }
1755 else if (t.startsWithIgnoreCase ("translate"))
1756 {
1758 }
1759 else if (t.startsWithIgnoreCase ("scale"))
1760 {
1761 trans = AffineTransform::scale (numbers[0], numbers[tokens.size() > 1 ? 1 : 0]);
1762 }
1763 else if (t.startsWithIgnoreCase ("rotate"))
1764 {
1766 }
1767 else if (t.startsWithIgnoreCase ("skewX"))
1768 {
1770 }
1771 else if (t.startsWithIgnoreCase ("skewY"))
1772 {
1774 }
1775
1776 result = trans.followedBy (result);
1777 t = t.fromFirstOccurrenceOf (")", false, false).trimStart();
1778 }
1779
1780 return result;
1781 }
1782
1783 static void endpointToCentreParameters (double x1, double y1,
1784 double x2, double y2,
1785 double angle,
1786 bool largeArc, bool sweep,
1787 double& rx, double& ry,
1788 double& centreX, double& centreY,
1789 double& startAngle, double& deltaAngle) noexcept
1790 {
1791 const double midX = (x1 - x2) * 0.5;
1792 const double midY = (y1 - y2) * 0.5;
1793
1794 const double cosAngle = std::cos (angle);
1795 const double sinAngle = std::sin (angle);
1796 const double xp = cosAngle * midX + sinAngle * midY;
1797 const double yp = cosAngle * midY - sinAngle * midX;
1798 const double xp2 = xp * xp;
1799 const double yp2 = yp * yp;
1800
1801 double rx2 = rx * rx;
1802 double ry2 = ry * ry;
1803
1804 const double s = (xp2 / rx2) + (yp2 / ry2);
1805 double c;
1806
1807 if (s <= 1.0)
1808 {
1809 c = std::sqrt (jmax (0.0, ((rx2 * ry2) - (rx2 * yp2) - (ry2 * xp2))
1810 / (( rx2 * yp2) + (ry2 * xp2))));
1811
1812 if (largeArc == sweep)
1813 c = -c;
1814 }
1815 else
1816 {
1817 const double s2 = std::sqrt (s);
1818 rx *= s2;
1819 ry *= s2;
1820 c = 0;
1821 }
1822
1823 const double cpx = ((rx * yp) / ry) * c;
1824 const double cpy = ((-ry * xp) / rx) * c;
1825
1826 centreX = ((x1 + x2) * 0.5) + (cosAngle * cpx) - (sinAngle * cpy);
1827 centreY = ((y1 + y2) * 0.5) + (sinAngle * cpx) + (cosAngle * cpy);
1828
1829 const double ux = (xp - cpx) / rx;
1830 const double uy = (yp - cpy) / ry;
1831 const double vx = (-xp - cpx) / rx;
1832 const double vy = (-yp - cpy) / ry;
1833
1834 const double length = juce_hypot (ux, uy);
1835
1836 startAngle = acos (jlimit (-1.0, 1.0, ux / length));
1837
1838 if (uy < 0)
1840
1842
1843 deltaAngle = acos (jlimit (-1.0, 1.0, ((ux * vx) + (uy * vy))
1844 / (length * juce_hypot (vx, vy))));
1845
1846 if ((ux * vy) - (uy * vx) < 0)
1848
1849 if (sweep)
1850 {
1851 if (deltaAngle < 0)
1853 }
1854 else
1855 {
1856 if (deltaAngle > 0)
1858 }
1859
1861 }
1862
1863 SVGState (const SVGState&) = default;
1864 SVGState& operator= (const SVGState&) = delete;
1865};
1866
1867
1868//==============================================================================
1870{
1871 if (! svgDocument.hasTagNameIgnoringNamespace ("svg"))
1872 return {};
1873
1874 SVGState state (&svgDocument);
1875 return std::unique_ptr<Drawable> (state.parseSVGElement (SVGState::XmlPath (&svgDocument, {})));
1876}
1877
1879{
1880 if (auto xml = parseXMLIfTagMatches (svgFile, "svg"))
1881 return createFromSVG (*xml);
1882
1883 return {};
1884}
1885
1887{
1888 SVGState state (nullptr);
1889 Path p;
1890 state.parsePathString (p, svgPath);
1891 return p;
1892}
1893
1894} // namespace juce
acos
T align(T... args)
Represents a 2D affine-transformation matrix.
static AffineTransform scale(float factorX, float factorY) noexcept
Returns a new transform which is a re-scale about the origin.
AffineTransform followedBy(const AffineTransform &other) const noexcept
Returns the result of concatenating another transformation after this one.
static AffineTransform translation(float deltaX, float deltaY) noexcept
Returns a new transform which is a translation.
static AffineTransform shear(float shearX, float shearY) noexcept
Returns a shear transform, centred around the origin (0, 0).
static AffineTransform rotation(float angleInRadians) noexcept
Returns a new transform which is a rotation about (0, 0).
size_t length() const noexcept
Returns the number of characters in this string.
static bool isDigit(char character) noexcept
Checks whether a character is a digit.
static int compareIgnoreCaseUpTo(CharPointerType1 s1, CharPointerType2 s2, int maxChars) noexcept
Compares two null-terminated character strings, using a case-independent match.
static bool isLetter(char character) noexcept
Checks whether a character is alphabetic.
static int getHexDigitValue(juce_wchar digit) noexcept
Returns 0 to 16 for '0' to 'F", or -1 for characters that aren't a legal hex digit.
static CharPointerType1 find(CharPointerType1 textToSearch, const CharPointerType2 substringToLookFor) noexcept
Returns a pointer to the first occurrence of a substring in a string.
Describes the layout and colours that should be used to paint a colour gradient.
static Colour fromHSL(float hue, float saturation, float lightness, float alpha) noexcept
Creates a colour using floating point hue, saturation, lightness and alpha values.
Colour withMultipliedAlpha(float alphaMultiplier) const noexcept
Returns a colour that's the same colour as this one, but with a modified alpha value.
A drawable object which acts as a container for a set of other Drawables.
The base class for objects which can draw themselves, e.g.
static std::unique_ptr< Drawable > createFromSVGFile(const File &svgFile)
Attempts to parse an SVG (Scalable Vector Graphics) document from a file, and to turn this into a Dra...
static Path parseSVGPath(const String &svgPath)
Parses an SVG path string and returns it.
static std::unique_ptr< Drawable > createFromSVG(const XmlElement &svgDocument)
Attempts to parse an SVG (Scalable Vector Graphics) document, and to turn this into a Drawable tree.
Represents a local file or directory.
Definition juce_File.h:45
Represents a colour or fill pattern to use for rendering paths.
static Image loadFrom(InputStream &input)
Tries to load an image from a stream.
JointStyle
The type of shape to use for the corners between two adjacent line segments.
@ curved
Indicates that corners should be drawn as rounded-off.
@ beveled
Indicates that corners should be drawn with a line flattening their outside edge.
@ mitered
Indicates that corners should be drawn with sharp joints.
EndCapStyle
The type shape to use for the ends of lines.
@ rounded
Ends of lines are rounded-off with a circular shape.
@ square
Ends of lines are flat, but stick out beyond the end point for half the thickness of the stroke.
@ butt
Ends of lines are flat and don't extend beyond the end point.
@ closePath
Indicates that the sub-path is being closed.
Definition juce_Path.h:747
A path is a sequence of lines and curves that may either form a closed shape or be open-ended.
Definition juce_Path.h:65
A pair of (x, y) coordinates.
Definition juce_Point.h:42
ValueType x
The point's X coordinate.
Definition juce_Point.h:251
Defines the method used to position some kind of rectangular object within a rectangular viewport.
AffineTransform getTransformToFit(const Rectangle< float > &source, const Rectangle< float > &destination) const noexcept
Returns the transform that should be applied to these source coordinates to fit them into the destina...
@ fillDestination
If this flag is set, then the source rectangle will be resized so that it is the minimum size to comp...
@ stretchToFit
If this flag is set, then the source rectangle will be resized to completely fill the destination rec...
Manages a rectangle and allows geometric operations to be performed on it.
The JUCE String class!
Definition juce_String.h:53
CharPointerType getCharPointer() const noexcept
Returns the character pointer currently being used to store this string.
String trim() const
Returns a copy of this string with any whitespace characters removed from the start and end.
String unquoted() const
Removes quotation marks from around the string, (if there are any).
CharPointer_UTF8 CharPointerType
This is the character encoding type used internally to store the string.
Used to build a tree of elements representing an XML document.
bool compareAttribute(StringRef attributeName, StringRef stringToCompareAgainst, bool ignoreCase=false) const noexcept
Compares the value of a named attribute with a value passed-in.
bool hasAttribute(StringRef attributeName) const noexcept
Checks whether the element contains an attribute with a certain name.
bool hasTagName(StringRef possibleTagName) const noexcept
Tests whether this element has a particular tag name.
Iterator< GetNextElement > getChildIterator() const
Allows iterating the children of an XmlElement using range-for syntax.
const String & getStringAttribute(StringRef attributeName) const noexcept
Returns the value of a named attribute.
bool hasTagNameIgnoringNamespace(StringRef possibleTagName) const
Tests whether this element has a particular tag name, ignoring any XML namespace prefix.
T cos(T... args)
T emplace_back(T... args)
T fill(T... args)
fmod
T format(T... args)
T hex(T... args)
T isinf(T... args)
T isnan(T... args)
#define jassert(expression)
Platform-independent assertion macro.
typedef int
link
T make_optional(T... args)
T move(T... args)
JUCE_API Colour findColourForName(const String &colourName, Colour defaultColour)
Attempts to look up a string in the list of known colour names, and return the appropriate colour.
JUCE Namespace.
wchar_t juce_wchar
A platform-independent 32-bit unicode character type.
constexpr bool approximatelyEqual(Type a, Type b, Tolerance< Type > tolerance=Tolerance< Type >{} .withAbsolute(std::numeric_limits< Type >::min()) .withRelative(std::numeric_limits< Type >::epsilon()))
Returns true if the two floating-point numbers are approximately equal.
std::unique_ptr< XmlElement > parseXMLIfTagMatches(const String &textToParse, StringRef requiredTag)
Does an inexpensive check to see whether the top-level element has the given tag name,...
constexpr Type jmax(Type a, Type b)
Returns the larger of two values.
RangedDirectoryIterator end(const RangedDirectoryIterator &)
Returns a default-constructed sentinel value.
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
Constrains a value to keep it within a given range.
Type juce_hypot(Type a, Type b) noexcept
Using juce_hypot is easier than dealing with the different types of hypot function that are provided ...
Type unalignedPointerCast(void *ptr) noexcept
Casts a pointer to another type via void*, which suppresses the cast-align warning which sometimes ar...
Definition juce_Memory.h:88
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
Returns true if a value is at least zero, and also below a specified upper limit.
unsigned int uint32
A platform-independent 32-bit unsigned integer type.
constexpr FloatType degreesToRadians(FloatType degrees) noexcept
Converts an angle in degrees to radians.
unsigned char uint8
A platform-independent 8-bit unsigned integer type.
int roundToInt(const FloatType value) noexcept
Fast floating-point-to-integer conversion.
constexpr int numElementsInArray(Type(&)[N]) noexcept
Handy function for getting the number of elements in a simple const C array.
T next(T... args)
T reset(T... args)
T sin(T... args)
T sqrt(T... args)
static bool convertFromBase64(OutputStream &binaryOutput, StringRef base64TextInput)
Converts a base-64 string back to its binary representation.
static constexpr FloatType halfPi
A predefined value for Pi / 2.
static constexpr FloatType twoPi
A predefined value for 2 * Pi.
T tan(T... args)
T transform(T... args)
y1