| /* |
| * Copyright 2016 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "experimental/svg/model/SkSVGAttributeParser.h" |
| #include "experimental/svg/model/SkSVGCircle.h" |
| #include "experimental/svg/model/SkSVGClipPath.h" |
| #include "experimental/svg/model/SkSVGDOM.h" |
| #include "experimental/svg/model/SkSVGDefs.h" |
| #include "experimental/svg/model/SkSVGEllipse.h" |
| #include "experimental/svg/model/SkSVGG.h" |
| #include "experimental/svg/model/SkSVGLine.h" |
| #include "experimental/svg/model/SkSVGLinearGradient.h" |
| #include "experimental/svg/model/SkSVGNode.h" |
| #include "experimental/svg/model/SkSVGPath.h" |
| #include "experimental/svg/model/SkSVGPattern.h" |
| #include "experimental/svg/model/SkSVGPoly.h" |
| #include "experimental/svg/model/SkSVGRadialGradient.h" |
| #include "experimental/svg/model/SkSVGRect.h" |
| #include "experimental/svg/model/SkSVGRenderContext.h" |
| #include "experimental/svg/model/SkSVGSVG.h" |
| #include "experimental/svg/model/SkSVGStop.h" |
| #include "experimental/svg/model/SkSVGText.h" |
| #include "experimental/svg/model/SkSVGTypes.h" |
| #include "experimental/svg/model/SkSVGUse.h" |
| #include "experimental/svg/model/SkSVGValue.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkString.h" |
| #include "include/private/SkTo.h" |
| #include "include/utils/SkParsePath.h" |
| #include "src/core/SkTSearch.h" |
| #include "src/xml/SkDOM.h" |
| |
| namespace { |
| |
| bool SetPaintAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGPaint paint; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parsePaint(&paint)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGPaintValue(paint)); |
| return true; |
| } |
| |
| bool SetColorAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGColorType color; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseColor(&color)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGColorValue(color)); |
| return true; |
| } |
| |
| bool SetIRIAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGStringType iri; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseIRI(&iri)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGStringValue(iri)); |
| return true; |
| } |
| |
| bool SetClipPathAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGClip clip; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseClipPath(&clip)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGClipValue(clip)); |
| return true; |
| } |
| |
| |
| bool SetPathDataAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkPath path; |
| if (!SkParsePath::FromSVGString(stringValue, &path)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGPathValue(path)); |
| return true; |
| } |
| |
| bool SetStringAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkString str(stringValue, strlen(stringValue)); |
| SkSVGStringType strType = SkSVGStringType(str); |
| node->setAttribute(attr, SkSVGStringValue(strType)); |
| return true; |
| } |
| |
| bool SetTransformAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGTransformType transform; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseTransform(&transform)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGTransformValue(transform)); |
| return true; |
| } |
| |
| bool SetLengthAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGLength length; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseLength(&length)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGLengthValue(length)); |
| return true; |
| } |
| |
| bool SetNumberAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGNumberType number; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseNumber(&number)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGNumberValue(number)); |
| return true; |
| } |
| |
| bool SetViewBoxAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGViewBoxType viewBox; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseViewBox(&viewBox)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGViewBoxValue(viewBox)); |
| return true; |
| } |
| |
| bool SetLineCapAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGLineCap lineCap; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseLineCap(&lineCap)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGLineCapValue(lineCap)); |
| return true; |
| } |
| |
| bool SetLineJoinAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGLineJoin lineJoin; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseLineJoin(&lineJoin)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGLineJoinValue(lineJoin)); |
| return true; |
| } |
| |
| bool SetSpreadMethodAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGSpreadMethod spread; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseSpreadMethod(&spread)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGSpreadMethodValue(spread)); |
| return true; |
| } |
| |
| bool SetStopColorAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGStopColor stopColor; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseStopColor(&stopColor)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGStopColorValue(stopColor)); |
| return true; |
| } |
| |
| bool SetPointsAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGPointsType points; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parsePoints(&points)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGPointsValue(points)); |
| return true; |
| } |
| |
| bool SetFillRuleAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGFillRule fillRule; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseFillRule(&fillRule)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGFillRuleValue(fillRule)); |
| return true; |
| } |
| |
| bool SetVisibilityAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGVisibility visibility; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseVisibility(&visibility)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGVisibilityValue(visibility)); |
| return true; |
| } |
| |
| bool SetDashArrayAttribute(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, |
| const char* stringValue) { |
| SkSVGDashArray dashArray; |
| SkSVGAttributeParser parser(stringValue); |
| if (!parser.parseDashArray(&dashArray)) { |
| return false; |
| } |
| |
| node->setAttribute(attr, SkSVGDashArrayValue(dashArray)); |
| return true; |
| } |
| |
| SkString TrimmedString(const char* first, const char* last) { |
| SkASSERT(first); |
| SkASSERT(last); |
| SkASSERT(first <= last); |
| |
| while (first <= last && *first <= ' ') { first++; } |
| while (first <= last && *last <= ' ') { last--; } |
| |
| SkASSERT(last - first + 1 >= 0); |
| return SkString(first, SkTo<size_t>(last - first + 1)); |
| } |
| |
| // Breaks a "foo: bar; baz: ..." string into key:value pairs. |
| class StyleIterator { |
| public: |
| StyleIterator(const char* str) : fPos(str) { } |
| |
| std::tuple<SkString, SkString> next() { |
| SkString name, value; |
| |
| if (fPos) { |
| const char* sep = this->nextSeparator(); |
| SkASSERT(*sep == ';' || *sep == '\0'); |
| |
| const char* valueSep = strchr(fPos, ':'); |
| if (valueSep && valueSep < sep) { |
| name = TrimmedString(fPos, valueSep - 1); |
| value = TrimmedString(valueSep + 1, sep - 1); |
| } |
| |
| fPos = *sep ? sep + 1 : nullptr; |
| } |
| |
| return std::make_tuple(name, value); |
| } |
| |
| private: |
| const char* nextSeparator() const { |
| const char* sep = fPos; |
| while (*sep != ';' && *sep != '\0') { |
| sep++; |
| } |
| return sep; |
| } |
| |
| const char* fPos; |
| }; |
| |
| bool set_string_attribute(const sk_sp<SkSVGNode>& node, const char* name, const char* value); |
| |
| bool SetStyleAttributes(const sk_sp<SkSVGNode>& node, SkSVGAttribute, |
| const char* stringValue) { |
| |
| SkString name, value; |
| StyleIterator iter(stringValue); |
| for (;;) { |
| std::tie(name, value) = iter.next(); |
| if (name.isEmpty()) { |
| break; |
| } |
| set_string_attribute(node, name.c_str(), value.c_str()); |
| } |
| |
| return true; |
| } |
| |
| template<typename T> |
| struct SortedDictionaryEntry { |
| const char* fKey; |
| const T fValue; |
| }; |
| |
| struct AttrParseInfo { |
| SkSVGAttribute fAttr; |
| bool (*fSetter)(const sk_sp<SkSVGNode>& node, SkSVGAttribute attr, const char* stringValue); |
| }; |
| |
| SortedDictionaryEntry<AttrParseInfo> gAttributeParseInfo[] = { |
| { "clip-path" , { SkSVGAttribute::kClipPath , SetClipPathAttribute }}, |
| { "clip-rule" , { SkSVGAttribute::kClipRule , SetFillRuleAttribute }}, |
| { "color" , { SkSVGAttribute::kColor , SetColorAttribute }}, |
| { "cx" , { SkSVGAttribute::kCx , SetLengthAttribute }}, |
| { "cy" , { SkSVGAttribute::kCy , SetLengthAttribute }}, |
| { "d" , { SkSVGAttribute::kD , SetPathDataAttribute }}, |
| { "fill" , { SkSVGAttribute::kFill , SetPaintAttribute }}, |
| { "fill-opacity" , { SkSVGAttribute::kFillOpacity , SetNumberAttribute }}, |
| { "fill-rule" , { SkSVGAttribute::kFillRule , SetFillRuleAttribute }}, |
| { "font-family" , { SkSVGAttribute::kFontFamily , SetStringAttribute }}, |
| { "font-size" , { SkSVGAttribute::kFontSize , SetLengthAttribute }}, |
| { "font-style" , { SkSVGAttribute::kFontStyle , SetStringAttribute }}, |
| { "font-weight" , { SkSVGAttribute::kFontWeight , SetStringAttribute }}, |
| // focal point x & y |
| { "fx" , { SkSVGAttribute::kFx , SetLengthAttribute }}, |
| { "fy" , { SkSVGAttribute::kFy , SetLengthAttribute }}, |
| { "gradientTransform", { SkSVGAttribute::kGradientTransform, SetTransformAttribute }}, |
| { "height" , { SkSVGAttribute::kHeight , SetLengthAttribute }}, |
| { "offset" , { SkSVGAttribute::kOffset , SetLengthAttribute }}, |
| { "opacity" , { SkSVGAttribute::kOpacity , SetNumberAttribute }}, |
| { "patternTransform" , { SkSVGAttribute::kPatternTransform , SetTransformAttribute }}, |
| { "points" , { SkSVGAttribute::kPoints , SetPointsAttribute }}, |
| { "r" , { SkSVGAttribute::kR , SetLengthAttribute }}, |
| { "rx" , { SkSVGAttribute::kRx , SetLengthAttribute }}, |
| { "ry" , { SkSVGAttribute::kRy , SetLengthAttribute }}, |
| { "spreadMethod" , { SkSVGAttribute::kSpreadMethod , SetSpreadMethodAttribute }}, |
| { "stop-color" , { SkSVGAttribute::kStopColor , SetStopColorAttribute }}, |
| { "stop-opacity" , { SkSVGAttribute::kStopOpacity , SetNumberAttribute }}, |
| { "stroke" , { SkSVGAttribute::kStroke , SetPaintAttribute }}, |
| { "stroke-dasharray" , { SkSVGAttribute::kStrokeDashArray , SetDashArrayAttribute }}, |
| { "stroke-dashoffset", { SkSVGAttribute::kStrokeDashOffset , SetLengthAttribute }}, |
| { "stroke-linecap" , { SkSVGAttribute::kStrokeLineCap , SetLineCapAttribute }}, |
| { "stroke-linejoin" , { SkSVGAttribute::kStrokeLineJoin , SetLineJoinAttribute }}, |
| { "stroke-miterlimit", { SkSVGAttribute::kStrokeMiterLimit , SetNumberAttribute }}, |
| { "stroke-opacity" , { SkSVGAttribute::kStrokeOpacity , SetNumberAttribute }}, |
| { "stroke-width" , { SkSVGAttribute::kStrokeWidth , SetLengthAttribute }}, |
| { "style" , { SkSVGAttribute::kUnknown , SetStyleAttributes }}, |
| { "text" , { SkSVGAttribute::kText , SetStringAttribute }}, |
| { "text-anchor" , { SkSVGAttribute::kTextAnchor , SetStringAttribute }}, |
| { "transform" , { SkSVGAttribute::kTransform , SetTransformAttribute }}, |
| { "viewBox" , { SkSVGAttribute::kViewBox , SetViewBoxAttribute }}, |
| { "visibility" , { SkSVGAttribute::kVisibility , SetVisibilityAttribute }}, |
| { "width" , { SkSVGAttribute::kWidth , SetLengthAttribute }}, |
| { "x" , { SkSVGAttribute::kX , SetLengthAttribute }}, |
| { "x1" , { SkSVGAttribute::kX1 , SetLengthAttribute }}, |
| { "x2" , { SkSVGAttribute::kX2 , SetLengthAttribute }}, |
| { "xlink:href" , { SkSVGAttribute::kHref , SetIRIAttribute }}, |
| { "y" , { SkSVGAttribute::kY , SetLengthAttribute }}, |
| { "y1" , { SkSVGAttribute::kY1 , SetLengthAttribute }}, |
| { "y2" , { SkSVGAttribute::kY2 , SetLengthAttribute }}, |
| }; |
| |
| SortedDictionaryEntry<sk_sp<SkSVGNode>(*)()> gTagFactories[] = { |
| { "a" , []() -> sk_sp<SkSVGNode> { return SkSVGG::Make(); }}, |
| { "circle" , []() -> sk_sp<SkSVGNode> { return SkSVGCircle::Make(); }}, |
| { "clipPath" , []() -> sk_sp<SkSVGNode> { return SkSVGClipPath::Make(); }}, |
| { "defs" , []() -> sk_sp<SkSVGNode> { return SkSVGDefs::Make(); }}, |
| { "ellipse" , []() -> sk_sp<SkSVGNode> { return SkSVGEllipse::Make(); }}, |
| { "g" , []() -> sk_sp<SkSVGNode> { return SkSVGG::Make(); }}, |
| { "line" , []() -> sk_sp<SkSVGNode> { return SkSVGLine::Make(); }}, |
| { "linearGradient", []() -> sk_sp<SkSVGNode> { return SkSVGLinearGradient::Make(); }}, |
| { "path" , []() -> sk_sp<SkSVGNode> { return SkSVGPath::Make(); }}, |
| { "pattern" , []() -> sk_sp<SkSVGNode> { return SkSVGPattern::Make(); }}, |
| { "polygon" , []() -> sk_sp<SkSVGNode> { return SkSVGPoly::MakePolygon(); }}, |
| { "polyline" , []() -> sk_sp<SkSVGNode> { return SkSVGPoly::MakePolyline(); }}, |
| { "radialGradient", []() -> sk_sp<SkSVGNode> { return SkSVGRadialGradient::Make(); }}, |
| { "rect" , []() -> sk_sp<SkSVGNode> { return SkSVGRect::Make(); }}, |
| { "stop" , []() -> sk_sp<SkSVGNode> { return SkSVGStop::Make(); }}, |
| { "svg" , []() -> sk_sp<SkSVGNode> { return SkSVGSVG::Make(); }}, |
| { "text" , []() -> sk_sp<SkSVGNode> { return SkSVGText::Make(); }}, |
| { "use" , []() -> sk_sp<SkSVGNode> { return SkSVGUse::Make(); }}, |
| }; |
| |
| struct ConstructionContext { |
| ConstructionContext(SkSVGIDMapper* mapper) : fParent(nullptr), fIDMapper(mapper) {} |
| ConstructionContext(const ConstructionContext& other, const sk_sp<SkSVGNode>& newParent) |
| : fParent(newParent.get()), fIDMapper(other.fIDMapper) {} |
| |
| const SkSVGNode* fParent; |
| SkSVGIDMapper* fIDMapper; |
| }; |
| |
| bool set_string_attribute(const sk_sp<SkSVGNode>& node, const char* name, const char* value) { |
| const int attrIndex = SkStrSearch(&gAttributeParseInfo[0].fKey, |
| SkTo<int>(SK_ARRAY_COUNT(gAttributeParseInfo)), |
| name, sizeof(gAttributeParseInfo[0])); |
| if (attrIndex < 0) { |
| #if defined(SK_VERBOSE_SVG_PARSING) |
| SkDebugf("unhandled attribute: %s\n", name); |
| #endif |
| return false; |
| } |
| |
| SkASSERT(SkTo<size_t>(attrIndex) < SK_ARRAY_COUNT(gAttributeParseInfo)); |
| const auto& attrInfo = gAttributeParseInfo[attrIndex].fValue; |
| if (!attrInfo.fSetter(node, attrInfo.fAttr, value)) { |
| #if defined(SK_VERBOSE_SVG_PARSING) |
| SkDebugf("could not parse attribute: '%s=\"%s\"'\n", name, value); |
| #endif |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void parse_node_attributes(const SkDOM& xmlDom, const SkDOM::Node* xmlNode, |
| const sk_sp<SkSVGNode>& svgNode, SkSVGIDMapper* mapper) { |
| const char* name, *value; |
| SkDOM::AttrIter attrIter(xmlDom, xmlNode); |
| while ((name = attrIter.next(&value))) { |
| // We're handling id attributes out of band for now. |
| if (!strcmp(name, "id")) { |
| mapper->set(SkString(value), svgNode); |
| continue; |
| } |
| set_string_attribute(svgNode, name, value); |
| } |
| } |
| |
| sk_sp<SkSVGNode> construct_svg_node(const SkDOM& dom, const ConstructionContext& ctx, |
| const SkDOM::Node* xmlNode) { |
| const char* elem = dom.getName(xmlNode); |
| const SkDOM::Type elemType = dom.getType(xmlNode); |
| |
| if (elemType == SkDOM::kText_Type) { |
| SkASSERT(dom.countChildren(xmlNode) == 0); |
| // TODO: text handling |
| return nullptr; |
| } |
| |
| SkASSERT(elemType == SkDOM::kElement_Type); |
| |
| const int tagIndex = SkStrSearch(&gTagFactories[0].fKey, |
| SkTo<int>(SK_ARRAY_COUNT(gTagFactories)), |
| elem, sizeof(gTagFactories[0])); |
| if (tagIndex < 0) { |
| #if defined(SK_VERBOSE_SVG_PARSING) |
| SkDebugf("unhandled element: <%s>\n", elem); |
| #endif |
| return nullptr; |
| } |
| |
| SkASSERT(SkTo<size_t>(tagIndex) < SK_ARRAY_COUNT(gTagFactories)); |
| sk_sp<SkSVGNode> node = gTagFactories[tagIndex].fValue(); |
| parse_node_attributes(dom, xmlNode, node, ctx.fIDMapper); |
| |
| ConstructionContext localCtx(ctx, node); |
| for (auto* child = dom.getFirstChild(xmlNode, nullptr); child; |
| child = dom.getNextSibling(child)) { |
| sk_sp<SkSVGNode> childNode = construct_svg_node(dom, localCtx, child); |
| if (childNode) { |
| node->appendChild(std::move(childNode)); |
| } |
| } |
| |
| return node; |
| } |
| |
| } // anonymous namespace |
| |
| SkSVGDOM::SkSVGDOM() |
| : fContainerSize(SkSize::Make(0, 0)) { |
| } |
| |
| sk_sp<SkSVGDOM> SkSVGDOM::MakeFromDOM(const SkDOM& xmlDom) { |
| sk_sp<SkSVGDOM> dom = sk_make_sp<SkSVGDOM>(); |
| |
| ConstructionContext ctx(&dom->fIDMapper); |
| dom->fRoot = construct_svg_node(xmlDom, ctx, xmlDom.getRootNode()); |
| |
| // Reset the default container size to match the intrinsic SVG size. |
| dom->setContainerSize(dom->intrinsicSize()); |
| |
| return dom; |
| } |
| |
| sk_sp<SkSVGDOM> SkSVGDOM::MakeFromStream(SkStream& svgStream) { |
| SkDOM xmlDom; |
| if (!xmlDom.build(svgStream)) { |
| return nullptr; |
| } |
| |
| return MakeFromDOM(xmlDom); |
| } |
| |
| void SkSVGDOM::render(SkCanvas* canvas) const { |
| if (fRoot) { |
| SkSVGLengthContext lctx(fContainerSize); |
| SkSVGPresentationContext pctx; |
| fRoot->render(SkSVGRenderContext(canvas, fIDMapper, lctx, pctx)); |
| } |
| } |
| |
| SkSize SkSVGDOM::intrinsicSize() const { |
| if (!fRoot || fRoot->tag() != SkSVGTag::kSvg) { |
| return SkSize::Make(0, 0); |
| } |
| |
| // Intrinsic sizes are never relative, so the viewport size is irrelevant. |
| const SkSVGLengthContext lctx(SkSize::Make(0, 0)); |
| return static_cast<const SkSVGSVG*>(fRoot.get())->intrinsicSize(lctx); |
| } |
| |
| const SkSize& SkSVGDOM::containerSize() const { |
| return fContainerSize; |
| } |
| |
| void SkSVGDOM::setContainerSize(const SkSize& containerSize) { |
| // TODO: inval |
| fContainerSize = containerSize; |
| } |
| |
| sk_sp<SkSVGNode>* SkSVGDOM::findNodeById(const char* id) { |
| SkString idStr(id); |
| return this->fIDMapper.find(idStr); |
| } |
| |
| void SkSVGDOM::setRoot(sk_sp<SkSVGNode> root) { |
| fRoot = std::move(root); |
| } |
| |
| // TODO(fuego): move this to SkSVGNode or its own CU. |
| bool SkSVGNode::setAttribute(const char* attributeName, const char* attributeValue) { |
| return set_string_attribute(sk_ref_sp(this), attributeName, attributeValue); |
| } |