#include "svgelement.h" #include "svgpaintelement.h" #include "svggeometryelement.h" #include "svgtextelement.h" #include "svgproperty.h" #include "svglayoutstate.h" #include "svgrenderstate.h" #include namespace lunasvg { ElementID elementid(std::string_view name) { static const struct { std::string_view name; ElementID value; } table[] = { {"a", ElementID::G}, {"circle", ElementID::Circle}, {"clipPath", ElementID::ClipPath}, {"defs", ElementID::Defs}, {"ellipse", ElementID::Ellipse}, {"g", ElementID::G}, {"image", ElementID::Image}, {"line", ElementID::Line}, {"linearGradient", ElementID::LinearGradient}, {"marker", ElementID::Marker}, {"mask", ElementID::Mask}, {"path", ElementID::Path}, {"pattern", ElementID::Pattern}, {"polygon", ElementID::Polygon}, {"polyline", ElementID::Polyline}, {"radialGradient", ElementID::RadialGradient}, {"rect", ElementID::Rect}, {"stop", ElementID::Stop}, {"style", ElementID::Style}, {"svg", ElementID::Svg}, {"symbol", ElementID::Symbol}, {"text", ElementID::Text}, {"tspan", ElementID::Tspan}, {"use", ElementID::Use} }; auto it = std::lower_bound(table, std::end(table), name, [](const auto& item, const auto& name) { return item.name < name; }); if(it == std::end(table) || it->name != name) return ElementID::Unknown; return it->value; } SVGTextNode::SVGTextNode(Document* document) : SVGNode(document) { } void SVGTextNode::setData(const std::string& data) { rootElement()->setNeedsLayout(); m_data.assign(data); } std::unique_ptr SVGTextNode::clone(bool deep) const { auto node = std::make_unique(document()); node->setData(m_data); return node; } const std::string emptyString; std::unique_ptr SVGElement::create(Document* document, ElementID id) { switch(id) { case ElementID::Svg: return std::make_unique(document); case ElementID::Path: return std::make_unique(document); case ElementID::G: return std::make_unique(document); case ElementID::Rect: return std::make_unique(document); case ElementID::Circle: return std::make_unique(document); case ElementID::Ellipse: return std::make_unique(document); case ElementID::Line: return std::make_unique(document); case ElementID::Defs: return std::make_unique(document); case ElementID::Polygon: case ElementID::Polyline: return std::make_unique(document, id); case ElementID::Stop: return std::make_unique(document); case ElementID::LinearGradient: return std::make_unique(document); case ElementID::RadialGradient: return std::make_unique(document); case ElementID::Symbol: return std::make_unique(document); case ElementID::Use: return std::make_unique(document); case ElementID::Pattern: return std::make_unique(document); case ElementID::Mask: return std::make_unique(document); case ElementID::ClipPath: return std::make_unique(document); case ElementID::Marker: return std::make_unique(document); case ElementID::Image: return std::make_unique(document); case ElementID::Style: return std::make_unique(document); case ElementID::Text: return std::make_unique(document); case ElementID::Tspan: return std::make_unique(document); default: assert(false); } return nullptr; } SVGElement::SVGElement(Document* document, ElementID id) : SVGNode(document) , m_id(id) { } bool SVGElement::hasAttribute(std::string_view name) const { auto id = propertyid(name); if(id == PropertyID::Unknown) return false; return hasAttribute(id); } const std::string& SVGElement::getAttribute(std::string_view name) const { auto id = propertyid(name); if(id == PropertyID::Unknown) return emptyString; return getAttribute(id); } bool SVGElement::setAttribute(std::string_view name, const std::string& value) { auto id = propertyid(name); if(id == PropertyID::Unknown) return false; return setAttribute(0x1000, id, value); } const Attribute* SVGElement::findAttribute(PropertyID id) const { for(const auto& attribute : m_attributes) { if(id == attribute.id()) { return &attribute; } } return nullptr; } bool SVGElement::hasAttribute(PropertyID id) const { for(const auto& attribute : m_attributes) { if(id == attribute.id()) { return true; } } return false; } const std::string& SVGElement::getAttribute(PropertyID id) const { for(const auto& attribute : m_attributes) { if(id == attribute.id()) { return attribute.value(); } } return emptyString; } bool SVGElement::setAttribute(int specificity, PropertyID id, const std::string& value) { for(auto& attribute : m_attributes) { if(id == attribute.id()) { if(specificity < attribute.specificity()) return false; parseAttribute(id, value); attribute = Attribute(specificity, id, value); return true; } } parseAttribute(id, value); m_attributes.emplace_front(specificity, id, value); return true; } void SVGElement::setAttributes(const AttributeList& attributes) { for(const auto& attribute : attributes) { setAttribute(attribute); } } bool SVGElement::setAttribute(const Attribute& attribute) { return setAttribute(attribute.specificity(), attribute.id(), attribute.value()); } void SVGElement::parseAttribute(PropertyID id, const std::string& value) { rootElement()->setNeedsLayout(); if(auto property = getProperty(id)) { property->parse(value); } } SVGElement* SVGElement::previousElement() const { auto parent = parentElement(); if(parent == nullptr) return nullptr; const auto& children = parent->children(); auto it = children.begin(); auto end = children.end(); SVGElement* element = nullptr; for(; it != end; ++it) { SVGNode* node = &**it; if(node->isTextNode()) continue; if(node == this) return element; element = static_cast(node); } return nullptr; } SVGElement* SVGElement::nextElement() const { auto parent = parentElement(); if(parent == nullptr) return nullptr; const auto& children = parent->children(); auto it = children.rbegin(); auto end = children.rend(); SVGElement* element = nullptr; for(; it != end; ++it) { SVGNode* node = &**it; if(node->isTextNode()) continue; if(node == this) return element; element = static_cast(node); } return nullptr; } SVGNode* SVGElement::addChild(std::unique_ptr child) { child->setParentElement(this); m_children.push_back(std::move(child)); return &*m_children.back(); } SVGNode* SVGElement::firstChild() const { if(m_children.empty()) return nullptr; return &*m_children.front(); } SVGNode* SVGElement::lastChild() const { if(m_children.empty()) return nullptr; return &*m_children.back(); } Rect SVGElement::fillBoundingBox() const { auto fillBoundingBox = Rect::Invalid; for(const auto& child : m_children) { if(auto element = toSVGElement(child); element && !element->isHiddenElement()) { fillBoundingBox.unite(element->localTransform().mapRect(element->fillBoundingBox())); } } if(!fillBoundingBox.isValid()) fillBoundingBox = Rect::Empty; return fillBoundingBox; } Rect SVGElement::strokeBoundingBox() const { auto strokeBoundingBox = Rect::Invalid; for(const auto& child : m_children) { if(auto element = toSVGElement(child); element && !element->isHiddenElement()) { strokeBoundingBox.unite(element->localTransform().mapRect(element->strokeBoundingBox())); } } if(!strokeBoundingBox.isValid()) strokeBoundingBox = Rect::Empty; return strokeBoundingBox; } Rect SVGElement::paintBoundingBox() const { if(m_paintBoundingBox.isValid()) return m_paintBoundingBox; m_paintBoundingBox = Rect::Empty; m_paintBoundingBox = strokeBoundingBox(); assert(m_paintBoundingBox.isValid()); if(m_clipper) m_paintBoundingBox.intersect(m_clipper->clipBoundingBox(this)); if(m_masker) m_paintBoundingBox.intersect(m_masker->maskBoundingBox(this)); return m_paintBoundingBox; } SVGMarkerElement* SVGElement::getMarker(std::string_view id) const { auto element = rootElement()->getElementById(id); if(element && element->id() == ElementID::Marker) return static_cast(element); return nullptr; } SVGClipPathElement* SVGElement::getClipper(std::string_view id) const { auto element = rootElement()->getElementById(id); if(element && element->id() == ElementID::ClipPath) return static_cast(element); return nullptr; } SVGMaskElement* SVGElement::getMasker(std::string_view id) const { auto element = rootElement()->getElementById(id); if(element && element->id() == ElementID::Mask) return static_cast(element); return nullptr; } SVGPaintElement* SVGElement::getPainter(std::string_view id) const { auto element = rootElement()->getElementById(id); if(element && element->isPaintElement()) return static_cast(element); return nullptr; } SVGElement* SVGElement::elementFromPoint(float x, float y) { auto it = m_children.rbegin(); auto end = m_children.rend(); for(; it != end; ++it) { auto child = toSVGElement(*it); if(child && !child->isHiddenElement()) { if(auto element = child->elementFromPoint(x, y)) { return element; } } } if(isPointableElement()) { auto transform = localTransform(); for(auto parent = parentElement(); parent; parent = parent->parentElement()) transform.postMultiply(parent->localTransform()); auto bbox = transform.mapRect(paintBoundingBox()); if(bbox.contains(x, y)) { return this; } } return nullptr; } void SVGElement::addProperty(SVGProperty& value) { m_properties.push_front(&value); } SVGProperty* SVGElement::getProperty(PropertyID id) const { for(auto property : m_properties) { if(id == property->id()) { return property; } } return nullptr; } Size SVGElement::currentViewportSize() const { auto parent = parentElement(); if(parent == nullptr) { auto element = static_cast(this); const auto& viewBox = element->viewBox(); if(viewBox.value().isValid()) return viewBox.value().size(); return Size(300, 150); } if(parent->id() == ElementID::Svg) { auto element = static_cast(parent); const auto& viewBox = element->viewBox(); if(viewBox.value().isValid()) return viewBox.value().size(); LengthContext lengthContext(element); auto width = lengthContext.valueForLength(element->width()); auto height = lengthContext.valueForLength(element->height()); return Size(width, height); } return parent->currentViewportSize(); } void SVGElement::cloneChildren(SVGElement* parentElement) const { for(const auto& child : m_children) { parentElement->addChild(child->clone(true)); } } std::unique_ptr SVGElement::clone(bool deep) const { auto element = SVGElement::create(document(), m_id); element->setAttributes(m_attributes); if(deep) { cloneChildren(element.get()); } return element; } void SVGElement::build() { for(const auto& child : m_children) { if(auto element = toSVGElement(child)) { element->build(); } } } void SVGElement::layoutElement(const SVGLayoutState& state) { m_paintBoundingBox = Rect::Invalid; m_clipper = getClipper(state.clip_path()); m_masker = getMasker(state.mask()); m_opacity = state.opacity(); m_font_size = state.font_size(); m_display = state.display(); m_overflow = state.overflow(); m_visibility = state.visibility(); m_pointer_events = state.pointer_events(); } void SVGElement::layoutChildren(SVGLayoutState& state) { for(const auto& child : m_children) { if(auto element = toSVGElement(child)) { element->layout(state); } } } void SVGElement::layout(SVGLayoutState& state) { SVGLayoutState newState(state, this); layoutElement(newState); layoutChildren(newState); } void SVGElement::renderChildren(SVGRenderState& state) const { for(const auto& child : m_children) { if(auto element = toSVGElement(child)) { element->render(state); } } } void SVGElement::render(SVGRenderState& state) const { } bool SVGElement::isHiddenElement() const { if(isDisplayNone()) return true; switch(m_id) { case ElementID::Defs: case ElementID::Symbol: case ElementID::Marker: case ElementID::ClipPath: case ElementID::Mask: case ElementID::LinearGradient: case ElementID::RadialGradient: case ElementID::Pattern: case ElementID::Stop: return true; default: return false; } } bool SVGElement::isPointableElement() const { if(m_pointer_events != PointerEvents::None && m_visibility != Visibility::Hidden && m_display != Display::None && m_opacity != 0.f) { switch(m_id) { case ElementID::Line: case ElementID::Rect: case ElementID::Ellipse: case ElementID::Circle: case ElementID::Polyline: case ElementID::Polygon: case ElementID::Path: case ElementID::Text: case ElementID::Image: return true; default: break; } } return false; } SVGStyleElement::SVGStyleElement(Document* document) : SVGElement(document, ElementID::Style) { } SVGFitToViewBox::SVGFitToViewBox(SVGElement* element) : m_viewBox(PropertyID::ViewBox) , m_preserveAspectRatio(PropertyID::PreserveAspectRatio) { element->addProperty(m_viewBox); element->addProperty(m_preserveAspectRatio); } Transform SVGFitToViewBox::viewBoxToViewTransform(const Size& viewportSize) const { const auto& viewBoxRect = m_viewBox.value(); if(viewBoxRect.isEmpty() || viewportSize.isEmpty()) return Transform::Identity; return m_preserveAspectRatio.getTransform(viewBoxRect, viewportSize); } Rect SVGFitToViewBox::getClipRect(const Size& viewportSize) const { const auto& viewBoxRect = m_viewBox.value(); if(viewBoxRect.isEmpty() || viewportSize.isEmpty()) return Rect(0, 0, viewportSize.w, viewportSize.h); return m_preserveAspectRatio.getClipRect(viewBoxRect, viewportSize); } SVGURIReference::SVGURIReference(SVGElement* element) : m_href(PropertyID::Href) { element->addProperty(m_href); } SVGElement* SVGURIReference::getTargetElement(const Document* document) const { std::string_view value(m_href.value()); if(value.empty() || value.front() != '#') return nullptr; return document->rootElement()->getElementById(value.substr(1)); } bool SVGPaintServer::applyPaint(SVGRenderState& state) const { if(!isRenderable()) return false; if(m_element) return m_element->applyPaint(state, m_opacity); state->setColor(m_color.colorWithAlpha(m_opacity)); return true; } SVGGraphicsElement::SVGGraphicsElement(Document* document, ElementID id) : SVGElement(document, id) , m_transform(PropertyID::Transform) { addProperty(m_transform); } SVGPaintServer SVGGraphicsElement::getPaintServer(const Paint& paint, float opacity) const { if(paint.isNone()) return SVGPaintServer(); if(auto element = getPainter(paint.id())) return SVGPaintServer(element, paint.color(), opacity); return SVGPaintServer(nullptr, paint.color(), opacity); } StrokeData SVGGraphicsElement::getStrokeData(const SVGLayoutState& state) const { LengthContext lengthContext(this); StrokeData strokeData(lengthContext.valueForLength(state.stroke_width(), LengthDirection::Diagonal)); strokeData.setMiterLimit(state.stroke_miterlimit()); strokeData.setLineCap(state.stroke_linecap()); strokeData.setLineJoin(state.stroke_linejoin()); strokeData.setDashOffset(lengthContext.valueForLength(state.stroke_dashoffset(), LengthDirection::Diagonal)); DashArray dashArray; for(const auto& dash : state.stroke_dasharray()) dashArray.push_back(lengthContext.valueForLength(dash, LengthDirection::Diagonal)); strokeData.setDashArray(std::move(dashArray)); return strokeData; } SVGSVGElement::SVGSVGElement(Document* document) : SVGGraphicsElement(document, ElementID::Svg) , SVGFitToViewBox(this) , m_x(PropertyID::X, LengthDirection::Horizontal, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_y(PropertyID::Y, LengthDirection::Vertical, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_width(PropertyID::Width, LengthDirection::Horizontal, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) , m_height(PropertyID::Height, LengthDirection::Vertical, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) { addProperty(m_x); addProperty(m_y); addProperty(m_width); addProperty(m_height); } Transform SVGSVGElement::localTransform() const { LengthContext lengthContext(this); const Rect viewportRect = { lengthContext.valueForLength(m_x), lengthContext.valueForLength(m_y), lengthContext.valueForLength(m_width), lengthContext.valueForLength(m_height) }; if(isRootElement()) return viewBoxToViewTransform(viewportRect.size()); return SVGGraphicsElement::localTransform() * Transform::translated(viewportRect.x, viewportRect.y) * viewBoxToViewTransform(viewportRect.size()); } void SVGSVGElement::render(SVGRenderState& state) const { if(isDisplayNone()) return; LengthContext lengthContext(this); const Size viewportSize = { lengthContext.valueForLength(m_width), lengthContext.valueForLength(m_height) }; if(viewportSize.isEmpty()) return; SVGBlendInfo blendInfo(this); SVGRenderState newState(this, state, localTransform()); newState.beginGroup(blendInfo); if(isOverflowHidden()) newState->clipRect(getClipRect(viewportSize), FillRule::NonZero, newState.currentTransform()); renderChildren(newState); newState.endGroup(blendInfo); } SVGRootElement::SVGRootElement(Document* document) : SVGSVGElement(document) { } SVGRootElement* SVGRootElement::layoutIfNeeded() { if(needsLayout()) forceLayout(); return this; } SVGElement* SVGRootElement::getElementById(std::string_view id) const { auto it = m_idCache.find(id); if(it == m_idCache.end()) return nullptr; return it->second; } void SVGRootElement::addElementById(const std::string& id, SVGElement* element) { m_idCache.emplace(id, element); } void SVGRootElement::layout(SVGLayoutState& state) { SVGSVGElement::layout(state); LengthContext lengthContext(this); if(!width().isPercent()) { m_intrinsicWidth = lengthContext.valueForLength(width()); } else { m_intrinsicWidth = 0.f; } if(!height().isPercent()) { m_intrinsicHeight = lengthContext.valueForLength(height()); } else { m_intrinsicHeight = 0.f; } const auto& viewBoxRect = viewBox().value(); if(!viewBoxRect.isEmpty() && (!m_intrinsicWidth || !m_intrinsicHeight)) { auto intrinsicRatio = viewBoxRect.w / viewBoxRect.h; if(!m_intrinsicWidth && m_intrinsicHeight) m_intrinsicWidth = m_intrinsicHeight * intrinsicRatio; else if(m_intrinsicWidth && !m_intrinsicHeight) { m_intrinsicHeight = m_intrinsicWidth / intrinsicRatio; } } if(viewBoxRect.isValid() && (!m_intrinsicWidth || !m_intrinsicHeight)) { m_intrinsicWidth = viewBoxRect.w; m_intrinsicHeight = viewBoxRect.h; } if(!m_intrinsicWidth || !m_intrinsicHeight) { auto boundingBox = paintBoundingBox(); if(!m_intrinsicWidth) m_intrinsicWidth = boundingBox.right(); if(!m_intrinsicHeight) { m_intrinsicHeight = boundingBox.bottom(); } } } void SVGRootElement::forceLayout() { SVGLayoutState state; layout(state); } SVGUseElement::SVGUseElement(Document* document) : SVGGraphicsElement(document, ElementID::Use) , SVGURIReference(this) , m_x(PropertyID::X, LengthDirection::Horizontal, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_y(PropertyID::Y, LengthDirection::Vertical, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_width(PropertyID::Width, LengthDirection::Horizontal, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) , m_height(PropertyID::Height, LengthDirection::Vertical, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) { addProperty(m_x); addProperty(m_y); addProperty(m_width); addProperty(m_height); } Transform SVGUseElement::localTransform() const { LengthContext lengthContext(this); const Point translation = { lengthContext.valueForLength(m_x), lengthContext.valueForLength(m_y) }; return SVGGraphicsElement::localTransform() * Transform::translated(translation.x, translation.y); } void SVGUseElement::render(SVGRenderState& state) const { if(isDisplayNone()) return; SVGBlendInfo blendInfo(this); SVGRenderState newState(this, state, localTransform()); newState.beginGroup(blendInfo); renderChildren(newState); newState.endGroup(blendInfo); } void SVGUseElement::build() { if(auto targetElement = getTargetElement(document())) { if(auto newElement = cloneTargetElement(targetElement)) { addChild(std::move(newElement)); } } SVGGraphicsElement::build(); } inline bool isDisallowedElement(const SVGElement* element) { switch(element->id()) { case ElementID::Circle: case ElementID::Ellipse: case ElementID::G: case ElementID::Image: case ElementID::Line: case ElementID::Path: case ElementID::Polygon: case ElementID::Polyline: case ElementID::Rect: case ElementID::Svg: case ElementID::Symbol: case ElementID::Text: case ElementID::Tspan: case ElementID::Use: return false; default: return true; } } std::unique_ptr SVGUseElement::cloneTargetElement(SVGElement* targetElement) { if(targetElement == this || isDisallowedElement(targetElement)) return nullptr; const auto& idAttr = targetElement->getAttribute(PropertyID::Id); auto parent = parentElement(); while(parent) { auto attribute = parent->findAttribute(PropertyID::Id); if(attribute && idAttr == attribute->value()) return nullptr; parent = parent->parentElement(); } auto tagId = targetElement->id(); if(tagId == ElementID::Symbol) { tagId = ElementID::Svg; } auto newElement = SVGElement::create(document(), tagId); newElement->setAttributes(targetElement->attributes()); if(newElement->id() == ElementID::Svg) { for(const auto& attribute : attributes()) { if(attribute.id() == PropertyID::Width || attribute.id() == PropertyID::Height) { newElement->setAttribute(attribute); } } } if(newElement->id() != ElementID::Use) targetElement->cloneChildren(newElement.get()); return newElement; } SVGImageElement::SVGImageElement(Document* document) : SVGGraphicsElement(document, ElementID::Image) , m_x(PropertyID::X, LengthDirection::Horizontal, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_y(PropertyID::Y, LengthDirection::Vertical, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_width(PropertyID::Width, LengthDirection::Horizontal, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) , m_height(PropertyID::Height, LengthDirection::Vertical, LengthNegativeMode::Forbid, 100.f, LengthUnits::Percent) , m_preserveAspectRatio(PropertyID::PreserveAspectRatio) { addProperty(m_x); addProperty(m_y); addProperty(m_width); addProperty(m_height); addProperty(m_preserveAspectRatio); } Rect SVGImageElement::fillBoundingBox() const { LengthContext lengthContext(this); const Rect viewportRect = { lengthContext.valueForLength(m_x), lengthContext.valueForLength(m_y), lengthContext.valueForLength(m_width), lengthContext.valueForLength(m_height) }; return viewportRect; } Rect SVGImageElement::strokeBoundingBox() const { return fillBoundingBox(); } void SVGImageElement::render(SVGRenderState& state) const { if(m_image.isNull() || isDisplayNone() || isVisibilityHidden()) return; Rect dstRect(fillBoundingBox()); Rect srcRect(0, 0, m_image.width(), m_image.height()); if(dstRect.isEmpty() || srcRect.isEmpty()) return; m_preserveAspectRatio.transformRect(dstRect, srcRect); SVGBlendInfo blendInfo(this); SVGRenderState newState(this, state, localTransform()); newState.beginGroup(blendInfo); newState->drawImage(m_image, dstRect, srcRect, newState.currentTransform()); newState.endGroup(blendInfo); } static Bitmap loadImageResource(const std::string& href) { if(href.compare(0, 5, "data:") == 0) { std::string_view input(href); auto index = input.find(',', 5); if(index == std::string_view::npos) return Bitmap(); input.remove_prefix(index + 1); return plutovg_surface_load_from_image_base64(input.data(), input.length()); } return plutovg_surface_load_from_image_file(href.data()); } void SVGImageElement::parseAttribute(PropertyID id, const std::string& value) { if(id == PropertyID::Href) { m_image = loadImageResource(value); } else { SVGGraphicsElement::parseAttribute(id, value); } } SVGSymbolElement::SVGSymbolElement(Document* document) : SVGGraphicsElement(document, ElementID::Symbol) , SVGFitToViewBox(this) { } SVGGElement::SVGGElement(Document* document) : SVGGraphicsElement(document, ElementID::G) { } void SVGGElement::render(SVGRenderState& state) const { if(isDisplayNone()) return; SVGBlendInfo blendInfo(this); SVGRenderState newState(this, state, localTransform()); newState.beginGroup(blendInfo); renderChildren(newState); newState.endGroup(blendInfo); } SVGDefsElement::SVGDefsElement(Document* document) : SVGGraphicsElement(document, ElementID::Defs) { } SVGMarkerElement::SVGMarkerElement(Document* document) : SVGElement(document, ElementID::Marker) , SVGFitToViewBox(this) , m_refX(PropertyID::RefX, LengthDirection::Horizontal, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_refY(PropertyID::RefY, LengthDirection::Vertical, LengthNegativeMode::Allow, 0.f, LengthUnits::None) , m_markerWidth(PropertyID::MarkerWidth, LengthDirection::Horizontal, LengthNegativeMode::Forbid, 3.f, LengthUnits::None) , m_markerHeight(PropertyID::MarkerHeight, LengthDirection::Vertical, LengthNegativeMode::Forbid, 3.f, LengthUnits::None) , m_markerUnits(PropertyID::MarkerUnits, MarkerUnits::StrokeWidth) , m_orient(PropertyID::Orient) { addProperty(m_refX); addProperty(m_refY); addProperty(m_markerWidth); addProperty(m_markerHeight); addProperty(m_markerUnits); addProperty(m_orient); } Point SVGMarkerElement::refPoint() const { LengthContext lengthContext(this); const Point refPoint = { lengthContext.valueForLength(m_refX), lengthContext.valueForLength(m_refY) }; return refPoint; } Size SVGMarkerElement::markerSize() const { LengthContext lengthContext(this); const Size markerSize = { lengthContext.valueForLength(m_markerWidth), lengthContext.valueForLength(m_markerHeight) }; return markerSize; } Transform SVGMarkerElement::markerTransform(const Point& origin, float angle, float strokeWidth) const { auto transform = Transform::translated(origin.x, origin.y); if(m_orient.orientType() == SVGAngle::OrientType::Angle) { transform.rotate(m_orient.value()); } else { transform.rotate(angle); } auto viewTransform = viewBoxToViewTransform(markerSize()); auto refOrigin = viewTransform.mapPoint(refPoint()); if(m_markerUnits.value() == MarkerUnits::StrokeWidth) transform.scale(strokeWidth, strokeWidth); transform.translate(-refOrigin.x, -refOrigin.y); return transform * viewTransform; } Rect SVGMarkerElement::markerBoundingBox(const Point& origin, float angle, float strokeWidth) const { return markerTransform(origin, angle, strokeWidth).mapRect(paintBoundingBox()); } void SVGMarkerElement::renderMarker(SVGRenderState& state, const Point& origin, float angle, float strokeWidth) const { if(state.hasCycleReference(this)) return; SVGBlendInfo blendInfo(this); SVGRenderState newState(this, state, markerTransform(origin, angle, strokeWidth)); newState.beginGroup(blendInfo); if(isOverflowHidden()) newState->clipRect(getClipRect(markerSize()), FillRule::NonZero, newState.currentTransform()); renderChildren(newState); newState.endGroup(blendInfo); } Transform SVGMarkerElement::localTransform() const { return viewBoxToViewTransform(markerSize()); } SVGClipPathElement::SVGClipPathElement(Document* document) : SVGGraphicsElement(document, ElementID::ClipPath) , m_clipPathUnits(PropertyID::ClipPathUnits, Units::UserSpaceOnUse) { addProperty(m_clipPathUnits); } Rect SVGClipPathElement::clipBoundingBox(const SVGElement* element) const { auto clipBoundingBox = paintBoundingBox(); if(m_clipPathUnits.value() == Units::ObjectBoundingBox) { auto bbox = element->fillBoundingBox(); clipBoundingBox.x = clipBoundingBox.x * bbox.w + bbox.x; clipBoundingBox.y = clipBoundingBox.y * bbox.h + bbox.y; clipBoundingBox.w = clipBoundingBox.w * bbox.w; clipBoundingBox.h = clipBoundingBox.h * bbox.h; } return localTransform().mapRect(clipBoundingBox); } void SVGClipPathElement::applyClipMask(SVGRenderState& state) const { if(state.hasCycleReference(this)) return; auto maskImage = Canvas::create(state.currentTransform().mapRect(state.paintBoundingBox())); auto currentTransform = state.currentTransform() * localTransform(); if(m_clipPathUnits.value() == Units::ObjectBoundingBox) { auto bbox = state.fillBoundingBox(); currentTransform.translate(bbox.x, bbox.y); currentTransform.scale(bbox.w, bbox.h); } SVGRenderState newState(this, &state, currentTransform, SVGRenderMode::Clipping, maskImage); renderChildren(newState); if(clipper()) { clipper()->applyClipMask(newState); } state->blendCanvas(*maskImage, BlendMode::Dst_In, 1.f); } inline const SVGGeometryElement* toSVGGeometryElement(const SVGNode* node) { if(node && node->isGeometryElement()) return static_cast(node); return nullptr; } void SVGClipPathElement::applyClipPath(SVGRenderState& state) const { auto currentTransform = state.currentTransform() * localTransform(); if(m_clipPathUnits.value() == Units::ObjectBoundingBox) { auto bbox = state.fillBoundingBox(); currentTransform.translate(bbox.x, bbox.y); currentTransform.scale(bbox.w, bbox.h); } for(const auto& child : children()) { auto element = toSVGElement(child); if(element == nullptr || element->isDisplayNone()) continue; Transform clipTransform(currentTransform); auto shapeElement = toSVGGeometryElement(element); if(shapeElement == nullptr) { if(element->id() != ElementID::Use) continue; clipTransform.multiply(element->localTransform()); shapeElement = toSVGGeometryElement(element->firstChild()); } if(shapeElement == nullptr || !shapeElement->isRenderable()) continue; state->clipPath(shapeElement->path(), shapeElement->clip_rule(), clipTransform * shapeElement->localTransform()); return; } state->clipRect(Rect::Empty, FillRule::NonZero, Transform::Identity); } bool SVGClipPathElement::requiresMasking() const { if(clipper()) return true; const SVGGeometryElement* prevShapeElement = nullptr; for(const auto& child : children()) { auto element = toSVGElement(child); if(element == nullptr || element->isDisplayNone()) continue; auto shapeElement = toSVGGeometryElement(element); if(shapeElement == nullptr) { if(element->isTextPositioningElement()) return true; if(element->id() != ElementID::Use) continue; if(element->clipper()) return true; shapeElement = toSVGGeometryElement(element->firstChild()); } if(shapeElement == nullptr || !shapeElement->isRenderable()) continue; if(prevShapeElement || shapeElement->clipper()) return true; prevShapeElement = shapeElement; } return false; } SVGMaskElement::SVGMaskElement(Document* document) : SVGElement(document, ElementID::Mask) , m_x(PropertyID::X, LengthDirection::Horizontal, LengthNegativeMode::Allow, -10.f, LengthUnits::Percent) , m_y(PropertyID::Y, LengthDirection::Vertical, LengthNegativeMode::Allow, -10.f, LengthUnits::Percent) , m_width(PropertyID::Width, LengthDirection::Horizontal, LengthNegativeMode::Forbid, 120.f, LengthUnits::Percent) , m_height(PropertyID::Height, LengthDirection::Vertical, LengthNegativeMode::Forbid, 120.f, LengthUnits::Percent) , m_maskUnits(PropertyID::MaskUnits, Units::ObjectBoundingBox) , m_maskContentUnits(PropertyID::MaskContentUnits, Units::UserSpaceOnUse) { addProperty(m_x); addProperty(m_y); addProperty(m_width); addProperty(m_height); addProperty(m_maskUnits); addProperty(m_maskContentUnits); } Rect SVGMaskElement::maskRect(const SVGElement* element) const { LengthContext lengthContext(this, m_maskUnits.value()); Rect maskRect = { lengthContext.valueForLength(m_x), lengthContext.valueForLength(m_y), lengthContext.valueForLength(m_width), lengthContext.valueForLength(m_height) }; if(m_maskUnits.value() == Units::ObjectBoundingBox) { auto bbox = element->fillBoundingBox(); maskRect.x = maskRect.x * bbox.w + bbox.x; maskRect.y = maskRect.y * bbox.h + bbox.y; maskRect.w = maskRect.w * bbox.w; maskRect.h = maskRect.h * bbox.h; } return maskRect; } Rect SVGMaskElement::maskBoundingBox(const SVGElement* element) const { auto maskBoundingBox = paintBoundingBox(); if(m_maskContentUnits.value() == Units::ObjectBoundingBox) { auto bbox = element->fillBoundingBox(); maskBoundingBox.x = maskBoundingBox.x * bbox.w + bbox.x; maskBoundingBox.y = maskBoundingBox.y * bbox.h + bbox.y; maskBoundingBox.w = maskBoundingBox.w * bbox.w; maskBoundingBox.h = maskBoundingBox.h * bbox.h; } return maskBoundingBox.intersected(maskRect(element)); } void SVGMaskElement::applyMask(SVGRenderState& state) const { if(state.hasCycleReference(this)) return; auto maskImage = Canvas::create(state.currentTransform().mapRect(state.paintBoundingBox())); maskImage->clipRect(maskRect(state.element()), FillRule::NonZero, state.currentTransform()); auto currentTransform = state.currentTransform(); if(m_maskContentUnits.value() == Units::ObjectBoundingBox) { auto bbox = state.fillBoundingBox(); currentTransform.translate(bbox.x, bbox.y); currentTransform.scale(bbox.w, bbox.h); } SVGRenderState newState(this, &state, currentTransform, SVGRenderMode::Painting, maskImage); renderChildren(newState); if(clipper()) clipper()->applyClipMask(newState); if(masker()) { masker()->applyMask(newState); } if(m_mask_type == MaskType::Luminance) maskImage->convertToLuminanceMask(); state->blendCanvas(*maskImage, BlendMode::Dst_In, 1.f); } void SVGMaskElement::layoutElement(const SVGLayoutState& state) { m_mask_type = state.mask_type(); SVGElement::layoutElement(state); } } // namespace lunasvg