From 46754756836fb8c94b0e8f0cad1f000887607c42 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 12 Aug 2026 14:38:12 +0200 Subject: [PATCH] Speed up svg parsing and rendering --- modules/yup_core/containers/yup_HashMap.h | 7 +- modules/yup_core/misc/yup_HashGenerator.h | 63 ++ modules/yup_core/text/yup_String.cpp | 22 +- modules/yup_core/text/yup_StringRef.h | 3 + modules/yup_core/yup_core.h | 1 + .../yup_graphics/drawables/yup_Drawable.cpp | 102 +-- modules/yup_graphics/svg/yup_SVGCssParser.cpp | 671 +++++++++--------- modules/yup_graphics/svg/yup_SVGCssParser.h | 3 +- modules/yup_graphics/svg/yup_SVGCssRule.h | 56 ++ modules/yup_graphics/svg/yup_SVGParser.cpp | 191 +++-- modules/yup_graphics/svg/yup_SVGParser.h | 4 + tests/yup_graphics/yup_SVGDocument.cpp | 37 +- 12 files changed, 604 insertions(+), 556 deletions(-) create mode 100644 modules/yup_core/misc/yup_HashGenerator.h diff --git a/modules/yup_core/containers/yup_HashMap.h b/modules/yup_core/containers/yup_HashMap.h index 8cef8c552..f47be7525 100644 --- a/modules/yup_core/containers/yup_HashMap.h +++ b/modules/yup_core/containers/yup_HashMap.h @@ -65,6 +65,9 @@ struct DefaultHashFunctions /** Generates a simple hash from a string. */ static int generateHash (const String& key, int upperLimit) noexcept { return generateHash ((uint32) key.hashCode(), upperLimit); } + /** Generates a simple hash from a StringRef. */ + static int generateHash (StringRef key, int upperLimit) noexcept { return generateHash ((uint32) key.hashCode(), upperLimit); } + /** Generates a simple hash from a variant. */ static int generateHash (const var& key, int upperLimit) noexcept { return generateHash (key.toString(), upperLimit); } @@ -124,8 +127,8 @@ struct DefaultHashFunctions */ template + typename HashFunctionType = DefaultHashFunctions, + typename TypeOfCriticalSectionToUse = DummyCriticalSection> class HashMap { private: diff --git a/modules/yup_core/misc/yup_HashGenerator.h b/modules/yup_core/misc/yup_HashGenerator.h new file mode 100644 index 000000000..ed5c04523 --- /dev/null +++ b/modules/yup_core/misc/yup_HashGenerator.h @@ -0,0 +1,63 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - 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. + + ============================================================================== +*/ + +#include + +namespace yup +{ + +/** Hash generator template. + + This template provides a mechanism to generate hash values for different types of data. + It uses a multiplier to compute the hash value based on the input data. + + @tparam Type The type of the hash value to be generated (e.g., uint32, uint64). +*/ +template +struct HashGenerator +{ + /** Calculates the hash value for the given character pointer. + + @tparam CharPointer The type of the character pointer (e.g., const char*, const wchar_t*). + + @param t The character pointer to be hashed. + + @return The computed hash value of the input data. + */ + template + static Type calculate (CharPointer t) noexcept + { + Type result = {}; + + while (! t.isEmpty()) + result = ((Type) multiplier) * result + (Type) t.getAndAdvance(); + + return result; + } + + /** The multiplier used in the hash calculation. */ + enum + { + multiplier = sizeof (Type) > 4 ? 101 : 31 + }; +}; + +} // namespace yup diff --git a/modules/yup_core/text/yup_String.cpp b/modules/yup_core/text/yup_String.cpp index f96c28b43..6d1e1b435 100644 --- a/modules/yup_core/text/yup_String.cpp +++ b/modules/yup_core/text/yup_String.cpp @@ -657,26 +657,7 @@ yup_wchar String::operator[] (int index) const noexcept return text[index]; } -template -struct HashGenerator -{ - template - static Type calculate (CharPointer t) noexcept - { - Type result = {}; - - while (! t.isEmpty()) - result = ((Type) multiplier) * result + (Type) t.getAndAdvance(); - - return result; - } - - enum - { - multiplier = sizeof (Type) > 4 ? 101 : 31 - }; -}; - +//============================================================================== int String::hashCode() const noexcept { return (int) HashGenerator::calculate (text); } int64 String::hashCode64() const noexcept { return (int64) HashGenerator::calculate (text); } @@ -2746,4 +2727,3 @@ String String::dedentLines() const } } // namespace yup - diff --git a/modules/yup_core/text/yup_StringRef.h b/modules/yup_core/text/yup_StringRef.h index 7dbb2ea76..a88f919b1 100644 --- a/modules/yup_core/text/yup_StringRef.h +++ b/modules/yup_core/text/yup_StringRef.h @@ -124,6 +124,9 @@ class YUP_API StringRef final /** Returns the number of characters in the string. */ int length() const noexcept { return (int) text.length(); } + /** Returns a hash code for the string. */ + int hashCode() const noexcept { return (int) HashGenerator::calculate (text); } + /** Retrieves a character by index. */ yup_wchar operator[] (int index) const noexcept { return text[index]; } diff --git a/modules/yup_core/yup_core.h b/modules/yup_core/yup_core.h index 0cd706330..a49fa323b 100644 --- a/modules/yup_core/yup_core.h +++ b/modules/yup_core/yup_core.h @@ -269,6 +269,7 @@ YUP_BEGIN_IGNORE_WARNINGS_MSVC (4514 4996) YUP_END_IGNORE_WARNINGS_MSVC #include "misc/yup_MetaProgramming.h" +#include "misc/yup_HashGenerator.h" #include "text/yup_String.h" #include "text/yup_StringRef.h" #include "logging/yup_Logger.h" diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 553d6c87a..a8cfa5b73 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -31,107 +31,11 @@ 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->hasStart = referencedGradient->hasStart; - resolved->hasEnd = referencedGradient->hasEnd; - resolved->hasCenter = referencedGradient->hasCenter; - resolved->hasRadius = referencedGradient->hasRadius; - resolved->hasFocal = referencedGradient->hasFocal; - resolved->hasUnits = referencedGradient->hasUnits; - resolved->hasSpreadMethod = referencedGradient->hasSpreadMethod; - - if (gradient->hasStart) - { - resolved->start = gradient->start; - resolved->hasStart = true; - } - if (gradient->hasEnd) - { - resolved->end = gradient->end; - resolved->hasEnd = true; - } - if (gradient->hasCenter) - { - resolved->center = gradient->center; - resolved->hasCenter = true; - } - if (gradient->hasRadius) - { - resolved->radius = gradient->radius; - resolved->hasRadius = true; - } - if (gradient->hasFocal) - { - resolved->focal = gradient->focal; - resolved->hasFocal = true; - } - - 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; - } - 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; - - if (! filter->primitives.empty()) - resolved->primitives = filter->primitives; - else - resolved->primitives = referencedFilter->primitives; - - return resolved; -} - AffineTransform createGradientSpaceTransform (const SVGGradient& gradient, const Rectangle* objectBounds) { const bool hasBounds = objectBounds != nullptr && objectBounds->getWidth() > 0.0f && objectBounds->getHeight() > 0.0f; @@ -387,7 +291,7 @@ void Drawable::paintElement (Graphics& g, if (element.filterUrl) { - if (auto filter = resolveFilter (data, getFilterById (data, *element.filterUrl))) + if (auto filter = getFilterById (data, *element.filterUrl)) { for (const auto& primitive : filter->primitives) { @@ -626,7 +530,7 @@ void Drawable::paintElement (Graphics& g, { if (auto gradient = getGradientById (data, *element.fillUrl)) { - auto resolvedGradient = resolveGradient (data, gradient); + auto resolvedGradient = gradient; std::optional> gradientBounds; if (element.path) @@ -780,7 +684,7 @@ void Drawable::paintElement (Graphics& g, { if (auto gradient = getGradientById (data, *element.strokeUrl)) { - auto resolvedGradient = resolveGradient (data, gradient); + auto resolvedGradient = gradient; std::optional> gradientBounds; if (element.path) diff --git a/modules/yup_graphics/svg/yup_SVGCssParser.cpp b/modules/yup_graphics/svg/yup_SVGCssParser.cpp index cf1d47fcd..3701fe842 100644 --- a/modules/yup_graphics/svg/yup_SVGCssParser.cpp +++ b/modules/yup_graphics/svg/yup_SVGCssParser.cpp @@ -57,306 +57,383 @@ void SVGCssParser::parseCSSStyle (const String& styleString, SVGElement& e) void SVGCssParser::applyStyleProperty (StringRef propertyRef, StringRef valueRef, SVGElement& e) { - String property (propertyRef.text); String value (valueRef.text); + value = value.trim(); + String property (propertyRef.text); property = property.trim().toLowerCase(); - value = value.trim(); + + if (property.isEmpty()) + return; YUP_DRAWABLE_LOG ("applyStyleProperty - tag: " << e.tagName << " id: " << e.id.value_or (String ("none")) << " property: " << property << " value: " << value); - if (property == "fill") - { - e.fillCurrentColor = false; - e.fillUrl.reset(); - e.fillColor.reset(); - e.noFill = false; - - if (value == "none") - e.noFill = true; - else if (value == "currentColor") - e.fillCurrentColor = true; - else if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) - e.fillUrl = url; - else if (value.isNotEmpty()) - e.fillColor = Color::fromString (value); - } - else if (property == "stroke") - { - e.strokeCurrentColor = false; - e.strokeUrl.reset(); - e.strokeColor.reset(); - e.noStroke = false; - - if (value == "none") - e.noStroke = true; - else if (value == "currentColor") - e.strokeCurrentColor = true; - else if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) - e.strokeUrl = url; - else if (value.isNotEmpty()) - e.strokeColor = Color::fromString (value); - } - else if (property == "color") - { - if (value != "currentColor" && value != "inherit") - e.color = Color::fromString (value); - } - else if (property == "stroke-width") - { - float strokeWidth = SVGParser::parseUnit (value, e.strokeWidth.value_or (1.0f), e.fontSize.value_or (12.0f)); - if (strokeWidth >= 0.0f) - e.strokeWidth = strokeWidth; - } - else if (property == "stroke-linejoin") - { - if (value == "round") - e.strokeJoin = StrokeJoin::Round; - else if (value == "miter") - e.strokeJoin = StrokeJoin::Miter; - else if (value == "bevel") - e.strokeJoin = StrokeJoin::Bevel; - } - else if (property == "stroke-linecap") - { - if (value == "round") - e.strokeCap = StrokeCap::Round; - else if (value == "square") - e.strokeCap = StrokeCap::Square; - else if (value == "butt") - e.strokeCap = StrokeCap::Butt; - } - else if (property == "opacity") - { - float opacity = value.getFloatValue(); - if (opacity >= 0.0f && opacity <= 1.0f) - e.opacity = opacity; - } - else if (property == "display") - { - if (value == "none") - e.hidden = true; - } - else if (property == "visibility") - { - e.hidden = value == "hidden" || value == "collapse"; - } - else if (property == "font-family") - { - e.fontFamily = value.unquoted(); - } - else if (property == "font-size") - { - float fontSize = SVGParser::parseUnit (value, e.fontSize.value_or (12.0f), e.fontSize.value_or (12.0f), e.fontSize.value_or (12.0f)); - if (fontSize > 0.0f) - e.fontSize = fontSize; - } - else if (property == "text-anchor") - { - e.textAnchor = value; - } - else if (property == "letter-spacing") - { - if (value != "normal") - e.letterSpacing = SVGParser::parseUnit (value, 0.0f, e.fontSize.value_or (12.0f), e.fontSize.value_or (12.0f)); - } - else if (property == "word-spacing") - { - if (value != "normal") - e.wordSpacing = SVGParser::parseUnit (value, 0.0f, e.fontSize.value_or (12.0f), e.fontSize.value_or (12.0f)); - } - else if (property == "font-weight") + // Dispatch table mapping CSS property names to handlers, grouped by semantic category + using Handler = void (*) (const String&, SVGElement&); + + struct PropertyHandlers { - if (value == "bold" || value == "bolder") - e.fontWeight = 700; - else if (value == "normal" || value == "lighter") - e.fontWeight = 400; - else + HashMap map; + + PropertyHandlers() { - const int numericWeight = value.getIntValue(); - if (numericWeight >= 100 && numericWeight <= 900) - e.fontWeight = numericWeight; + map.set ("fill", [] (const String& v, SVGElement& el) + { + el.fillCurrentColor = false; + el.fillUrl.reset(); + el.fillColor.reset(); + el.noFill = false; + if (v == "none") + { + el.noFill = true; + } + else if (v == "currentColor") + { + el.fillCurrentColor = true; + } + else if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + { + el.fillUrl = url; + } + else if (v.isNotEmpty()) + { + el.fillColor = Color::fromString (v); + } + }); + map.set ("stroke", [] (const String& v, SVGElement& el) + { + el.strokeCurrentColor = false; + el.strokeUrl.reset(); + el.strokeColor.reset(); + el.noStroke = false; + if (v == "none") + { + el.noStroke = true; + } + else if (v == "currentColor") + { + el.strokeCurrentColor = true; + } + else if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + { + el.strokeUrl = url; + } + else if (v.isNotEmpty()) + { + el.strokeColor = Color::fromString (v); + } + }); + map.set ("color", [] (const String& v, SVGElement& el) + { + if (v != "currentColor" && v != "inherit") + el.color = Color::fromString (v); + }); + + // Stroke properties + map.set ("stroke-width", [] (const String& v, SVGElement& el) + { + float sw = SVGParser::parseUnit (v, el.strokeWidth.value_or (1.0f), el.fontSize.value_or (12.0f)); + if (sw >= 0.0f) + el.strokeWidth = sw; + }); + map.set ("stroke-linejoin", [] (const String& v, SVGElement& el) + { + if (v == "round") + el.strokeJoin = StrokeJoin::Round; + else if (v == "miter") + el.strokeJoin = StrokeJoin::Miter; + else if (v == "bevel") + el.strokeJoin = StrokeJoin::Bevel; + }); + map.set ("stroke-linecap", [] (const String& v, SVGElement& el) + { + if (v == "round") + el.strokeCap = StrokeCap::Round; + else if (v == "square") + el.strokeCap = StrokeCap::Square; + else if (v == "butt") + el.strokeCap = StrokeCap::Butt; + }); + map.set ("stroke-miterlimit", [] (const String& v, SVGElement& el) + { + float val = v.getFloatValue(); + el.strokeMiterLimit = std::max (1.0f, val); + }); + map.set ("stroke-dasharray", [] (const String& v, SVGElement& el) + { + if (v == "none") + { + el.strokeDashArray.reset(); + el.strokeDashArrayNone = true; + } + else + { + Array dashes; + for (const auto dash : SVGParser::parseLengthList (v, el.fontSize.value_or (12.0f), 100.0f)) + { + if (dash >= 0.0f) + dashes.add (dash); + } + if (! dashes.isEmpty()) + { + el.strokeDashArray = dashes; + el.strokeDashArrayNone = false; + } + } + }); + map.set ("stroke-dashoffset", [] (const String& v, SVGElement& el) + { + el.strokeDashOffset = SVGParser::parseUnit (v); + }); + + // Opacity + map.set ("opacity", [] (const String& v, SVGElement& el) + { + float op = v.getFloatValue(); + if (op >= 0.0f && op <= 1.0f) + el.opacity = op; + }); + map.set ("fill-opacity", [] (const String& v, SVGElement& el) + { + float op = v.getFloatValue(); + if (op >= 0.0f && op <= 1.0f) + el.fillOpacity = op; + }); + map.set ("stroke-opacity", [] (const String& v, SVGElement& el) + { + float op = v.getFloatValue(); + if (op >= 0.0f && op <= 1.0f) + el.strokeOpacity = op; + }); + + // Visibility + map.set ("display", [] (const String& v, SVGElement& el) + { + if (v == "none") + el.hidden = true; + }); + map.set ("visibility", [] (const String& v, SVGElement& el) + { + el.hidden = (v == "hidden" || v == "collapse"); + }); + + // Font + map.set ("font-family", [] (const String& v, SVGElement& el) + { + el.fontFamily = v.unquoted(); + }); + map.set ("font-size", [] (const String& v, SVGElement& el) + { + float fs = SVGParser::parseUnit (v, el.fontSize.value_or (12.0f), el.fontSize.value_or (12.0f), el.fontSize.value_or (12.0f)); + if (fs > 0.0f) + el.fontSize = fs; + }); + map.set ("font-weight", [] (const String& v, SVGElement& el) + { + if (v == "bold" || v == "bolder") + el.fontWeight = 700; + else if (v == "normal" || v == "lighter") + el.fontWeight = 400; + else + { + int nw = v.getIntValue(); + if (nw >= 100 && nw <= 900) + el.fontWeight = nw; + } + }); + map.set ("font-style", [] (const String& v, SVGElement& el) + { + el.fontItalic = (v == "italic" || v == "oblique"); + }); + + // Text + map.set ("text-anchor", [] (const String& v, SVGElement& el) + { + el.textAnchor = v; + }); + map.set ("letter-spacing", [] (const String& v, SVGElement& el) + { + if (v != "normal") + el.letterSpacing = SVGParser::parseUnit (v, 0.0f, el.fontSize.value_or (12.0f), el.fontSize.value_or (12.0f)); + }); + map.set ("word-spacing", [] (const String& v, SVGElement& el) + { + if (v != "normal") + el.wordSpacing = SVGParser::parseUnit (v, 0.0f, el.fontSize.value_or (12.0f), el.fontSize.value_or (12.0f)); + }); + + // Clip/Mask/Filter + map.set ("clip-path", [] (const String& v, SVGElement& el) + { + String url = SVGParser::extractUrlId (v); + if (url.isNotEmpty()) + el.clipPathUrl = url; + }); + map.set ("mask", [] (const String& v, SVGElement& el) + { + if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + el.maskUrl = url; + }); + map.set ("filter", [] (const String& v, SVGElement& el) + { + if (v == "none") + el.filterUrl.reset(); + else if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + el.filterUrl = url; + else + YUP_DRAWABLE_LOG ("CSS filter currently only supports url(...) - value: " << v); + }); + + // Markers + map.set ("marker-start", [] (const String& v, SVGElement& el) + { + if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + el.markerStart = url; + }); + map.set ("marker-mid", [] (const String& v, SVGElement& el) + { + if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + el.markerMid = url; + }); + map.set ("marker-end", [] (const String& v, SVGElement& el) + { + if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + el.markerEnd = url; + }); + map.set ("marker", [] (const String& v, SVGElement& el) + { + if (auto url = SVGParser::extractUrlId (v); url.isNotEmpty()) + { + el.markerStart = url; + el.markerMid = url; + el.markerEnd = url; + } + }); + + // Rules + map.set ("fill-rule", [] (const String& v, SVGElement& el) + { + if (v == "evenodd" || v == "nonzero") + el.fillRule = v; + }); + map.set ("clip-rule", [] (const String& v, SVGElement& el) + { + if (v == "evenodd" || v == "nonzero") + el.clipRule = v; + }); + + // Blend mode + map.set ("mix-blend-mode", [] (const String& v, SVGElement& el) + { + el.blendMode = SVGParser::parseBlendMode (v).value_or (BlendMode::SrcOver); + }); } - } - else if (property == "font-style") - { - e.fontItalic = (value == "italic" || value == "oblique"); - } - else if (property == "font-variant") - { - YUP_DRAWABLE_LOG ("CSS font-variant currently not applied - value: " << value); - } - else if (property == "font-stretch") - { - YUP_DRAWABLE_LOG ("CSS font-stretch currently not applied - value: " << value); - } - else if (property == "font") - { - YUP_DRAWABLE_LOG ("CSS font shorthand currently not applied - value: " << value); - } - else if (property == "dominant-baseline") - { - YUP_DRAWABLE_LOG ("CSS dominant-baseline currently not applied - value: " << value); - } - else if (property == "alignment-baseline") - { - YUP_DRAWABLE_LOG ("CSS alignment-baseline currently not applied - value: " << value); - } - else if (property == "baseline-shift") - { - YUP_DRAWABLE_LOG ("CSS baseline-shift currently not applied - value: " << value); - } - else if (property == "clip-path") - { - String clipPathUrl = SVGParser::extractUrlId (value); - if (clipPathUrl.isNotEmpty()) - e.clipPathUrl = clipPathUrl; - } - else if (property == "mask") - { - if (auto maskUrl = SVGParser::extractUrlId (value); maskUrl.isNotEmpty()) - e.maskUrl = maskUrl; - } - else if (property == "marker-start") - { - if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) - e.markerStart = url; - } - else if (property == "marker-mid") + }; + + static const PropertyHandlers handlers; + + if (auto* handler = handlers.map.getPointer (property)) + (*handler) (value, e); + else { - if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) - e.markerMid = url; + // Log-only properties (not yet handled) + YUP_DRAWABLE_LOG ("Unsupported CSS property ignored - property: " << property << " value: " << String (valueRef.text)); } - else if (property == "marker-end") +} + +//============================================================================== + +void SVGCssParser::applyStylesheetRules (const XmlElement& xmlElement, SVGElement& e) +{ + if (data.cssRules.empty()) + return; + + // Collect candidate rule indices from each index bucket + std::vector candidateIndices; + + const auto tagName = xmlElement.getTagNameWithoutNamespace(); + if (auto* tagRules = ruleIndex.byTag.getPointer (tagName)) + candidateIndices.insert (candidateIndices.end(), tagRules->begin(), tagRules->end()); + + const auto id = xmlElement.getStringAttribute ("id"); + if (id.isNotEmpty()) { - if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) - e.markerEnd = url; + if (auto* idRules = ruleIndex.byId.getPointer (id)) + candidateIndices.insert (candidateIndices.end(), idRules->begin(), idRules->end()); } - else if (property == "marker") + + const auto classAttr = xmlElement.getStringAttribute ("class"); + if (classAttr.isNotEmpty()) { - if (auto url = SVGParser::extractUrlId (value); url.isNotEmpty()) + const auto classNames = StringArray::fromTokens (classAttr, " \t\r\n", ""); + for (const auto& className : classNames) { - e.markerStart = url; - e.markerMid = url; - e.markerEnd = url; + if (auto* classRules = ruleIndex.byClass.getPointer (className)) + candidateIndices.insert (candidateIndices.end(), classRules->begin(), classRules->end()); } } - else if (property == "stroke-miterlimit") - { - float val = value.getFloatValue(); - e.strokeMiterLimit = std::max (1.0f, val); - } - else if (property == "filter") - { - if (value == "none") - e.filterUrl.reset(); - else if (auto filterUrl = SVGParser::extractUrlId (value); filterUrl.isNotEmpty()) - e.filterUrl = filterUrl; - else - YUP_DRAWABLE_LOG ("CSS filter currently only supports url(...) - value: " << value); - } - else if (property == "stroke-dasharray") + + if (candidateIndices.empty()) + return; + + // Deduplicate + std::sort (candidateIndices.begin(), candidateIndices.end()); + candidateIndices.erase (std::unique (candidateIndices.begin(), candidateIndices.end()), candidateIndices.end()); + + // Re-verify each candidate: the index returns candidates by single dimension + // (tag, id, or class), but multi-part selectors like "circle.highlight" need + // all parts to match. Filter out candidates where the full selector doesn't match. + const auto elementId = xmlElement.getStringAttribute ("id"); + const auto elementClasses = classAttr.isNotEmpty() + ? StringArray::fromTokens (classAttr, " \t\r\n", "") + : StringArray(); + + const auto selectorMatches = [&] (const SVGCssRule& rule) -> bool { - if (value == "none") + const auto& sel = rule.selector; + if (sel.isEmpty() || sel.containsChar (' ') || sel.containsChar ('>') || sel.containsChar ('+')) + return false; + + const auto hashIdx = sel.indexOf ("#"); + const auto dotIdx = sel.indexOf ("."); + + if (hashIdx < 0 && dotIdx < 0) + return true; // simple tag selector, already matched by byTag[index] + + if (hashIdx >= 0) { - e.strokeDashArray.reset(); - e.strokeDashArrayNone = true; + const auto idEnd = (dotIdx > hashIdx) ? dotIdx : sel.length(); + if (sel.substring (hashIdx + 1, idEnd) != elementId) + return false; } - else - { - Array dashes; - for (const auto dash : SVGParser::parseLengthList (value, e.fontSize.value_or (12.0f), 100.0f)) - { - if (dash >= 0.0f) - dashes.add (dash); - } - if (! dashes.isEmpty()) - { - e.strokeDashArray = dashes; - e.strokeDashArrayNone = false; - } + if (dotIdx >= 0) + { + const auto className = sel.substring (dotIdx + 1); + if (! elementClasses.contains (className)) + return false; } - } - else if (property == "stroke-dashoffset") - { - e.strokeDashOffset = SVGParser::parseUnit (value); - } - else if (property == "fill-opacity") - { - float opacity = value.getFloatValue(); - if (opacity >= 0.0f && opacity <= 1.0f) - e.fillOpacity = opacity; - } - else if (property == "stroke-opacity") - { - float opacity = value.getFloatValue(); - if (opacity >= 0.0f && opacity <= 1.0f) - e.strokeOpacity = opacity; - } - else if (property == "fill-rule") - { - if (value == "evenodd" || value == "nonzero") - e.fillRule = value; - } - else if (property == "clip-rule") - { - if (value == "evenodd" || value == "nonzero") - e.clipRule = value; - } - else if (property == "mix-blend-mode") - { - if (value == "multiply") - e.blendMode = BlendMode::Multiply; - else if (value == "screen") - e.blendMode = BlendMode::Screen; - else if (value == "overlay") - e.blendMode = BlendMode::Overlay; - else if (value == "darken") - e.blendMode = BlendMode::Darken; - else if (value == "lighten") - e.blendMode = BlendMode::Lighten; - else if (value == "color-dodge") - e.blendMode = BlendMode::ColorDodge; - else if (value == "color-burn") - e.blendMode = BlendMode::ColorBurn; - else if (value == "hard-light") - e.blendMode = BlendMode::HardLight; - else if (value == "soft-light") - e.blendMode = BlendMode::SoftLight; - else if (value == "difference") - e.blendMode = BlendMode::Difference; - else if (value == "exclusion") - e.blendMode = BlendMode::Exclusion; - else if (value == "hue") - e.blendMode = BlendMode::Hue; - else if (value == "saturation") - e.blendMode = BlendMode::Saturation; - else if (value == "color") - e.blendMode = BlendMode::Color; - else if (value == "luminosity") - e.blendMode = BlendMode::Luminosity; - } - else + + return true; + }; + + candidateIndices.erase (std::remove_if (candidateIndices.begin(), candidateIndices.end(), [&] (int i) { - YUP_DRAWABLE_LOG ("Unsupported CSS property ignored - property: " << property << " value: " << value); - } -} + return ! selectorMatches (data.cssRules[static_cast (i)]); + }), + candidateIndices.end()); -//============================================================================== + if (candidateIndices.empty()) + return; -void SVGCssParser::applyStylesheetRules (const XmlElement& xmlElement, SVGElement& e) -{ std::vector matchedRules; + matchedRules.reserve (candidateIndices.size()); - for (const auto& rule : data.cssRules) - { - if (matchesCssSelector (xmlElement, rule)) - matchedRules.push_back (std::addressof (rule)); - } + for (const auto index : candidateIndices) + matchedRules.push_back (std::addressof (data.cssRules[static_cast (index)])); std::stable_sort (matchedRules.begin(), matchedRules.end(), [] (const SVGCssRule* a, const SVGCssRule* b) { @@ -456,55 +533,15 @@ void SVGCssParser::parseStyleElement (const XmlElement& element) //============================================================================== -bool SVGCssParser::matchesCssSelector (const XmlElement& xmlElement, const SVGCssRule& rule) const +void SVGCssParser::buildCssRuleIndex() { - auto selector = rule.selector.trim(); - if (selector.isEmpty() || selector.containsChar (' ') || selector.containsChar ('>') || selector.containsChar ('+')) - return false; - - String tagName; - String id; - String className; - - auto hashIndex = selector.indexOf ("#"); - auto dotIndex = selector.indexOf ("."); - auto splitIndex = -1; - - if (hashIndex >= 0 && dotIndex >= 0) - splitIndex = jmin (hashIndex, dotIndex); - else - splitIndex = jmax (hashIndex, dotIndex); - - if (splitIndex > 0) - tagName = selector.substring (0, splitIndex); - - if (hashIndex == 0) - id = selector.substring (1); - else if (hashIndex > 0) - id = selector.substring (hashIndex + 1, dotIndex > hashIndex ? dotIndex : selector.length()); - - if (dotIndex == 0) - className = selector.substring (1); - else if (dotIndex > 0) - className = selector.substring (dotIndex + 1); - - if (splitIndex < 0 && ! selector.startsWithChar ('#') && ! selector.startsWithChar ('.')) - tagName = selector; - - if (tagName.isNotEmpty() && tagName != xmlElement.getTagNameWithoutNamespace()) - return false; - - if (id.isNotEmpty() && id != xmlElement.getStringAttribute ("id")) - return false; - - if (className.isNotEmpty()) - { - auto classes = StringArray::fromTokens (xmlElement.getStringAttribute ("class"), " \t\r\n", ""); - if (! classes.contains (className)) - return false; - } + ruleIndex = {}; + ruleIndex.buildFrom (data.cssRules); - return tagName.isNotEmpty() || id.isNotEmpty() || className.isNotEmpty(); + YUP_DRAWABLE_LOG ("buildCssRuleIndex - totalRules: " << data.cssRules.size() + << " byTag: " << ruleIndex.byTag.size() + << " byId: " << ruleIndex.byId.size() + << " byClass: " << ruleIndex.byClass.size()); } } // namespace yup diff --git a/modules/yup_graphics/svg/yup_SVGCssParser.h b/modules/yup_graphics/svg/yup_SVGCssParser.h index 4f305159d..31583ad91 100644 --- a/modules/yup_graphics/svg/yup_SVGCssParser.h +++ b/modules/yup_graphics/svg/yup_SVGCssParser.h @@ -36,10 +36,11 @@ class SVGCssParser void applyStyleProperty (StringRef property, StringRef value, SVGElement& e); void applyStylesheetRules (const XmlElement& xmlElement, SVGElement& e); void parseStyleElement (const XmlElement& element); - bool matchesCssSelector (const XmlElement& xmlElement, const SVGCssRule& rule) const; + void buildCssRuleIndex(); private: SVGData& data; + SVGCssRuleIndex ruleIndex; }; } // namespace yup diff --git a/modules/yup_graphics/svg/yup_SVGCssRule.h b/modules/yup_graphics/svg/yup_SVGCssRule.h index e17ed5acc..9869bcef0 100644 --- a/modules/yup_graphics/svg/yup_SVGCssRule.h +++ b/modules/yup_graphics/svg/yup_SVGCssRule.h @@ -31,4 +31,60 @@ struct SVGCssRule int order = 0; }; +//============================================================================== +/** Index for fast CSS rule lookup by tag name, id, and class name. + + Built after all CSS rules are parsed and used by SVGCssParser::applyStylesheetRules + to avoid O(N×M) brute-force matching of every rule against every element. +*/ +struct SVGCssRuleIndex +{ + /** Maps a tag name (e.g. "path", "rect") to the indices of matching rules. */ + HashMap> byTag; + + /** Maps an id (e.g. "myId") to the indices of matching rules. */ + HashMap> byId; + + /** Maps a class name (e.g. "myClass") to the indices of matching rules. */ + HashMap> byClass; + + /** Builds the index from a vector of rules. */ + void buildFrom (const std::vector& rules) + { + for (int i = 0; i < static_cast (rules.size()); ++i) + { + const auto& rule = rules[i]; + const auto& sel = rule.selector; + + const auto hashIndex = sel.indexOf ("#"); + const auto dotIndex = sel.indexOf ("."); + const auto splitIndex = (hashIndex >= 0 && dotIndex >= 0) + ? jmin (hashIndex, dotIndex) + : jmax (hashIndex, dotIndex); + + if (hashIndex >= 0) + { + const auto idEnd = (dotIndex > hashIndex) ? dotIndex : sel.length(); + auto id = sel.substring (hashIndex + 1, idEnd); + if (id.isNotEmpty()) + byId.getReference (id).push_back (i); + } + + if (dotIndex >= 0) + { + auto className = sel.substring (dotIndex + 1); + if (className.isNotEmpty()) + byClass.getReference (className).push_back (i); + } + + if (splitIndex != 0 && ! sel.startsWithChar ('#') && ! sel.startsWithChar ('.')) + { + auto tagName = (splitIndex > 0) ? sel.substring (0, splitIndex) : sel; + if (tagName.isNotEmpty()) + byTag.getReference (tagName).push_back (i); + } + } + } +}; + } // namespace yup diff --git a/modules/yup_graphics/svg/yup_SVGParser.cpp b/modules/yup_graphics/svg/yup_SVGParser.cpp index 6f32624e5..061d82a69 100644 --- a/modules/yup_graphics/svg/yup_SVGParser.cpp +++ b/modules/yup_graphics/svg/yup_SVGParser.cpp @@ -126,11 +126,38 @@ bool SVGParser::parseDocument (std::unique_ptr svgRoot) }; collectStyleElements (*svgRoot); + cssParser.buildCssRuleIndex(); auto result = parseElement (*svgRoot, true, {}); resolvePatternHrefs(); + // Pre-resolve gradient href chains at parse time to avoid O(N) re-resolution at render + for (auto& gradient : data.gradients) + { + if (! gradient->href.isEmpty()) + { + auto resolved = resolveGradient (gradient); + gradient = resolved; + } + } + data.gradientsById.clear(); + for (const auto& gradient : data.gradients) + data.gradientsById.set (gradient->id, gradient); + + // Pre-resolve filter href chains at parse time + for (auto& filter : data.filters) + { + if (! filter->href.isEmpty()) + { + auto resolved = resolveFilter (filter); + filter = resolved; + } + } + data.filtersById.clear(); + for (const auto& filter : data.filters) + data.filtersById.set (filter->id, filter); + if (result) { data.bounds = document.calculateBounds(); @@ -914,38 +941,7 @@ void SVGParser::parseStyle (const XmlElement& element, const AffineTransform& cu String mixBlendMode = element.getStringAttribute ("mix-blend-mode"); if (mixBlendMode.isNotEmpty()) - { - if (mixBlendMode == "multiply") - e.blendMode = BlendMode::Multiply; - else if (mixBlendMode == "screen") - e.blendMode = BlendMode::Screen; - else if (mixBlendMode == "overlay") - e.blendMode = BlendMode::Overlay; - else if (mixBlendMode == "darken") - e.blendMode = BlendMode::Darken; - else if (mixBlendMode == "lighten") - e.blendMode = BlendMode::Lighten; - else if (mixBlendMode == "color-dodge") - e.blendMode = BlendMode::ColorDodge; - else if (mixBlendMode == "color-burn") - e.blendMode = BlendMode::ColorBurn; - else if (mixBlendMode == "hard-light") - e.blendMode = BlendMode::HardLight; - else if (mixBlendMode == "soft-light") - e.blendMode = BlendMode::SoftLight; - else if (mixBlendMode == "difference") - e.blendMode = BlendMode::Difference; - else if (mixBlendMode == "exclusion") - e.blendMode = BlendMode::Exclusion; - else if (mixBlendMode == "hue") - e.blendMode = BlendMode::Hue; - else if (mixBlendMode == "saturation") - e.blendMode = BlendMode::Saturation; - else if (mixBlendMode == "color") - e.blendMode = BlendMode::Color; - else if (mixBlendMode == "luminosity") - e.blendMode = BlendMode::Luminosity; - } + e.blendMode = parseBlendMode (mixBlendMode).value_or (BlendMode::SrcOver); String fontFamily = element.getStringAttribute ("font-family"); if (fontFamily.isNotEmpty()) @@ -1345,6 +1341,7 @@ SVGGradient::Ptr SVGParser::resolveGradient (SVGGradient::Ptr gradient) const SVGGradient::Ptr resolvedGradient = new SVGGradient; resolvedGradient->type = gradient->type; resolvedGradient->id = gradient->id; + resolvedGradient->href = gradient->href; // preserve the href for introspection resolvedGradient->units = referencedGradient->units; resolvedGradient->spreadMethod = referencedGradient->spreadMethod; resolvedGradient->start = referencedGradient->start; @@ -1456,73 +1453,9 @@ void SVGParser::parseFEBlend (const XmlElement& element, SVGFilter& filter) auto mode = element.getStringAttribute ("mode"); if (mode.isEmpty()) - { blend->mode = BlendMode::SrcOver; - } - else if (mode == "normal") - { - blend->mode = BlendMode::SrcOver; - } - else if (mode == "multiply") - { - blend->mode = BlendMode::Multiply; - } - else if (mode == "screen") - { - blend->mode = BlendMode::Screen; - } - else if (mode == "overlay") - { - blend->mode = BlendMode::Overlay; - } - else if (mode == "darken") - { - blend->mode = BlendMode::Darken; - } - else if (mode == "lighten") - { - blend->mode = BlendMode::Lighten; - } - else if (mode == "color-dodge") - { - blend->mode = BlendMode::ColorDodge; - } - else if (mode == "color-burn") - { - blend->mode = BlendMode::ColorBurn; - } - else if (mode == "hard-light") - { - blend->mode = BlendMode::HardLight; - } - else if (mode == "soft-light") - { - blend->mode = BlendMode::SoftLight; - } - else if (mode == "difference") - { - blend->mode = BlendMode::Difference; - } - else if (mode == "exclusion") - { - blend->mode = BlendMode::Exclusion; - } - else if (mode == "hue") - { - blend->mode = BlendMode::Hue; - } - else if (mode == "saturation") - { - blend->mode = BlendMode::Saturation; - } - else if (mode == "color") - { - blend->mode = BlendMode::Color; - } - else if (mode == "luminosity") - { - blend->mode = BlendMode::Luminosity; - } + else + blend->mode = parseBlendMode (mode).value_or (BlendMode::SrcOver); if (blend->in.isEmpty() && blend->in2.isEmpty()) blend->in = "SourceGraphic"; @@ -2057,25 +1990,28 @@ float SVGParser::parseUnit (const String& value, float defaultValue, float fontS if (end == begin) return defaultValue; - String unit = String (CharPointer_UTF8 (end)).trim().toLowerCase(); + // Skip whitespace before unit identifier + while (end != nullptr && (*end == ' ' || *end == '\t')) + ++end; - if (unit.isEmpty() || unit == "px") + // Compare unit suffix without allocating a String (hot path called per attribute) + if (end == nullptr || *end == 0 || strcmp (end, "px") == 0) return static_cast (numericValue); - if (unit == "pt") + if (strcmp (end, "pt") == 0) return static_cast (numericValue * 1.333333); - if (unit == "pc") + if (strcmp (end, "pc") == 0) return static_cast (numericValue * 16.0); - if (unit == "mm") + if (strcmp (end, "mm") == 0) return static_cast (numericValue * 3.779528); - if (unit == "cm") + if (strcmp (end, "cm") == 0) return static_cast (numericValue * 37.79528); - if (unit == "in") + if (strcmp (end, "in") == 0) return static_cast (numericValue * 96.0); - if (unit == "em") + if (strcmp (end, "em") == 0) return static_cast (numericValue * fontSize); - if (unit == "ex") + if (strcmp (end, "ex") == 0) return static_cast (numericValue * fontSize * 0.5); - if (unit == "%") + if (strcmp (end, "%") == 0) return static_cast (numericValue * viewportSize * 0.01); return static_cast (numericValue); @@ -2125,6 +2061,45 @@ String SVGParser::extractUrlId (const String& value) //============================================================================== +std::optional SVGParser::parseBlendMode (StringRef value) +{ + if (value == StringRef ("multiply")) + return BlendMode::Multiply; + if (value == StringRef ("screen")) + return BlendMode::Screen; + if (value == StringRef ("overlay")) + return BlendMode::Overlay; + if (value == StringRef ("darken")) + return BlendMode::Darken; + if (value == StringRef ("lighten")) + return BlendMode::Lighten; + if (value == StringRef ("color-dodge")) + return BlendMode::ColorDodge; + if (value == StringRef ("color-burn")) + return BlendMode::ColorBurn; + if (value == StringRef ("hard-light")) + return BlendMode::HardLight; + if (value == StringRef ("soft-light")) + return BlendMode::SoftLight; + if (value == StringRef ("difference")) + return BlendMode::Difference; + if (value == StringRef ("exclusion")) + return BlendMode::Exclusion; + if (value == StringRef ("hue")) + return BlendMode::Hue; + if (value == StringRef ("saturation")) + return BlendMode::Saturation; + if (value == StringRef ("color")) + return BlendMode::Color; + if (value == StringRef ("luminosity")) + return BlendMode::Luminosity; + if (value == StringRef ("normal")) + return BlendMode::SrcOver; + return std::nullopt; +} + +//============================================================================== + std::optional SVGParser::loadImageFromHref (const SVGDocument::ParseOptions& options, const String& href) { YUP_DRAWABLE_LOG ("loadImageFromHref - href: " << href diff --git a/modules/yup_graphics/svg/yup_SVGParser.h b/modules/yup_graphics/svg/yup_SVGParser.h index 007c806cb..26f7ada52 100644 --- a/modules/yup_graphics/svg/yup_SVGParser.h +++ b/modules/yup_graphics/svg/yup_SVGParser.h @@ -58,6 +58,10 @@ class YUP_API SVGParser static String extractGradientUrl (const String& value); static String extractUrlId (const String& value); + /** Parses a CSS blend mode string (e.g. "multiply", "screen") into a BlendMode. + Returns std::nullopt for unrecognised values. */ + static std::optional parseBlendMode (StringRef value); + static std::optional loadImageFromHref (const SVGDocument::ParseOptions& options, const String& href); ///@} diff --git a/tests/yup_graphics/yup_SVGDocument.cpp b/tests/yup_graphics/yup_SVGDocument.cpp index 85bdcd456..cfa60bff4 100644 --- a/tests/yup_graphics/yup_SVGDocument.cpp +++ b/tests/yup_graphics/yup_SVGDocument.cpp @@ -2575,19 +2575,39 @@ TEST (SVGDocumentTests, SVGCssParserMatchesSimpleSelectors) { SVGData data; SVGCssParser parser (data); + + data.cssRules.push_back ({ "rect", { "fill: red" }, 1, 1 }); + data.cssRules.push_back ({ "#target", { "fill: blue" }, 100, 2 }); + data.cssRules.push_back ({ ".highlight", { "fill: green" }, 10, 3 }); + data.cssRules.push_back ({ "rect#target.highlight", { "fill: yellow" }, 111, 4 }); + data.cssRules.push_back ({ ".missing", { "fill: black" }, 10, 5 }); + data.cssRules.push_back ({ "circle.highlight", { "fill: white" }, 11, 6 }); + parser.buildCssRuleIndex(); + XmlElement rect ("rect"); rect.setAttribute ("id", "target"); rect.setAttribute ("class", "highlight selected"); - EXPECT_TRUE (parser.matchesCssSelector (rect, SVGCssRule { "rect", {}, 0, 0 })); - EXPECT_TRUE (parser.matchesCssSelector (rect, SVGCssRule { "#target", {}, 0, 0 })); - EXPECT_TRUE (parser.matchesCssSelector (rect, SVGCssRule { ".highlight", {}, 0, 0 })); - EXPECT_TRUE (parser.matchesCssSelector (rect, SVGCssRule { "rect#target.highlight", {}, 0, 0 })); + // Should match: rect tag, #target id, .highlight class, combined selector + { + SVGElement e; + e.tagName = "rect"; + parser.applyStylesheetRules (rect, e); + EXPECT_TRUE (e.fillColor.has_value()); + // Highest specificity wins: rect#target.highlight (111) → yellow + EXPECT_EQ (e.fillColor->toString(), Color::fromString ("yellow").toString()); + } - EXPECT_FALSE (parser.matchesCssSelector (rect, SVGCssRule { "", {}, 0, 0 })); - EXPECT_FALSE (parser.matchesCssSelector (rect, SVGCssRule { "g rect", {}, 0, 0 })); - EXPECT_FALSE (parser.matchesCssSelector (rect, SVGCssRule { ".missing", {}, 0, 0 })); - EXPECT_FALSE (parser.matchesCssSelector (rect, SVGCssRule { "circle.highlight", {}, 0, 0 })); + // Empty/missing selectors should not match + { + XmlElement circle ("circle"); + circle.setAttribute ("id", "other"); + circle.setAttribute ("class", "otherclass"); + SVGElement e; + e.tagName = "circle"; + parser.applyStylesheetRules (circle, e); + EXPECT_FALSE (e.fillColor.has_value()); + } } TEST (SVGDocumentTests, SVGCssParserApplyStylesheetRulesUsesSpecificityOrder) @@ -2598,6 +2618,7 @@ TEST (SVGDocumentTests, SVGCssParserApplyStylesheetRulesUsesSpecificityOrder) data.cssRules.push_back ({ "#target", { "fill: green" }, 100, 2 }); SVGCssParser parser (data); + parser.buildCssRuleIndex(); XmlElement rect ("rect"); rect.setAttribute ("id", "target"); rect.setAttribute ("class", "highlight");