diff --git a/examples/graphics/data/svg/tiger.svg b/examples/graphics/data/svg/tiger.svg
new file mode 100644
index 000000000..679edec2e
--- /dev/null
+++ b/examples/graphics/data/svg/tiger.svg
@@ -0,0 +1,725 @@
+
+
diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h
new file mode 100644
index 000000000..08c76f66d
--- /dev/null
+++ b/examples/graphics/source/examples/Svg.h
@@ -0,0 +1,99 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2025 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+#pragma once
+
+namespace yup
+{
+
+class SvgDemo : public yup::Component
+{
+public:
+ SvgDemo()
+ {
+ updateListOfSvgFiles();
+
+ parseSvgFile (currentSvgFileIndex);
+ }
+
+ void resized() override
+ {
+ //drawable.setBounds (getLocalBounds());
+ }
+
+ void mouseDown (const yup::MouseEvent& event) override
+ {
+ ++currentSvgFileIndex;
+
+ parseSvgFile (currentSvgFileIndex);
+ }
+
+ void paint (Graphics& g) override
+ {
+ g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray));
+ g.fillAll();
+
+ drawable.paint (g, getLocalBounds());
+ }
+
+private:
+ void updateListOfSvgFiles()
+ {
+ yup::File riveBasePath = yup::File (__FILE__)
+ .getParentDirectory()
+ .getParentDirectory()
+ .getParentDirectory();
+
+ auto files = riveBasePath.getChildFile ("data/svg").findChildFiles (yup::File::findFiles, false, "*.svg");
+ if (files.isEmpty())
+ return;
+
+ for (const auto& svgFile : files)
+ svgFiles.add (svgFile);
+ }
+
+ void parseSvgFile (int index)
+ {
+ if (svgFiles.isEmpty())
+ return;
+
+ if (index < 0)
+ index = svgFiles.size() - 1;
+
+ if (index >= svgFiles.size())
+ index = 0;
+
+ currentSvgFileIndex = index;
+
+ YUP_DBG ("Showing " << svgFiles[currentSvgFileIndex].getFullPathName());
+
+ drawable.clear();
+ drawable.parseSVG (svgFiles[currentSvgFileIndex]);
+
+ repaint();
+ }
+
+ yup::Drawable drawable;
+ Array svgFiles;
+ int currentSvgFileIndex = 0;
+};
+
+} // namespace yup
diff --git a/examples/graphics/source/examples/VariableFonts.h b/examples/graphics/source/examples/VariableFonts.h
index 38150412f..02ab41957 100644
--- a/examples/graphics/source/examples/VariableFonts.h
+++ b/examples/graphics/source/examples/VariableFonts.h
@@ -128,7 +128,7 @@ class VariableFontsExample : public yup::Component
g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray));
g.fillAll();
- g.setTransform (yup::AffineTransform::rotation (
+ g.addTransform (yup::AffineTransform::rotation (
yup::degreesToRadians (-rotation), getLocalBounds().getCenterX(), 100.0f));
if (feather > 0.0f)
diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp
index 25d8eaa20..53f7d9fbc 100644
--- a/examples/graphics/source/main.cpp
+++ b/examples/graphics/source/main.cpp
@@ -40,6 +40,7 @@
#include "examples/Paths.h"
#include "examples/PopupMenu.h"
#include "examples/TextEditor.h"
+#include "examples/Svg.h"
#include "examples/VariableFonts.h"
#include "examples/Widgets.h"
@@ -222,6 +223,19 @@ class CustomWindow
addChildComponent (components.getLast());
}
+ {
+ auto button = std::make_unique ("SVG");
+ button->onClick = [this, number = counter++]
+ {
+ selectComponent (number);
+ };
+ addAndMakeVisible (button.get());
+ buttons.add (std::move (button));
+
+ components.add (std::make_unique());
+ addChildComponent (components.getLast());
+ }
+
selectComponent (0);
startTimerHz (10);
diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp
new file mode 100644
index 000000000..c55fc9d2b
--- /dev/null
+++ b/modules/yup_graphics/drawables/yup_Drawable.cpp
@@ -0,0 +1,1600 @@
+/*
+ ==============================================================================
+
+ This file is part of the YUP library.
+ Copyright (c) 2025 - kunitoki@gmail.com
+
+ YUP is an open source library subject to open-source licensing.
+
+ The code included in this file is provided under the terms of the ISC license
+ http://www.isc.org/downloads/software-support-policy/isc-license. Permission
+ to use, copy, modify, and/or distribute this software for any purpose with or
+ without fee is hereby granted provided that the above copyright notice and
+ this permission notice appear in all copies.
+
+ YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
+ EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
+ DISCLAIMED.
+
+ ==============================================================================
+*/
+
+namespace yup
+{
+
+//==============================================================================
+
+Drawable::Drawable()
+{
+}
+
+//==============================================================================
+
+bool Drawable::parseSVG (const File& svgFile)
+{
+ clear();
+
+ XmlDocument svgDoc (svgFile);
+ std::unique_ptr svgRoot (svgDoc.getDocumentElement());
+
+ if (svgRoot == nullptr || ! svgRoot->hasTagName ("svg"))
+ return false;
+
+ if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty())
+ {
+ auto coords = StringArray::fromTokens (view, " ,", "");
+ if (coords.size() == 4)
+ {
+ viewBox.setX (coords.getReference (0).getFloatValue());
+ viewBox.setY (coords.getReference (1).getFloatValue());
+ viewBox.setWidth (coords.getReference (2).getFloatValue());
+ viewBox.setHeight (coords.getReference (3).getFloatValue());
+ }
+ }
+
+ auto width = svgRoot->getDoubleAttribute ("width");
+ size.setWidth (width == 0.0 ? viewBox.getWidth() : width);
+
+ auto height = svgRoot->getDoubleAttribute ("height");
+ size.setHeight (height == 0.0 ? viewBox.getHeight() : height);
+
+ // ViewBox transform is now calculated at render-time based on actual target area
+ YUP_DBG ("Parse complete - viewBox: " << viewBox.toString() << " size: " << size.getWidth() << "x" << size.getHeight());
+
+ auto result = parseElement (*svgRoot, true, {});
+
+ if (result)
+ bounds = calculateBounds();
+
+ return result;
+}
+
+//==============================================================================
+
+void Drawable::clear()
+{
+ viewBox = { 0.0f, 0.0f, 0.0f, 0.0f };
+ size = { 0.0f, 0.0f };
+ bounds = { 0.0f, 0.0f, 0.0f, 0.0f };
+ transform = AffineTransform::identity();
+
+ elements.clear();
+ elementsById.clear();
+ gradients.clear();
+ gradientsById.clear();
+ clipPaths.clear();
+ clipPathsById.clear();
+}
+
+//==============================================================================
+
+Rectangle Drawable::getBounds() const
+{
+ return bounds;
+}
+
+//==============================================================================
+
+void Drawable::paint (Graphics& g)
+{
+ const auto savedState = g.saveState();
+
+ g.setStrokeWidth (1.0f);
+ g.setFillColor (Colors::black);
+
+ if (! transform.isIdentity())
+ g.addTransform (transform);
+
+ for (const auto& element : elements)
+ paintElement (g, *element, true, false);
+}
+
+void Drawable::paint (Graphics& g, const Rectangle& targetArea, Fitting fitting, Justification justification)
+{
+ YUP_DBG ("Fitted paint called - bounds: " << bounds.toString() << " targetArea: " << targetArea.toString());
+
+ if (bounds.isEmpty())
+ return;
+
+ const auto savedState = g.saveState();
+
+ auto finalBounds = viewBox.isEmpty() ? bounds : viewBox;
+ auto finalTransform = calculateTransformForTarget (finalBounds, targetArea, fitting, justification);
+ if (! finalTransform.isIdentity())
+ g.addTransform (finalTransform);
+
+ g.setStrokeWidth (1.0f);
+ g.setFillColor (Colors::black);
+
+ for (const auto& element : elements)
+ paintElement (g, *element, true, false);
+}
+
+//==============================================================================
+
+void Drawable::paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled)
+{
+ const auto savedState = g.saveState();
+
+ bool isFillDefined = hasParentFillEnabled;
+ bool isStrokeDefined = hasParentStrokeEnabled;
+
+ YUP_DBG ("paintElement called - hasPath: " << (element.path ? "true" : "false") << " hasTransform: " << (element.transform ? "true" : "false"));
+
+ // Apply element transform if present - use proper composition for coordinate systems
+ if (element.transform)
+ {
+ YUP_DBG ("Applying element transform - before: " << g.getTransform().toString() << " adding: " << element.transform->toString());
+ // For proper coordinate system handling, we need to apply element transform
+ // in the element's local space, then transform to viewport space
+ g.setTransform (element.transform->followedBy (g.getTransform()));
+ YUP_DBG ("After transform: " << g.getTransform().toString());
+ }
+
+ if (element.opacity)
+ g.setOpacity (g.getOpacity() * (*element.opacity));
+
+ // Apply clipping path if specified
+ bool hasClipping = false;
+ if (element.clipPathUrl)
+ {
+ if (auto clipPath = getClipPathById (*element.clipPathUrl))
+ {
+ // Create a combined path from all clip path elements
+ Path combinedClipPath;
+ for (const auto& clipElement : clipPath->elements)
+ {
+ if (clipElement->path)
+ combinedClipPath.appendPath (*clipElement->path);
+ }
+
+ if (! combinedClipPath.isEmpty())
+ {
+ g.setClipPath (combinedClipPath);
+ hasClipping = true;
+ }
+ }
+ }
+
+ // Setup fill
+ if (element.fillColor)
+ {
+ Color fillColor = *element.fillColor;
+ if (element.fillOpacity)
+ fillColor = fillColor.withMultipliedAlpha (*element.fillOpacity);
+ g.setFillColor (fillColor);
+ isFillDefined = true;
+ }
+ else if (element.fillUrl)
+ {
+ YUP_DBG ("Looking for gradient with ID: " << *element.fillUrl);
+ if (auto gradient = getGradientById (*element.fillUrl))
+ {
+ YUP_DBG ("Found gradient, resolving references...");
+ auto resolvedGradient = resolveGradient (gradient);
+ ColorGradient colorGradient = createColorGradientFromSVG (*resolvedGradient, g.getTransform());
+ g.setFillColorGradient (colorGradient);
+ isFillDefined = true;
+ YUP_DBG ("Applied gradient to fill");
+ }
+ else
+ {
+ YUP_DBG ("Gradient not found for ID: " << *element.fillUrl);
+ }
+ }
+ else if (hasParentFillEnabled)
+ {
+ // Inherit parent fill - don't change graphics state, just mark as defined
+ isFillDefined = true;
+ }
+
+ if (isFillDefined && ! element.noFill)
+ {
+ if (element.path)
+ {
+ if (element.path->isClosed())
+ {
+ // TODO: Apply fill-rule when Graphics class supports it
+ // if (element.fillRule)
+ // g.setFillRule (*element.fillRule == "evenodd" ? FillRule::EvenOdd : FillRule::NonZero);
+ g.fillPath (*element.path);
+ }
+ }
+ else if (element.reference)
+ {
+ if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path)
+ {
+ YUP_DBG ("Rendering use element - reference: " << *element.reference);
+ YUP_DBG ("Use element transform: " << (element.transform ? element.transform->toString() : "none"));
+ YUP_DBG ("Referenced element local transform: " << (refElement->localTransform ? refElement->localTransform->toString() : "none"));
+ YUP_DBG ("Graphics transform during use fill: " << g.getTransform().toString());
+
+ // For