svg: added filter and gaussian blur nodes support

@Issue: https://github.com/thorvg/thorvg/issues/1367
This commit is contained in:
Mira Grudzinska 2025-01-12 14:37:44 +01:00
parent b79d12a04d
commit fbdc958623
3 changed files with 323 additions and 14 deletions

View file

@ -1088,6 +1088,17 @@ static void _handleMaskAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, con
}
static void _handleFilterAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, const char* value)
{
SvgStyleProperty* style = node->style;
int len = strlen(value);
if (len >= 3 && !strncmp(value, "url", 3)) {
if (style->filter.url) free(style->filter.url);
style->filter.url = _idFromUrl((const char*)(value + 3));
}
}
static void _handleMaskTypeAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, const char* value)
{
node->node.mask.type = _toMaskType(value);
@ -1166,7 +1177,8 @@ static constexpr struct
STYLE_DEF(mask, Mask, SvgStyleFlags::Mask),
STYLE_DEF(mask-type, MaskType, SvgStyleFlags::MaskType),
STYLE_DEF(display, Display, SvgStyleFlags::Display),
STYLE_DEF(paint-order, PaintOrder, SvgStyleFlags::PaintOrder)
STYLE_DEF(paint-order, PaintOrder, SvgStyleFlags::PaintOrder),
STYLE_DEF(filter, Filter, SvgStyleFlags::Filter)
};
@ -1240,6 +1252,8 @@ static bool _attrParseGNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else {
return _parseStyleAttr(loader, key, value, false);
}
@ -1343,6 +1357,108 @@ static bool _attrParseSymbolNode(void* data, const char* key, const char* value)
}
static constexpr struct
{
const char* tag;
SvgParserLengthType type;
int sz;
size_t offset;
} boxTags[] = {
{"x", SvgParserLengthType::Horizontal, sizeof("x"), offsetof(Box, x)},
{"y", SvgParserLengthType::Vertical, sizeof("y"), offsetof(Box, y)},
{"width", SvgParserLengthType::Horizontal, sizeof("width"), offsetof(Box, w)},
{"height", SvgParserLengthType::Vertical, sizeof("height"), offsetof(Box, h)}
};
static bool _parseBox(const char* key, const char* value, Box* box, bool (&isPercentage)[4])
{
auto array = (unsigned char*)box;
int sz = strlen(key);
for (unsigned int i = 0; i < sizeof(boxTags) / sizeof(boxTags[0]); i++) {
if (boxTags[i].sz - 1 == sz && !strncmp(boxTags[i].tag, key, sz)) {
*(float*)(array + boxTags[i].offset) = _gradientToFloat(nullptr, value, isPercentage[i]);
return true;
}
}
return false;
}
static void _recalcBox(const SvgLoaderData* loader, Box* box, bool (&isPercentage)[4])
{
auto array = (unsigned char*)box;
for (unsigned int i = 0; i < sizeof(boxTags) / sizeof(boxTags[0]); i++) {
if (!isPercentage[i]) continue;
if (boxTags[i].type == SvgParserLengthType::Horizontal) *(float*)(array + boxTags[i].offset) *= loader->svgParse->global.w;
else *(float*)(array + boxTags[i].offset) *= loader->svgParse->global.h;
}
}
static bool _attrParseFilterNode(void* data, const char* key, const char* value)
{
SvgLoaderData* loader = (SvgLoaderData*)data;
SvgNode* node = loader->svgParse->node;
SvgFilterNode* filter = &node->node.filter;
_parseBox(key, value, &filter->box, filter->isPercentage);
if (!strcmp(key, "id")) {
if (node->id && value) free(node->id);
node->id = _copyId(value);
} else if (!strcmp(key, "primitiveUnits")) {
if (!strcmp(value, "objectBoundingBox")) filter->primitiveUserSpace = false;
} else if (!strcmp(key, "filterUnits")) {
if (!strcmp(value, "userSpaceOnUse")) filter->filterUserSpace = true;
}
return true;
}
static void _parseGaussianBlurStdDeviation(const char** content, float* x, float* y)
{
auto str = *content;
char* end = nullptr;
float deviation[2] = {0, 0};
int n = 0;
while (*str && n < 2) {
str = _skipComma(str);
auto parsedValue = strToFloat(str, &end);
if (parsedValue < 0.0f) break;
deviation[n++] = parsedValue;
str = end;
}
*x = deviation[0];
*y = n == 1 ? deviation[0] : deviation[1];
}
static bool _attrParseGaussianBlurNode(void* data, const char* key, const char* value)
{
SvgLoaderData* loader = (SvgLoaderData*)data;
SvgNode* node = loader->svgParse->node;
SvgGaussianBlurNode* gaussianBlur = &node->node.gaussianBlur;
if (_parseBox(key, value, &gaussianBlur->box, gaussianBlur->isPercentage)) gaussianBlur->hasBox = true;
if (!strcmp(key, "id")) {
if (node->id && value) free(node->id);
node->id = _copyId(value);
} else if (!strcmp(key, "stdDeviation")) {
_parseGaussianBlurStdDeviation(&value, &gaussianBlur->stdDevX, &gaussianBlur->stdDevY);
} else if (!strcmp(key, "edgeMode")) {
if (!strcmp(value, "wrap")) gaussianBlur->edgeModeWrap = true;
} else {
return _parseStyleAttr(loader, key, value, false);
}
return true;
}
static SvgNode* _createNode(SvgNode* parent, SvgNodeType type)
{
SvgNode* node = (SvgNode*)calloc(1, sizeof(SvgNode));
@ -1504,6 +1620,48 @@ static SvgNode* _createSymbolNode(SvgLoaderData* loader, SvgNode* parent, const
}
static SvgNode* _createGaussianBlurNode(SvgLoaderData* loader, SvgNode* parent, const char* buf, unsigned bufLength, parseAttributes func)
{
loader->svgParse->node = _createNode(parent, SvgNodeType::GaussianBlur);
if (!loader->svgParse->node) return nullptr;
SvgGaussianBlurNode& gaussianBlur = loader->svgParse->node->node.gaussianBlur;
loader->svgParse->node->style->display = false;
gaussianBlur.stdDevX = 0.0f;
gaussianBlur.stdDevY = 0.0f;
gaussianBlur.box = {0.0f, 0.0f, 1.0f, 1.0f};
for (auto& p : gaussianBlur.isPercentage) p = false;
gaussianBlur.hasBox = false;
gaussianBlur.edgeModeWrap = false;
func(buf, bufLength, _attrParseGaussianBlurNode, loader);
return loader->svgParse->node;
}
static SvgNode* _createFilterNode(SvgLoaderData* loader, SvgNode* parent, const char* buf, unsigned bufLength, parseAttributes func)
{
loader->svgParse->node = _createNode(parent, SvgNodeType::Filter);
if (!loader->svgParse->node) return nullptr;
SvgFilterNode& filter = loader->svgParse->node->node.filter;
loader->svgParse->node->style->display = false;
filter.box = {-0.1f, -0.1f, 1.2f, 1.2f};
for (auto& p : filter.isPercentage) p = false;
filter.filterUserSpace = false;
filter.primitiveUserSpace = true;
func(buf, bufLength, _attrParseFilterNode, loader);
if (filter.filterUserSpace) _recalcBox(loader, &filter.box, filter.isPercentage);
return loader->svgParse->node;
}
static bool _attrParsePathNode(void* data, const char* key, const char* value)
{
SvgLoaderData* loader = (SvgLoaderData*)data;
@ -1520,6 +1678,8 @@ static bool _attrParsePathNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else if (!strcmp(key, "id")) {
if (value) free(node->id);
node->id = _copyId(value);
@ -1582,6 +1742,8 @@ static bool _attrParseCircleNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else if (!strcmp(key, "id")) {
if (value) free(node->id);
node->id = _copyId(value);
@ -1649,6 +1811,8 @@ static bool _attrParseEllipseNode(void* data, const char* key, const char* value
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else {
return _parseStyleAttr(loader, key, value, false);
}
@ -1698,6 +1862,8 @@ static bool _attrParsePolygonNode(void* data, const char* key, const char* value
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else if (!strcmp(key, "id")) {
if (value) free(node->id);
node->id = _copyId(value);
@ -1785,6 +1951,8 @@ static bool _attrParseRectNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else {
ret = _parseStyleAttr(loader, key, value, false);
}
@ -1850,6 +2018,8 @@ static bool _attrParseLineNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else {
return _parseStyleAttr(loader, key, value, false);
}
@ -1923,6 +2093,8 @@ static bool _attrParseImageNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else if (!strcmp(key, "transform")) {
node->transform = _parseTransformationMatrix(value);
} else {
@ -2103,6 +2275,8 @@ static bool _attrParseTextNode(void* data, const char* key, const char* value)
_handleClipPathAttr(loader, node, value);
} else if (!strcmp(key, "mask")) {
_handleMaskAttr(loader, node, value);
} else if (!strcmp(key, "filter")) {
_handleFilterAttr(loader, node, value);
} else if (!strcmp(key, "id")) {
if (value) free(node->id);
node->id = _copyId(value);
@ -2146,7 +2320,8 @@ static constexpr struct
{"polyline", sizeof("polyline"), _createPolylineNode},
{"line", sizeof("line"), _createLineNode},
{"image", sizeof("image"), _createImageNode},
{"text", sizeof("text"), _createTextNode}
{"text", sizeof("text"), _createTextNode},
{"feGaussianBlur", sizeof("feGaussianBlur"), _createGaussianBlurNode}
};
@ -2162,7 +2337,8 @@ static constexpr struct
{"mask", sizeof("mask"), _createMaskNode},
{"clipPath", sizeof("clipPath"), _createClipPathNode},
{"style", sizeof("style"), _createCssStyleNode},
{"symbol", sizeof("symbol"), _createSymbolNode}
{"symbol", sizeof("symbol"), _createSymbolNode},
{"filter", sizeof("filter"), _createFilterNode}
};
@ -3084,6 +3260,10 @@ static void _copyAttr(SvgNode* to, const SvgNode* from)
free(to->style->mask.url);
to->style->mask.url = strdup(from->style->mask.url);
}
if (from->style->filter.url) {
if (to->style->filter.url) free(to->style->filter.url);
to->style->filter.url = strdup(from->style->filter.url);
}
//Copy node attribute
switch (from->type) {
@ -3554,13 +3734,29 @@ static void _updateComposite(SvgNode* node, SvgNode* root)
}
static void _updateFilter(SvgNode* node, SvgNode* root)
{
if (node->style->filter.url && !node->style->filter.node) {
SvgNode* findResult = _findNodeById(root, node->style->filter.url);
if (findResult) node->style->filter.node = findResult;
}
if (node->child.count > 0) {
auto child = node->child.data;
for (uint32_t i = 0; i < node->child.count; ++i, ++child) {
_updateFilter(*child, root);
}
}
}
static void _freeNodeStyle(SvgStyleProperty* style)
{
if (!style) return;
//style->clipPath.node and style->mask.node has only the addresses of node. Therefore, node is released from _freeNode.
//style->clipPath.node/mask.node/filter.node has only the addresses of node. Therefore, node is released from _freeNode.
free(style->clipPath.url);
free(style->mask.url);
free(style->filter.url);
free(style->cssClass);
if (style->fill.paint.gradient) {
@ -3761,6 +3957,9 @@ void SvgLoader::run(unsigned tid)
_updateComposite(loaderData.doc, loaderData.doc);
if (defs) _updateComposite(loaderData.doc, defs);
_updateFilter(loaderData.doc, loaderData.doc);
if (defs) _updateFilter(loaderData.doc, defs);
_updateStyle(loaderData.doc, nullptr);
if (defs) _updateStyle(defs, nullptr);

View file

@ -26,6 +26,27 @@
#include "tvgCommon.h"
#include "tvgArray.h"
struct Box
{
float x, y, w, h;
void intersect(const Box& box)
{
auto x1 = x + w;
auto y1 = y + h;
auto x2 = box.x + box.w;
auto y2 = box.y + box.h;
x = x > box.x ? x : box.x;
y = y > box.y ? y : box.y;
w = (x1 < x2 ? x1 : x2) - x;
h = (y1 < y2 ? y1 : y2) - y;
if (w < 0.0f) w = 0.0f;
if (h < 0.0f) h = 0.0f;
}
};
struct SvgNode;
struct SvgStyleGradient;
@ -54,6 +75,8 @@ enum class SvgNodeType
Mask,
CssStyle,
Symbol,
Filter,
GaussianBlur,
Unknown
};
@ -142,6 +165,7 @@ enum class SvgStyleFlags
PaintOrder = 0x10000,
StrokeMiterlimit = 0x20000,
StrokeDashOffset = 0x40000,
Filter = 0x80000
};
constexpr bool operator &(SvgStyleFlags a, SvgStyleFlags b)
@ -363,6 +387,23 @@ struct SvgTextNode
float fontSize;
};
struct SvgGaussianBlurNode
{
float stdDevX, stdDevY;
Box box;
bool isPercentage[4];
bool hasBox;
bool edgeModeWrap;
};
struct SvgFilterNode
{
Box box;
bool isPercentage[4];
bool filterUserSpace;
bool primitiveUserSpace;
};
struct SvgLinearGradient
{
float x1, y1, x2, y2;
@ -456,12 +497,19 @@ struct SvgStyleStroke
SvgDash dash;
};
struct SvgFilter
{
char *url;
SvgNode* node;
};
struct SvgStyleProperty
{
SvgStyleFill fill;
SvgStyleStroke stroke;
SvgComposite clipPath;
SvgComposite mask;
SvgFilter filter;
int opacity;
SvgColor color;
char* cssClass;
@ -498,6 +546,8 @@ struct SvgNode
SvgCssStyleNode cssStyle;
SvgSymbolNode symbol;
SvgTextNode text;
SvgFilterNode filter;
SvgGaussianBlurNode gaussianBlur;
} node;
~SvgNode();
};
@ -550,9 +600,4 @@ struct SvgLoaderData
SvgNode* currentGraphicsNode = nullptr;
};
struct Box
{
float x, y, w, h;
};
#endif

View file

@ -41,7 +41,7 @@ static Scene* _sceneBuildHelper(SvgLoaderData& loaderData, const SvgNode* node,
static inline bool _isGroupType(SvgNodeType type)
{
if (type == SvgNodeType::Doc || type == SvgNodeType::G || type == SvgNodeType::Use || type == SvgNodeType::ClipPath || type == SvgNodeType::Symbol) return true;
if (type == SvgNodeType::Doc || type == SvgNodeType::G || type == SvgNodeType::Use || type == SvgNodeType::ClipPath || type == SvgNodeType::Symbol || type == SvgNodeType::Filter) return true;
return false;
}
@ -288,6 +288,66 @@ static Paint* _applyComposition(SvgLoaderData& loaderData, Paint* paint, const S
}
static Paint* _applyFilter(SvgLoaderData& loaderData, Paint* paint, const SvgNode* node, const Box& vBox, const string& svgPath)
{
auto filterNode = node->style->filter.node;
if (!filterNode || filterNode->child.count == 0) return paint;
auto& filter = filterNode->node.filter;
auto scene = Scene::gen();
Box bbox{};
paint->bounds(&bbox.x, &bbox.y, &bbox.w, &bbox.h, false);
Box clipBox = filter.filterUserSpace ? filter.box : Box{bbox.x + filter.box.x * bbox.w, bbox.y + filter.box.y * bbox.h, filter.box.w * bbox.w, filter.box.h * bbox.h};
auto primitiveUserSpace = filter.primitiveUserSpace;
auto sx = paint->transform().e11;
auto sy = paint->transform().e22;
auto child = filterNode->child.data;
for (uint32_t i = 0; i < filterNode->child.count; ++i, ++child) {
if ((*child)->type == SvgNodeType::GaussianBlur) {
auto& gauss = (*child)->node.gaussianBlur;
auto direction = gauss.stdDevX > 0.0f ? (gauss.stdDevY > 0.0f ? 0 : 1) : (gauss.stdDevY > 0.0f ? 2 : -1);
if (direction == -1) continue;
auto stdDevX = gauss.stdDevX;
auto stdDevY = gauss.stdDevY;
if (gauss.hasBox) {
auto gaussBox = gauss.box;
auto isPercent = gauss.isPercentage;
if (primitiveUserSpace) {
if (isPercent[0]) gaussBox.x *= loaderData.svgParse->global.w;
if (isPercent[1]) gaussBox.y *= loaderData.svgParse->global.h;
if (isPercent[2]) gaussBox.w *= loaderData.svgParse->global.w;
if (isPercent[3]) gaussBox.h *= loaderData.svgParse->global.h;
} else {
stdDevX *= bbox.w;
stdDevY *= bbox.h;
if (isPercent[0]) gaussBox.x = bbox.x + gauss.box.x * bbox.w;
if (isPercent[1]) gaussBox.y = bbox.y + gauss.box.y * bbox.h;
if (isPercent[2]) gaussBox.w *= bbox.w;
if (isPercent[3]) gaussBox.h *= bbox.h;
}
clipBox.intersect(gaussBox);
} else if (!primitiveUserSpace) {
stdDevX *= bbox.w;
stdDevY *= bbox.h;
}
scene->push(SceneEffect::GaussianBlur, 1.25f * (direction == 2 ? stdDevY * sy : stdDevX * sx), direction, gauss.edgeModeWrap, 55);
}
}
scene->push(paint);
auto clip = Shape::gen();
clip->appendRect(clipBox.x, clipBox.y, clipBox.w, clipBox.h);
clip->transform(paint->transform());
scene->clip(clip);
return scene;
}
static Paint* _applyProperty(SvgLoaderData& loaderData, SvgNode* node, Shape* vg, const Box& vBox, const string& svgPath, bool clip)
{
SvgStyleProperty* style = node->style;
@ -352,7 +412,8 @@ static Paint* _applyProperty(SvgLoaderData& loaderData, SvgNode* node, Shape* vg
vg->strokeFill(style->stroke.paint.color.r, style->stroke.paint.color.g, style->stroke.paint.color.b, style->stroke.opacity);
}
return _applyComposition(loaderData, vg, node, vBox, svgPath);
auto p = _applyFilter(loaderData, vg, node, vBox, svgPath);
return _applyComposition(loaderData, p, node, vBox, svgPath);
}
@ -596,7 +657,8 @@ static Paint* _imageBuildHelper(SvgLoaderData& loaderData, SvgNode* node, const
if (node->transform) m = *node->transform * m;
picture->transform(m);
return _applyComposition(loaderData, picture, node, vBox, svgPath);
auto p = _applyFilter(loaderData, picture, node, vBox, svgPath);
return _applyComposition(loaderData, p, node, vBox, svgPath);
}
@ -782,7 +844,8 @@ static Paint* _textBuildHelper(SvgLoaderData& loaderData, const SvgNode* node, c
_applyTextFill(node->style, text, vBox);
return _applyComposition(loaderData, text, node, vBox, svgPath);
auto p = _applyFilter(loaderData, text, node, vBox, svgPath);
return _applyComposition(loaderData, p, node, vBox, svgPath);
}
@ -823,7 +886,9 @@ static Scene* _sceneBuildHelper(SvgLoaderData& loaderData, const SvgNode* node,
}
}
scene->opacity(node->style->opacity);
return static_cast<Scene*>(_applyComposition(loaderData, scene, node, vBox, svgPath));
auto p = _applyFilter(loaderData, scene, node, vBox, svgPath);
return static_cast<Scene*>(_applyComposition(loaderData, p, node, vBox, svgPath));
}