diff options
Diffstat (limited to 'src/object/sp-pattern.cpp')
-rw-r--r-- | src/object/sp-pattern.cpp | 742 |
1 files changed, 742 insertions, 0 deletions
diff --git a/src/object/sp-pattern.cpp b/src/object/sp-pattern.cpp new file mode 100644 index 0000000..b5417aa --- /dev/null +++ b/src/object/sp-pattern.cpp @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <pattern> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-pattern.h" + +#include <string> +#include <cstring> + +#include <2geom/transforms.h> + +#include <glibmm.h> + +#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<SPPattern*>(URIReference::getObject()); +} + +bool SPPatternReference::_acceptObject(SPObject *obj) const +{ + return is<SPPattern>(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<SPItem>(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<SPItem>(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<SPItem>(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<SPPattern>(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<SPItem>(&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<SPItem>(&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<SPPattern>(SP_STYLE_FILL_SERVER(style)) && + cast<SPPattern>(SP_STYLE_FILL_SERVER(style)) == this) { + i++; + } + if (style && style->stroke.isPaintserver() && is<SPPattern>(SP_STYLE_STROKE_SERVER(style)) && + cast<SPPattern>(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<SPPattern>(child)); + + return cast<SPPattern>(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<Inkscape::XML::Node*> 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<SPItem>(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<SPPattern*>(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<SPItem>(&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<Inkscape::DrawingPattern>(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<Inkscape::DrawingPattern> 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 : |