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