// SPDX-License-Identifier: GPL-2.0-or-later /* * SVG implementation * * Author: * Lauris Kaplinski * bulia byak * Jon A. Cruz * Abhishek Sharma * * Copyright (C) 2002 Lauris Kaplinski * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "sp-pattern.h" #include #include #include <2geom/transforms.h> #include #include "attributes.h" #include "bad-uri-exception.h" #include "document.h" #include "sp-defs.h" #include "sp-factory.h" #include "sp-item.h" #include "display/cairo-utils.h" #include "display/drawing-context.h" #include "display/drawing-surface.h" #include "display/drawing.h" #include "display/drawing-group.h" #include "display/drawing-pattern.h" #include "svg/svg.h" #include "xml/href-attribute-helper.h" SPPatternReference::SPPatternReference(SPPattern *owner) : URIReference(owner) { } SPPattern *SPPatternReference::getObject() const { return static_cast(URIReference::getObject()); } bool SPPatternReference::_acceptObject(SPObject *obj) const { return is(obj) && URIReference::_acceptObject(obj); } /* * */ SPPattern::SPPattern() : ref(this) , _pattern_units(UNITS_OBJECTBOUNDINGBOX) , _pattern_units_set(false) , _pattern_content_units(UNITS_USERSPACEONUSE) , _pattern_content_units_set(false) , _pattern_transform_set(false) , shown(nullptr) { ref.changedSignal().connect(sigc::mem_fun(*this, &SPPattern::_onRefChanged)); } SPPattern::~SPPattern() = default; void SPPattern::build(SPDocument *doc, Inkscape::XML::Node *repr) { SPPaintServer::build(doc, repr); readAttr(SPAttr::PATTERNUNITS); readAttr(SPAttr::PATTERNCONTENTUNITS); readAttr(SPAttr::PATTERNTRANSFORM); readAttr(SPAttr::X); readAttr(SPAttr::Y); readAttr(SPAttr::WIDTH); readAttr(SPAttr::HEIGHT); readAttr(SPAttr::VIEWBOX); readAttr(SPAttr::PRESERVEASPECTRATIO); readAttr(SPAttr::XLINK_HREF); readAttr(SPAttr::STYLE); doc->addResource("pattern", this); } void SPPattern::release() { if (document) { document->removeResource("pattern", this); } // Should have been unattached by their owners on the release signal. assert(attached_views.empty()); set_shown(nullptr); views.clear(); _modified_connection.disconnect(); ref.detach(); SPPaintServer::release(); } void SPPattern::set(SPAttr key, char const *value) { switch (key) { case SPAttr::PATTERNUNITS: _pattern_units = UNITS_OBJECTBOUNDINGBOX; _pattern_units_set = false; if (value) { if (!std::strcmp(value, "userSpaceOnUse")) { _pattern_units = UNITS_USERSPACEONUSE; _pattern_units_set = true; } else if (!std::strcmp(value, "objectBoundingBox")) { _pattern_units_set = true; } } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::PATTERNCONTENTUNITS: _pattern_content_units = UNITS_USERSPACEONUSE; _pattern_content_units_set = false; if (value) { if (!std::strcmp(value, "userSpaceOnUse")) { _pattern_content_units_set = true; } else if (!std::strcmp(value, "objectBoundingBox")) { _pattern_content_units = UNITS_OBJECTBOUNDINGBOX; _pattern_content_units_set = true; } } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::PATTERNTRANSFORM: { _pattern_transform = Geom::identity(); _pattern_transform_set = false; if (value) { Geom::Affine t; if (sp_svg_transform_read(value, &t)) { _pattern_transform = t; _pattern_transform_set = true; } } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; } case SPAttr::X: _x.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::Y: _y.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::WIDTH: _width.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::HEIGHT: _height.readOrUnset(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; case SPAttr::VIEWBOX: set_viewBox(value); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); break; case SPAttr::PRESERVEASPECTRATIO: set_preserveAspectRatio(value); requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); break; case SPAttr::XLINK_HREF: if (!value) { if (href.empty()) { break; } href.clear(); ref.detach(); } else { if (href == value) { break; } href = value; // Attempt to attach ref, which emits the changed signal. try { ref.attach(Inkscape::URI(href.data())); } catch (Inkscape::BadURIException const &e) { g_warning("%s", e.what()); href.clear(); ref.detach(); } } requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; default: SPPaintServer::set(key, value); break; } } void SPPattern::update(SPCtx *ctx, unsigned flags) { auto const cflags = cascade_flags(flags); for (auto c : childList(true)) { if (cflags || (c->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { c->updateDisplay(ctx, cflags); } sp_object_unref(c, nullptr); } for (auto &v : views) { update_view(v); } } void SPPattern::update_view(View &v) { // * "width" and "height" determine tile size. // * "viewBox" (if defined) or "patternContentUnits" determines placement of content inside tile. // * "x", "y", and "patternTransform" transform tile to user space after tile is generated. // These functions recursively search up the tree to find the values. double tile_x = x(); double tile_y = y(); double tile_width = width(); double tile_height = height(); if (v.bbox && patternUnits() == UNITS_OBJECTBOUNDINGBOX) { tile_x *= v.bbox->width(); tile_y *= v.bbox->height(); tile_width *= v.bbox->width(); tile_height *= v.bbox->height(); } // Pattern size in pattern space auto pattern_tile = Geom::Rect::from_xywh(0, 0, tile_width, tile_height); // Content to tile (pattern space) Geom::Affine content2ps; if (auto effective_view_box = viewbox()) { // viewBox to pattern server (using SPViewBox) viewBox = *effective_view_box; c2p.setIdentity(); apply_viewbox(pattern_tile); content2ps = c2p; } else { // Content to bbox if (v.bbox && patternContentUnits() == UNITS_OBJECTBOUNDINGBOX) { content2ps = Geom::Affine(v.bbox->width(), 0.0, 0.0, v.bbox->height(), 0, 0); } } // Tile (pattern space) to user. Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * getTransform(); v.drawingitem->setTileRect(pattern_tile); v.drawingitem->setChildTransform(content2ps); v.drawingitem->setPatternToUserTransform(ps2user); } void SPPattern::modified(unsigned flags) { auto const cflags = cascade_flags(flags); for (auto c : childList(true)) { if (cflags || (c->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { c->emitModified(cflags); } sp_object_unref(c); } set_shown(rootPattern()); } // The following three functions are based on SPGroup. void SPPattern::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { SPPaintServer::child_added(child, ref); auto last_child = lastChild(); if (last_child && last_child->getRepr() == child) { if (auto item = cast(last_child)) { for (auto &v : attached_views) { auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_SHOW_DISPLAY); if (ac) { v.drawingitem->appendChild(ac); } } } } else { if (auto item = cast(get_child_by_repr(child))) { unsigned position = item->pos_in_parent(); for (auto &v : attached_views) { auto ac = item->invoke_show(v.drawingitem->drawing(), v.key, SP_ITEM_SHOW_DISPLAY); if (ac) { v.drawingitem->prependChild(ac); ac->setZOrder(position); } } } } requestModified(SP_OBJECT_MODIFIED_FLAG); } void SPPattern::remove_child(Inkscape::XML::Node *child) { SPPaintServer::remove_child(child); // no need to do anything as child will automatically remove itself requestModified(SP_OBJECT_MODIFIED_FLAG); } void SPPattern::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_prev, Inkscape::XML::Node *new_prev) { SPPaintServer::order_changed(child, old_prev, new_prev); if (auto item = cast(get_child_by_repr(child))) { unsigned position = item->pos_in_parent(); for (auto &v : attached_views) { auto ac = item->get_arenaitem(v.key); ac->setZOrder(position); } } requestModified(SP_OBJECT_MODIFIED_FLAG); } void SPPattern::_onRefChanged(SPObject *old_ref, SPObject *ref) { if (old_ref) { _modified_connection.disconnect(); } if (is(ref)) { _modified_connection = ref->connectModified(sigc::mem_fun(*this, &SPPattern::_onRefModified)); } _onRefModified(ref, 0); } void SPPattern::_onRefModified(SPObject */*ref*/, unsigned /*flags*/) { requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); } void SPPattern::set_shown(SPPattern *new_shown) { if (shown == new_shown) { return; } if (shown) { for (auto &v : views) { shown->unattach_view(v.drawingitem.get()); } shown_released_connection.disconnect(); } shown = new_shown; if (shown) { for (auto &v : views) { shown->attach_view(v.drawingitem.get(), v.key); } shown_released_connection = shown->connectRelease([this] (auto) { set_shown(nullptr); }); } } void SPPattern::attach_view(Inkscape::DrawingPattern *di, unsigned key) { attached_views.push_back({di, key}); for (auto &c : children) { if (auto child = cast(&c)) { auto item = child->invoke_show(di->drawing(), key, SP_ITEM_SHOW_DISPLAY); di->appendChild(item); } } } void SPPattern::unattach_view(Inkscape::DrawingPattern *di) { auto it = std::find_if(attached_views.begin(), attached_views.end(), [di] (auto const &v) { return v.drawingitem == di; }); assert(it != attached_views.end()); for (auto &c : children) { if (auto child = cast(&c)) { child->invoke_hide(it->key); } } attached_views.erase(it); } unsigned SPPattern::_countHrefs(SPObject *o) const { if (!o) return 1; guint i = 0; SPStyle *style = o->style; if (style && style->fill.isPaintserver() && is(SP_STYLE_FILL_SERVER(style)) && cast(SP_STYLE_FILL_SERVER(style)) == this) { i++; } if (style && style->stroke.isPaintserver() && is(SP_STYLE_STROKE_SERVER(style)) && cast(SP_STYLE_STROKE_SERVER(style)) == this) { i++; } for (auto& child: o->children) { i += _countHrefs(&child); } return i; } SPPattern *SPPattern::_chain() const { Inkscape::XML::Document *xml_doc = document->getReprDoc(); Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); repr->setAttribute("inkscape:collect", "always"); Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id")); Inkscape::setHrefAttribute(*repr, parent_ref); // this attribute is used to express uniform pattern scaling in pattern editor, so keep it repr->setAttribute("preserveAspectRatio", getRepr()->attribute("preserveAspectRatio")); defsrepr->addChild(repr, nullptr); SPObject *child = document->getObjectByRepr(repr); assert(child == document->getObjectById(repr->attribute("id"))); g_assert(is(child)); return cast(child); } SPPattern *SPPattern::clone_if_necessary(SPItem *item, const gchar *property) { SPPattern *pattern = this; if (pattern->href.empty() || pattern->hrefcount > _countHrefs(item)) { pattern = _chain(); Glib::ustring href = Glib::ustring::compose("url(#%1)", pattern->getRepr()->attribute("id")); SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, property, href.c_str()); sp_repr_css_change_recursive(item->getRepr(), css, "style"); } return pattern; } // do not remove identity transform in pattern elements; when patterns are referenced then linking // pattern transform overrides root/referenced pattern transform; if it disappears then root transform // takes over and that's not what we want static std::string write_transform(const Geom::Affine& transform) { if (transform.isIdentity()) { return "scale(1)"; } return sp_svg_transform_write(transform); } void SPPattern::transform_multiply(Geom::Affine postmul, bool set) { // this formula is for a different interpretation of pattern transforms as described in (*) in sp-pattern.cpp // for it to work, we also need sp_object_read_attr( item, "transform"); // pattern->patternTransform = premul * item->transform * pattern->patternTransform * item->transform.inverse() * // postmul; // otherwise the formula is much simpler if (set) { _pattern_transform = postmul; } else { _pattern_transform = getTransform() * postmul; } _pattern_transform_set = true; setAttributeOrRemoveIfEmpty("patternTransform", write_transform(_pattern_transform)); } char const *SPPattern::produce(std::vector const &reprs, Geom::Rect const &bounds, SPDocument *document, Geom::Affine const &transform, Geom::Affine const &move) { Inkscape::XML::Document *xml_doc = document->getReprDoc(); Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); repr->setAttribute("patternUnits", "userSpaceOnUse"); repr->setAttributeSvgDouble("width", bounds.dimensions()[Geom::X]); repr->setAttributeSvgDouble("height", bounds.dimensions()[Geom::Y]); repr->setAttributeOrRemoveIfEmpty("patternTransform", write_transform(transform)); // by default use uniform scaling repr->setAttribute("preserveAspectRatio", "xMidYMid"); defsrepr->appendChild(repr); const gchar *pd = repr->attribute("id"); SPObject *pat_object = document->getObjectById(pd); bool can_colorize = false; for (auto node : reprs) { auto copy = cast(pat_object->appendChildRepr(node)); if (!repr->attribute("inkscape:label") && node->attribute("inkscape:label")) { repr->setAttribute("inkscape:label", node->attribute("inkscape:label")); } // if some elements have undefined color or solid black, then their fill color is customizable if (copy->style && copy->style->isSet(SPAttr::FILL)) { if (auto paint = copy->style->getFillOrStroke(true)) { if (paint->isColor() && paint->value.color.toRGBA32(255) == 255) { // black color set? can_colorize = true; // remove black fill, it will be inherited from pattern paint->clear(); } } } else { // no color - it will be inherited can_colorize = true; } Geom::Affine dup_transform; if (!sp_svg_transform_read(node->attribute("transform"), &dup_transform)) dup_transform = Geom::identity(); dup_transform *= move; copy->doWriteTransform(dup_transform, nullptr, false); } if (can_colorize && pat_object->style) { // add black fill style to the pattern object - it will tell pattern editor to enable color selector pat_object->style->readIfUnset(SPAttr::FILL, "black"); } Inkscape::GC::release(repr); return pd; } SPPattern const *SPPattern::rootPattern() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->firstChild()) { // find the first one with children return p; } } return this; // document is broken, we can't get to root; but at least we can return ourself which is supposedly a valid pattern } SPPattern *SPPattern::rootPattern() { return const_cast(std::as_const(*this).rootPattern()); } // Access functions that look up fields up the chain of referenced patterns and return the first one which is set SPPattern::PatternUnits SPPattern::patternUnits() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_pattern_units_set) return p->_pattern_units; } return _pattern_units; } SPPattern::PatternUnits SPPattern::patternContentUnits() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_pattern_content_units_set) return p->_pattern_content_units; } return _pattern_content_units; } Geom::Affine const &SPPattern::getTransform() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_pattern_transform_set) return p->_pattern_transform; } return _pattern_transform; } const Geom::Affine& SPPattern::get_this_transform() const { return _pattern_transform; } double SPPattern::x() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_x._set) return p->_x.computed; } return 0; } double SPPattern::y() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_y._set) return p->_y.computed; } return 0; } double SPPattern::width() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_width._set) return p->_width.computed; } return 0; } double SPPattern::height() const { for (auto p = this; p; p = p->ref.getObject()) { if (p->_height._set) return p->_height.computed; } return 0; } Geom::OptRect SPPattern::viewbox() const { Geom::OptRect viewbox; for (auto p = this; p; p = p->ref.getObject()) { if (p->viewBox_set) { viewbox = p->viewBox; break; } } return viewbox; } bool SPPattern::_hasItemChildren() const { for (auto &child : children) { if (is(&child)) { return true; } } return false; } bool SPPattern::isValid() const { return width() > 0 && height() > 0; } Inkscape::DrawingPattern *SPPattern::show(Inkscape::Drawing &drawing, unsigned key, Geom::OptRect const &bbox) { views.emplace_back(make_drawingitem(drawing), bbox, key); auto &v = views.back(); auto root = v.drawingitem.get(); if (shown) { shown->attach_view(root, key); } root->setStyle(style); update_view(v); return root; } void SPPattern::hide(unsigned key) { auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { return v.key == key; }); if (it == views.end()) { return; } if (shown) { shown->unattach_view(it->drawingitem.get()); } views.erase(it); } void SPPattern::setBBox(unsigned key, Geom::OptRect const &bbox) { auto it = std::find_if(views.begin(), views.end(), [=] (auto &v) { return v.key == key; }); assert(it != views.end()); auto &v = *it; v.bbox = bbox; update_view(v); } SPPattern::View::View(DrawingItemPtr drawingitem, Geom::OptRect const &bbox, unsigned key) : drawingitem(std::move(drawingitem)) , bbox(bbox) , key(key) {} /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :