1226 lines
37 KiB
C++
1226 lines
37 KiB
C++
#include "svgelement.h"
|
|
#include "svgpaintelement.h"
|
|
#include "svggeometryelement.h"
|
|
#include "svgtextelement.h"
|
|
#include "svgproperty.h"
|
|
#include "svglayoutstate.h"
|
|
#include "svgrenderstate.h"
|
|
|
|
#include <cassert>
|
|
|
|
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<SVGNode> SVGTextNode::clone(bool deep) const
|
|
{
|
|
auto node = std::make_unique<SVGTextNode>(document());
|
|
node->setData(m_data);
|
|
return node;
|
|
}
|
|
|
|
const std::string emptyString;
|
|
|
|
std::unique_ptr<SVGElement> SVGElement::create(Document* document, ElementID id)
|
|
{
|
|
switch(id) {
|
|
case ElementID::Svg:
|
|
return std::make_unique<SVGSVGElement>(document);
|
|
case ElementID::Path:
|
|
return std::make_unique<SVGPathElement>(document);
|
|
case ElementID::G:
|
|
return std::make_unique<SVGGElement>(document);
|
|
case ElementID::Rect:
|
|
return std::make_unique<SVGRectElement>(document);
|
|
case ElementID::Circle:
|
|
return std::make_unique<SVGCircleElement>(document);
|
|
case ElementID::Ellipse:
|
|
return std::make_unique<SVGEllipseElement>(document);
|
|
case ElementID::Line:
|
|
return std::make_unique<SVGLineElement>(document);
|
|
case ElementID::Defs:
|
|
return std::make_unique<SVGDefsElement>(document);
|
|
case ElementID::Polygon:
|
|
case ElementID::Polyline:
|
|
return std::make_unique<SVGPolyElement>(document, id);
|
|
case ElementID::Stop:
|
|
return std::make_unique<SVGStopElement>(document);
|
|
case ElementID::LinearGradient:
|
|
return std::make_unique<SVGLinearGradientElement>(document);
|
|
case ElementID::RadialGradient:
|
|
return std::make_unique<SVGRadialGradientElement>(document);
|
|
case ElementID::Symbol:
|
|
return std::make_unique<SVGSymbolElement>(document);
|
|
case ElementID::Use:
|
|
return std::make_unique<SVGUseElement>(document);
|
|
case ElementID::Pattern:
|
|
return std::make_unique<SVGPatternElement>(document);
|
|
case ElementID::Mask:
|
|
return std::make_unique<SVGMaskElement>(document);
|
|
case ElementID::ClipPath:
|
|
return std::make_unique<SVGClipPathElement>(document);
|
|
case ElementID::Marker:
|
|
return std::make_unique<SVGMarkerElement>(document);
|
|
case ElementID::Image:
|
|
return std::make_unique<SVGImageElement>(document);
|
|
case ElementID::Style:
|
|
return std::make_unique<SVGStyleElement>(document);
|
|
case ElementID::Text:
|
|
return std::make_unique<SVGTextElement>(document);
|
|
case ElementID::Tspan:
|
|
return std::make_unique<SVGTSpanElement>(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<SVGElement*>(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<SVGElement*>(node);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
SVGNode* SVGElement::addChild(std::unique_ptr<SVGNode> 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<SVGMarkerElement*>(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<SVGClipPathElement*>(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<SVGMaskElement*>(element);
|
|
return nullptr;
|
|
}
|
|
|
|
SVGPaintElement* SVGElement::getPainter(std::string_view id) const
|
|
{
|
|
auto element = rootElement()->getElementById(id);
|
|
if(element && element->isPaintElement())
|
|
return static_cast<SVGPaintElement*>(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<const SVGSVGElement*>(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<const SVGSVGElement*>(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<SVGNode> 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<SVGElement> 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<const SVGGeometryElement*>(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
|