diff --git a/examples/graphics/data/svg/mozilla2.svg b/examples/graphics/data/svg/mozilla2.svg
new file mode 100644
index 000000000..8f537cb8c
--- /dev/null
+++ b/examples/graphics/data/svg/mozilla2.svg
@@ -0,0 +1,60 @@
+
+
\ No newline at end of file
diff --git a/examples/graphics/data/svg/pulpitrock.svg b/examples/graphics/data/svg/pulpitrock.svg
new file mode 100644
index 000000000..4a3102021
--- /dev/null
+++ b/examples/graphics/data/svg/pulpitrock.svg
@@ -0,0 +1,6 @@
+
diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h
index 2f03c93e4..bef85201f 100644
--- a/examples/graphics/source/examples/Svg.h
+++ b/examples/graphics/source/examples/Svg.h
@@ -27,18 +27,17 @@ class SvgDemo : public yup::Component
SvgDemo()
{
updateListOfSvgFiles();
+ loadDemoFont();
parseSvgFile (currentSvgFileIndex);
}
- void resized() override
- {
- //drawable.setBounds (getLocalBounds());
- }
-
void mouseDown (const yup::MouseEvent& event) override
{
- ++currentSvgFileIndex;
+ if (event.isLeftButtonDown())
+ ++currentSvgFileIndex;
+ else if (event.isRightButtonDown())
+ --currentSvgFileIndex;
parseSvgFile (currentSvgFileIndex);
}
@@ -59,12 +58,17 @@ class SvgDemo : public yup::Component
.getParentDirectory()
.getParentDirectory();
+ dataDirectory = riveBasePath.getChildFile ("data");
+
auto files = riveBasePath.getChildFile ("data/svg").findChildFiles (yup::File::findFiles, false, "*.svg");
if (files.isEmpty())
return;
for (const auto& svgFile : files)
+ {
+ //if (svgFile.getFileName() == "mozilla2.svg")
svgFiles.add (svgFile);
+ }
}
void parseSvgFile (int index)
@@ -83,12 +87,97 @@ class SvgDemo : public yup::Component
YUP_DBG ("Showing " << svgFiles[currentSvgFileIndex].getFullPathName());
drawable.clear();
- drawable.parseSVG (svgFiles[currentSvgFileIndex]);
+ drawable.parseSVG (svgFiles[currentSvgFileIndex], createParseOptions (svgFiles[currentSvgFileIndex]));
repaint();
}
+ void loadDemoFont()
+ {
+ yup::Font font;
+ if (font.loadFromFile (dataDirectory.getChildFile ("RobotoFlex-VariableFont.ttf")).wasOk())
+ demoFont = std::move (font);
+ }
+
+ std::optional fetchHttpImage (const yup::String& href)
+ {
+ if (! href.startsWithIgnoreCase ("http:") && ! href.startsWithIgnoreCase ("https:"))
+ return std::nullopt;
+
+ if (httpImageCache.contains (href))
+ return httpImageCache[href];
+
+ yup::MemoryBlock imageData;
+ int statusCode = 0;
+
+ auto streamOptions = yup::URL::InputStreamOptions (yup::URL::ParameterHandling::inAddress)
+ .withConnectionTimeoutMs (5000)
+ .withNumRedirectsToFollow (5)
+ .withStatusCode (&statusCode)
+ .withExtraHeaders ("User-Agent: YUP SVG Demo\r\nAccept: image/*\r\n");
+
+ auto stream = yup::URL (href).createInputStream (streamOptions);
+ if (stream == nullptr)
+ {
+ YUP_DBG ("Unable to fetch SVG image href: " << href);
+ return std::nullopt;
+ }
+
+ stream->readIntoMemoryBlock (imageData);
+
+ if ((statusCode != 0 && (statusCode < 200 || statusCode >= 300)) || imageData.isEmpty())
+ {
+ YUP_DBG ("Unable to fetch SVG image href: " << href << " status: " << statusCode);
+ return std::nullopt;
+ }
+
+ auto imageResult = yup::Image::loadFromData (imageData.asBytes());
+ if (imageResult.failed())
+ {
+ YUP_DBG ("Unable to decode SVG image href: " << href << " error: " << imageResult.getErrorMessage());
+ return std::nullopt;
+ }
+
+ auto image = imageResult.getValue();
+ httpImageCache.set (href, image);
+ return image;
+ }
+
+ yup::Drawable::ParseOptions createParseOptions (const yup::File& svgFile)
+ {
+ yup::Drawable::ParseOptions options;
+ options.baseDirectory = svgFile.getParentDirectory();
+ options.imageResolver = [this] (yup::StringRef href, const yup::File&) -> std::optional
+ {
+ return fetchHttpImage (yup::String (href.text));
+ };
+
+ options.fontResolver = [this] (yup::StringRef, float fontSize, int weight, bool italic) -> std::optional
+ {
+ if (demoFont)
+ {
+ auto font = *demoFont;
+ font.setAxisValue ("wght", static_cast (weight));
+ if (italic)
+ font.setAxisValue ("slnt", -10.0f);
+ else
+ font.setAxisValue ("slnt", 0.0f);
+ return font.withHeight (fontSize);
+ }
+
+ if (auto theme = yup::ApplicationTheme::getGlobalTheme())
+ return theme->getDefaultFont().withHeight (fontSize);
+
+ return std::nullopt;
+ };
+
+ return options;
+ }
+
yup::Drawable drawable;
yup::Array svgFiles;
+ yup::File dataDirectory;
+ std::optional demoFont;
+ yup::HashMap httpImageCache;
int currentSvgFileIndex = 0;
};
diff --git a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp
index 64b494993..cdeb97d57 100644
--- a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp
+++ b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.cpp
@@ -107,16 +107,12 @@ void BufferingAudioSource::releaseResources()
buffer.setSize (numberOfChannels, 0);
- // MSVC2017 seems to need this if statement to not generate a warning during linking.
- // As source is set in the constructor, there is no way that source could
- // ever equal this, but it seems to make MSVC2017 happy.
- if (source != this)
- source->releaseResources();
+ source->releaseResources();
}
void BufferingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
- const auto bufferRange = getValidBufferRange (info.numSamples);
+ auto [playPos, bufferRange] = getValidBufferRangeAndAdvance (info.numSamples);
if (bufferRange.isEmpty())
{
@@ -143,8 +139,8 @@ void BufferingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info
{
jassert (buffer.getNumSamples() > 0);
- const auto startBufferIndex = (int) ((validStart + nextPlayPos) % buffer.getNumSamples());
- const auto endBufferIndex = (int) ((validEnd + nextPlayPos) % buffer.getNumSamples());
+ const auto startBufferIndex = (int) ((validStart + playPos) % buffer.getNumSamples());
+ const auto endBufferIndex = (int) ((validEnd + playPos) % buffer.getNumSamples());
if (startBufferIndex < endBufferIndex)
{
@@ -155,13 +151,10 @@ void BufferingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info
const auto initialSize = buffer.getNumSamples() - startBufferIndex;
info.buffer->copyFrom (chan, info.startSample + validStart, buffer, chan, startBufferIndex, initialSize);
-
info.buffer->copyFrom (chan, info.startSample + validStart + initialSize, buffer, chan, 0, (validEnd - validStart) - initialSize);
}
}
}
-
- nextPlayPos += info.numSamples;
}
bool BufferingAudioSource::waitForNextAudioBlockReady (const AudioSourceChannelInfo& info, uint32 timeout)
@@ -235,6 +228,18 @@ Range BufferingAudioSource::getValidBufferRange (int numSamples) const
(int) (jlimit (bufferValidStart, bufferValidEnd, pos + numSamples) - pos) };
}
+std::tuple> BufferingAudioSource::getValidBufferRangeAndAdvance (int numSamples)
+{
+ const ScopedLock sl (bufferRangeLock);
+
+ const auto pos = nextPlayPos.load();
+
+ nextPlayPos = pos + numSamples;
+
+ return std::make_tuple (
+ pos, Range { (int) (jlimit (bufferValidStart, bufferValidEnd, pos) - pos), (int) (jlimit (bufferValidStart, bufferValidEnd, pos + numSamples) - pos) });
+}
+
bool BufferingAudioSource::readNextBufferChunk()
{
int64 newBVS, newBVE, sectionToReadStart, sectionToReadEnd;
diff --git a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h
index 6c839de13..8589358a8 100644
--- a/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h
+++ b/modules/yup_audio_basics/sources/yup_BufferingAudioSource.h
@@ -117,6 +117,7 @@ class YUP_API BufferingAudioSource : public PositionableAudioSource
private:
//==============================================================================
Range getValidBufferRange (int numSamples) const;
+ std::tuple> getValidBufferRangeAndAdvance (int numSamples);
bool readNextBufferChunk();
void readBufferSection (int64 start, int length, int bufferOffset);
int useTimeSlice() override;
diff --git a/modules/yup_core/containers/yup_HashMap.h b/modules/yup_core/containers/yup_HashMap.h
index 85b93ac80..8cef8c552 100644
--- a/modules/yup_core/containers/yup_HashMap.h
+++ b/modules/yup_core/containers/yup_HashMap.h
@@ -134,6 +134,25 @@ class HashMap
public:
//==============================================================================
+ /** Creates an empty hash-map.
+ */
+ HashMap()
+ : HashMap (defaultHashTableSize, HashFunctionType())
+ {
+ }
+
+ /** Creates an empty hash-map.
+
+ @param numberOfSlots Specifies the number of hash entries the map will use. This will be
+ the "upperLimit" parameter that is passed to your generateHash()
+ function. The number of hash slots will grow automatically if necessary,
+ or it can be remapped manually using remapTable().
+ */
+ explicit HashMap (int numberOfSlots)
+ : HashMap (numberOfSlots, HashFunctionType())
+ {
+ }
+
/** Creates an empty hash-map.
@param numberOfSlots Specifies the number of hash entries the map will use. This will be
@@ -141,11 +160,9 @@ class HashMap
function. The number of hash slots will grow automatically if necessary,
or it can be remapped manually using remapTable().
@param hashFunction An instance of HashFunctionType, which will be copied and
- stored to use with the HashMap. This parameter can be omitted
- if HashFunctionType has a default constructor.
+ stored to use with the HashMap.
*/
- explicit HashMap (int numberOfSlots = defaultHashTableSize,
- HashFunctionType hashFunction = HashFunctionType())
+ HashMap (int numberOfSlots, HashFunctionType hashFunction)
: hashFunctionToUse (hashFunction)
{
hashSlots.insertMultiple (0, nullptr, numberOfSlots);
diff --git a/modules/yup_core/files/yup_File.h b/modules/yup_core/files/yup_File.h
index 775be9911..c102e04c8 100644
--- a/modules/yup_core/files/yup_File.h
+++ b/modules/yup_core/files/yup_File.h
@@ -81,7 +81,7 @@ class YUP_API File final
On the Mac/Linux, the path can include "~" notation for referring to
user home directories.
*/
- File (const String& absolutePath);
+ explicit File (const String& absolutePath);
/** Creates a copy of another file object. */
File (const File&);
diff --git a/modules/yup_core/files/yup_FileSearchPath.cpp b/modules/yup_core/files/yup_FileSearchPath.cpp
index 9426c2be8..0b09c7c3b 100644
--- a/modules/yup_core/files/yup_FileSearchPath.cpp
+++ b/modules/yup_core/files/yup_FileSearchPath.cpp
@@ -138,7 +138,7 @@ void FileSearchPath::removeRedundantPaths()
{
const auto checkedIsChildOf = [&] (const auto& a, const auto& b)
{
- return File::isAbsolutePath (a) && File::isAbsolutePath (b) && File (a).isAChildOf (b);
+ return File::isAbsolutePath (a) && File::isAbsolutePath (b) && File (a).isAChildOf (File (b));
};
const auto fContainsDirectory = [&] (const auto& f)
diff --git a/modules/yup_core/native/yup_Files_android.cpp b/modules/yup_core/native/yup_Files_android.cpp
index 35e5b35ae..d63ec2e88 100644
--- a/modules/yup_core/native/yup_Files_android.cpp
+++ b/modules/yup_core/native/yup_Files_android.cpp
@@ -251,12 +251,12 @@ File AndroidContentUriResolver::getLocalFileFromContentUri (const URL& url)
if (type == "image")
type = "images";
- return getCursorDataColumn (URL ("content://media/external/" + type + "/media"),
- "_id=?",
- StringArray { mediaId });
+ return File (getCursorDataColumn (URL ("content://media/external/" + type + "/media"),
+ "_id=?",
+ StringArray { mediaId }));
}
- return getCursorDataColumn (url);
+ return File (getCursorDataColumn (url));
}
String AndroidContentUriResolver::getFileNameFromContentUri (const URL& url)
@@ -745,7 +745,7 @@ static File getAppDataDir (bool dataDir)
LocalRef applicationInfo (env->CallObjectMethod (getAppContext().get(), AndroidContext.getApplicationInfo));
LocalRef jString (env->GetObjectField (applicationInfo.get(), dataDir ? AndroidApplicationInfo.dataDir : AndroidApplicationInfo.publicSourceDir));
- return { yupString ((jstring) jString.get()) };
+ return File (yupString ((jstring) jString.get()));
}
File File::getSpecialLocation (const SpecialLocationType type)
diff --git a/modules/yup_core/native/yup_Files_apple.mm b/modules/yup_core/native/yup_Files_apple.mm
index 613f83ebc..d5abe7fc5 100644
--- a/modules/yup_core/native/yup_Files_apple.mm
+++ b/modules/yup_core/native/yup_Files_apple.mm
@@ -163,11 +163,11 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
StringArray params;
params.addTokens(parameters, true);
- NSMutableArray* paramArray = [NSMutableArray array];
+ NSMutableArray* paramArray = [NSMutableArray array];
for (int i = 0; i < params.size(); ++i)
[paramArray addObject:yupStringToNS(params[i])];
- NSMutableDictionary* envDict = [NSMutableDictionary dictionary];
+ NSMutableDictionary* envDict = [NSMutableDictionary dictionary];
if (environment)
{
for (int i = 0; i < environment->size(); ++i)
@@ -202,7 +202,7 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
YUP_BEGIN_IGNORE_DEPRECATION_WARNINGS
- NSMutableDictionary* dict = [NSMutableDictionary dictionary];
+ NSMutableDictionary* dict = [NSMutableDictionary dictionary];
if (params.size())
{
@@ -306,6 +306,7 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
case userDocumentsDirectory:
resultPath = MacFileHelpers::getIOSSystemLocation(NSDocumentDirectory);
break;
+
case userDesktopDirectory:
resultPath = MacFileHelpers::getIOSSystemLocation(NSDesktopDirectory);
break;
@@ -315,13 +316,14 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
File tmp(MacFileHelpers::getIOSSystemLocation(NSCachesDirectory));
tmp = tmp.getChildFile(yup_getExecutableFile().getFileNameWithoutExtension());
tmp.createDirectory();
- return tmp.getFullPathName();
+ return File(tmp.getFullPathName());
}
#else
case userDocumentsDirectory:
resultPath = "~/Documents";
break;
+
case userDesktopDirectory:
resultPath = "~/Desktop";
break;
@@ -336,21 +338,27 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
case userMusicDirectory:
resultPath = "~/Music";
break;
+
case userMoviesDirectory:
resultPath = "~/Movies";
break;
+
case userPicturesDirectory:
resultPath = "~/Pictures";
break;
+
case userApplicationDataDirectory:
resultPath = "~/Library";
break;
+
case commonApplicationDataDirectory:
resultPath = "/Library";
break;
+
case commonDocumentsDirectory:
resultPath = "/Users/Shared";
break;
+
case globalApplicationsDirectory:
resultPath = "/Applications";
break;
@@ -481,11 +489,11 @@ bool openDocument(const String& fileName, const String& parameters, const Array<
{
YUP_AUTORELEASEPOOL
{
- enumerator = [[NSFileManager defaultManager] enumeratorAtPath:yupStringToNS(directory.getFullPathName())];
- }
- }
-
- ~Pimpl() = default;
+ enumerator = [[NSFileManager defaultManager] enumeratorAtPath:yupStringToNS(directory.getFullPathName())];
+ }
+ }
+
+ ~Pimpl() = default;
bool next(String& filenameFound,
bool* const isDir,
diff --git a/modules/yup_core/native/yup_Files_wasm.cpp b/modules/yup_core/native/yup_Files_wasm.cpp
index afb942314..bec374437 100644
--- a/modules/yup_core/native/yup_Files_wasm.cpp
+++ b/modules/yup_core/native/yup_Files_wasm.cpp
@@ -135,7 +135,7 @@ File File::getSpecialLocation (const SpecialLocationType type)
}
*/
- return { "/" };
+ return File ("/");
}
//==============================================================================
diff --git a/modules/yup_core/native/yup_Files_windows.cpp b/modules/yup_core/native/yup_Files_windows.cpp
index 621fbf557..aa0c6fefb 100644
--- a/modules/yup_core/native/yup_Files_windows.cpp
+++ b/modules/yup_core/native/yup_Files_windows.cpp
@@ -687,7 +687,7 @@ void File::findFileSystemRoots (Array& destArray)
roots.sort (true);
for (int i = 0; i < roots.size(); ++i)
- destArray.add (roots[i]);
+ destArray.add (File (roots[i]));
}
//==============================================================================
diff --git a/modules/yup_core/native/yup_Network_wasm.cpp b/modules/yup_core/native/yup_Network_wasm.cpp
index e828a3a4e..2e7e40a2a 100644
--- a/modules/yup_core/native/yup_Network_wasm.cpp
+++ b/modules/yup_core/native/yup_Network_wasm.cpp
@@ -37,7 +37,7 @@ bool YUP_CALLTYPE Process::openEmailWithAttachments (const String& /* targetEmai
}
//==============================================================================
-#if YUP_EMSCRIPTEN && ! YUP_USE_CURL
+#if YUP_EMSCRIPTEN
class WebInputStream::Pimpl
{
public:
diff --git a/modules/yup_core/network/yup_URL.cpp b/modules/yup_core/network/yup_URL.cpp
index f72dbf3bc..d41e608b0 100644
--- a/modules/yup_core/network/yup_URL.cpp
+++ b/modules/yup_core/network/yup_URL.cpp
@@ -854,7 +854,7 @@ File URL::fileFromFileSchemeURL (const URL& fileURL)
path = path.substring (1);
#endif
- return path;
+ return File (path);
}
int URL::getPort() const
diff --git a/modules/yup_core/yup_core.cpp b/modules/yup_core/yup_core.cpp
index 57fd7f003..d955f37ba 100644
--- a/modules/yup_core/yup_core.cpp
+++ b/modules/yup_core/yup_core.cpp
@@ -323,11 +323,11 @@ extern char** environ;
#include "threads/yup_HighResolutionTimer.cpp"
#include "threads/yup_WaitableEvent.cpp"
#include "network/yup_URL.cpp"
+#include "network/yup_WebInputStream.cpp"
+#include "streams/yup_URLInputSource.cpp"
#if ! YUP_WASM
#include "threads/yup_ChildProcess.cpp"
-#include "network/yup_WebInputStream.cpp"
-#include "streams/yup_URLInputSource.cpp"
#endif
//==============================================================================
diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp
index e3e478c15..d25a5ed1a 100644
--- a/modules/yup_graphics/drawables/yup_Drawable.cpp
+++ b/modules/yup_graphics/drawables/yup_Drawable.cpp
@@ -2,7 +2,7 @@
==============================================================================
This file is part of the YUP library.
- Copyright (c) 2025 - kunitoki@gmail.com
+ Copyright (c) 2026 - kunitoki@gmail.com
YUP is an open source library subject to open-source licensing.
@@ -23,17 +23,112 @@ namespace yup
{
//==============================================================================
-#ifndef YUP_DRAWABLE_LOGGING
-#define YUP_DRAWABLE_LOGGING 0
-#endif
-
-#if YUP_DRAWABLE_LOGGING
-#define YUP_DRAWABLE_LOG(textToWrite) YUP_DBG (textToWrite)
-#else
-#define YUP_DRAWABLE_LOG(textToWrite) \
- { \
+
+namespace
+{
+SVGGradient::Ptr getGradientById (const SVGData& data, const String& id)
+{
+ return data.gradientsById[id];
+}
+
+SVGGradient::Ptr resolveGradient (const SVGData& data, SVGGradient::Ptr gradient)
+{
+ if (gradient == nullptr || gradient->href.isEmpty())
+ return gradient;
+
+ auto referencedGradient = getGradientById (data, gradient->href);
+ if (referencedGradient == nullptr)
+ return gradient;
+
+ referencedGradient = resolveGradient (data, referencedGradient);
+
+ SVGGradient::Ptr resolved = new SVGGradient;
+ resolved->type = gradient->type;
+ resolved->id = gradient->id;
+ resolved->units = referencedGradient->units;
+ resolved->spreadMethod = referencedGradient->spreadMethod;
+ resolved->start = referencedGradient->start;
+ resolved->end = referencedGradient->end;
+ resolved->center = referencedGradient->center;
+ resolved->radius = referencedGradient->radius;
+ resolved->focal = referencedGradient->focal;
+ resolved->transform = referencedGradient->transform;
+ resolved->stops = referencedGradient->stops;
+ resolved->hasUnits = referencedGradient->hasUnits;
+ resolved->hasSpreadMethod = referencedGradient->hasSpreadMethod;
+
+ if (gradient->hasStart)
+ resolved->start = gradient->start;
+ if (gradient->hasEnd)
+ resolved->end = gradient->end;
+ if (gradient->hasCenter)
+ resolved->center = gradient->center;
+ if (gradient->hasRadius)
+ resolved->radius = gradient->radius;
+ if (gradient->hasFocal)
+ resolved->focal = gradient->focal;
+
+ if (! gradient->transform.isIdentity())
+ resolved->transform = gradient->transform;
+ if (gradient->hasUnits)
+ {
+ resolved->units = gradient->units;
+ resolved->hasUnits = true;
+ }
+ if (gradient->hasSpreadMethod)
+ {
+ resolved->spreadMethod = gradient->spreadMethod;
+ resolved->hasSpreadMethod = true;
}
-#endif
+ if (! gradient->stops.empty())
+ resolved->stops = gradient->stops;
+
+ return resolved;
+}
+
+SVGFilter::Ptr getFilterById (const SVGData& data, const String& id)
+{
+ return data.filtersById[id];
+}
+
+SVGFilter::Ptr resolveFilter (const SVGData& data, SVGFilter::Ptr filter)
+{
+ if (filter == nullptr || filter->href.isEmpty())
+ return filter;
+
+ auto referencedFilter = resolveFilter (data, getFilterById (data, filter->href));
+ if (referencedFilter == nullptr)
+ return filter;
+
+ SVGFilter::Ptr resolved = new SVGFilter;
+ resolved->id = filter->id;
+ resolved->href = filter->href;
+ resolved->gaussianBlurStdDeviation = filter->gaussianBlurStdDeviation
+ ? filter->gaussianBlurStdDeviation
+ : referencedFilter->gaussianBlurStdDeviation;
+ return resolved;
+}
+
+SVGClipPath::Ptr getClipPathById (const SVGData& data, const String& id)
+{
+ return data.clipPathsById[id];
+}
+
+SVGMask::Ptr getMaskById (const SVGData& data, const String& id)
+{
+ return data.masksById[id];
+}
+
+SVGMarker::Ptr getMarkerById (const SVGData& data, const String& id)
+{
+ return data.markersById[id];
+}
+
+SVGPattern::Ptr getPatternById (const SVGData& data, const String& id)
+{
+ return data.patternsById[id];
+}
+} // namespace
//==============================================================================
@@ -45,177 +140,315 @@ Drawable::Drawable()
bool Drawable::parseSVG (const File& svgFile)
{
- clear();
+ YUP_DRAWABLE_LOG ("parseSVG(file) - file: " << svgFile.getFullPathName());
- XmlDocument svgDoc (svgFile);
- std::unique_ptr svgRoot (svgDoc.getDocumentElement());
+ ParseOptions options;
+ options.baseDirectory = svgFile.getParentDirectory();
- if (svgRoot == nullptr || ! svgRoot->hasTagName ("svg"))
- return false;
+ return parseSVG (svgFile, options);
+}
- 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());
- }
- }
+bool Drawable::parseSVG (const File& svgFile, const ParseOptions& options)
+{
+ YUP_DRAWABLE_LOG ("parseSVG(file, options) - file: " << svgFile.getFullPathName());
- auto width = svgRoot->getFloatAttribute ("width");
- size.setWidth (width == 0.0f ? viewBox.getWidth() : width);
+ document = SVGParser::parse (svgFile, options);
+ return document != nullptr;
+}
- auto height = svgRoot->getFloatAttribute ("height");
- size.setHeight (height == 0.0f ? viewBox.getHeight() : height);
+//==============================================================================
- // ViewBox transform is now calculated at render-time based on actual target area
- YUP_DRAWABLE_LOG ("Parse complete - viewBox: " << viewBox.toString() << " size: " << size.getWidth() << "x" << size.getHeight());
+bool Drawable::parseSVG (StringRef svgText)
+{
+ YUP_DRAWABLE_LOG ("parseSVG(text) - length: " << String (svgText.text).length());
- auto result = parseElement (*svgRoot, true, {});
+ return parseSVG (svgText, ParseOptions());
+}
- if (result)
- bounds = calculateBounds();
+bool Drawable::parseSVG (StringRef svgText, const ParseOptions& options)
+{
+ YUP_DRAWABLE_LOG ("parseSVG(text, options) - length: " << String (svgText.text).length());
- return result;
+ document = SVGParser::parse (svgText, options);
+ return document != nullptr;
}
//==============================================================================
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();
-
- // Reset root element's default presentation attributes to SVG defaults
- rootHasFill = true; // SVG default fill is black
- rootHasStroke = false; // SVG default stroke is none
- rootFillColor = std::nullopt;
- rootStrokeColor = std::nullopt;
+ document = nullptr;
}
//==============================================================================
Rectangle Drawable::getBounds() const
{
- return bounds;
+ if (document == nullptr)
+ return {};
+
+ return document->getBounds();
}
//==============================================================================
void Drawable::paint (Graphics& g)
{
- const auto savedState = g.saveState();
+ if (document == nullptr)
+ return;
+
+ document->visit ([&] (const SVGData& data)
+ {
+ YUP_DRAWABLE_LOG ("paint - bounds: " << data.bounds.toString()
+ << " topLevelElements: " << data.elements.size()
+ << " rootHasFill: " << (data.rootHasFill ? "true" : "false")
+ << " rootHasStroke: " << (data.rootHasStroke ? "true" : "false"));
+
+ const auto savedState = g.saveState();
- g.setStrokeWidth (1.0f);
+ g.setStrokeWidth (1.0f);
- // Set default fill color based on root SVG element or SVG default (black)
- if (rootFillColor)
- g.setFillColor (*rootFillColor);
- else
- g.setFillColor (Colors::black);
+ if (data.rootFillColor)
+ g.setFillColor (*data.rootFillColor);
+ else
+ g.setFillColor (Colors::black);
- // Set default stroke color if root SVG element specified one
- if (rootStrokeColor)
- g.setStrokeColor (*rootStrokeColor);
+ if (data.rootStrokeColor)
+ g.setStrokeColor (*data.rootStrokeColor);
- if (! transform.isIdentity())
- g.addTransform (transform);
+ if (! data.transform.isIdentity())
+ g.addTransform (data.transform);
- // Pass root element's fill/stroke state to top-level elements
- for (const auto& element : elements)
- paintElement (g, *element, rootHasFill, rootHasStroke);
+ std::unordered_set visiting;
+ for (const auto& element : data.elements)
+ paintElement (g, data, *element, data.rootHasFill, data.rootHasStroke, data.rootFillColor.value_or (Colors::black), visiting);
+ });
}
void Drawable::paint (Graphics& g, const Rectangle& targetArea, Fitting fitting, Justification justification)
{
- YUP_DRAWABLE_LOG ("Fitted paint called - bounds: " << bounds.toString() << " targetArea: " << targetArea.toString());
-
- if (bounds.isEmpty())
+ if (document == nullptr)
return;
- const auto savedState = g.saveState();
+ document->visit ([&] (const SVGData& data)
+ {
+ YUP_DRAWABLE_LOG ("Fitted paint called - bounds: " << data.bounds.toString() << " targetArea: " << targetArea.toString());
+
+ if (data.bounds.isEmpty())
+ {
+ YUP_DRAWABLE_LOG ("Fitted paint skipped - drawable bounds are empty");
+ 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);
+ auto finalBounds = data.viewBox.isEmpty() ? data.bounds : data.viewBox;
+ auto finalTransform = calculateTransformForTarget (finalBounds, targetArea, fitting, justification);
- g.setStrokeWidth (1.0f);
+ if (! finalTransform.isIdentity())
+ g.addTransform (finalTransform);
- // Set default fill color based on root SVG element or SVG default (black)
- if (rootFillColor)
- g.setFillColor (*rootFillColor);
- else
- g.setFillColor (Colors::black);
+ g.setStrokeWidth (1.0f);
+
+ if (data.rootFillColor)
+ g.setFillColor (*data.rootFillColor);
+ else
+ g.setFillColor (Colors::black);
- // Set default stroke color if root SVG element specified one
- if (rootStrokeColor)
- g.setStrokeColor (*rootStrokeColor);
+ if (data.rootStrokeColor)
+ g.setStrokeColor (*data.rootStrokeColor);
- // Pass root element's fill/stroke state to top-level elements
- for (const auto& element : elements)
- paintElement (g, *element, rootHasFill, rootHasStroke);
+ std::unordered_set visiting;
+ for (const auto& element : data.elements)
+ paintElement (g, data, *element, data.rootHasFill, data.rootHasStroke, data.rootFillColor.value_or (Colors::black), visiting);
+ });
}
//==============================================================================
-void Drawable::paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled)
+void Drawable::paintElement (Graphics& g, const SVGData& data, const SVGElement& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled, Color currentColor, std::unordered_set& visitingElements, int recursionDepth)
{
+ if (element.hidden)
+ {
+ YUP_DRAWABLE_LOG ("paintElement skipped - hidden tag: " << element.tagName
+ << " id: " << (element.id ? *element.id : "none")
+ << " depth: " << recursionDepth);
+ return;
+ }
+
+ if (recursionDepth > 64)
+ {
+ YUP_DRAWABLE_LOG ("paintElement skipped - recursion limit tag: " << element.tagName
+ << " id: " << (element.id ? *element.id : "none")
+ << " depth: " << recursionDepth);
+ return;
+ }
+
const auto savedState = g.saveState();
bool isFillDefined = hasParentFillEnabled;
bool isStrokeDefined = hasParentStrokeEnabled;
+ if (element.color)
+ currentColor = *element.color;
- YUP_DRAWABLE_LOG ("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_DRAWABLE_LOG ("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_DRAWABLE_LOG ("After transform: " << g.getTransform().toString());
- }
if (element.opacity)
g.setOpacity (g.getOpacity() * (*element.opacity));
- // Apply clipping path if specified
+ if (element.blendMode)
+ g.setBlendMode (*element.blendMode);
+
+ if (element.filterUrl)
+ {
+ if (auto filter = resolveFilter (data, getFilterById (data, *element.filterUrl)))
+ {
+ if (filter->gaussianBlurStdDeviation)
+ {
+ const auto svgStdDeviationToFeather = 2.0f;
+ const auto feather = jmax (g.getFeather(), *filter->gaussianBlurStdDeviation * svgStdDeviationToFeather);
+ g.setFeather (feather);
+ }
+ }
+ }
+
+ if (element.viewBox && element.viewportSize)
+ {
+ Rectangle viewport (0.0f, 0.0f, element.viewportSize->getWidth(), element.viewportSize->getHeight());
+ auto viewBoxTransform = calculateTransformForTarget (*element.viewBox, viewport, element.preserveAspectRatioFitting, element.preserveAspectRatioJustification);
+ if (! viewBoxTransform.isIdentity())
+ g.addTransform (viewBoxTransform);
+ }
+
bool hasClipping = false;
if (element.clipPathUrl)
{
- if (auto clipPath = getClipPathById (*element.clipPathUrl))
+ if (auto clipPath = getClipPathById (data, *element.clipPathUrl))
{
- // Create a combined path from all clip path elements
+ std::optional> clipObjectBounds;
+
+ if (clipPath->units == SVGClipPath::ObjectBoundingBox)
+ {
+ if (element.path)
+ clipObjectBounds = element.path->getBounds();
+ else if (element.reference)
+ {
+ if (auto refElement = data.elementsById[*element.reference]; refElement != nullptr && refElement->path)
+ clipObjectBounds = refElement->path->getBounds();
+ }
+ else if (element.imageBounds)
+ {
+ clipObjectBounds = *element.imageBounds;
+ }
+ }
+
Path combinedClipPath;
+ bool clipUsesNonZeroWinding = true;
+
for (const auto& clipElement : clipPath->elements)
{
if (clipElement->path)
- combinedClipPath.appendPath (*clipElement->path);
+ {
+ if (clipElement->clipRule == "evenodd")
+ clipUsesNonZeroWinding = false;
+
+ AffineTransform clipTransform = clipElement->transform.value_or (AffineTransform::identity());
+
+ if (clipPath->units == SVGClipPath::ObjectBoundingBox)
+ {
+ if (! clipObjectBounds || clipObjectBounds->isEmpty())
+ continue;
+
+ auto unitsTransform = AffineTransform::translation (clipObjectBounds->getX(), clipObjectBounds->getY())
+ .scaled (clipObjectBounds->getWidth(), clipObjectBounds->getHeight());
+ clipTransform = clipTransform.followedBy (unitsTransform);
+ }
+
+ if (! clipTransform.isIdentity())
+ combinedClipPath.appendPath (*clipElement->path, clipTransform);
+ else
+ combinedClipPath.appendPath (*clipElement->path);
+ }
}
if (! combinedClipPath.isEmpty())
{
- g.setClipPath (combinedClipPath);
+ combinedClipPath.setUsingNonZeroWinding (clipUsesNonZeroWinding);
+
+ auto clipTransform = g.getTransform().translated (g.getDrawingArea().getTopLeft());
+ auto transformedClipPath = combinedClipPath.transformed (clipTransform);
+
+ const auto savedClipTransform = g.getTransform();
+ g.setTransform (AffineTransform::identity());
+ g.setClipPath (transformedClipPath);
+ g.setTransform (savedClipTransform);
+
hasClipping = true;
}
}
}
- // Setup fill
+ if (element.maskUrl)
+ {
+ if (auto mask = getMaskById (data, *element.maskUrl))
+ {
+ std::optional> maskObjectBounds;
+
+ if (mask->maskUnits == SVGMask::ObjectBoundingBox)
+ {
+ if (element.path)
+ maskObjectBounds = element.path->getBounds();
+ else if (element.reference)
+ {
+ if (auto refElement = data.elementsById[*element.reference]; refElement != nullptr && refElement->path)
+ maskObjectBounds = refElement->path->getBounds();
+ }
+ else if (element.imageBounds)
+ {
+ maskObjectBounds = *element.imageBounds;
+ }
+ }
+
+ Path combinedMaskPath;
+
+ for (const auto& maskElement : mask->elements)
+ {
+ if (maskElement->path)
+ {
+ AffineTransform maskTransform = maskElement->transform.value_or (AffineTransform::identity());
+
+ if (mask->maskUnits == SVGMask::ObjectBoundingBox)
+ {
+ if (! maskObjectBounds || maskObjectBounds->isEmpty())
+ continue;
+
+ auto unitsTransform = AffineTransform::translation (maskObjectBounds->getX(), maskObjectBounds->getY())
+ .scaled (maskObjectBounds->getWidth(), maskObjectBounds->getHeight());
+ maskTransform = maskTransform.followedBy (unitsTransform);
+ }
+
+ if (! maskTransform.isIdentity())
+ combinedMaskPath.appendPath (*maskElement->path, maskTransform);
+ else
+ combinedMaskPath.appendPath (*maskElement->path);
+ }
+ }
+
+ if (! combinedMaskPath.isEmpty())
+ {
+ auto maskClipTransform = g.getTransform().translated (g.getDrawingArea().getTopLeft());
+ auto transformedMaskPath = combinedMaskPath.transformed (maskClipTransform);
+
+ const auto savedMaskTransform = g.getTransform();
+ g.setTransform (AffineTransform::identity());
+ g.setClipPath (transformedMaskPath);
+ g.setTransform (savedMaskTransform);
+ }
+ }
+ }
+
+ // Fill setup
if (element.fillColor)
{
Color fillColor = *element.fillColor;
@@ -224,20 +457,26 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent
g.setFillColor (fillColor);
isFillDefined = true;
}
+ else if (element.fillCurrentColor)
+ {
+ Color fillColor = currentColor;
+ if (element.fillOpacity)
+ fillColor = fillColor.withMultipliedAlpha (*element.fillOpacity);
+ g.setFillColor (fillColor);
+ isFillDefined = true;
+ }
else if (element.fillUrl)
{
- YUP_DRAWABLE_LOG ("Looking for gradient with ID: " << *element.fillUrl);
- if (auto gradient = getGradientById (*element.fillUrl))
+ if (auto gradient = getGradientById (data, *element.fillUrl))
{
- YUP_DRAWABLE_LOG ("Found gradient, resolving references...");
- auto resolvedGradient = resolveGradient (gradient);
+ auto resolvedGradient = resolveGradient (data, gradient);
std::optional> gradientBounds;
if (element.path)
gradientBounds = element.path->getBounds();
else if (element.reference)
{
- if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path)
+ if (auto refElement = data.elementsById[*element.reference]; refElement != nullptr && refElement->path)
gradientBounds = refElement->path->getBounds();
}
@@ -245,16 +484,14 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent
gradientBounds ? std::addressof (*gradientBounds) : nullptr);
g.setFillColorGradient (colorGradient);
isFillDefined = true;
- YUP_DRAWABLE_LOG ("Applied gradient to fill");
}
- else
+ else if (getPatternById (data, *element.fillUrl))
{
- YUP_DRAWABLE_LOG ("Gradient not found for ID: " << *element.fillUrl);
+ isFillDefined = true;
}
}
else if (hasParentFillEnabled)
{
- // Inherit parent fill - don't change graphics state, just mark as defined
isFillDefined = true;
}
@@ -262,79 +499,79 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent
{
if (element.path)
{
- // SVG spec: fill is applied to both closed and unclosed paths
- // For unclosed paths, an implicit line connects the last point to the first point
- // 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);
+ if (element.fillUrl)
+ {
+ if (auto pattern = getPatternById (data, *element.fillUrl))
+ {
+ paintPatternFill (g, data, *element.path, element, *pattern, currentColor, visitingElements, recursionDepth);
+ }
+ else
+ {
+ YUP_DRAWABLE_LOG ("Filling path - tag: " << element.tagName
+ << " id: " << (element.id ? *element.id : "none")
+ << " bounds: " << element.path->getBounds().toString()
+ << " clip: " << (hasClipping ? "true" : "false"));
+ g.fillPath (*element.path);
+ }
+ }
+ else
+ {
+ YUP_DRAWABLE_LOG ("Filling path - tag: " << element.tagName
+ << " id: " << (element.id ? *element.id : "none")
+ << " bounds: " << element.path->getBounds().toString()
+ << " clip: " << (hasClipping ? "true" : "false"));
+ g.fillPath (*element.path);
+ }
}
else if (element.reference)
{
- if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path)
+ if (auto refElement = data.elementsById[*element.reference]; refElement != nullptr && refElement->path)
{
- YUP_DRAWABLE_LOG ("Rendering use element - reference: " << *element.reference);
- YUP_DRAWABLE_LOG ("Use element transform: " << (element.transform ? element.transform->toString() : "none"));
- YUP_DRAWABLE_LOG ("Referenced element local transform: " << (refElement->localTransform ? refElement->localTransform->toString() : "none"));
- YUP_DRAWABLE_LOG ("Graphics transform during use fill: " << g.getTransform().toString());
+ const bool useDefinesFill = element.fillColor || element.fillCurrentColor || element.fillUrl || element.noFill;
+ if (useDefinesFill || ! refElement->noFill)
+ {
+ const auto savedReferenceState = g.saveState();
- // For