Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1742 lines
58KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. class SVGState
  16. {
  17. public:
  18. //==============================================================================
  19. explicit SVGState (const XmlElement* topLevel, const File& svgFile = {})
  20. : originalFile (svgFile), topLevelXml (topLevel, nullptr)
  21. {
  22. }
  23. struct XmlPath
  24. {
  25. XmlPath (const XmlElement* e, const XmlPath* p) noexcept : xml (e), parent (p) {}
  26. const XmlElement& operator*() const noexcept { jassert (xml != nullptr); return *xml; }
  27. const XmlElement* operator->() const noexcept { return xml; }
  28. XmlPath getChild (const XmlElement* e) const noexcept { return XmlPath (e, this); }
  29. template <typename OperationType>
  30. bool applyOperationToChildWithID (const String& id, OperationType& op) const
  31. {
  32. forEachXmlChildElement (*xml, e)
  33. {
  34. XmlPath child (e, this);
  35. if (e->compareAttribute ("id", id)
  36. && ! child->hasTagName ("defs"))
  37. return op (child);
  38. if (child.applyOperationToChildWithID (id, op))
  39. return true;
  40. }
  41. return false;
  42. }
  43. const XmlElement* xml;
  44. const XmlPath* parent;
  45. };
  46. //==============================================================================
  47. struct UsePathOp
  48. {
  49. const SVGState* state;
  50. Path* targetPath;
  51. bool operator() (const XmlPath& xmlPath) const
  52. {
  53. return state->parsePathElement (xmlPath, *targetPath);
  54. }
  55. };
  56. struct UseTextOp
  57. {
  58. const SVGState* state;
  59. AffineTransform* transform;
  60. Drawable* target;
  61. bool operator() (const XmlPath& xmlPath)
  62. {
  63. target = state->parseText (xmlPath, true, transform);
  64. return target != nullptr;
  65. }
  66. };
  67. struct UseImageOp
  68. {
  69. const SVGState* state;
  70. AffineTransform* transform;
  71. Drawable* target;
  72. bool operator() (const XmlPath& xmlPath)
  73. {
  74. target = state->parseImage (xmlPath, true, transform);
  75. return target != nullptr;
  76. }
  77. };
  78. struct GetClipPathOp
  79. {
  80. SVGState* state;
  81. Drawable* target;
  82. bool operator() (const XmlPath& xmlPath)
  83. {
  84. return state->applyClipPath (*target, xmlPath);
  85. }
  86. };
  87. struct SetGradientStopsOp
  88. {
  89. const SVGState* state;
  90. ColourGradient* gradient;
  91. bool operator() (const XmlPath& xml) const
  92. {
  93. return state->addGradientStopsIn (*gradient, xml);
  94. }
  95. };
  96. struct GetFillTypeOp
  97. {
  98. const SVGState* state;
  99. const Path* path;
  100. float opacity;
  101. FillType fillType;
  102. bool operator() (const XmlPath& xml)
  103. {
  104. if (xml->hasTagNameIgnoringNamespace ("linearGradient")
  105. || xml->hasTagNameIgnoringNamespace ("radialGradient"))
  106. {
  107. fillType = state->getGradientFillType (xml, *path, opacity);
  108. return true;
  109. }
  110. return false;
  111. }
  112. };
  113. //==============================================================================
  114. Drawable* parseSVGElement (const XmlPath& xml)
  115. {
  116. auto drawable = new DrawableComposite();
  117. setCommonAttributes (*drawable, xml);
  118. SVGState newState (*this);
  119. if (xml->hasAttribute ("transform"))
  120. newState.addTransform (xml);
  121. newState.width = getCoordLength (xml->getStringAttribute ("width", String (newState.width)), viewBoxW);
  122. newState.height = getCoordLength (xml->getStringAttribute ("height", String (newState.height)), viewBoxH);
  123. if (newState.width <= 0) newState.width = 100;
  124. if (newState.height <= 0) newState.height = 100;
  125. Point<float> viewboxXY;
  126. if (xml->hasAttribute ("viewBox"))
  127. {
  128. auto viewBoxAtt = xml->getStringAttribute ("viewBox");
  129. auto viewParams = viewBoxAtt.getCharPointer();
  130. Point<float> vwh;
  131. if (parseCoords (viewParams, viewboxXY, true)
  132. && parseCoords (viewParams, vwh, true)
  133. && vwh.x > 0
  134. && vwh.y > 0)
  135. {
  136. newState.viewBoxW = vwh.x;
  137. newState.viewBoxH = vwh.y;
  138. auto placementFlags = parsePlacementFlags (xml->getStringAttribute ("preserveAspectRatio").trim());
  139. if (placementFlags != 0)
  140. newState.transform = RectanglePlacement (placementFlags)
  141. .getTransformToFit (Rectangle<float> (viewboxXY.x, viewboxXY.y, vwh.x, vwh.y),
  142. Rectangle<float> (newState.width, newState.height))
  143. .followedBy (newState.transform);
  144. }
  145. }
  146. else
  147. {
  148. if (viewBoxW == 0.0f) newState.viewBoxW = newState.width;
  149. if (viewBoxH == 0.0f) newState.viewBoxH = newState.height;
  150. }
  151. newState.parseSubElements (xml, *drawable);
  152. drawable->setContentArea ({ viewboxXY.x, viewboxXY.y, newState.viewBoxW, newState.viewBoxH });
  153. drawable->resetBoundingBoxToContentArea();
  154. return drawable;
  155. }
  156. //==============================================================================
  157. void parsePathString (Path& path, const String& pathString) const
  158. {
  159. auto d = pathString.getCharPointer().findEndOfWhitespace();
  160. Point<float> subpathStart, last, last2, p1, p2, p3;
  161. juce_wchar currentCommand = 0, previousCommand = 0;
  162. bool isRelative = true;
  163. bool carryOn = true;
  164. while (! d.isEmpty())
  165. {
  166. if (CharPointer_ASCII ("MmLlHhVvCcSsQqTtAaZz").indexOf (*d) >= 0)
  167. {
  168. currentCommand = d.getAndAdvance();
  169. isRelative = currentCommand >= 'a';
  170. }
  171. switch (currentCommand)
  172. {
  173. case 'M':
  174. case 'm':
  175. case 'L':
  176. case 'l':
  177. if (parseCoordsOrSkip (d, p1, false))
  178. {
  179. if (isRelative)
  180. p1 += last;
  181. if (currentCommand == 'M' || currentCommand == 'm')
  182. {
  183. subpathStart = p1;
  184. path.startNewSubPath (p1);
  185. currentCommand = 'l';
  186. }
  187. else
  188. path.lineTo (p1);
  189. last2 = last = p1;
  190. }
  191. break;
  192. case 'H':
  193. case 'h':
  194. if (parseCoord (d, p1.x, false, true))
  195. {
  196. if (isRelative)
  197. p1.x += last.x;
  198. path.lineTo (p1.x, last.y);
  199. last2.x = last.x;
  200. last.x = p1.x;
  201. }
  202. else
  203. {
  204. ++d;
  205. }
  206. break;
  207. case 'V':
  208. case 'v':
  209. if (parseCoord (d, p1.y, false, false))
  210. {
  211. if (isRelative)
  212. p1.y += last.y;
  213. path.lineTo (last.x, p1.y);
  214. last2.y = last.y;
  215. last.y = p1.y;
  216. }
  217. else
  218. {
  219. ++d;
  220. }
  221. break;
  222. case 'C':
  223. case 'c':
  224. if (parseCoordsOrSkip (d, p1, false)
  225. && parseCoordsOrSkip (d, p2, false)
  226. && parseCoordsOrSkip (d, p3, false))
  227. {
  228. if (isRelative)
  229. {
  230. p1 += last;
  231. p2 += last;
  232. p3 += last;
  233. }
  234. path.cubicTo (p1, p2, p3);
  235. last2 = p2;
  236. last = p3;
  237. }
  238. break;
  239. case 'S':
  240. case 's':
  241. if (parseCoordsOrSkip (d, p1, false)
  242. && parseCoordsOrSkip (d, p3, false))
  243. {
  244. if (isRelative)
  245. {
  246. p1 += last;
  247. p3 += last;
  248. }
  249. p2 = last;
  250. if (CharPointer_ASCII ("CcSs").indexOf (previousCommand) >= 0)
  251. p2 += (last - last2);
  252. path.cubicTo (p2, p1, p3);
  253. last2 = p1;
  254. last = p3;
  255. }
  256. break;
  257. case 'Q':
  258. case 'q':
  259. if (parseCoordsOrSkip (d, p1, false)
  260. && parseCoordsOrSkip (d, p2, false))
  261. {
  262. if (isRelative)
  263. {
  264. p1 += last;
  265. p2 += last;
  266. }
  267. path.quadraticTo (p1, p2);
  268. last2 = p1;
  269. last = p2;
  270. }
  271. break;
  272. case 'T':
  273. case 't':
  274. if (parseCoordsOrSkip (d, p1, false))
  275. {
  276. if (isRelative)
  277. p1 += last;
  278. p2 = last;
  279. if (CharPointer_ASCII ("QqTt").indexOf (previousCommand) >= 0)
  280. p2 += (last - last2);
  281. path.quadraticTo (p2, p1);
  282. last2 = p2;
  283. last = p1;
  284. }
  285. break;
  286. case 'A':
  287. case 'a':
  288. if (parseCoordsOrSkip (d, p1, false))
  289. {
  290. String num;
  291. bool flagValue = false;
  292. if (parseNextNumber (d, num, false))
  293. {
  294. auto angle = degreesToRadians (num.getFloatValue());
  295. if (parseNextFlag (d, flagValue))
  296. {
  297. auto largeArc = flagValue;
  298. if (parseNextFlag (d, flagValue))
  299. {
  300. auto sweep = flagValue;
  301. if (parseCoordsOrSkip (d, p2, false))
  302. {
  303. if (isRelative)
  304. p2 += last;
  305. if (last != p2)
  306. {
  307. double centreX, centreY, startAngle, deltaAngle;
  308. double rx = p1.x, ry = p1.y;
  309. endpointToCentreParameters (last.x, last.y, p2.x, p2.y,
  310. angle, largeArc, sweep,
  311. rx, ry, centreX, centreY,
  312. startAngle, deltaAngle);
  313. path.addCentredArc ((float) centreX, (float) centreY,
  314. (float) rx, (float) ry,
  315. angle, (float) startAngle, (float) (startAngle + deltaAngle),
  316. false);
  317. path.lineTo (p2);
  318. }
  319. last2 = last;
  320. last = p2;
  321. }
  322. }
  323. }
  324. }
  325. }
  326. break;
  327. case 'Z':
  328. case 'z':
  329. path.closeSubPath();
  330. last = last2 = subpathStart;
  331. d = d.findEndOfWhitespace();
  332. currentCommand = 'M';
  333. break;
  334. default:
  335. carryOn = false;
  336. break;
  337. }
  338. if (! carryOn)
  339. break;
  340. previousCommand = currentCommand;
  341. }
  342. // paths that finish back at their start position often seem to be
  343. // left without a 'z', so need to be closed explicitly..
  344. if (path.getCurrentPosition() == subpathStart)
  345. path.closeSubPath();
  346. }
  347. private:
  348. //==============================================================================
  349. const File originalFile;
  350. const XmlPath topLevelXml;
  351. float width = 512, height = 512, viewBoxW = 0, viewBoxH = 0;
  352. AffineTransform transform;
  353. String cssStyleText;
  354. static bool isNone (const String& s) noexcept
  355. {
  356. return s.equalsIgnoreCase ("none");
  357. }
  358. static void setCommonAttributes (Drawable& d, const XmlPath& xml)
  359. {
  360. auto compID = xml->getStringAttribute ("id");
  361. d.setName (compID);
  362. d.setComponentID (compID);
  363. if (isNone (xml->getStringAttribute ("display")))
  364. d.setVisible (false);
  365. }
  366. //==============================================================================
  367. void parseSubElements (const XmlPath& xml, DrawableComposite& parentDrawable, const bool shouldParseClip = true)
  368. {
  369. forEachXmlChildElement (*xml, e)
  370. {
  371. const XmlPath child (xml.getChild (e));
  372. if (auto* drawable = parseSubElement (child))
  373. {
  374. parentDrawable.addChildComponent (drawable);
  375. if (! isNone (getStyleAttribute (child, "display")))
  376. drawable->setVisible (true);
  377. if (shouldParseClip)
  378. parseClipPath (child, *drawable);
  379. }
  380. }
  381. }
  382. Drawable* parseSubElement (const XmlPath& xml)
  383. {
  384. {
  385. Path path;
  386. if (parsePathElement (xml, path))
  387. return parseShape (xml, path);
  388. }
  389. auto tag = xml->getTagNameWithoutNamespace();
  390. if (tag == "g") return parseGroupElement (xml, true);
  391. if (tag == "svg") return parseSVGElement (xml);
  392. if (tag == "text") return parseText (xml, true);
  393. if (tag == "image") return parseImage (xml, true);
  394. if (tag == "switch") return parseSwitch (xml);
  395. if (tag == "a") return parseLinkElement (xml);
  396. if (tag == "use") return parseUseOther (xml);
  397. if (tag == "style") parseCSSStyle (xml);
  398. if (tag == "defs") parseDefs (xml);
  399. return nullptr;
  400. }
  401. bool parsePathElement (const XmlPath& xml, Path& path) const
  402. {
  403. auto tag = xml->getTagNameWithoutNamespace();
  404. if (tag == "path") { parsePath (xml, path); return true; }
  405. if (tag == "rect") { parseRect (xml, path); return true; }
  406. if (tag == "circle") { parseCircle (xml, path); return true; }
  407. if (tag == "ellipse") { parseEllipse (xml, path); return true; }
  408. if (tag == "line") { parseLine (xml, path); return true; }
  409. if (tag == "polyline") { parsePolygon (xml, true, path); return true; }
  410. if (tag == "polygon") { parsePolygon (xml, false, path); return true; }
  411. if (tag == "use") { return parseUsePath (xml, path); }
  412. return false;
  413. }
  414. DrawableComposite* parseSwitch (const XmlPath& xml)
  415. {
  416. if (auto* group = xml->getChildByName ("g"))
  417. return parseGroupElement (xml.getChild (group), true);
  418. return nullptr;
  419. }
  420. DrawableComposite* parseGroupElement (const XmlPath& xml, bool shouldParseTransform)
  421. {
  422. if (shouldParseTransform && xml->hasAttribute ("transform"))
  423. {
  424. SVGState newState (*this);
  425. newState.addTransform (xml);
  426. return newState.parseGroupElement (xml, false);
  427. }
  428. auto* drawable = new DrawableComposite();
  429. setCommonAttributes (*drawable, xml);
  430. parseSubElements (xml, *drawable);
  431. drawable->resetContentAreaAndBoundingBoxToFitChildren();
  432. return drawable;
  433. }
  434. DrawableComposite* parseLinkElement (const XmlPath& xml)
  435. {
  436. return parseGroupElement (xml, true); // TODO: support for making this clickable
  437. }
  438. //==============================================================================
  439. void parsePath (const XmlPath& xml, Path& path) const
  440. {
  441. parsePathString (path, xml->getStringAttribute ("d"));
  442. if (getStyleAttribute (xml, "fill-rule").trim().equalsIgnoreCase ("evenodd"))
  443. path.setUsingNonZeroWinding (false);
  444. }
  445. void parseRect (const XmlPath& xml, Path& rect) const
  446. {
  447. const bool hasRX = xml->hasAttribute ("rx");
  448. const bool hasRY = xml->hasAttribute ("ry");
  449. if (hasRX || hasRY)
  450. {
  451. float rx = getCoordLength (xml, "rx", viewBoxW);
  452. float ry = getCoordLength (xml, "ry", viewBoxH);
  453. if (! hasRX)
  454. rx = ry;
  455. else if (! hasRY)
  456. ry = rx;
  457. rect.addRoundedRectangle (getCoordLength (xml, "x", viewBoxW),
  458. getCoordLength (xml, "y", viewBoxH),
  459. getCoordLength (xml, "width", viewBoxW),
  460. getCoordLength (xml, "height", viewBoxH),
  461. rx, ry);
  462. }
  463. else
  464. {
  465. rect.addRectangle (getCoordLength (xml, "x", viewBoxW),
  466. getCoordLength (xml, "y", viewBoxH),
  467. getCoordLength (xml, "width", viewBoxW),
  468. getCoordLength (xml, "height", viewBoxH));
  469. }
  470. }
  471. void parseCircle (const XmlPath& xml, Path& circle) const
  472. {
  473. auto cx = getCoordLength (xml, "cx", viewBoxW);
  474. auto cy = getCoordLength (xml, "cy", viewBoxH);
  475. auto radius = getCoordLength (xml, "r", viewBoxW);
  476. circle.addEllipse (cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
  477. }
  478. void parseEllipse (const XmlPath& xml, Path& ellipse) const
  479. {
  480. auto cx = getCoordLength (xml, "cx", viewBoxW);
  481. auto cy = getCoordLength (xml, "cy", viewBoxH);
  482. auto radiusX = getCoordLength (xml, "rx", viewBoxW);
  483. auto radiusY = getCoordLength (xml, "ry", viewBoxH);
  484. ellipse.addEllipse (cx - radiusX, cy - radiusY, radiusX * 2.0f, radiusY * 2.0f);
  485. }
  486. void parseLine (const XmlPath& xml, Path& line) const
  487. {
  488. auto x1 = getCoordLength (xml, "x1", viewBoxW);
  489. auto y1 = getCoordLength (xml, "y1", viewBoxH);
  490. auto x2 = getCoordLength (xml, "x2", viewBoxW);
  491. auto y2 = getCoordLength (xml, "y2", viewBoxH);
  492. line.startNewSubPath (x1, y1);
  493. line.lineTo (x2, y2);
  494. }
  495. void parsePolygon (const XmlPath& xml, const bool isPolyline, Path& path) const
  496. {
  497. auto pointsAtt = xml->getStringAttribute ("points");
  498. auto points = pointsAtt.getCharPointer();
  499. Point<float> p;
  500. if (parseCoords (points, p, true))
  501. {
  502. Point<float> first (p), last;
  503. path.startNewSubPath (first);
  504. while (parseCoords (points, p, true))
  505. {
  506. last = p;
  507. path.lineTo (p);
  508. }
  509. if ((! isPolyline) || first == last)
  510. path.closeSubPath();
  511. }
  512. }
  513. static String getLinkedID (const XmlPath& xml)
  514. {
  515. auto link = xml->getStringAttribute ("xlink:href");
  516. if (link.startsWithChar ('#'))
  517. return link.substring (1);
  518. return {};
  519. }
  520. bool parseUsePath (const XmlPath& xml, Path& path) const
  521. {
  522. auto linkedID = getLinkedID (xml);
  523. if (linkedID.isNotEmpty())
  524. {
  525. UsePathOp op = { this, &path };
  526. return topLevelXml.applyOperationToChildWithID (linkedID, op);
  527. }
  528. return false;
  529. }
  530. Drawable* parseUseOther (const XmlPath& xml) const
  531. {
  532. if (auto* drawableText = parseText (xml, false)) return drawableText;
  533. if (auto* drawableImage = parseImage (xml, false)) return drawableImage;
  534. return nullptr;
  535. }
  536. static String parseURL (const String& str)
  537. {
  538. if (str.startsWithIgnoreCase ("url"))
  539. return str.fromFirstOccurrenceOf ("#", false, false)
  540. .upToLastOccurrenceOf (")", false, false).trim();
  541. return {};
  542. }
  543. //==============================================================================
  544. Drawable* parseShape (const XmlPath& xml, Path& path,
  545. const bool shouldParseTransform = true,
  546. AffineTransform* additonalTransform = nullptr) const
  547. {
  548. if (shouldParseTransform && xml->hasAttribute ("transform"))
  549. {
  550. SVGState newState (*this);
  551. newState.addTransform (xml);
  552. return newState.parseShape (xml, path, false, additonalTransform);
  553. }
  554. auto dp = new DrawablePath();
  555. setCommonAttributes (*dp, xml);
  556. dp->setFill (Colours::transparentBlack);
  557. path.applyTransform (transform);
  558. if (additonalTransform != nullptr)
  559. path.applyTransform (*additonalTransform);
  560. dp->setPath (path);
  561. dp->setFill (getPathFillType (path, xml, "fill",
  562. getStyleAttribute (xml, "fill-opacity"),
  563. getStyleAttribute (xml, "opacity"),
  564. pathContainsClosedSubPath (path) ? Colours::black
  565. : Colours::transparentBlack));
  566. auto strokeType = getStyleAttribute (xml, "stroke");
  567. if (strokeType.isNotEmpty() && ! isNone (strokeType))
  568. {
  569. dp->setStrokeFill (getPathFillType (path, xml, "stroke",
  570. getStyleAttribute (xml, "stroke-opacity"),
  571. getStyleAttribute (xml, "opacity"),
  572. Colours::transparentBlack));
  573. dp->setStrokeType (getStrokeFor (xml));
  574. }
  575. auto strokeDashArray = getStyleAttribute (xml, "stroke-dasharray");
  576. if (strokeDashArray.isNotEmpty())
  577. parseDashArray (strokeDashArray, *dp);
  578. return dp;
  579. }
  580. static bool pathContainsClosedSubPath (const Path& path) noexcept
  581. {
  582. for (Path::Iterator iter (path); iter.next();)
  583. if (iter.elementType == Path::Iterator::closePath)
  584. return true;
  585. return false;
  586. }
  587. void parseDashArray (const String& dashList, DrawablePath& dp) const
  588. {
  589. if (dashList.equalsIgnoreCase ("null") || isNone (dashList))
  590. return;
  591. Array<float> dashLengths;
  592. for (auto t = dashList.getCharPointer();;)
  593. {
  594. float value;
  595. if (! parseCoord (t, value, true, true))
  596. break;
  597. dashLengths.add (value);
  598. t = t.findEndOfWhitespace();
  599. if (*t == ',')
  600. ++t;
  601. }
  602. if (dashLengths.size() > 0)
  603. {
  604. auto* dashes = dashLengths.getRawDataPointer();
  605. for (int i = 0; i < dashLengths.size(); ++i)
  606. {
  607. if (dashes[i] <= 0) // SVG uses zero-length dashes to mean a dotted line
  608. {
  609. if (dashLengths.size() == 1)
  610. return;
  611. const float nonZeroLength = 0.001f;
  612. dashes[i] = nonZeroLength;
  613. const int pairedIndex = i ^ 1;
  614. if (isPositiveAndBelow (pairedIndex, dashLengths.size())
  615. && dashes[pairedIndex] > nonZeroLength)
  616. dashes[pairedIndex] -= nonZeroLength;
  617. }
  618. }
  619. dp.setDashLengths (dashLengths);
  620. }
  621. }
  622. bool parseClipPath (const XmlPath& xml, Drawable& d)
  623. {
  624. const String clipPath (getStyleAttribute (xml, "clip-path"));
  625. if (clipPath.isNotEmpty())
  626. {
  627. auto urlID = parseURL (clipPath);
  628. if (urlID.isNotEmpty())
  629. {
  630. GetClipPathOp op = { this, &d };
  631. return topLevelXml.applyOperationToChildWithID (urlID, op);
  632. }
  633. }
  634. return false;
  635. }
  636. bool applyClipPath (Drawable& target, const XmlPath& xmlPath)
  637. {
  638. if (xmlPath->hasTagNameIgnoringNamespace ("clipPath"))
  639. {
  640. std::unique_ptr<DrawableComposite> drawableClipPath (new DrawableComposite());
  641. parseSubElements (xmlPath, *drawableClipPath, false);
  642. if (drawableClipPath->getNumChildComponents() > 0)
  643. {
  644. setCommonAttributes (*drawableClipPath, xmlPath);
  645. target.setClipPath (std::move (drawableClipPath));
  646. return true;
  647. }
  648. }
  649. return false;
  650. }
  651. bool addGradientStopsIn (ColourGradient& cg, const XmlPath& fillXml) const
  652. {
  653. bool result = false;
  654. if (fillXml.xml != nullptr)
  655. {
  656. forEachXmlChildElementWithTagName (*fillXml, e, "stop")
  657. {
  658. auto col = parseColour (fillXml.getChild (e), "stop-color", Colours::black);
  659. auto opacity = getStyleAttribute (fillXml.getChild (e), "stop-opacity", "1");
  660. col = col.withMultipliedAlpha (jlimit (0.0f, 1.0f, opacity.getFloatValue()));
  661. double offset = e->getDoubleAttribute ("offset");
  662. if (e->getStringAttribute ("offset").containsChar ('%'))
  663. offset *= 0.01;
  664. cg.addColour (jlimit (0.0, 1.0, offset), col);
  665. result = true;
  666. }
  667. }
  668. return result;
  669. }
  670. FillType getGradientFillType (const XmlPath& fillXml,
  671. const Path& path,
  672. const float opacity) const
  673. {
  674. ColourGradient gradient;
  675. {
  676. auto linkedID = getLinkedID (fillXml);
  677. if (linkedID.isNotEmpty())
  678. {
  679. SetGradientStopsOp op = { this, &gradient, };
  680. topLevelXml.applyOperationToChildWithID (linkedID, op);
  681. }
  682. }
  683. addGradientStopsIn (gradient, fillXml);
  684. if (int numColours = gradient.getNumColours())
  685. {
  686. if (gradient.getColourPosition (0) > 0)
  687. gradient.addColour (0.0, gradient.getColour (0));
  688. if (gradient.getColourPosition (numColours - 1) < 1.0)
  689. gradient.addColour (1.0, gradient.getColour (numColours - 1));
  690. }
  691. else
  692. {
  693. gradient.addColour (0.0, Colours::black);
  694. gradient.addColour (1.0, Colours::black);
  695. }
  696. if (opacity < 1.0f)
  697. gradient.multiplyOpacity (opacity);
  698. jassert (gradient.getNumColours() > 0);
  699. gradient.isRadial = fillXml->hasTagNameIgnoringNamespace ("radialGradient");
  700. float gradientWidth = viewBoxW;
  701. float gradientHeight = viewBoxH;
  702. float dx = 0.0f;
  703. float dy = 0.0f;
  704. const bool userSpace = fillXml->getStringAttribute ("gradientUnits").equalsIgnoreCase ("userSpaceOnUse");
  705. if (! userSpace)
  706. {
  707. auto bounds = path.getBounds();
  708. dx = bounds.getX();
  709. dy = bounds.getY();
  710. gradientWidth = bounds.getWidth();
  711. gradientHeight = bounds.getHeight();
  712. }
  713. if (gradient.isRadial)
  714. {
  715. if (userSpace)
  716. gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute ("cx", "50%"), gradientWidth),
  717. dy + getCoordLength (fillXml->getStringAttribute ("cy", "50%"), gradientHeight));
  718. else
  719. gradient.point1.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute ("cx", "50%"), 1.0f),
  720. dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("cy", "50%"), 1.0f));
  721. auto radius = getCoordLength (fillXml->getStringAttribute ("r", "50%"), gradientWidth);
  722. gradient.point2 = gradient.point1 + Point<float> (radius, 0.0f);
  723. //xxx (the fx, fy focal point isn't handled properly here..)
  724. }
  725. else
  726. {
  727. if (userSpace)
  728. {
  729. gradient.point1.setXY (dx + getCoordLength (fillXml->getStringAttribute ("x1", "0%"), gradientWidth),
  730. dy + getCoordLength (fillXml->getStringAttribute ("y1", "0%"), gradientHeight));
  731. gradient.point2.setXY (dx + getCoordLength (fillXml->getStringAttribute ("x2", "100%"), gradientWidth),
  732. dy + getCoordLength (fillXml->getStringAttribute ("y2", "0%"), gradientHeight));
  733. }
  734. else
  735. {
  736. gradient.point1.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute ("x1", "0%"), 1.0f),
  737. dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("y1", "0%"), 1.0f));
  738. gradient.point2.setXY (dx + gradientWidth * getCoordLength (fillXml->getStringAttribute ("x2", "100%"), 1.0f),
  739. dy + gradientHeight * getCoordLength (fillXml->getStringAttribute ("y2", "0%"), 1.0f));
  740. }
  741. if (gradient.point1 == gradient.point2)
  742. return Colour (gradient.getColour (gradient.getNumColours() - 1));
  743. }
  744. FillType type (gradient);
  745. auto gradientTransform = parseTransform (fillXml->getStringAttribute ("gradientTransform"));
  746. if (gradient.isRadial)
  747. {
  748. type.transform = gradientTransform;
  749. }
  750. else
  751. {
  752. // Transform the perpendicular vector into the new coordinate space for the gradient.
  753. // This vector is now the slope of the linear gradient as it should appear in the new coord space
  754. auto perpendicular = Point<float> (gradient.point2.y - gradient.point1.y,
  755. gradient.point1.x - gradient.point2.x)
  756. .transformedBy (gradientTransform.withAbsoluteTranslation (0, 0));
  757. auto newGradPoint1 = gradient.point1.transformedBy (gradientTransform);
  758. auto newGradPoint2 = gradient.point2.transformedBy (gradientTransform);
  759. // Project the transformed gradient vector onto the transformed slope of the linear
  760. // gradient as it should appear in the new coordinate space
  761. const float scale = perpendicular.getDotProduct (newGradPoint2 - newGradPoint1)
  762. / perpendicular.getDotProduct (perpendicular);
  763. type.gradient->point1 = newGradPoint1;
  764. type.gradient->point2 = newGradPoint2 - perpendicular * scale;
  765. }
  766. return type;
  767. }
  768. FillType getPathFillType (const Path& path,
  769. const XmlPath& xml,
  770. StringRef fillAttribute,
  771. const String& fillOpacity,
  772. const String& overallOpacity,
  773. const Colour defaultColour) const
  774. {
  775. float opacity = 1.0f;
  776. if (overallOpacity.isNotEmpty())
  777. opacity = jlimit (0.0f, 1.0f, overallOpacity.getFloatValue());
  778. if (fillOpacity.isNotEmpty())
  779. opacity *= (jlimit (0.0f, 1.0f, fillOpacity.getFloatValue()));
  780. String fill (getStyleAttribute (xml, fillAttribute));
  781. String urlID = parseURL (fill);
  782. if (urlID.isNotEmpty())
  783. {
  784. GetFillTypeOp op = { this, &path, opacity, FillType() };
  785. if (topLevelXml.applyOperationToChildWithID (urlID, op))
  786. return op.fillType;
  787. }
  788. if (isNone (fill))
  789. return Colours::transparentBlack;
  790. return parseColour (xml, fillAttribute, defaultColour).withMultipliedAlpha (opacity);
  791. }
  792. static PathStrokeType::JointStyle getJointStyle (const String& join) noexcept
  793. {
  794. if (join.equalsIgnoreCase ("round")) return PathStrokeType::curved;
  795. if (join.equalsIgnoreCase ("bevel")) return PathStrokeType::beveled;
  796. return PathStrokeType::mitered;
  797. }
  798. static PathStrokeType::EndCapStyle getEndCapStyle (const String& cap) noexcept
  799. {
  800. if (cap.equalsIgnoreCase ("round")) return PathStrokeType::rounded;
  801. if (cap.equalsIgnoreCase ("square")) return PathStrokeType::square;
  802. return PathStrokeType::butt;
  803. }
  804. float getStrokeWidth (const String& strokeWidth) const noexcept
  805. {
  806. auto transformScale = std::sqrt (std::abs (transform.getDeterminant()));
  807. return transformScale * getCoordLength (strokeWidth, viewBoxW);
  808. }
  809. PathStrokeType getStrokeFor (const XmlPath& xml) const
  810. {
  811. return PathStrokeType (getStrokeWidth (getStyleAttribute (xml, "stroke-width", "1")),
  812. getJointStyle (getStyleAttribute (xml, "stroke-linejoin")),
  813. getEndCapStyle (getStyleAttribute (xml, "stroke-linecap")));
  814. }
  815. //==============================================================================
  816. Drawable* useText (const XmlPath& xml) const
  817. {
  818. auto translation = AffineTransform::translation ((float) xml->getDoubleAttribute ("x", 0.0),
  819. (float) xml->getDoubleAttribute ("y", 0.0));
  820. UseTextOp op = { this, &translation, nullptr };
  821. auto linkedID = getLinkedID (xml);
  822. if (linkedID.isNotEmpty())
  823. topLevelXml.applyOperationToChildWithID (linkedID, op);
  824. return op.target;
  825. }
  826. Drawable* parseText (const XmlPath& xml, bool shouldParseTransform,
  827. AffineTransform* additonalTransform = nullptr) const
  828. {
  829. if (shouldParseTransform && xml->hasAttribute ("transform"))
  830. {
  831. SVGState newState (*this);
  832. newState.addTransform (xml);
  833. return newState.parseText (xml, false, additonalTransform);
  834. }
  835. if (xml->hasTagName ("use"))
  836. return useText (xml);
  837. if (! xml->hasTagName ("text") && ! xml->hasTagNameIgnoringNamespace ("tspan"))
  838. return nullptr;
  839. Array<float> xCoords, yCoords, dxCoords, dyCoords;
  840. getCoordList (xCoords, getInheritedAttribute (xml, "x"), true, true);
  841. getCoordList (yCoords, getInheritedAttribute (xml, "y"), true, false);
  842. getCoordList (dxCoords, getInheritedAttribute (xml, "dx"), true, true);
  843. getCoordList (dyCoords, getInheritedAttribute (xml, "dy"), true, false);
  844. auto font = getFont (xml);
  845. auto anchorStr = getStyleAttribute (xml, "text-anchor");
  846. auto dc = new DrawableComposite();
  847. setCommonAttributes (*dc, xml);
  848. forEachXmlChildElement (*xml, e)
  849. {
  850. if (e->isTextElement())
  851. {
  852. auto text = e->getText().trim();
  853. auto dt = new DrawableText();
  854. dc->addAndMakeVisible (dt);
  855. dt->setText (text);
  856. dt->setFont (font, true);
  857. if (additonalTransform != nullptr)
  858. dt->setTransform (transform.followedBy (*additonalTransform));
  859. else
  860. dt->setTransform (transform);
  861. dt->setColour (parseColour (xml, "fill", Colours::black)
  862. .withMultipliedAlpha (getStyleAttribute (xml, "fill-opacity", "1").getFloatValue()));
  863. Rectangle<float> bounds (xCoords[0], yCoords[0] - font.getAscent(),
  864. font.getStringWidthFloat (text), font.getHeight());
  865. if (anchorStr == "middle") bounds.setX (bounds.getX() - bounds.getWidth() / 2.0f);
  866. else if (anchorStr == "end") bounds.setX (bounds.getX() - bounds.getWidth());
  867. dt->setBoundingBox (bounds);
  868. }
  869. else if (e->hasTagNameIgnoringNamespace ("tspan"))
  870. {
  871. dc->addAndMakeVisible (parseText (xml.getChild (e), true));
  872. }
  873. }
  874. return dc;
  875. }
  876. Font getFont (const XmlPath& xml) const
  877. {
  878. Font f;
  879. auto family = getStyleAttribute (xml, "font-family").unquoted();
  880. if (family.isNotEmpty())
  881. f.setTypefaceName (family);
  882. if (getStyleAttribute (xml, "font-style").containsIgnoreCase ("italic"))
  883. f.setItalic (true);
  884. if (getStyleAttribute (xml, "font-weight").containsIgnoreCase ("bold"))
  885. f.setBold (true);
  886. return f.withPointHeight (getCoordLength (getStyleAttribute (xml, "font-size", "15"), 1.0f));
  887. }
  888. //==============================================================================
  889. Drawable* useImage (const XmlPath& xml) const
  890. {
  891. auto translation = AffineTransform::translation ((float) xml->getDoubleAttribute ("x", 0.0),
  892. (float) xml->getDoubleAttribute ("y", 0.0));
  893. UseImageOp op = { this, &translation, nullptr };
  894. auto linkedID = getLinkedID (xml);
  895. if (linkedID.isNotEmpty())
  896. topLevelXml.applyOperationToChildWithID (linkedID, op);
  897. return op.target;
  898. }
  899. Drawable* parseImage (const XmlPath& xml, bool shouldParseTransform,
  900. AffineTransform* additionalTransform = nullptr) const
  901. {
  902. if (shouldParseTransform && xml->hasAttribute ("transform"))
  903. {
  904. SVGState newState (*this);
  905. newState.addTransform (xml);
  906. return newState.parseImage (xml, false, additionalTransform);
  907. }
  908. if (xml->hasTagName ("use"))
  909. return useImage (xml);
  910. if (! xml->hasTagName ("image"))
  911. return nullptr;
  912. auto link = xml->getStringAttribute ("xlink:href");
  913. std::unique_ptr<InputStream> inputStream;
  914. MemoryOutputStream imageStream;
  915. if (link.startsWith ("data:"))
  916. {
  917. const auto indexOfComma = link.indexOf (",");
  918. auto format = link.substring (5, indexOfComma).trim();
  919. auto indexOfSemi = format.indexOf (";");
  920. if (format.substring (indexOfSemi + 1).trim().equalsIgnoreCase ("base64"))
  921. {
  922. auto mime = format.substring (0, indexOfSemi).trim();
  923. if (mime.equalsIgnoreCase ("image/png") || mime.equalsIgnoreCase ("image/jpeg"))
  924. {
  925. auto base64text = link.substring (indexOfComma + 1).removeCharacters ("\t\n\r ");
  926. if (Base64::convertFromBase64 (imageStream, base64text))
  927. inputStream.reset (new MemoryInputStream (imageStream.getData(), imageStream.getDataSize(), false));
  928. }
  929. }
  930. }
  931. else
  932. {
  933. auto linkedFile = originalFile.getParentDirectory().getChildFile (link);
  934. if (linkedFile.existsAsFile())
  935. inputStream = linkedFile.createInputStream();
  936. }
  937. if (inputStream != nullptr)
  938. {
  939. auto image = ImageFileFormat::loadFrom (*inputStream);
  940. if (image.isValid())
  941. {
  942. auto* di = new DrawableImage();
  943. setCommonAttributes (*di, xml);
  944. Rectangle<float> imageBounds ((float) xml->getDoubleAttribute ("x", 0.0), (float) xml->getDoubleAttribute ("y", 0.0),
  945. (float) xml->getDoubleAttribute ("width", image.getWidth()), (float) xml->getDoubleAttribute ("height", image.getHeight()));
  946. di->setImage (image.rescaled ((int) imageBounds.getWidth(), (int) imageBounds.getHeight()));
  947. di->setTransformToFit (imageBounds, RectanglePlacement (parsePlacementFlags (xml->getStringAttribute ("preserveAspectRatio").trim())));
  948. if (additionalTransform != nullptr)
  949. di->setTransform (di->getTransform().followedBy (transform).followedBy (*additionalTransform));
  950. else
  951. di->setTransform (di->getTransform().followedBy (transform));
  952. return di;
  953. }
  954. }
  955. return nullptr;
  956. }
  957. //==============================================================================
  958. void addTransform (const XmlPath& xml)
  959. {
  960. transform = parseTransform (xml->getStringAttribute ("transform"))
  961. .followedBy (transform);
  962. }
  963. //==============================================================================
  964. bool parseCoord (String::CharPointerType& s, float& value, const bool allowUnits, const bool isX) const
  965. {
  966. String number;
  967. if (! parseNextNumber (s, number, allowUnits))
  968. {
  969. value = 0;
  970. return false;
  971. }
  972. value = getCoordLength (number, isX ? viewBoxW : viewBoxH);
  973. return true;
  974. }
  975. bool parseCoords (String::CharPointerType& s, Point<float>& p, const bool allowUnits) const
  976. {
  977. return parseCoord (s, p.x, allowUnits, true)
  978. && parseCoord (s, p.y, allowUnits, false);
  979. }
  980. bool parseCoordsOrSkip (String::CharPointerType& s, Point<float>& p, const bool allowUnits) const
  981. {
  982. if (parseCoords (s, p, allowUnits))
  983. return true;
  984. if (! s.isEmpty()) ++s;
  985. return false;
  986. }
  987. float getCoordLength (const String& s, const float sizeForProportions) const noexcept
  988. {
  989. float n = s.getFloatValue();
  990. const int len = s.length();
  991. if (len > 2)
  992. {
  993. auto dpi = 96.0f;
  994. auto n1 = s[len - 2];
  995. auto n2 = s[len - 1];
  996. if (n1 == 'i' && n2 == 'n') n *= dpi;
  997. else if (n1 == 'm' && n2 == 'm') n *= dpi / 25.4f;
  998. else if (n1 == 'c' && n2 == 'm') n *= dpi / 2.54f;
  999. else if (n1 == 'p' && n2 == 'c') n *= 15.0f;
  1000. else if (n2 == '%') n *= 0.01f * sizeForProportions;
  1001. }
  1002. return n;
  1003. }
  1004. float getCoordLength (const XmlPath& xml, const char* attName, const float sizeForProportions) const noexcept
  1005. {
  1006. return getCoordLength (xml->getStringAttribute (attName), sizeForProportions);
  1007. }
  1008. void getCoordList (Array<float>& coords, const String& list, bool allowUnits, const bool isX) const
  1009. {
  1010. auto text = list.getCharPointer();
  1011. float value;
  1012. while (parseCoord (text, value, allowUnits, isX))
  1013. coords.add (value);
  1014. }
  1015. //==============================================================================
  1016. void parseCSSStyle (const XmlPath& xml)
  1017. {
  1018. cssStyleText = xml->getAllSubText() + "\n" + cssStyleText;
  1019. }
  1020. void parseDefs (const XmlPath& xml)
  1021. {
  1022. if (auto* style = xml->getChildByName ("style"))
  1023. parseCSSStyle (xml.getChild (style));
  1024. }
  1025. static String::CharPointerType findStyleItem (String::CharPointerType source, String::CharPointerType name)
  1026. {
  1027. auto nameLength = (int) name.length();
  1028. while (! source.isEmpty())
  1029. {
  1030. if (source.getAndAdvance() == '.'
  1031. && CharacterFunctions::compareIgnoreCaseUpTo (source, name, nameLength) == 0)
  1032. {
  1033. auto endOfName = (source + nameLength).findEndOfWhitespace();
  1034. if (*endOfName == '{')
  1035. return endOfName;
  1036. if (*endOfName == ',')
  1037. return CharacterFunctions::find (endOfName, (juce_wchar) '{');
  1038. }
  1039. }
  1040. return source;
  1041. }
  1042. String getStyleAttribute (const XmlPath& xml, StringRef attributeName, const String& defaultValue = String()) const
  1043. {
  1044. if (xml->hasAttribute (attributeName))
  1045. return xml->getStringAttribute (attributeName, defaultValue);
  1046. auto styleAtt = xml->getStringAttribute ("style");
  1047. if (styleAtt.isNotEmpty())
  1048. {
  1049. auto value = getAttributeFromStyleList (styleAtt, attributeName, {});
  1050. if (value.isNotEmpty())
  1051. return value;
  1052. }
  1053. else if (xml->hasAttribute ("class"))
  1054. {
  1055. for (auto i = cssStyleText.getCharPointer();;)
  1056. {
  1057. auto openBrace = findStyleItem (i, xml->getStringAttribute ("class").getCharPointer());
  1058. if (openBrace.isEmpty())
  1059. break;
  1060. auto closeBrace = CharacterFunctions::find (openBrace, (juce_wchar) '}');
  1061. if (closeBrace.isEmpty())
  1062. break;
  1063. auto value = getAttributeFromStyleList (String (openBrace + 1, closeBrace),
  1064. attributeName, defaultValue);
  1065. if (value.isNotEmpty())
  1066. return value;
  1067. i = closeBrace + 1;
  1068. }
  1069. }
  1070. if (xml.parent != nullptr)
  1071. return getStyleAttribute (*xml.parent, attributeName, defaultValue);
  1072. return defaultValue;
  1073. }
  1074. String getInheritedAttribute (const XmlPath& xml, StringRef attributeName) const
  1075. {
  1076. if (xml->hasAttribute (attributeName))
  1077. return xml->getStringAttribute (attributeName);
  1078. if (xml.parent != nullptr)
  1079. return getInheritedAttribute (*xml.parent, attributeName);
  1080. return {};
  1081. }
  1082. static int parsePlacementFlags (const String& align) noexcept
  1083. {
  1084. if (align.isEmpty())
  1085. return 0;
  1086. if (isNone (align))
  1087. return RectanglePlacement::stretchToFit;
  1088. return (align.containsIgnoreCase ("slice") ? RectanglePlacement::fillDestination : 0)
  1089. | (align.containsIgnoreCase ("xMin") ? RectanglePlacement::xLeft
  1090. : (align.containsIgnoreCase ("xMax") ? RectanglePlacement::xRight
  1091. : RectanglePlacement::xMid))
  1092. | (align.containsIgnoreCase ("yMin") ? RectanglePlacement::yTop
  1093. : (align.containsIgnoreCase ("yMax") ? RectanglePlacement::yBottom
  1094. : RectanglePlacement::yMid));
  1095. }
  1096. //==============================================================================
  1097. static bool isIdentifierChar (juce_wchar c)
  1098. {
  1099. return CharacterFunctions::isLetter (c) || c == '-';
  1100. }
  1101. static String getAttributeFromStyleList (const String& list, StringRef attributeName, const String& defaultValue)
  1102. {
  1103. int i = 0;
  1104. for (;;)
  1105. {
  1106. i = list.indexOf (i, attributeName);
  1107. if (i < 0)
  1108. break;
  1109. if ((i == 0 || (i > 0 && ! isIdentifierChar (list [i - 1])))
  1110. && ! isIdentifierChar (list [i + attributeName.length()]))
  1111. {
  1112. i = list.indexOfChar (i, ':');
  1113. if (i < 0)
  1114. break;
  1115. int end = list.indexOfChar (i, ';');
  1116. if (end < 0)
  1117. end = 0x7ffff;
  1118. return list.substring (i + 1, end).trim();
  1119. }
  1120. ++i;
  1121. }
  1122. return defaultValue;
  1123. }
  1124. //==============================================================================
  1125. static bool isStartOfNumber (juce_wchar c) noexcept
  1126. {
  1127. return CharacterFunctions::isDigit (c) || c == '-' || c == '+';
  1128. }
  1129. static bool parseNextNumber (String::CharPointerType& text, String& value, const bool allowUnits)
  1130. {
  1131. auto s = text;
  1132. while (s.isWhitespace() || *s == ',')
  1133. ++s;
  1134. auto start = s;
  1135. if (isStartOfNumber (*s))
  1136. ++s;
  1137. while (s.isDigit())
  1138. ++s;
  1139. if (*s == '.')
  1140. {
  1141. ++s;
  1142. while (s.isDigit())
  1143. ++s;
  1144. }
  1145. if ((*s == 'e' || *s == 'E') && isStartOfNumber (s[1]))
  1146. {
  1147. s += 2;
  1148. while (s.isDigit())
  1149. ++s;
  1150. }
  1151. if (allowUnits)
  1152. while (s.isLetter())
  1153. ++s;
  1154. if (s == start)
  1155. {
  1156. text = s;
  1157. return false;
  1158. }
  1159. value = String (start, s);
  1160. while (s.isWhitespace() || *s == ',')
  1161. ++s;
  1162. text = s;
  1163. return true;
  1164. }
  1165. static bool parseNextFlag (String::CharPointerType& text, bool& value)
  1166. {
  1167. while (text.isWhitespace() || *text == ',')
  1168. ++text;
  1169. if (*text != '0' && *text != '1')
  1170. return false;
  1171. value = *(text++) != '0';
  1172. while (text.isWhitespace() || *text == ',')
  1173. ++text;
  1174. return true;
  1175. }
  1176. //==============================================================================
  1177. Colour parseColour (const XmlPath& xml, StringRef attributeName, const Colour defaultColour) const
  1178. {
  1179. auto text = getStyleAttribute (xml, attributeName);
  1180. if (text.startsWithChar ('#'))
  1181. {
  1182. uint32 hex[6] = { 0 };
  1183. int numChars = 0;
  1184. auto s = text.getCharPointer();
  1185. while (numChars < 6)
  1186. {
  1187. auto hexValue = CharacterFunctions::getHexDigitValue (*++s);
  1188. if (hexValue >= 0)
  1189. hex [numChars++] = (uint32) hexValue;
  1190. else
  1191. break;
  1192. }
  1193. if (numChars <= 3)
  1194. return Colour ((uint8) (hex[0] * 0x11),
  1195. (uint8) (hex[1] * 0x11),
  1196. (uint8) (hex[2] * 0x11));
  1197. return Colour ((uint8) ((hex[0] << 4) + hex[1]),
  1198. (uint8) ((hex[2] << 4) + hex[3]),
  1199. (uint8) ((hex[4] << 4) + hex[5]));
  1200. }
  1201. if (text.startsWith ("rgb"))
  1202. {
  1203. auto openBracket = text.indexOfChar ('(');
  1204. auto closeBracket = text.indexOfChar (openBracket, ')');
  1205. if (openBracket >= 3 && closeBracket > openBracket)
  1206. {
  1207. StringArray tokens;
  1208. tokens.addTokens (text.substring (openBracket + 1, closeBracket), ",", "");
  1209. tokens.trim();
  1210. tokens.removeEmptyStrings();
  1211. if (tokens[0].containsChar ('%'))
  1212. return Colour ((uint8) roundToInt (2.55 * tokens[0].getDoubleValue()),
  1213. (uint8) roundToInt (2.55 * tokens[1].getDoubleValue()),
  1214. (uint8) roundToInt (2.55 * tokens[2].getDoubleValue()));
  1215. return Colour ((uint8) tokens[0].getIntValue(),
  1216. (uint8) tokens[1].getIntValue(),
  1217. (uint8) tokens[2].getIntValue());
  1218. }
  1219. }
  1220. if (text == "inherit")
  1221. {
  1222. for (const XmlPath* p = xml.parent; p != nullptr; p = p->parent)
  1223. if (getStyleAttribute (*p, attributeName).isNotEmpty())
  1224. return parseColour (*p, attributeName, defaultColour);
  1225. }
  1226. return Colours::findColourForName (text, defaultColour);
  1227. }
  1228. static AffineTransform parseTransform (String t)
  1229. {
  1230. AffineTransform result;
  1231. while (t.isNotEmpty())
  1232. {
  1233. StringArray tokens;
  1234. tokens.addTokens (t.fromFirstOccurrenceOf ("(", false, false)
  1235. .upToFirstOccurrenceOf (")", false, false),
  1236. ", ", "");
  1237. tokens.removeEmptyStrings (true);
  1238. float numbers[6];
  1239. for (int i = 0; i < numElementsInArray (numbers); ++i)
  1240. numbers[i] = tokens[i].getFloatValue();
  1241. AffineTransform trans;
  1242. if (t.startsWithIgnoreCase ("matrix"))
  1243. {
  1244. trans = AffineTransform (numbers[0], numbers[2], numbers[4],
  1245. numbers[1], numbers[3], numbers[5]);
  1246. }
  1247. else if (t.startsWithIgnoreCase ("translate"))
  1248. {
  1249. trans = AffineTransform::translation (numbers[0], numbers[1]);
  1250. }
  1251. else if (t.startsWithIgnoreCase ("scale"))
  1252. {
  1253. trans = AffineTransform::scale (numbers[0], numbers[tokens.size() > 1 ? 1 : 0]);
  1254. }
  1255. else if (t.startsWithIgnoreCase ("rotate"))
  1256. {
  1257. trans = AffineTransform::rotation (degreesToRadians (numbers[0]), numbers[1], numbers[2]);
  1258. }
  1259. else if (t.startsWithIgnoreCase ("skewX"))
  1260. {
  1261. trans = AffineTransform::shear (std::tan (degreesToRadians (numbers[0])), 0.0f);
  1262. }
  1263. else if (t.startsWithIgnoreCase ("skewY"))
  1264. {
  1265. trans = AffineTransform::shear (0.0f, std::tan (degreesToRadians (numbers[0])));
  1266. }
  1267. result = trans.followedBy (result);
  1268. t = t.fromFirstOccurrenceOf (")", false, false).trimStart();
  1269. }
  1270. return result;
  1271. }
  1272. static void endpointToCentreParameters (double x1, double y1,
  1273. double x2, double y2,
  1274. double angle,
  1275. bool largeArc, bool sweep,
  1276. double& rx, double& ry,
  1277. double& centreX, double& centreY,
  1278. double& startAngle, double& deltaAngle) noexcept
  1279. {
  1280. const double midX = (x1 - x2) * 0.5;
  1281. const double midY = (y1 - y2) * 0.5;
  1282. const double cosAngle = std::cos (angle);
  1283. const double sinAngle = std::sin (angle);
  1284. const double xp = cosAngle * midX + sinAngle * midY;
  1285. const double yp = cosAngle * midY - sinAngle * midX;
  1286. const double xp2 = xp * xp;
  1287. const double yp2 = yp * yp;
  1288. double rx2 = rx * rx;
  1289. double ry2 = ry * ry;
  1290. const double s = (xp2 / rx2) + (yp2 / ry2);
  1291. double c;
  1292. if (s <= 1.0)
  1293. {
  1294. c = std::sqrt (jmax (0.0, ((rx2 * ry2) - (rx2 * yp2) - (ry2 * xp2))
  1295. / (( rx2 * yp2) + (ry2 * xp2))));
  1296. if (largeArc == sweep)
  1297. c = -c;
  1298. }
  1299. else
  1300. {
  1301. const double s2 = std::sqrt (s);
  1302. rx *= s2;
  1303. ry *= s2;
  1304. c = 0;
  1305. }
  1306. const double cpx = ((rx * yp) / ry) * c;
  1307. const double cpy = ((-ry * xp) / rx) * c;
  1308. centreX = ((x1 + x2) * 0.5) + (cosAngle * cpx) - (sinAngle * cpy);
  1309. centreY = ((y1 + y2) * 0.5) + (sinAngle * cpx) + (cosAngle * cpy);
  1310. const double ux = (xp - cpx) / rx;
  1311. const double uy = (yp - cpy) / ry;
  1312. const double vx = (-xp - cpx) / rx;
  1313. const double vy = (-yp - cpy) / ry;
  1314. const double length = juce_hypot (ux, uy);
  1315. startAngle = acos (jlimit (-1.0, 1.0, ux / length));
  1316. if (uy < 0)
  1317. startAngle = -startAngle;
  1318. startAngle += MathConstants<double>::halfPi;
  1319. deltaAngle = acos (jlimit (-1.0, 1.0, ((ux * vx) + (uy * vy))
  1320. / (length * juce_hypot (vx, vy))));
  1321. if ((ux * vy) - (uy * vx) < 0)
  1322. deltaAngle = -deltaAngle;
  1323. if (sweep)
  1324. {
  1325. if (deltaAngle < 0)
  1326. deltaAngle += MathConstants<double>::twoPi;
  1327. }
  1328. else
  1329. {
  1330. if (deltaAngle > 0)
  1331. deltaAngle -= MathConstants<double>::twoPi;
  1332. }
  1333. deltaAngle = fmod (deltaAngle, MathConstants<double>::twoPi);
  1334. }
  1335. SVGState& operator= (const SVGState&) = delete;
  1336. };
  1337. //==============================================================================
  1338. std::unique_ptr<Drawable> Drawable::createFromSVG (const XmlElement& svgDocument)
  1339. {
  1340. if (! svgDocument.hasTagNameIgnoringNamespace ("svg"))
  1341. return {};
  1342. SVGState state (&svgDocument);
  1343. return std::unique_ptr<Drawable> (state.parseSVGElement (SVGState::XmlPath (&svgDocument, {})));
  1344. }
  1345. std::unique_ptr<Drawable> Drawable::createFromSVGFile (const File& svgFile)
  1346. {
  1347. if (auto xml = parseXMLIfTagMatches (svgFile, "svg"))
  1348. return createFromSVG (*xml);
  1349. return {};
  1350. }
  1351. Path Drawable::parseSVGPath (const String& svgPath)
  1352. {
  1353. SVGState state (nullptr);
  1354. Path p;
  1355. state.parsePathString (p, svgPath);
  1356. return p;
  1357. }
  1358. } // namespace juce