34 : originalFile (svgFile), topLevelXml (topLevel,
nullptr)
42 const XmlElement& operator*()
const noexcept {
jassert (xml !=
nullptr);
return *xml; }
43 const XmlElement* operator->()
const noexcept {
return xml; }
46 template <
typename OperationType>
47 bool applyOperationToChildWithID (
const String&
id, OperationType& op)
const
57 if (child.applyOperationToChildWithID (
id, op))
74 bool operator() (
const XmlPath& xmlPath)
const
76 return state->parsePathElement (xmlPath, *targetPath);
86 bool operator() (
const XmlPath& xmlPath)
88 target = state->parseText (xmlPath,
true, transform);
89 return target !=
nullptr;
99 bool operator() (
const XmlPath& xmlPath)
101 target = state->parseImage (xmlPath,
true, transform);
102 return target !=
nullptr;
111 bool operator() (
const XmlPath& xmlPath)
113 return state->applyClipPath (*target, xmlPath);
122 bool operator() (
const XmlPath& xml)
const
124 return state->addGradientStopsIn (*gradient, xml);
135 bool operator() (
const XmlPath& xml)
140 fillType = state->getGradientFillType (xml, *path, opacity);
152 setCommonAttributes (*drawable, xml);
157 newState.addTransform (xml);
162 if (newState.width <= 0) newState.width = 100;
163 if (newState.height <= 0) newState.height = 100;
173 if (parseCoords (viewParams, viewboxXY,
true)
174 && parseCoords (viewParams, vwh,
true)
178 newState.viewBoxW = vwh.
x;
179 newState.viewBoxH = vwh.
y;
183 if (placementFlags != 0)
196 newState.parseSubElements (xml, *drawable);
198 drawable->setContentArea ({ viewboxXY.
x, viewboxXY.
y, newState.viewBoxW, newState.viewBoxH });
199 drawable->resetBoundingBoxToContentArea();
205 void parsePathString (Path& path,
const String& pathString)
const
207 auto d = pathString.getCharPointer().findEndOfWhitespace();
209 Point<float> subpathStart, last, last2, p1, p2, p3;
210 juce_wchar currentCommand = 0, previousCommand = 0;
211 bool isRelative =
true;
214 while (! d.isEmpty())
216 if (CharPointer_ASCII (
"MmLlHhVvCcSsQqTtAaZz").indexOf (*d) >= 0)
218 currentCommand = d.getAndAdvance();
219 isRelative = currentCommand >=
'a';
222 switch (currentCommand)
228 if (parseCoordsOrSkip (d, p1,
false))
233 if (currentCommand ==
'M' || currentCommand ==
'm')
236 path.startNewSubPath (p1);
237 currentCommand =
'l';
248 if (parseCoord (d, p1.x,
false, Axis::x))
253 path.lineTo (p1.x, last.y);
266 if (parseCoord (d, p1.y,
false, Axis::y))
271 path.lineTo (last.x, p1.y);
284 if (parseCoordsOrSkip (d, p1,
false)
285 && parseCoordsOrSkip (d, p2,
false)
286 && parseCoordsOrSkip (d, p3,
false))
295 path.cubicTo (p1, p2, p3);
304 if (parseCoordsOrSkip (d, p1,
false)
305 && parseCoordsOrSkip (d, p3,
false))
315 if (CharPointer_ASCII (
"CcSs").indexOf (previousCommand) >= 0)
316 p2 += (last - last2);
318 path.cubicTo (p2, p1, p3);
327 if (parseCoordsOrSkip (d, p1,
false)
328 && parseCoordsOrSkip (d, p2,
false))
336 path.quadraticTo (p1, p2);
345 if (parseCoordsOrSkip (d, p1,
false))
352 if (CharPointer_ASCII (
"QqTt").indexOf (previousCommand) >= 0)
353 p2 += (last - last2);
355 path.quadraticTo (p2, p1);
364 if (parseCoordsOrSkip (d, p1,
false))
367 bool flagValue =
false;
369 if (parseNextNumber (d, num,
false))
373 if (parseNextFlag (d, flagValue))
375 auto largeArc = flagValue;
377 if (parseNextFlag (d, flagValue))
379 auto sweep = flagValue;
381 if (parseCoordsOrSkip (d, p2,
false))
388 double centreX, centreY, startAngle, deltaAngle;
389 double rx = p1.x, ry = p1.y;
391 endpointToCentreParameters (last.x, last.y, p2.x, p2.y,
392 angle, largeArc, sweep,
393 rx, ry, centreX, centreY,
394 startAngle, deltaAngle);
396 path.addCentredArc ((
float) centreX, (
float) centreY,
397 (
float) rx, (
float) ry,
398 angle, (
float) startAngle, (
float) (startAngle + deltaAngle),
417 last = last2 = subpathStart;
418 d.incrementToEndOfWhitespace();
419 currentCommand =
'M';
430 previousCommand = currentCommand;
435 if (path.getCurrentPosition() == subpathStart)
441 const File originalFile;
442 const XmlPath topLevelXml;
443 float width = 512, height = 512, viewBoxW = 0, viewBoxH = 0;
444 AffineTransform transform;
447 static bool isNone (
const String& s)
noexcept
449 return s.equalsIgnoreCase (
"none");
452 static void setCommonAttributes (Drawable& d,
const XmlPath& xml)
454 auto compID = xml->getStringAttribute (
"id");
456 d.setComponentID (compID);
458 if (isNone (xml->getStringAttribute (
"display")))
459 d.setVisible (
false);
463 void parseSubElements (
const XmlPath& xml, DrawableComposite& parentDrawable,
bool shouldParseClip =
true)
465 for (
auto* e : xml->getChildIterator())
467 const XmlPath child (xml.getChild (e));
469 if (
auto* drawable = parseSubElement (child))
471 parentDrawable.addChildComponent (drawable);
473 if (! isNone (getStyleAttribute (child,
"display")))
474 drawable->setVisible (
true);
477 parseClipPath (child, *drawable);
482 Drawable* parseSubElement (
const XmlPath& xml)
486 if (parsePathElement (xml, path))
487 return parseShape (xml, path);
490 auto tag = xml->getTagNameWithoutNamespace();
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);
505 bool parsePathElement (
const XmlPath& xml, Path& path)
const
507 auto tag = xml->getTagNameWithoutNamespace();
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); }
521 DrawableComposite* parseSwitch (
const XmlPath& xml)
523 if (
auto* group = xml->getChildByName (
"g"))
524 return parseGroupElement (xml.getChild (group),
true);
529 DrawableComposite* parseGroupElement (
const XmlPath& xml,
bool shouldParseTransform)
531 if (shouldParseTransform && xml->hasAttribute (
"transform"))
533 SVGState newState (*
this);
534 newState.addTransform (xml);
536 return newState.parseGroupElement (xml,
false);
539 auto* drawable =
new DrawableComposite();
540 setCommonAttributes (*drawable, xml);
541 parseSubElements (xml, *drawable);
543 drawable->resetContentAreaAndBoundingBoxToFitChildren();
547 DrawableComposite* parseLinkElement (
const XmlPath& xml)
549 return parseGroupElement (xml,
true);
553 void parsePath (
const XmlPath& xml, Path& path)
const
555 parsePathString (path, xml->getStringAttribute (
"d"));
557 if (getStyleAttribute (xml,
"fill-rule").trim().equalsIgnoreCase (
"evenodd"))
558 path.setUsingNonZeroWinding (
false);
561 void parseRect (
const XmlPath& xml, Path& rect)
const
563 const bool hasRX = xml->hasAttribute (
"rx");
564 const bool hasRY = xml->hasAttribute (
"ry");
568 float rx = getCoordLength (xml,
"rx", viewBoxW);
569 float ry = getCoordLength (xml,
"ry", viewBoxH);
576 rect.addRoundedRectangle (getCoordLength (xml,
"x", viewBoxW),
577 getCoordLength (xml,
"y", viewBoxH),
578 getCoordLength (xml,
"width", viewBoxW),
579 getCoordLength (xml,
"height", viewBoxH),
584 rect.addRectangle (getCoordLength (xml,
"x", viewBoxW),
585 getCoordLength (xml,
"y", viewBoxH),
586 getCoordLength (xml,
"width", viewBoxW),
587 getCoordLength (xml,
"height", viewBoxH));
591 void parseCircle (
const XmlPath& xml, Path& circle)
const
593 auto cx = getCoordLength (xml,
"cx", viewBoxW);
594 auto cy = getCoordLength (xml,
"cy", viewBoxH);
595 auto radius = getCoordLength (xml,
"r", viewBoxW);
597 circle.addEllipse (cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
600 void parseEllipse (
const XmlPath& xml, Path& ellipse)
const
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);
607 ellipse.addEllipse (cx - radiusX, cy - radiusY, radiusX * 2.0f, radiusY * 2.0f);
610 void parseLine (
const XmlPath& xml, Path& line)
const
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);
617 line.startNewSubPath (x1, y1);
618 line.lineTo (x2, y2);
621 void parsePolygon (
const XmlPath& xml,
bool isPolyline, Path& path)
const
623 auto pointsAtt = xml->getStringAttribute (
"points");
624 auto points = pointsAtt.getCharPointer();
627 if (parseCoords (points, p,
true))
629 Point<float> first (p), last;
631 path.startNewSubPath (first);
633 while (parseCoords (points, p,
true))
639 if ((! isPolyline) || first == last)
644 static String getLinkedID (
const XmlPath& xml)
646 auto link = xml->getStringAttribute (
"xlink:href");
648 if (
link.startsWithChar (
'#'))
649 return link.substring (1);
654 bool parseUsePath (
const XmlPath& xml, Path& path)
const
656 auto linkedID = getLinkedID (xml);
658 if (linkedID.isNotEmpty())
660 UsePathOp op = {
this, &path };
661 return topLevelXml.applyOperationToChildWithID (linkedID, op);
667 Drawable* parseUseOther (
const XmlPath& xml)
const
669 if (
auto* drawableText = parseText (xml,
false,
nullptr))
return drawableText;
670 if (
auto* drawableImage = parseImage (xml,
false))
return drawableImage;
675 static String parseURL (
const String& str)
677 if (str.startsWithIgnoreCase (
"url"))
678 return str.fromFirstOccurrenceOf (
"#",
false,
false)
679 .upToLastOccurrenceOf (
")",
false,
false).trim();
685 Drawable* parseShape (
const XmlPath& xml, Path& path,
686 bool shouldParseTransform =
true,
687 AffineTransform* additonalTransform =
nullptr)
const
689 if (shouldParseTransform && xml->hasAttribute (
"transform"))
691 SVGState newState (*
this);
692 newState.addTransform (xml);
694 return newState.parseShape (xml, path,
false, additonalTransform);
697 auto dp =
new DrawablePath();
698 setCommonAttributes (*dp, xml);
699 dp->setFill (Colours::transparentBlack);
701 path.applyTransform (transform);
703 if (additonalTransform !=
nullptr)
704 path.applyTransform (*additonalTransform);
708 dp->setFill (getPathFillType (path, xml,
"fill",
709 getStyleAttribute (xml,
"fill-opacity"),
710 getStyleAttribute (xml,
"opacity"),
711 pathContainsClosedSubPath (path) ? Colours::black
712 : Colours::transparentBlack));
714 auto strokeType = getStyleAttribute (xml,
"stroke");
716 if (strokeType.isNotEmpty() && ! isNone (strokeType))
718 dp->setStrokeFill (getPathFillType (path, xml,
"stroke",
719 getStyleAttribute (xml,
"stroke-opacity"),
720 getStyleAttribute (xml,
"opacity"),
721 Colours::transparentBlack));
723 dp->setStrokeType (getStrokeFor (xml));
726 auto strokeDashArray = getStyleAttribute (xml,
"stroke-dasharray");
728 if (strokeDashArray.isNotEmpty())
729 parseDashArray (strokeDashArray, *dp);
734 static bool pathContainsClosedSubPath (
const Path& path)
noexcept
736 for (Path::Iterator iter (path); iter.next();)
743 void parseDashArray (
const String& dashList, DrawablePath& dp)
const
745 if (dashList.equalsIgnoreCase (
"null") || isNone (dashList))
748 Array<float> dashLengths;
750 for (
auto t = dashList.getCharPointer();;)
753 if (! parseCoord (t, value,
true, Axis::x))
756 dashLengths.add (value);
758 t.incrementToEndOfWhitespace();
764 if (dashLengths.size() > 0)
766 auto* dashes = dashLengths.getRawDataPointer();
768 for (
int i = 0; i < dashLengths.size(); ++i)
772 if (dashLengths.size() == 1)
775 const float nonZeroLength = 0.001f;
776 dashes[i] = nonZeroLength;
778 const int pairedIndex = i ^ 1;
781 && dashes[pairedIndex] > nonZeroLength)
782 dashes[pairedIndex] -= nonZeroLength;
786 dp.setDashLengths (dashLengths);
790 bool parseClipPath (
const XmlPath& xml, Drawable& d)
792 const String clipPath (getStyleAttribute (xml,
"clip-path"));
794 if (clipPath.isNotEmpty())
796 auto urlID = parseURL (clipPath);
798 if (urlID.isNotEmpty())
800 GetClipPathOp op = {
this, &d };
801 return topLevelXml.applyOperationToChildWithID (urlID, op);
808 bool applyClipPath (Drawable& target,
const XmlPath& xmlPath)
810 if (xmlPath->hasTagNameIgnoringNamespace (
"clipPath"))
814 parseSubElements (xmlPath, *drawableClipPath,
false);
816 if (drawableClipPath->getNumChildComponents() > 0)
818 setCommonAttributes (*drawableClipPath, xmlPath);
819 target.setClipPath (std::move (drawableClipPath));
827 bool addGradientStopsIn (ColourGradient& cg,
const XmlPath& fillXml)
const
831 if (fillXml.xml !=
nullptr)
833 for (
auto* e : fillXml->getChildWithTagNameIterator (
"stop"))
835 auto col = parseColour (fillXml.getChild (e),
"stop-color", Colours::black);
837 auto opacity = getStyleAttribute (fillXml.getChild (e),
"stop-opacity",
"1");
838 col = col.withMultipliedAlpha (
jlimit (0.0f, 1.0f, parseSafeFloat (opacity)));
840 auto offset = parseSafeFloat (e->getStringAttribute (
"offset"));
842 if (e->getStringAttribute (
"offset").containsChar (
'%'))
845 cg.addColour (
jlimit (0.0f, 1.0f, offset), col);
853 FillType getGradientFillType (
const XmlPath& fillXml,
855 const float opacity)
const
857 ColourGradient gradient;
860 auto linkedID = getLinkedID (fillXml);
862 if (linkedID.isNotEmpty())
864 SetGradientStopsOp op = {
this, &gradient, };
865 topLevelXml.applyOperationToChildWithID (linkedID, op);
869 addGradientStopsIn (gradient, fillXml);
871 if (
int numColours = gradient.getNumColours())
873 if (gradient.getColourPosition (0) > 0)
874 gradient.addColour (0.0, gradient.getColour (0));
876 if (gradient.getColourPosition (numColours - 1) < 1.0)
877 gradient.addColour (1.0, gradient.getColour (numColours - 1));
881 gradient.addColour (0.0, Colours::black);
882 gradient.addColour (1.0, Colours::black);
886 gradient.multiplyOpacity (opacity);
888 jassert (gradient.getNumColours() > 0);
890 gradient.isRadial = fillXml->hasTagNameIgnoringNamespace (
"radialGradient");
892 float gradientWidth = viewBoxW;
893 float gradientHeight = viewBoxH;
897 const bool userSpace = fillXml->getStringAttribute (
"gradientUnits").equalsIgnoreCase (
"userSpaceOnUse");
901 auto bounds = path.getBounds();
904 gradientWidth = bounds.getWidth();
905 gradientHeight = bounds.getHeight();
908 if (gradient.isRadial)
911 gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute (
"cx",
"50%"), gradientWidth),
912 dy + getCoordLength (fillXml->getStringAttribute (
"cy",
"50%"), gradientHeight));
914 gradient.point1.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute (
"cx",
"50%"), 1.0f),
915 dy + gradientHeight * getCoordLength (fillXml->getStringAttribute (
"cy",
"50%"), 1.0f));
917 auto radius = getCoordLength (fillXml->getStringAttribute (
"r",
"50%"), gradientWidth);
918 gradient.point2 = gradient.point1 + Point<float> (radius, 0.0f);
926 gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute (
"x1",
"0%"), gradientWidth),
927 dy + getCoordLength (fillXml->getStringAttribute (
"y1",
"0%"), gradientHeight));
929 gradient.point2.setXY (dx + getCoordLength (fillXml->getStringAttribute (
"x2",
"100%"), gradientWidth),
930 dy + getCoordLength (fillXml->getStringAttribute (
"y2",
"0%"), gradientHeight));
934 gradient.point1.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute (
"x1",
"0%"), 1.0f),
935 dy + gradientHeight * getCoordLength (fillXml->getStringAttribute (
"y1",
"0%"), 1.0f));
937 gradient.point2.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute (
"x2",
"100%"), 1.0f),
938 dy + gradientHeight * getCoordLength (fillXml->getStringAttribute (
"y2",
"0%"), 1.0f));
941 if (gradient.point1 == gradient.point2)
942 return Colour (gradient.getColour (gradient.getNumColours() - 1));
945 FillType type (gradient);
947 auto gradientTransform = parseTransform (fillXml->getStringAttribute (
"gradientTransform"));
949 if (gradient.isRadial)
951 type.transform = gradientTransform;
957 auto perpendicular = Point<float> (gradient.point2.y - gradient.point1.y,
958 gradient.point1.x - gradient.point2.x)
959 .transformedBy (gradientTransform.withAbsoluteTranslation (0, 0));
961 auto newGradPoint1 = gradient.point1.transformedBy (gradientTransform);
962 auto newGradPoint2 = gradient.point2.transformedBy (gradientTransform);
966 const float scale = perpendicular.getDotProduct (newGradPoint2 - newGradPoint1)
967 / perpendicular.getDotProduct (perpendicular);
969 type.gradient->point1 = newGradPoint1;
970 type.gradient->point2 = newGradPoint2 - perpendicular * scale;
976 FillType getPathFillType (
const Path& path,
978 StringRef fillAttribute,
979 const String& fillOpacity,
980 const String& overallOpacity,
981 const Colour defaultColour)
const
983 float opacity = 1.0f;
985 if (overallOpacity.isNotEmpty())
986 opacity =
jlimit (0.0f, 1.0f, parseSafeFloat (overallOpacity));
988 if (fillOpacity.isNotEmpty())
989 opacity *=
jlimit (0.0f, 1.0f, parseSafeFloat (fillOpacity));
991 String
fill (getStyleAttribute (xml, fillAttribute));
992 String urlID = parseURL (fill);
994 if (urlID.isNotEmpty())
996 GetFillTypeOp op = {
this, &path, opacity, FillType() };
998 if (topLevelXml.applyOperationToChildWithID (urlID, op))
1003 return Colours::transparentBlack;
1024 float getStrokeWidth (
const String& strokeWidth)
const noexcept
1027 return transformScale * getCoordLength (strokeWidth, viewBoxW);
1030 PathStrokeType getStrokeFor (
const XmlPath& xml)
const
1032 return PathStrokeType (getStrokeWidth (getStyleAttribute (xml,
"stroke-width",
"1")),
1033 getJointStyle (getStyleAttribute (xml,
"stroke-linejoin")),
1034 getEndCapStyle (getStyleAttribute (xml,
"stroke-linecap")));
1038 Drawable* useText (
const XmlPath& xml)
const
1041 parseSafeFloat (xml->getStringAttribute (
"y")));
1043 UseTextOp op = {
this, &translation,
nullptr };
1045 auto linkedID = getLinkedID (xml);
1047 if (linkedID.isNotEmpty())
1048 topLevelXml.applyOperationToChildWithID (linkedID, op);
1065 class StringLayoutState
1068 StringLayoutState (StringLayoutState* parentIn, Array<float> xIn, Array<float> yIn)
1069 : parent (parentIn),
1075 Point<float> getNextStartingPos()
const
1077 if (parent !=
nullptr)
1078 return parent->getNextStartingPos();
1080 return nextStartingPos;
1083 void setNextStartingPos (Point<float> newPos)
1085 nextStartingPos = newPos;
1087 if (parent !=
nullptr)
1088 parent->setNextStartingPos (newPos);
1096 if (parent !=
nullptr)
1098 auto [parentX, parentY] = parent->popCoords();
1100 if (! x.has_value())
1103 if (! y.has_value())
1110 bool hasMoreCoords()
const
1112 if (! xCoords.isEmpty() || ! yCoords.isEmpty())
1115 if (parent !=
nullptr)
1116 return parent->hasMoreCoords();
1122 StringLayoutState* parent =
nullptr;
1123 Point<float> nextStartingPos;
1124 Array<float> xCoords, yCoords;
1127 Drawable* parseText (
const XmlPath& xml,
bool shouldParseTransform,
1128 AffineTransform* additonalTransform,
1129 StringLayoutState* parentLayoutState =
nullptr)
const
1131 if (shouldParseTransform && xml->hasAttribute (
"transform"))
1133 SVGState newState (*
this);
1134 newState.addTransform (xml);
1136 return newState.parseText (xml,
false, additonalTransform);
1139 if (xml->hasTagName (
"use"))
1140 return useText (xml);
1142 if (! xml->hasTagName (
"text") && ! xml->hasTagNameIgnoringNamespace (
"tspan"))
1147 StringLayoutState layoutState { parentLayoutState,
1148 getCoordList (*xml, Axis::x),
1149 getCoordList (*xml, Axis::y) };
1151 auto font = getFont (xml);
1152 auto anchorStr = getStyleAttribute (xml,
"text-anchor");
1154 auto dc =
new DrawableComposite();
1155 setCommonAttributes (*dc, xml);
1157 for (
auto* e : xml->getChildIterator())
1159 if (e->isTextElement())
1161 auto fullText = e->getText();
1163 const auto subtextElements = [&]
1167 for (
auto it = fullText.begin(),
end = fullText.end(); it !=
end;)
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);
1178 for (
const auto& [text, optX, optY] : subtextElements)
1180 auto dt =
new DrawableText();
1181 dc->addAndMakeVisible (dt);
1184 dt->setFont (font,
true);
1186 if (additonalTransform !=
nullptr)
1187 dt->setDrawableTransform (
transform.followedBy (*additonalTransform));
1189 dt->setDrawableTransform (transform);
1191 dt->setColour (parseColour (xml,
"fill", Colours::black)
1192 .withMultipliedAlpha (parseSafeFloat (getStyleAttribute (xml,
"fill-opacity",
"1"))));
1194 const auto x = optX.value_or (layoutState.getNextStartingPos().getX());
1195 const auto y = optY.value_or (layoutState.getNextStartingPos().getY());
1197 Rectangle<float> bounds (x, y - font.getAscent(),
1198 font.getStringWidthFloat (text), font.getHeight());
1200 if (anchorStr ==
"middle") bounds.setX (bounds.getX() - bounds.getWidth() / 2.0f);
1201 else if (anchorStr ==
"end") bounds.setX (bounds.getX() - bounds.getWidth());
1203 dt->setBoundingBox (bounds);
1205 layoutState.setNextStartingPos ({ bounds.getRight(), y });
1208 else if (e->hasTagNameIgnoringNamespace (
"tspan"))
1210 dc->addAndMakeVisible (parseText (xml.getChild (e),
true,
nullptr, &layoutState));
1217 Font getFont (
const XmlPath& xml)
const
1220 auto family = getStyleAttribute (xml,
"font-family").
unquoted();
1222 if (family.isNotEmpty())
1223 f.setTypefaceName (family);
1225 if (getStyleAttribute (xml,
"font-style").containsIgnoreCase (
"italic"))
1228 if (getStyleAttribute (xml,
"font-weight").containsIgnoreCase (
"bold"))
1231 return f.withPointHeight (getCoordLength (getStyleAttribute (xml,
"font-size",
"15"), 1.0f));
1235 Drawable* useImage (
const XmlPath& xml)
const
1238 parseSafeFloat (xml->getStringAttribute (
"y")));
1240 UseImageOp op = {
this, &translation,
nullptr };
1242 auto linkedID = getLinkedID (xml);
1244 if (linkedID.isNotEmpty())
1245 topLevelXml.applyOperationToChildWithID (linkedID, op);
1250 Drawable* parseImage (
const XmlPath& xml,
bool shouldParseTransform,
1251 AffineTransform* additionalTransform =
nullptr)
const
1253 if (shouldParseTransform && xml->hasAttribute (
"transform"))
1255 SVGState newState (*
this);
1256 newState.addTransform (xml);
1258 return newState.parseImage (xml,
false, additionalTransform);
1261 if (xml->hasTagName (
"use"))
1262 return useImage (xml);
1264 if (! xml->hasTagName (
"image"))
1267 auto link = xml->getStringAttribute (
"xlink:href");
1270 MemoryOutputStream imageStream;
1272 if (
link.startsWith (
"data:"))
1274 const auto indexOfComma =
link.indexOf (
",");
1275 auto format =
link.substring (5, indexOfComma).trim();
1276 auto indexOfSemi =
format.indexOf (
";");
1278 if (
format.substring (indexOfSemi + 1).trim().equalsIgnoreCase (
"base64"))
1280 auto mime =
format.substring (0, indexOfSemi).trim();
1282 if (mime.equalsIgnoreCase (
"image/png") || mime.equalsIgnoreCase (
"image/jpeg"))
1284 auto base64text =
link.substring (indexOfComma + 1).removeCharacters (
"\t\n\r ");
1287 inputStream.
reset (
new MemoryInputStream (imageStream.getData(), imageStream.getDataSize(),
false));
1293 auto linkedFile = originalFile.getParentDirectory().getChildFile (link);
1295 if (linkedFile.existsAsFile())
1296 inputStream = linkedFile.createInputStream();
1299 if (inputStream !=
nullptr)
1303 if (image.isValid())
1305 auto* di =
new DrawableImage();
1307 setCommonAttributes (*di, xml);
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()))));
1314 di->setImage (image.rescaled ((
int) imageBounds.getWidth(),
1315 (
int) imageBounds.getHeight()));
1317 di->setTransformToFit (imageBounds, RectanglePlacement (parsePlacementFlags (xml->getStringAttribute (
"preserveAspectRatio").trim())));
1319 if (additionalTransform !=
nullptr)
1320 di->setTransform (di->getTransform().followedBy (transform).followedBy (*additionalTransform));
1322 di->setTransform (di->getTransform().followedBy (transform));
1332 void addTransform (
const XmlPath& xml)
1334 transform = parseTransform (xml->getStringAttribute (
"transform"))
1339 enum class Axis { x, y };
1345 if (! parseNextNumber (s, number, allowUnits))
1351 value = getCoordLength (number, axis == Axis::x ? viewBoxW : viewBoxH);
1357 return parseCoord (s, p.x, allowUnits, Axis::x)
1358 && parseCoord (s, p.y, allowUnits, Axis::y);
1363 if (parseCoords (s, p, allowUnits))
1366 if (! s.isEmpty()) ++s;
1370 float getCoordLength (
const String& s,
const float sizeForProportions)
const noexcept
1372 auto n = parseSafeFloat (s);
1373 auto len = s.length();
1379 auto n1 = s[len - 2];
1380 auto n2 = s[len - 1];
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;
1392 float getCoordLength (
const XmlPath& xml,
const char* attName,
const float sizeForProportions)
const noexcept
1394 return getCoordLength (xml->getStringAttribute (attName), sizeForProportions);
1397 Array<float> getCoordList (
const XmlElement& xml, Axis axis)
const
1399 const String attributeName { axis == Axis::x ?
"x" :
"y" };
1401 if (! xml.hasAttribute (attributeName))
1404 return getCoordList (xml.getStringAttribute (attributeName),
true, axis);
1407 Array<float> getCoordList (
const String& list,
bool allowUnits, Axis axis)
const
1409 auto text = list.getCharPointer();
1411 Array<float> coords;
1413 while (parseCoord (text, value, allowUnits, axis))
1419 static float parseSafeFloat (
const String& s)
1421 auto n = s.getFloatValue();
1426 void parseCSSStyle (
const XmlPath& xml)
1428 cssStyleText = xml->getAllSubText() +
"\n" + cssStyleText;
1431 void parseDefs (
const XmlPath& xml)
1433 if (
auto* style = xml->getChildByName (
"style"))
1434 parseCSSStyle (xml.getChild (style));
1441 while (! source.isEmpty())
1443 if (source.getAndAdvance() ==
'.'
1446 auto endOfName = (source + nameLength).findEndOfWhitespace();
1448 if (*endOfName ==
'{')
1451 if (*endOfName ==
',')
1459 String getStyleAttribute (
const XmlPath& xml, StringRef attributeName,
const String& defaultValue = String())
const
1461 if (xml->hasAttribute (attributeName))
1462 return xml->getStringAttribute (attributeName, defaultValue);
1464 auto styleAtt = xml->getStringAttribute (
"style");
1466 if (styleAtt.isNotEmpty())
1468 auto value = getAttributeFromStyleList (styleAtt, attributeName, {});
1470 if (value.isNotEmpty())
1473 else if (xml->hasAttribute (
"class"))
1475 for (
auto i = cssStyleText.getCharPointer();;)
1477 auto openBrace = findStyleItem (i, xml->getStringAttribute (
"class").getCharPointer());
1479 if (openBrace.isEmpty())
1484 if (closeBrace.isEmpty())
1487 auto value = getAttributeFromStyleList (String (openBrace + 1, closeBrace),
1488 attributeName, defaultValue);
1489 if (value.isNotEmpty())
1496 if (xml.parent !=
nullptr)
1497 return getStyleAttribute (*xml.parent, attributeName, defaultValue);
1499 return defaultValue;
1502 String getInheritedAttribute (
const XmlPath& xml, StringRef attributeName)
const
1504 if (xml->hasAttribute (attributeName))
1505 return xml->getStringAttribute (attributeName);
1507 if (xml.parent !=
nullptr)
1508 return getInheritedAttribute (*xml.parent, attributeName);
1513 static int parsePlacementFlags (
const String& align)
noexcept
1515 if (
align.isEmpty())
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));
1536 static String getAttributeFromStyleList (
const String& list, StringRef attributeName,
const String& defaultValue)
1542 i = list.indexOf (i, attributeName);
1547 if ((i == 0 || (i > 0 && ! isIdentifierChar (list [i - 1])))
1548 && ! isIdentifierChar (list [i + attributeName.length()]))
1550 i = list.indexOfChar (i,
':');
1555 int end = list.indexOfChar (i,
';');
1560 return list.substring (i + 1,
end).trim();
1566 return defaultValue;
1570 static bool isStartOfNumber (
juce_wchar c)
noexcept
1579 while (s.isWhitespace() || *s ==
',')
1584 if (isStartOfNumber (*s))
1598 if ((*s ==
'e' || *s ==
'E') && isStartOfNumber (s[1]))
1607 while (s.isLetter())
1616 value = String (start, s);
1618 while (s.isWhitespace() || *s ==
',')
1627 while (text.isWhitespace() || *text ==
',')
1630 if (*text !=
'0' && *text !=
'1')
1633 value = *(text++) !=
'0';
1635 while (text.isWhitespace() || *text ==
',')
1642 Colour parseColour (
const XmlPath& xml, StringRef attributeName,
const Colour defaultColour)
const
1644 auto text = getStyleAttribute (xml, attributeName);
1646 if (text.startsWithChar (
'#'))
1652 auto s = text.getCharPointer();
1654 while (numChars < 8)
1665 return Colour ((
uint8) (hex[0] * 0x11),
1666 (
uint8) (hex[1] * 0x11),
1667 (
uint8) (hex[2] * 0x11));
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]));
1675 if (text.startsWith (
"rgb") || text.startsWith (
"hsl"))
1677 auto tokens = [&text]
1679 auto openBracket = text.indexOfChar (
'(');
1680 auto closeBracket = text.indexOfChar (openBracket,
')');
1684 if (openBracket >= 3 && closeBracket > openBracket)
1686 arr.addTokens (text.substring (openBracket + 1, closeBracket),
",",
"");
1688 arr.removeEmptyStrings();
1694 auto alpha = [&tokens, &text]
1696 if ((text.startsWith (
"rgba") || text.startsWith (
"hsla")) && tokens.size() == 4)
1697 return parseSafeFloat (tokens[3]);
1702 if (text.startsWith (
"hsl"))
1704 parseSafeFloat (tokens[1]) / 100.0f,
1705 parseSafeFloat (tokens[2]) / 100.0f,
1708 if (tokens[0].containsChar (
'%'))
1709 return Colour ((
uint8)
roundToInt (2.55f * parseSafeFloat (tokens[0])),
1714 return Colour ((
uint8) tokens[0].getIntValue(),
1715 (
uint8) tokens[1].getIntValue(),
1716 (
uint8) tokens[2].getIntValue(),
1720 if (text ==
"inherit")
1722 for (
const XmlPath* p = xml.parent; p !=
nullptr; p = p->parent)
1723 if (getStyleAttribute (*p, attributeName).isNotEmpty())
1724 return parseColour (*p, attributeName, defaultColour);
1730 static AffineTransform parseTransform (String t)
1732 AffineTransform result;
1734 while (t.isNotEmpty())
1737 tokens.addTokens (t.fromFirstOccurrenceOf (
"(",
false,
false)
1738 .upToFirstOccurrenceOf (
")",
false,
false),
1741 tokens.removeEmptyStrings (
true);
1746 numbers[i] = parseSafeFloat (tokens[i]);
1748 AffineTransform trans;
1750 if (t.startsWithIgnoreCase (
"matrix"))
1752 trans = AffineTransform (numbers[0], numbers[2], numbers[4],
1753 numbers[1], numbers[3], numbers[5]);
1755 else if (t.startsWithIgnoreCase (
"translate"))
1759 else if (t.startsWithIgnoreCase (
"scale"))
1763 else if (t.startsWithIgnoreCase (
"rotate"))
1767 else if (t.startsWithIgnoreCase (
"skewX"))
1771 else if (t.startsWithIgnoreCase (
"skewY"))
1776 result = trans.followedBy (result);
1777 t = t.fromFirstOccurrenceOf (
")",
false,
false).trimStart();
1783 static void endpointToCentreParameters (
double x1,
double y1,
1784 double x2,
double y2,
1786 bool largeArc,
bool sweep,
1787 double& rx,
double& ry,
1788 double& centreX,
double& centreY,
1789 double& startAngle,
double& deltaAngle)
noexcept
1791 const double midX = (x1 - x2) * 0.5;
1792 const double midY = (
y1 - y2) * 0.5;
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;
1801 double rx2 = rx * rx;
1802 double ry2 = ry * ry;
1804 const double s = (xp2 / rx2) + (yp2 / ry2);
1809 c =
std::sqrt (
jmax (0.0, ((rx2 * ry2) - (rx2 * yp2) - (ry2 * xp2))
1810 / (( rx2 * yp2) + (ry2 * xp2))));
1812 if (largeArc == sweep)
1823 const double cpx = ((rx * yp) / ry) * c;
1824 const double cpy = ((-ry * xp) / rx) * c;
1826 centreX = ((x1 + x2) * 0.5) + (cosAngle * cpx) - (sinAngle * cpy);
1827 centreY = ((
y1 + y2) * 0.5) + (sinAngle * cpx) + (cosAngle * cpy);
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;
1836 startAngle =
acos (
jlimit (-1.0, 1.0, ux / length));
1839 startAngle = -startAngle;
1843 deltaAngle =
acos (
jlimit (-1.0, 1.0, ((ux * vx) + (uy * vy))
1846 if ((ux * vy) - (uy * vx) < 0)
1847 deltaAngle = -deltaAngle;
1863 SVGState (
const SVGState&) =
default;
1864 SVGState& operator= (
const SVGState&) =
delete;