| @@ -28,7 +28,6 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| explicit SVGState (const XmlElement* const topLevel) | explicit SVGState (const XmlElement* const topLevel) | ||||
| : topLevelXml (topLevel, nullptr), | : topLevelXml (topLevel, nullptr), | ||||
| elementX (0), elementY (0), | |||||
| width (512), height (512), | width (512), height (512), | ||||
| viewBoxW (0), viewBoxH (0) | viewBoxW (0), viewBoxH (0) | ||||
| { | { | ||||
| @@ -66,63 +65,59 @@ public: | |||||
| const XmlPath* parent; | const XmlPath* parent; | ||||
| }; | }; | ||||
| //============================================================================== | |||||
| struct UsePathOp | |||||
| { | |||||
| const SVGState* state; | |||||
| Path* targetPath; | |||||
| void operator() (const XmlPath& xmlPath) | |||||
| //============================================================================== | |||||
| struct UsePathOp | |||||
| { | { | ||||
| state->parsePathElement (xmlPath, *targetPath); | |||||
| } | |||||
| }; | |||||
| const SVGState* state; | |||||
| Path* targetPath; | |||||
| struct GetClipPathOp | |||||
| { | |||||
| const SVGState* state; | |||||
| Drawable* target; | |||||
| void operator() (const XmlPath& xmlPath) | |||||
| { | |||||
| state->parsePathElement (xmlPath, *targetPath); | |||||
| } | |||||
| }; | |||||
| void operator() (const XmlPath& xmlPath) | |||||
| struct GetClipPathOp | |||||
| { | { | ||||
| state->applyClipPath (*target, xmlPath); | |||||
| } | |||||
| }; | |||||
| const SVGState* state; | |||||
| Drawable* target; | |||||
| struct SetGradientStopsOp | |||||
| { | |||||
| const SVGState* state; | |||||
| ColourGradient* gradient; | |||||
| void operator() (const XmlPath& xmlPath) | |||||
| { | |||||
| state->applyClipPath (*target, xmlPath); | |||||
| } | |||||
| }; | |||||
| void operator() (const XmlPath& xml) | |||||
| struct SetGradientStopsOp | |||||
| { | { | ||||
| state->addGradientStopsIn (*gradient, xml); | |||||
| } | |||||
| }; | |||||
| const SVGState* state; | |||||
| ColourGradient* gradient; | |||||
| struct GetFillTypeOp | |||||
| { | |||||
| const SVGState* state; | |||||
| const Path* path; | |||||
| float opacity; | |||||
| FillType fillType; | |||||
| void operator() (const XmlPath& xml) | |||||
| { | |||||
| state->addGradientStopsIn (*gradient, xml); | |||||
| } | |||||
| }; | |||||
| void operator() (const XmlPath& xml) | |||||
| struct GetFillTypeOp | |||||
| { | { | ||||
| if (xml->hasTagNameIgnoringNamespace ("linearGradient") | |||||
| || xml->hasTagNameIgnoringNamespace ("radialGradient")) | |||||
| fillType = state->getGradientFillType (xml, *path, opacity); | |||||
| } | |||||
| }; | |||||
| const SVGState* state; | |||||
| const Path* path; | |||||
| float opacity; | |||||
| FillType fillType; | |||||
| void operator() (const XmlPath& xml) | |||||
| { | |||||
| if (xml->hasTagNameIgnoringNamespace ("linearGradient") | |||||
| || xml->hasTagNameIgnoringNamespace ("radialGradient")) | |||||
| fillType = state->getGradientFillType (xml, *path, opacity); | |||||
| } | |||||
| }; | |||||
| //============================================================================== | //============================================================================== | ||||
| Drawable* parseSVGElement (const XmlPath& xml) | Drawable* parseSVGElement (const XmlPath& xml) | ||||
| { | { | ||||
| if (! xml->hasTagNameIgnoringNamespace ("svg")) | |||||
| return nullptr; | |||||
| DrawableComposite* const drawable = new DrawableComposite(); | DrawableComposite* const drawable = new DrawableComposite(); | ||||
| setCommonAttributes (*drawable, xml); | setCommonAttributes (*drawable, xml); | ||||
| SVGState newState (*this); | SVGState newState (*this); | ||||
| @@ -130,10 +125,8 @@ public: | |||||
| if (xml->hasAttribute ("transform")) | if (xml->hasAttribute ("transform")) | ||||
| newState.addTransform (xml); | newState.addTransform (xml); | ||||
| newState.elementX = getCoordLength (xml->getStringAttribute ("x", String (newState.elementX)), viewBoxW); | |||||
| newState.elementY = getCoordLength (xml->getStringAttribute ("y", String (newState.elementY)), viewBoxH); | |||||
| newState.width = getCoordLength (xml->getStringAttribute ("width", String (newState.width)), viewBoxW); | |||||
| newState.height = getCoordLength (xml->getStringAttribute ("height", String (newState.height)), viewBoxH); | |||||
| newState.width = getCoordLength (xml->getStringAttribute ("width", String (newState.width)), viewBoxW); | |||||
| newState.height = getCoordLength (xml->getStringAttribute ("height", String (newState.height)), viewBoxH); | |||||
| if (newState.width <= 0) newState.width = 100; | if (newState.width <= 0) newState.width = 100; | ||||
| if (newState.height <= 0) newState.height = 100; | if (newState.height <= 0) newState.height = 100; | ||||
| @@ -186,21 +179,19 @@ public: | |||||
| String::CharPointerType d (pathString.getCharPointer().findEndOfWhitespace()); | String::CharPointerType d (pathString.getCharPointer().findEndOfWhitespace()); | ||||
| Point<float> subpathStart, last, last2, p1, p2, p3; | Point<float> subpathStart, last, last2, p1, p2, p3; | ||||
| juce_wchar lastCommandChar = 0; | |||||
| juce_wchar currentCommand = 0, previousCommand = 0; | |||||
| bool isRelative = true; | bool isRelative = true; | ||||
| bool carryOn = true; | bool carryOn = true; | ||||
| const CharPointer_ASCII validCommandChars ("MmLlHhVvCcSsQqTtAaZz"); | |||||
| while (! d.isEmpty()) | while (! d.isEmpty()) | ||||
| { | { | ||||
| if (validCommandChars.indexOf (*d) >= 0) | |||||
| if (CharPointer_ASCII ("MmLlHhVvCcSsQqTtAaZz").indexOf (*d) >= 0) | |||||
| { | { | ||||
| lastCommandChar = d.getAndAdvance(); | |||||
| isRelative = (lastCommandChar >= 'a' && lastCommandChar <= 'z'); | |||||
| currentCommand = d.getAndAdvance(); | |||||
| isRelative = currentCommand >= 'a'; | |||||
| } | } | ||||
| switch (lastCommandChar) | |||||
| switch (currentCommand) | |||||
| { | { | ||||
| case 'M': | case 'M': | ||||
| case 'm': | case 'm': | ||||
| @@ -211,11 +202,11 @@ public: | |||||
| if (isRelative) | if (isRelative) | ||||
| p1 += last; | p1 += last; | ||||
| if (lastCommandChar == 'M' || lastCommandChar == 'm') | |||||
| if (currentCommand == 'M' || currentCommand == 'm') | |||||
| { | { | ||||
| subpathStart = p1; | subpathStart = p1; | ||||
| path.startNewSubPath (p1); | path.startNewSubPath (p1); | ||||
| lastCommandChar = 'l'; | |||||
| currentCommand = 'l'; | |||||
| } | } | ||||
| else | else | ||||
| path.lineTo (p1); | path.lineTo (p1); | ||||
| @@ -325,7 +316,8 @@ public: | |||||
| if (isRelative) | if (isRelative) | ||||
| p1 += last; | p1 += last; | ||||
| p2 = last + (last - last2); | |||||
| p2 = CharPointer_ASCII ("QqTt").indexOf (previousCommand) >= 0 ? last + (last - last2) | |||||
| : p1; | |||||
| path.quadraticTo (p2, p1); | path.quadraticTo (p2, p1); | ||||
| last2 = p2; | last2 = p2; | ||||
| @@ -389,7 +381,7 @@ public: | |||||
| path.closeSubPath(); | path.closeSubPath(); | ||||
| last = last2 = subpathStart; | last = last2 = subpathStart; | ||||
| d = d.findEndOfWhitespace(); | d = d.findEndOfWhitespace(); | ||||
| lastCommandChar = 'M'; | |||||
| currentCommand = 'M'; | |||||
| break; | break; | ||||
| default: | default: | ||||
| @@ -399,6 +391,8 @@ public: | |||||
| if (! carryOn) | if (! carryOn) | ||||
| break; | break; | ||||
| previousCommand = currentCommand; | |||||
| } | } | ||||
| // paths that finish back at their start position often seem to be | // paths that finish back at their start position often seem to be | ||||
| @@ -410,7 +404,7 @@ public: | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| const XmlPath topLevelXml; | const XmlPath topLevelXml; | ||||
| float elementX, elementY, width, height, viewBoxW, viewBoxH; | |||||
| float width, height, viewBoxW, viewBoxH; | |||||
| AffineTransform transform; | AffineTransform transform; | ||||
| String cssStyleText; | String cssStyleText; | ||||
| @@ -485,7 +479,6 @@ private: | |||||
| { | { | ||||
| SVGState newState (*this); | SVGState newState (*this); | ||||
| newState.addTransform (xml); | newState.addTransform (xml); | ||||
| newState.parseSubElements (xml, *drawable); | newState.parseSubElements (xml, *drawable); | ||||
| } | } | ||||
| else | else | ||||
| @@ -635,8 +628,7 @@ private: | |||||
| path.applyTransform (transform); | path.applyTransform (transform); | ||||
| dp->setPath (path); | dp->setPath (path); | ||||
| dp->setFill (getPathFillType (path, | |||||
| getStyleAttribute (xml, "fill"), | |||||
| dp->setFill (getPathFillType (path, xml, "fill", | |||||
| getStyleAttribute (xml, "fill-opacity"), | getStyleAttribute (xml, "fill-opacity"), | ||||
| getStyleAttribute (xml, "opacity"), | getStyleAttribute (xml, "opacity"), | ||||
| pathContainsClosedSubPath (path) ? Colours::black | pathContainsClosedSubPath (path) ? Colours::black | ||||
| @@ -646,7 +638,7 @@ private: | |||||
| if (strokeType.isNotEmpty() && ! strokeType.equalsIgnoreCase ("none")) | if (strokeType.isNotEmpty() && ! strokeType.equalsIgnoreCase ("none")) | ||||
| { | { | ||||
| dp->setStrokeFill (getPathFillType (path, strokeType, | |||||
| dp->setStrokeFill (getPathFillType (path, xml, "stroke", | |||||
| getStyleAttribute (xml, "stroke-opacity"), | getStyleAttribute (xml, "stroke-opacity"), | ||||
| getStyleAttribute (xml, "opacity"), | getStyleAttribute (xml, "opacity"), | ||||
| Colours::transparentBlack)); | Colours::transparentBlack)); | ||||
| @@ -751,8 +743,7 @@ private: | |||||
| { | { | ||||
| forEachXmlChildElementWithTagName (*fillXml, e, "stop") | forEachXmlChildElementWithTagName (*fillXml, e, "stop") | ||||
| { | { | ||||
| int index = 0; | |||||
| Colour col (parseColour (getStyleAttribute (fillXml.getChild (e), "stop-color"), index, Colours::black)); | |||||
| Colour col (parseColour (fillXml.getChild (e), "stop-color", Colours::black)); | |||||
| const String opacity (getStyleAttribute (fillXml.getChild (e), "stop-opacity", "1")); | const String opacity (getStyleAttribute (fillXml.getChild (e), "stop-opacity", "1")); | ||||
| col = col.withMultipliedAlpha (jlimit (0.0f, 1.0f, opacity.getFloatValue())); | col = col.withMultipliedAlpha (jlimit (0.0f, 1.0f, opacity.getFloatValue())); | ||||
| @@ -892,7 +883,8 @@ private: | |||||
| } | } | ||||
| FillType getPathFillType (const Path& path, | FillType getPathFillType (const Path& path, | ||||
| const String& fill, | |||||
| const XmlPath& xml, | |||||
| StringRef fillAttribute, | |||||
| const String& fillOpacity, | const String& fillOpacity, | ||||
| const String& overallOpacity, | const String& overallOpacity, | ||||
| const Colour defaultColour) const | const Colour defaultColour) const | ||||
| @@ -905,6 +897,7 @@ private: | |||||
| if (fillOpacity.isNotEmpty()) | if (fillOpacity.isNotEmpty()) | ||||
| opacity *= (jlimit (0.0f, 1.0f, fillOpacity.getFloatValue())); | opacity *= (jlimit (0.0f, 1.0f, fillOpacity.getFloatValue())); | ||||
| String fill (getStyleAttribute (xml, fillAttribute)); | |||||
| String urlID = parseURL (fill); | String urlID = parseURL (fill); | ||||
| if (urlID.isNotEmpty()) | if (urlID.isNotEmpty()) | ||||
| @@ -918,8 +911,7 @@ private: | |||||
| if (fill.equalsIgnoreCase ("none")) | if (fill.equalsIgnoreCase ("none")) | ||||
| return Colours::transparentBlack; | return Colours::transparentBlack; | ||||
| int i = 0; | |||||
| return parseColour (fill, i, defaultColour).withMultipliedAlpha (opacity); | |||||
| return parseColour (xml, fillAttribute, defaultColour).withMultipliedAlpha (opacity); | |||||
| } | } | ||||
| static PathStrokeType::JointStyle getJointStyle (const String& join) noexcept | static PathStrokeType::JointStyle getJointStyle (const String& join) noexcept | ||||
| @@ -987,8 +979,7 @@ private: | |||||
| dt->setFont (font, true); | dt->setFont (font, true); | ||||
| dt->setTransform (transform); | dt->setTransform (transform); | ||||
| int i = 0; | |||||
| dt->setColour (parseColour (getStyleAttribute (xml, "fill"), i, Colours::black) | |||||
| dt->setColour (parseColour (xml, "fill", Colours::black) | |||||
| .withMultipliedAlpha (getStyleAttribute (xml, "fill-opacity", "1").getFloatValue())); | .withMultipliedAlpha (getStyleAttribute (xml, "fill-opacity", "1").getFloatValue())); | ||||
| Rectangle<float> bounds (xCoords[0], yCoords[0] - font.getAscent(), | Rectangle<float> bounds (xCoords[0], yCoords[0] - font.getAscent(), | ||||
| @@ -1167,7 +1158,7 @@ private: | |||||
| return xml->getStringAttribute (attributeName); | return xml->getStringAttribute (attributeName); | ||||
| if (xml.parent != nullptr) | if (xml.parent != nullptr) | ||||
| return getInheritedAttribute (*xml.parent, attributeName); | |||||
| return getInheritedAttribute (*xml.parent, attributeName); | |||||
| return String(); | return String(); | ||||
| } | } | ||||
| @@ -1285,16 +1276,19 @@ private: | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| static Colour parseColour (const String& s, int& index, const Colour defaultColour) | |||||
| Colour parseColour (const XmlPath& xml, StringRef attributeName, const Colour defaultColour) const | |||||
| { | { | ||||
| if (s [index] == '#') | |||||
| const String text (getStyleAttribute (xml, attributeName)); | |||||
| if (text.startsWithChar ('#')) | |||||
| { | { | ||||
| uint32 hex[6] = { 0 }; | uint32 hex[6] = { 0 }; | ||||
| int numChars = 0; | int numChars = 0; | ||||
| String::CharPointerType s = text.getCharPointer(); | |||||
| for (int i = 6; --i >= 0;) | |||||
| while (numChars < 6) | |||||
| { | { | ||||
| const int hexValue = CharacterFunctions::getHexDigitValue (s [++index]); | |||||
| const int hexValue = CharacterFunctions::getHexDigitValue (*++s); | |||||
| if (hexValue >= 0) | if (hexValue >= 0) | ||||
| hex [numChars++] = (uint32) hexValue; | hex [numChars++] = (uint32) hexValue; | ||||
| @@ -1303,28 +1297,24 @@ private: | |||||
| } | } | ||||
| if (numChars <= 3) | if (numChars <= 3) | ||||
| return Colour ((uint8) (hex [0] * 0x11), | |||||
| (uint8) (hex [1] * 0x11), | |||||
| (uint8) (hex [2] * 0x11)); | |||||
| return Colour ((uint8) (hex[0] * 0x11), | |||||
| (uint8) (hex[1] * 0x11), | |||||
| (uint8) (hex[2] * 0x11)); | |||||
| return Colour ((uint8) ((hex [0] << 4) + hex [1]), | |||||
| (uint8) ((hex [2] << 4) + hex [3]), | |||||
| (uint8) ((hex [4] << 4) + hex [5])); | |||||
| return Colour ((uint8) ((hex[0] << 4) + hex[1]), | |||||
| (uint8) ((hex[2] << 4) + hex[3]), | |||||
| (uint8) ((hex[4] << 4) + hex[5])); | |||||
| } | } | ||||
| if (s [index] == 'r' | |||||
| && s [index + 1] == 'g' | |||||
| && s [index + 2] == 'b') | |||||
| if (text.startsWith ("rgb")) | |||||
| { | { | ||||
| const int openBracket = s.indexOfChar (index, '('); | |||||
| const int closeBracket = s.indexOfChar (openBracket, ')'); | |||||
| const int openBracket = text.indexOfChar ('('); | |||||
| const int closeBracket = text.indexOfChar (openBracket, ')'); | |||||
| if (openBracket >= 3 && closeBracket > openBracket) | if (openBracket >= 3 && closeBracket > openBracket) | ||||
| { | { | ||||
| index = closeBracket; | |||||
| StringArray tokens; | StringArray tokens; | ||||
| tokens.addTokens (s.substring (openBracket + 1, closeBracket), ",", ""); | |||||
| tokens.addTokens (text.substring (openBracket + 1, closeBracket), ",", ""); | |||||
| tokens.trim(); | tokens.trim(); | ||||
| tokens.removeEmptyStrings(); | tokens.removeEmptyStrings(); | ||||
| @@ -1332,14 +1322,21 @@ private: | |||||
| return Colour ((uint8) roundToInt (2.55 * tokens[0].getDoubleValue()), | return Colour ((uint8) roundToInt (2.55 * tokens[0].getDoubleValue()), | ||||
| (uint8) roundToInt (2.55 * tokens[1].getDoubleValue()), | (uint8) roundToInt (2.55 * tokens[1].getDoubleValue()), | ||||
| (uint8) roundToInt (2.55 * tokens[2].getDoubleValue())); | (uint8) roundToInt (2.55 * tokens[2].getDoubleValue())); | ||||
| else | |||||
| return Colour ((uint8) tokens[0].getIntValue(), | |||||
| (uint8) tokens[1].getIntValue(), | |||||
| (uint8) tokens[2].getIntValue()); | |||||
| return Colour ((uint8) tokens[0].getIntValue(), | |||||
| (uint8) tokens[1].getIntValue(), | |||||
| (uint8) tokens[2].getIntValue()); | |||||
| } | } | ||||
| } | } | ||||
| return Colours::findColourForName (s, defaultColour); | |||||
| if (text == "inherit") | |||||
| { | |||||
| for (const XmlPath* p = xml.parent; p != nullptr; p = p->parent) | |||||
| if (getStyleAttribute (*p, attributeName).isNotEmpty()) | |||||
| return parseColour (*p, attributeName, defaultColour); | |||||
| } | |||||
| return Colours::findColourForName (text, defaultColour); | |||||
| } | } | ||||
| static AffineTransform parseTransform (String t) | static AffineTransform parseTransform (String t) | ||||
| @@ -1482,6 +1479,9 @@ private: | |||||
| //============================================================================== | //============================================================================== | ||||
| Drawable* Drawable::createFromSVG (const XmlElement& svgDocument) | Drawable* Drawable::createFromSVG (const XmlElement& svgDocument) | ||||
| { | { | ||||
| if (! svgDocument.hasTagNameIgnoringNamespace ("svg")) | |||||
| return nullptr; | |||||
| SVGState state (&svgDocument); | SVGState state (&svgDocument); | ||||
| return state.parseSVGElement (SVGState::XmlPath (&svgDocument, nullptr)); | return state.parseSVGElement (SVGState::XmlPath (&svgDocument, nullptr)); | ||||
| } | } | ||||