// SPDX-License-Identifier: GPL-2.0-or-later /* * SPObject implementation. * * Authors: * Lauris Kaplinski * bulia byak * Stephen Silver * Jon A. Cruz * Abhishek Sharma * Adrian Boguszewski * * Copyright (C) 1999-2016 authors * Copyright (C) 2001-2002 Ximian, Inc. * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include #include #include #include "helper/sp-marshal.h" #include "xml/node-event-vector.h" #include "attributes.h" #include "attribute-rel-util.h" #include "color-profile.h" #include "document.h" #include "io/fix-broken-links.h" #include "preferences.h" #include "style.h" #include "live_effects/lpeobject.h" #include "sp-factory.h" #include "sp-font.h" #include "sp-paint-server.h" #include "sp-root.h" #include "sp-style-elem.h" #include "sp-script.h" #include "streq.h" #include "strneq.h" #include "xml/node-fns.h" #include "debug/event-tracker.h" #include "debug/simple-event.h" #include "debug/demangle.h" #include "svg/css-ostringstream.h" #include "util/format.h" #include "util/longest-common-suffix.h" #define noSP_OBJECT_DEBUG_CASCADE #define noSP_OBJECT_DEBUG #ifdef SP_OBJECT_DEBUG # define debug(f, a...) { g_print("%s(%d) %s:", \ __FILE__,__LINE__,__FUNCTION__); \ g_print(f, ## a); \ g_print("\n"); \ } #else # define debug(f, a...) /* */ #endif // Define to enable indented tracing of SPObject. //#define OBJECT_TRACE unsigned SPObject::indent_level = 0; Inkscape::XML::NodeEventVector object_event_vector = { SPObject::repr_child_added, SPObject::repr_child_removed, SPObject::repr_attr_changed, SPObject::repr_content_changed, SPObject::repr_order_changed, SPObject::repr_name_changed }; /** * A friend class used to set internal members on SPObject so as to not expose settors in SPObject's public API */ class SPObjectImpl { public: /** * Null's the id member of an SPObject without attempting to free prior contents. * * @param[inout] obj Pointer to the object which's id shall be nulled. */ static void setIdNull( SPObject* obj ) { if (obj) { obj->id = nullptr; } } /** * Sets the id member of an object, freeing any prior content. * * @param[inout] obj Pointer to the object which's id shall be set. * @param[in] id New id */ static void setId( SPObject* obj, gchar const* id ) { if (obj && (id != obj->id) ) { if (obj->id) { g_free(obj->id); obj->id = nullptr; } if (id) { obj->id = g_strdup(id); } } } }; /** * Constructor, sets all attributes to default values. */ SPObject::SPObject() : cloned(0), clone_original(nullptr), uflags(0), mflags(0), hrefcount(0), _total_hrefcount(0), document(nullptr), parent(nullptr), id(nullptr), repr(nullptr), refCount(1), hrefList(std::list()), _successor(nullptr), _collection_policy(SPObject::COLLECT_WITH_PARENT), _label(nullptr), _default_label(nullptr) { debug("id=%p, typename=%s",this, g_type_name_from_instance((GTypeInstance*)this)); //used XML Tree here. this->getRepr(); // TODO check why this call is made SPObjectImpl::setIdNull(this); // FIXME: now we create style for all objects, but per SVG, only the following can have style attribute: // vg, g, defs, desc, title, symbol, use, image, switch, path, rect, circle, ellipse, line, polyline, // polygon, text, tspan, tref, textPath, altGlyph, glyphRef, marker, linearGradient, radialGradient, // stop, pattern, clipPath, mask, filter, feImage, a, font, glyph, missing-glyph, foreignObject this->style = new SPStyle( nullptr, this ); // Is it necessary to call with "this"? this->context_style = nullptr; } /** * Destructor, frees the used memory and unreferences a potential successor of the object. */ SPObject::~SPObject() { g_free(this->_label); g_free(this->_default_label); this->_label = nullptr; this->_default_label = nullptr; if (this->_successor) { sp_object_unref(this->_successor, nullptr); this->_successor = nullptr; } if (parent) { parent->children.erase(parent->children.iterator_to(*this)); } if( style == nullptr ) { // style pointer could be NULL if unreffed too many times. // Conjecture: style pointer is never NULL. std::cerr << "SPObject::~SPObject(): style pointer is NULL" << std::endl; } else if( style->refCount() > 1 ) { // Conjecture: style pointer should be unreffed by other classes before reaching here. // Conjecture is false for SPTSpan where ref is held by InputStreamTextSource. // As an additional note: // The outer tspan of a nested tspan will result in a ref count of five: one for the // TSpan itself, one for the InputStreamTextSource instance before the inner tspan and // one for the one after, along with one for each corresponding DrawingText instance. // std::cerr << "SPObject::~SPObject(): someone else still holding ref to style" << std::endl; // sp_style_unref( this->style ); } else { delete this->style; } } // CPPIFY: make pure virtual void SPObject::read_content() { //throw; } void SPObject::update(SPCtx* /*ctx*/, unsigned int /*flags*/) { //throw; } void SPObject::modified(unsigned int /*flags*/) { #ifdef OBJECT_TRACE objectTrace( "SPObject::modified (default) (empty function)" ); objectTrace( "SPObject::modified (default)", false ); #endif //throw; } namespace { namespace Debug = Inkscape::Debug; namespace Util = Inkscape::Util; typedef Debug::SimpleEvent BaseRefCountEvent; class RefCountEvent : public BaseRefCountEvent { public: RefCountEvent(SPObject *object, int bias, char const *name) : BaseRefCountEvent(name) { _addProperty("object", Util::format("%p", object).pointer()); _addProperty("class", Debug::demangle(typeid(*object).name())); _addProperty("new-refcount", Util::format("%d", object->refCount + bias).pointer()); } }; class RefEvent : public RefCountEvent { public: RefEvent(SPObject *object) : RefCountEvent(object, 1, "sp-object-ref") {} }; class UnrefEvent : public RefCountEvent { public: UnrefEvent(SPObject *object) : RefCountEvent(object, -1, "sp-object-unref") {} }; } gchar const* SPObject::getId() const { return id; } /** * Accumulate this id and all it's descendants ids */ void SPObject::getIds(std::set &ret) const { if (id) { ret.insert(std::string(id)); } for (auto &child : children) { child.getIds(ret); } } /** * Returns the id as a url param, in the form 'url(#{id})' */ std::string SPObject::getUrl() const { if (id) { return std::string("url(#") + id + ")"; } return ""; } Inkscape::XML::Node * SPObject::getRepr() { return repr; } Inkscape::XML::Node const* SPObject::getRepr() const{ return repr; } SPObject *sp_object_ref(SPObject *object, SPObject *owner) { g_return_val_if_fail(object != nullptr, NULL); g_return_val_if_fail(SP_IS_OBJECT(object), NULL); g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL); Inkscape::Debug::EventTracker tracker(object); object->refCount++; return object; } SPObject *sp_object_unref(SPObject *object, SPObject *owner) { g_return_val_if_fail(object != nullptr, NULL); g_return_val_if_fail(SP_IS_OBJECT(object), NULL); g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL); Inkscape::Debug::EventTracker tracker(object); object->refCount--; if (object->refCount <= 0) { delete object; } return nullptr; } void SPObject::hrefObject(SPObject* owner) { // if (owner) std::cout << " owner: " << *owner << std::endl; // If owner is a clone, do not increase hrefcount, it's already href'ed by original. if (!owner || !owner->cloned) { hrefcount++; _updateTotalHRefCount(1); } if(owner) hrefList.push_front(owner); } void SPObject::unhrefObject(SPObject* owner) { if (!owner || !owner->cloned) { g_return_if_fail(hrefcount > 0); hrefcount--; _updateTotalHRefCount(-1); } if(owner) hrefList.remove(owner); } void SPObject::_updateTotalHRefCount(int increment) { SPObject *topmost_collectable = nullptr; for ( SPObject *iter = this ; iter ; iter = iter->parent ) { iter->_total_hrefcount += increment; if ( iter->_total_hrefcount < iter->hrefcount ) { g_critical("HRefs overcounted"); } if ( iter->_total_hrefcount == 0 && iter->_collection_policy != COLLECT_WITH_PARENT ) { topmost_collectable = iter; } } if (topmost_collectable) { topmost_collectable->requestOrphanCollection(); } } bool SPObject::isAncestorOf(SPObject const *object) const { g_return_val_if_fail(object != nullptr, false); object = object->parent; while (object) { if ( object == this ) { return true; } object = object->parent; } return false; } SPObject const *SPObject::nearestCommonAncestor(SPObject const *object) const { g_return_val_if_fail(object != nullptr, NULL); using Inkscape::Algorithms::nearest_common_ancestor; return nearest_common_ancestor(this, object, nullptr); } static SPObject const *AncestorSon(SPObject const *obj, SPObject const *ancestor) { SPObject const *result = nullptr; if ( obj && ancestor ) { if (obj->parent == ancestor) { result = obj; } else { result = AncestorSon(obj->parent, ancestor); } } return result; } int sp_object_compare_position(SPObject const *first, SPObject const *second) { int result = 0; if (first != second) { SPObject const *ancestor = first->nearestCommonAncestor(second); // Need a common ancestor to be able to compare if ( ancestor ) { // we have an object and its ancestor (should not happen when sorting selection) if (ancestor == first) { result = 1; } else if (ancestor == second) { result = -1; } else { SPObject const *to_first = AncestorSon(first, ancestor); SPObject const *to_second = AncestorSon(second, ancestor); g_assert(to_second->parent == to_first->parent); result = sp_repr_compare_position(to_first->getRepr(), to_second->getRepr()); } } } return result; } bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second){ return sp_object_compare_position(first,second)<0; } SPObject *SPObject::appendChildRepr(Inkscape::XML::Node *repr) { if ( !cloned ) { getRepr()->appendChild(repr); return document->getObjectByRepr(repr); } else { g_critical("Attempt to append repr as child of cloned object"); return nullptr; } } void SPObject::setCSS(SPCSSAttr *css, gchar const *attr) { g_assert(this->getRepr() != nullptr); sp_repr_css_set(this->getRepr(), css, attr); } void SPObject::changeCSS(SPCSSAttr *css, gchar const *attr) { g_assert(this->getRepr() != nullptr); sp_repr_css_change(this->getRepr(), css, attr); } std::vector SPObject::childList(bool add_ref, Action) { std::vector l; for (auto& child: children) { if (add_ref) { sp_object_ref(&child); } l.push_back(&child); } return l; } std::vector SPObject::ancestorList(bool root_to_tip) { std::vector ancestors; for (SPObject::ParentIterator iter=parent ; iter ; ++iter) { ancestors.push_back(iter); } if (root_to_tip) { std::reverse(ancestors.begin(), ancestors.end()); } return ancestors; } gchar const *SPObject::label() const { return _label; } gchar const *SPObject::defaultLabel() const { if (_label) { return _label; } else { if (!_default_label) { if (getId()) { _default_label = g_strdup_printf("#%s", getId()); } else if (getRepr()) { _default_label = g_strdup_printf("<%s>", getRepr()->name()); } else { _default_label = g_strdup("Default label"); } } return _default_label; } } void SPObject::setLabel(gchar const *label) { getRepr()->setAttribute("inkscape:label", label); } void SPObject::requestOrphanCollection() { g_return_if_fail(document != nullptr); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); // do not remove style or script elements (Bug #276244) if (dynamic_cast(this)) { // leave it } else if (dynamic_cast(this)) { // leave it } else if (dynamic_cast(this)) { // leave it } else if ((! prefs->getBool("/options/cleanupswatches/value", false)) && SP_IS_PAINT_SERVER(this) && static_cast(this)->isSwatch() ) { // leave it } else if (IS_COLORPROFILE(this)) { // leave it } else if (dynamic_cast(this)) { document->queueForOrphanCollection(this); } else { document->queueForOrphanCollection(this); /** \todo * This is a temporary hack added to make fill&stroke rebuild its * gradient list when the defs are vacuumed. gradient-vector.cpp * listens to the modified signal on defs, and now we give it that * signal. Mental says that this should be made automatic by * merging SPObjectGroup with SPObject; SPObjectGroup would issue * this signal automatically. Or maybe just derive SPDefs from * SPObjectGroup? */ this->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); } } void SPObject::_sendDeleteSignalRecursive() { for (auto& child: children) { child._delete_signal.emit(&child); child._sendDeleteSignalRecursive(); } } void SPObject::deleteObject(bool propagate, bool propagate_descendants) { sp_object_ref(this, nullptr); if (SP_IS_LPE_ITEM(this)) { SP_LPE_ITEM(this)->removeAllPathEffects(false, propagate_descendants); } if (propagate) { _delete_signal.emit(this); } if (propagate_descendants) { this->_sendDeleteSignalRecursive(); } Inkscape::XML::Node *repr = getRepr(); if (repr && repr->parent()) { sp_repr_unparent(repr); } if (_successor) { _successor->deleteObject(propagate, propagate_descendants); } sp_object_unref(this, nullptr); } void SPObject::cropToObject(SPObject *except) { std::vector toDelete; for (auto &child : children) { if (SP_IS_ITEM(&child)) { if (child.isAncestorOf(except)) { child.cropToObject(except); } else if (&child != except) { sp_object_ref(&child, nullptr); toDelete.push_back(&child); } } } for (auto &i : toDelete) { i->deleteObject(true, true); sp_object_unref(i, nullptr); } } /** * Removes objects which are not related to given list of objects. * * Use Case: Group[MyRect1 , MyRect2] , MyRect3 * List Provided: MyRect1, MyRect3 * Output doc: Group[MyRect1], MyRect3 * List Provided: MyRect1, Group * Output doc: Group[MyRect1, MyRect2] (notice MyRect2 is not deleted as it is related to Group) */ void SPObject::cropToObjects(std::vector except_objects) { if (except_objects.empty()) { return; } std::vector toDelete; for (auto &child : children) { if (SP_IS_ITEM(&child)) { std::vector except_in_child; bool child_delete_flag = true; for (auto except : except_objects) { if (&child == except) { child_delete_flag = false; except_in_child.clear(); break; } if (child.isAncestorOf(except)) { except_in_child.push_back(except); child_delete_flag = false; } } if (child_delete_flag) { sp_object_ref(&child, nullptr); toDelete.push_back(&child); } else { child.cropToObjects(except_in_child); } } } for (auto &i : toDelete) { i->deleteObject(true, true); sp_object_unref(i, nullptr); } } void SPObject::attach(SPObject *object, SPObject *prev) { //g_return_if_fail(parent != NULL); //g_return_if_fail(SP_IS_OBJECT(parent)); g_return_if_fail(object != nullptr); g_return_if_fail(SP_IS_OBJECT(object)); g_return_if_fail(!prev || SP_IS_OBJECT(prev)); g_return_if_fail(!prev || prev->parent == this); g_return_if_fail(!object->parent); sp_object_ref(object, this); object->parent = this; this->_updateTotalHRefCount(object->_total_hrefcount); auto it = children.begin(); if (prev != nullptr) { it = ++children.iterator_to(*prev); } children.insert(it, *object); if (!object->xml_space.set) object->xml_space.value = this->xml_space.value; } void SPObject::reorder(SPObject* obj, SPObject* prev) { g_return_if_fail(obj != nullptr); g_return_if_fail(obj->parent); g_return_if_fail(obj->parent == this); g_return_if_fail(obj != prev); g_return_if_fail(!prev || prev->parent == obj->parent); auto it = children.begin(); if (prev != nullptr) { it = ++children.iterator_to(*prev); } children.splice(it, children, children.iterator_to(*obj)); } void SPObject::detach(SPObject *object) { //g_return_if_fail(parent != NULL); //g_return_if_fail(SP_IS_OBJECT(parent)); g_return_if_fail(object != nullptr); g_return_if_fail(SP_IS_OBJECT(object)); g_return_if_fail(object->parent == this); children.erase(children.iterator_to(*object)); object->releaseReferences(); object->parent = nullptr; this->_updateTotalHRefCount(-object->_total_hrefcount); sp_object_unref(object, this); } SPObject *SPObject::get_child_by_repr(Inkscape::XML::Node *repr) { g_return_val_if_fail(repr != nullptr, NULL); SPObject *result = nullptr; if (children.size() > 0 && children.back().getRepr() == repr) { result = &children.back(); // optimization for common scenario } else { for (auto& child: children) { if (child.getRepr() == repr) { result = &child; break; } } } return result; } /** * Get closest child to a reference representation. May traverse backwards * until it finds a child SPObject node. * * @param obj Parent object * @param ref Reference node, may be NULL * @return Child, or NULL if not found */ static SPObject *get_closest_child_by_repr(SPObject &obj, Inkscape::XML::Node *ref) { for (; ref; ref = ref->prev()) { // The most likely situation is that `ref` is indeed a child of `obj`, // so try that first, before checking getObjectByRepr. if (auto result = obj.get_child_by_repr(ref)) { return result; } // Only continue if `ref` is not an SPObject, but e.g. an XML comment if (obj.document->getObjectByRepr(ref)) { break; } } return nullptr; } void SPObject::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { SPObject* object = this; const std::string type_string = NodeTraits::get_type_string(*child); SPObject* ochild = SPFactory::createObject(type_string); if (ochild == nullptr) { // Currently, there are many node types that do not have // corresponding classes in the SPObject tree. // (rdf:RDF, inkscape:clipboard, ...) // Thus, simply ignore this case for now. return; } SPObject *prev = get_closest_child_by_repr(*object, ref); object->attach(ochild, prev); sp_object_unref(ochild, nullptr); ochild->invoke_build(object->document, child, object->cloned); } void SPObject::release() { SPObject* object = this; debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); auto tmp = children | boost::adaptors::transformed([](SPObject& obj){return &obj;}); std::vector toRelease(tmp.begin(), tmp.end()); for (auto& p: toRelease) { object->detach(p); } } void SPObject::remove_child(Inkscape::XML::Node* child) { debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); SPObject *ochild = this->get_child_by_repr(child); // If the xml node has got a corresponding child in the object tree if (ochild) { this->detach(ochild); } } void SPObject::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node * /*old_ref*/, Inkscape::XML::Node *new_ref) { SPObject* object = this; SPObject *ochild = object->get_child_by_repr(child); g_return_if_fail(ochild != nullptr); SPObject *prev = get_closest_child_by_repr(*object, new_ref); object->reorder(ochild, prev); ochild->_position_changed_signal.emit(ochild); } void SPObject::tag_name_changed(gchar const* oldname, gchar const* newname) { g_warning("XML Element renamed from %s to %s!", oldname, newname); } void SPObject::build(SPDocument *document, Inkscape::XML::Node *repr) { #ifdef OBJECT_TRACE objectTrace( "SPObject::build" ); #endif SPObject* object = this; /* Nothing specific here */ debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); object->readAttr(SPAttr::XML_SPACE); object->readAttr(SPAttr::LANG); object->readAttr(SPAttr::XML_LANG); // "xml:lang" overrides "lang" per spec, read it last. object->readAttr(SPAttr::INKSCAPE_LABEL); object->readAttr(SPAttr::INKSCAPE_COLLECT); // Inherit if not set if (lang.empty() && object->parent) { lang = object->parent->lang; } if(object->cloned && (repr->attribute("id")) ) // The cases where this happens are when the "original" has no id. This happens // if it is a SPString (a TextNode, e.g. in a ), or when importing // stuff externally modified to have no id. object->clone_original = document->getObjectById(repr->attribute("id")); for (Inkscape::XML::Node *rchild = repr->firstChild() ; rchild != nullptr; rchild = rchild->next()) { const std::string typeString = NodeTraits::get_type_string(*rchild); SPObject* child = SPFactory::createObject(typeString); if (child == nullptr) { // Currently, there are many node types that do not have // corresponding classes in the SPObject tree. // (rdf:RDF, inkscape:clipboard, ...) // Thus, simply ignore this case for now. continue; } object->attach(child, object->lastChild()); sp_object_unref(child, nullptr); child->invoke_build(document, rchild, object->cloned); } #ifdef OBJECT_TRACE objectTrace( "SPObject::build", false ); #endif } void SPObject::invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned) { #ifdef OBJECT_TRACE objectTrace( "SPObject::invoke_build" ); #endif debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); //g_assert(object != NULL); //g_assert(SP_IS_OBJECT(object)); g_assert(document != nullptr); g_assert(repr != nullptr); g_assert(this->document == nullptr); g_assert(this->repr == nullptr); g_assert(this->getId() == nullptr); /* Bookkeeping */ this->document = document; this->repr = repr; if (!cloned) { Inkscape::GC::anchor(repr); } this->cloned = cloned; /* Invoke derived methods, if any */ this->build(document, repr); if ( !cloned ) { this->document->bindObjectToRepr(this->repr, this); if (Inkscape::XML::id_permitted(this->repr)) { /* If we are not cloned, and not seeking, force unique id */ gchar const *id = this->repr->attribute("id"); if (!document->isSeeking()) { { gchar *realid = sp_object_get_unique_id(this, id); g_assert(realid != nullptr); this->document->bindObjectToId(realid, this); SPObjectImpl::setId(this, realid); g_free(realid); } /* Redefine ID, if required */ if ((id == nullptr) || (std::strcmp(id, this->getId()) != 0)) { this->repr->setAttribute("id", this->getId()); } } else if (id) { // bind if id, but no conflict -- otherwise, we can expect // a subsequent setting of the id attribute if (!this->document->getObjectById(id)) { this->document->bindObjectToId(id, this); SPObjectImpl::setId(this, id); } } } } else { g_assert(this->getId() == nullptr); } /* Signalling (should be connected AFTER processing derived methods */ sp_repr_add_listener(repr, &object_event_vector, this); #ifdef OBJECT_TRACE objectTrace( "SPObject::invoke_build", false ); #endif } int SPObject::getIntAttribute(char const *key, int def) { return getRepr()->getAttributeInt(key, def); } unsigned SPObject::getPosition(){ g_assert(this->repr); return repr->position(); } void SPObject::appendChild(Inkscape::XML::Node *child) { g_assert(this->repr); repr->appendChild(child); } SPObject* SPObject::nthChild(unsigned index) { g_assert(this->repr); if (hasChildren()) { std::vector<SPObject*> l; unsigned counter = 0; for (auto& child: children) { if (counter == index) { return &child; } counter++; } } return nullptr; } void SPObject::addChild(Inkscape::XML::Node *child, Inkscape::XML::Node * prev) { g_assert(this->repr); repr->addChild(child,prev); } void SPObject::releaseReferences() { g_assert(this->document); g_assert(this->repr); sp_repr_remove_listener_by_data(this->repr, this); this->_release_signal.emit(this); this->release(); /* all hrefs should be released by the "release" handlers */ g_assert(this->hrefcount == 0); if (!cloned) { if (this->id) { this->document->bindObjectToId(this->id, nullptr); } g_free(this->id); this->id = nullptr; g_free(this->_default_label); this->_default_label = nullptr; this->document->bindObjectToRepr(this->repr, nullptr); Inkscape::GC::release(this->repr); } else { g_assert(!this->id); } // style belongs to SPObject, we should not need to unref here. // if (this->style) { // this->style = sp_style_unref(this->style); // } this->document = nullptr; this->repr = nullptr; } SPObject *SPObject::getPrev() { SPObject *prev = nullptr; if (parent && !parent->children.empty() && &parent->children.front() != this) { prev = &*(--parent->children.iterator_to(*this)); } return prev; } SPObject* SPObject::getNext() { SPObject *next = nullptr; if (parent && !parent->children.empty() && &parent->children.back() != this) { next = &*(++parent->children.iterator_to(*this)); } return next; } void SPObject::repr_child_added(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data) { auto object = static_cast<SPObject *>(data); object->child_added(child, ref); } void SPObject::repr_child_removed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/, gpointer data) { auto object = static_cast<SPObject *>(data); object->remove_child(child); } void SPObject::repr_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *old, Inkscape::XML::Node *newer, gpointer data) { auto object = static_cast<SPObject *>(data); object->order_changed(child, old, newer); } void SPObject::repr_name_changed(Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, void *data) { auto object = static_cast<SPObject *>(data); object->tag_name_changed(oldname, newname); } void SPObject::set(SPAttr key, gchar const* value) { #ifdef OBJECT_TRACE std::stringstream temp; temp << "SPObject::set: " << sp_attribute_name(key) << " " << (value?value:"null"); objectTrace( temp.str() ); #endif g_assert(key != SPAttr::INVALID); SPObject* object = this; switch (key) { case SPAttr::ID: //XML Tree being used here. if ( !object->cloned && object->getRepr()->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) { SPDocument *document=object->document; SPObject *conflict=nullptr; gchar const *new_id = value; if (new_id) { conflict = document->getObjectById((char const *)new_id); } if ( conflict && conflict != object ) { if (!document->isSeeking()) { sp_object_ref(conflict, nullptr); // give the conflicting object a new ID gchar *new_conflict_id = sp_object_get_unique_id(conflict, nullptr); conflict->setAttribute("id", new_conflict_id); g_free(new_conflict_id); sp_object_unref(conflict, nullptr); } else { new_id = nullptr; } } if (object->getId()) { document->bindObjectToId(object->getId(), nullptr); SPObjectImpl::setId(object, nullptr); } if (new_id) { SPObjectImpl::setId(object, new_id); document->bindObjectToId(object->getId(), object); } g_free(object->_default_label); object->_default_label = nullptr; } break; case SPAttr::INKSCAPE_LABEL: g_free(object->_label); if (value) { object->_label = g_strdup(value); } else { object->_label = nullptr; } g_free(object->_default_label); object->_default_label = nullptr; break; case SPAttr::INKSCAPE_COLLECT: if ( value && !std::strcmp(value, "always") ) { object->setCollectionPolicy(SPObject::ALWAYS_COLLECT); } else { object->setCollectionPolicy(SPObject::COLLECT_WITH_PARENT); } break; case SPAttr::XML_SPACE: if (value && !std::strcmp(value, "preserve")) { object->xml_space.value = SP_XML_SPACE_PRESERVE; object->xml_space.set = TRUE; } else if (value && !std::strcmp(value, "default")) { object->xml_space.value = SP_XML_SPACE_DEFAULT; object->xml_space.set = TRUE; } else if (object->parent) { SPObject *parent; parent = object->parent; object->xml_space.value = parent->xml_space.value; } object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); break; case SPAttr::LANG: if (value) { lang = value; // To do: sanity check } break; case SPAttr::XML_LANG: if (value) { lang = value; // To do: sanity check } break; case SPAttr::STYLE: object->style->readFromObject( object ); object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); break; default: break; } #ifdef OBJECT_TRACE objectTrace( "SPObject::set", false ); #endif } void SPObject::setKeyValue(SPAttr key, gchar const *value) { //g_assert(object != NULL); //g_assert(SP_IS_OBJECT(object)); this->set(key, value); } void SPObject::readAttr(SPAttr keyid) { char const *key = sp_attribute_name(keyid); assert(key != nullptr); assert(getRepr() != nullptr); char const *value = getRepr()->attribute(key); setKeyValue(keyid, value); } void SPObject::readAttr(gchar const *key) { //g_assert(object != NULL); //g_assert(SP_IS_OBJECT(object)); g_assert(key != nullptr); //XML Tree being used here. g_assert(this->getRepr() != nullptr); auto keyid = sp_attribute_lookup(key); if (keyid != SPAttr::INVALID) { /* Retrieve the 'key' attribute from the object's XML representation */ gchar const *value = getRepr()->attribute(key); setKeyValue(keyid, value); } } void SPObject::repr_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const *key, gchar const * /*oldval*/, gchar const * /*newval*/, bool is_interactive, gpointer data) { auto object = static_cast<SPObject *>(data); object->readAttr(key); // manual changes to extension attributes require the normal // attributes, which depend on them, to be updated immediately if (is_interactive) { object->updateRepr(0); } } void SPObject::repr_content_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*oldcontent*/, gchar const * /*newcontent*/, gpointer data) { auto object = static_cast<SPObject *>(data); object->read_content(); } /** * Return string representation of space value. */ static gchar const *sp_xml_get_space_string(unsigned int space) { switch (space) { case SP_XML_SPACE_DEFAULT: return "default"; case SP_XML_SPACE_PRESERVE: return "preserve"; default: return nullptr; } } Inkscape::XML::Node* SPObject::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { #ifdef OBJECT_TRACE objectTrace( "SPObject::write" ); #endif if (!repr && (flags & SP_OBJECT_WRITE_BUILD)) { repr = this->getRepr()->duplicate(doc); if (!( flags & SP_OBJECT_WRITE_EXT )) { repr->removeAttribute("inkscape:collect"); } } else if (repr) { repr->setAttribute("id", this->getId()); if (this->xml_space.set) { char const *xml_space; xml_space = sp_xml_get_space_string(this->xml_space.value); repr->setAttribute("xml:space", xml_space); } if ( flags & SP_OBJECT_WRITE_EXT && this->collectionPolicy() == SPObject::ALWAYS_COLLECT ) { repr->setAttribute("inkscape:collect", "always"); } else { repr->removeAttribute("inkscape:collect"); } if (style) { // Write if property set by style attribute in this object Glib::ustring style_prop = style->write(SPStyleSrc::STYLE_PROP); // Write style attributes (SPStyleSrc::ATTRIBUTE) back to xml object bool any_written = false; auto properties = style->properties(); for (auto * prop : properties) { if(prop->shall_write(SP_STYLE_FLAG_IFSET | SP_STYLE_FLAG_IFSRC, SPStyleSrc::ATTRIBUTE)) { // WARNING: We don't know for sure if the css names are the same as the attribute names repr->setAttributeOrRemoveIfEmpty(prop->name(), prop->get_value()); any_written = true; } } if(any_written) { // We need to ask the object to update the style and keep things in sync // see `case SPAttr::STYLE` above for how the style attr itself does this. style->readFromObject(this); requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); } // Check for valid attributes. This may be time consuming. // It is useful, though, for debugging Inkscape code. Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if( prefs->getBool("/options/svgoutput/check_on_editing") ) { unsigned int flags = sp_attribute_clean_get_prefs(); style_prop = sp_attribute_clean_style(repr, style_prop.c_str(), flags); } repr->setAttributeOrRemoveIfEmpty("style", style_prop); } else { /** \todo I'm not sure what to do in this case. Bug #1165868 * suggests that it can arise, but the submitter doesn't know * how to do so reliably. The main two options are either * leave repr's style attribute unchanged, or explicitly clear it. * Must also consider what to do with property attributes for * the element; see below. */ char const *style_str = repr->attribute("style"); if (!style_str) { style_str = "NULL"; } g_warning("Item's style is NULL; repr style attribute is %s", style_str); } } #ifdef OBJECT_TRACE objectTrace( "SPObject::write", false ); #endif return repr; } Inkscape::XML::Node * SPObject::updateRepr(unsigned int flags) { #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 1" ); #endif if ( !cloned ) { Inkscape::XML::Node *repr = getRepr(); if (repr) { #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 1", false ); #endif return updateRepr(repr->document(), repr, flags); } else { g_critical("Attempt to update non-existent repr"); #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 1", false ); #endif return nullptr; } } else { /* cloned objects have no repr */ #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 1", false ); #endif return nullptr; } } Inkscape::XML::Node * SPObject::updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags) { #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 2" ); #endif g_assert(doc != nullptr); if (cloned) { /* cloned objects have no repr */ #ifdef OBJECT_TRACE objectTrace( "SPObject::updateRepr 2", false ); #endif return nullptr; } if (!(flags & SP_OBJECT_WRITE_BUILD) && !repr) { repr = getRepr(); } #ifdef OBJECT_TRACE Inkscape::XML::Node *node = write(doc, repr, flags); objectTrace( "SPObject::updateRepr 2", false ); return node; #else return this->write(doc, repr, flags); #endif } /* Modification */ void SPObject::requestDisplayUpdate(unsigned int flags) { g_return_if_fail( this->document != nullptr ); #ifndef NDEBUG // expect no nested update calls if (document->update_in_progress) { // observed with LPE on <rect> g_print("WARNING: Requested update while update in progress, counter = %d\n", document->update_in_progress); } #endif /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or * SP_OBJECT_CHILD_MODIFIED_FLAG */ g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); #ifdef OBJECT_TRACE objectTrace( "SPObject::requestDisplayUpdate" ); #endif bool already_propagated = (!(this->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); //https://stackoverflow.com/a/7841333 if ((this->uflags & flags) != flags ) { this->uflags |= flags; } /* If requestModified has already been called on this object or one of its children, then we * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. */ if (already_propagated) { if(this->document) { if (parent) { parent->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); } else { this->document->requestModified(); } } } #ifdef OBJECT_TRACE objectTrace( "SPObject::requestDisplayUpdate", false ); #endif } void SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) { g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); #ifdef OBJECT_TRACE objectTrace( "SPObject::updateDisplay" ); #endif assert(++(document->update_in_progress)); #ifdef SP_OBJECT_DEBUG_CASCADE g_print("Update %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); #endif /* Get this flags */ flags |= this->uflags; /* Copy flags to modified cascade for later processing */ this->mflags |= this->uflags; /* We have to clear flags here to allow rescheduling update */ this->uflags = 0; // Merge style if we have good reasons to think that parent style is changed */ /** \todo * I am not sure whether we should check only propagated * flag. We are currently assuming that style parsing is * done immediately. I think this is correct (Lauris). */ if (style) { if ((flags & SP_OBJECT_STYLESHEET_MODIFIED_FLAG)) { style->readFromObject(this); } else if (parent && (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) && (flags & SP_OBJECT_PARENT_MODIFIED_FLAG)) { style->cascade( this->parent->style ); } } try { this->update(ctx, flags); } catch(...) { /** \todo * in case of catching an exception we need to inform the user somehow that the document is corrupted * maybe by implementing an document flag documentOk * or by a modal error dialog */ g_warning("SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) : throw in ((SPObjectClass *) G_OBJECT_GET_CLASS(this))->update(this, ctx, flags);"); } assert((document->update_in_progress)--); #ifdef OBJECT_TRACE objectTrace( "SPObject::updateDisplay", false ); #endif } void SPObject::requestModified(unsigned int flags) { g_return_if_fail( this->document != nullptr ); /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or * SP_OBJECT_CHILD_MODIFIED_FLAG */ g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); #ifdef OBJECT_TRACE objectTrace( "SPObject::requestModified" ); #endif bool already_propagated = (!(this->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); this->mflags |= flags; /* If requestModified has already been called on this object or one of its children, then we * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. */ if (already_propagated) { if (parent) { parent->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); } else { document->requestModified(); } } #ifdef OBJECT_TRACE objectTrace( "SPObject::requestModified", false ); #endif } void SPObject::emitModified(unsigned int flags) { /* only the MODIFIED_CASCADE flag is legal here */ g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); #ifdef OBJECT_TRACE objectTrace( "SPObject::emitModified", true, flags ); #endif #ifdef SP_OBJECT_DEBUG_CASCADE g_print("Modified %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); #endif flags |= this->mflags; /* We have to clear mflags beforehand, as signal handlers may * make changes and therefore queue new modification notifications * themselves. */ this->mflags = 0; sp_object_ref(this); this->modified(flags); _modified_signal.emit(this, flags); sp_object_unref(this); #ifdef OBJECT_TRACE objectTrace( "SPObject::emitModified", false ); #endif } gchar const *SPObject::getTagName() const { g_assert(repr != nullptr); /// \todo fixme: Exception if object is NULL? */ //XML Tree being used here. return getRepr()->name(); } gchar const *SPObject::getAttribute(gchar const *key) const { g_assert(this->repr != nullptr); /// \todo fixme: Exception if object is NULL? */ //XML Tree being used here. return (gchar const *) getRepr()->attribute(key); } void SPObject::setAttribute(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value) { g_assert(this->repr != nullptr); /// \todo fixme: Exception if object is NULL? */ //XML Tree being used here. getRepr()->setAttribute(key, value); } void SPObject::setAttributeDouble(Inkscape::Util::const_char_ptr key, double value) { Inkscape::CSSOStringStream os; os << value; setAttribute(key, os.str()); } void SPObject::removeAttribute(gchar const *key) { /// \todo fixme: Exception if object is NULL? */ //XML Tree being used here. getRepr()->removeAttribute(key); } bool SPObject::storeAsDouble( gchar const *key, double *val ) const { g_assert(this->getRepr()!= nullptr); double nan = std::numeric_limits<double>::quiet_NaN(); double temp_val = ((Inkscape::XML::Node *)(this->getRepr()))->getAttributeDouble(key, nan); if (std::isnan(temp_val)) { return false; } *val = temp_val; return true; } /** Helper */ gchar * sp_object_get_unique_id(SPObject *object, gchar const *id) { static unsigned long count = 0; g_assert(SP_IS_OBJECT(object)); count++; //XML Tree being used here. gchar const *name = object->getRepr()->name(); g_assert(name != nullptr); gchar const *local = std::strchr(name, ':'); if (local) { name = local + 1; } if (id != nullptr) { if (object->document->getObjectById(id) == nullptr) { return g_strdup(id); } } size_t const name_len = std::strlen(name); size_t const buflen = name_len + (sizeof(count) * 10 / 4) + 1; gchar *const buf = (gchar *) g_malloc(buflen); std::memcpy(buf, name, name_len); gchar *const count_buf = buf + name_len; size_t const count_buflen = buflen - name_len; do { ++count; g_snprintf(count_buf, count_buflen, "%lu", count); } while ( object->document->getObjectById(buf) != nullptr ); return buf; } void SPObject::_requireSVGVersion(Inkscape::Version version) { for ( SPObject::ParentIterator iter=this ; iter ; ++iter ) { SPObject *object = iter; if (SP_IS_ROOT(object)) { SPRoot *root = SP_ROOT(object); if ( root->version.svg < version ) { root->version.svg = version; } } } } // Titles and descriptions /* Note: Titles and descriptions are stored in 'title' and 'desc' child elements (see section 5.4 of the SVG 1.0 and 1.1 specifications). The spec allows an element to have more than one 'title' child element, but strongly recommends against this and requires using the first one if a choice must be made. The same applies to 'desc' elements. Therefore, these functions ignore all but the first 'title' child element and first 'desc' child element, except when deleting a title or description. This will change in SVG 2, where multiple 'title' and 'desc' elements will be allowed with different localized strings. */ gchar * SPObject::title() const { return getTitleOrDesc("svg:title"); } bool SPObject::setTitle(gchar const *title, bool verbatim) { return setTitleOrDesc(title, "svg:title", verbatim); } gchar * SPObject::desc() const { return getTitleOrDesc("svg:desc"); } bool SPObject::setDesc(gchar const *desc, bool verbatim) { return setTitleOrDesc(desc, "svg:desc", verbatim); } char * SPObject::getTitleOrDesc(gchar const *svg_tagname) const { char *result = nullptr; SPObject *elem = findFirstChild(svg_tagname); if ( elem ) { //This string copy could be avoided by changing //the return type of SPObject::getTitleOrDesc //to std::unique_ptr<Glib::ustring> result = g_strdup(elem->textualContent().c_str()); } return result; } bool SPObject::setTitleOrDesc(gchar const *value, gchar const *svg_tagname, bool verbatim) { if (!verbatim) { // If the new title/description is just whitespace, // treat it as though it were NULL. if (value) { bool just_whitespace = true; for (const gchar *cp = value; *cp; ++cp) { if (!std::strchr("\r\n \t", *cp)) { just_whitespace = false; break; } } if (just_whitespace) { value = nullptr; } } // Don't stomp on mark-up if there is no real change. if (value) { gchar *current_value = getTitleOrDesc(svg_tagname); if (current_value) { bool different = std::strcmp(current_value, value); g_free(current_value); if (!different) { return false; } } } } SPObject *elem = findFirstChild(svg_tagname); if (value == nullptr) { if (elem == nullptr) { return false; } // delete the title/description(s) while (elem) { elem->deleteObject(); elem = findFirstChild(svg_tagname); } return true; } Inkscape::XML::Document *xml_doc = document->getReprDoc(); if (elem == nullptr) { // create a new 'title' or 'desc' element, putting it at the // beginning (in accordance with the spec's recommendations) Inkscape::XML::Node *xml_elem = xml_doc->createElement(svg_tagname); repr->addChild(xml_elem, nullptr); elem = document->getObjectByRepr(xml_elem); Inkscape::GC::release(xml_elem); } else { // remove the current content of the 'text' or 'desc' element auto tmp = elem->children | boost::adaptors::transformed([](SPObject& obj) { return &obj; }); std::vector<SPObject*> vec(tmp.begin(), tmp.end()); for (auto &child: vec) { child->deleteObject(); } } // add the new content elem->appendChildRepr(xml_doc->createTextNode(value)); return true; } SPObject* SPObject::findFirstChild(gchar const *tagname) const { for (auto& child: const_cast<SPObject*>(this)->children) { if (child.repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && !std::strcmp(child.repr->name(), tagname)) { return &child; } } return nullptr; } Glib::ustring SPObject::textualContent() const { Glib::ustring text; for (auto& child: children) { Inkscape::XML::NodeType child_type = child.repr->type(); if (child_type == Inkscape::XML::NodeType::ELEMENT_NODE) { text += child.textualContent(); } else if (child_type == Inkscape::XML::NodeType::TEXT_NODE) { text += child.repr->content(); } } return text; } Glib::ustring SPObject::getExportFilename() const { if (auto filename = repr->attribute("inkscape:export-filename")) { return Glib::ustring(filename); } return ""; } void SPObject::setExportFilename(Glib::ustring filename) { // Is this svg has been saved before. const char *doc_filename = document->getDocumentFilename(); std::string base = Glib::path_get_dirname(doc_filename ? doc_filename : filename); filename = Inkscape::convertPathToRelative(filename, base); repr->setAttributeOrRemoveIfEmpty("inkscape:export-filename", filename.c_str()); } Geom::Point SPObject::getExportDpi() const { return Geom::Point( repr->getAttributeDouble("inkscape:export-xdpi", 0.0), repr->getAttributeDouble("inkscape:export-ydpi", 0.0)); } void SPObject::setExportDpi(Geom::Point dpi) { if (!dpi.x() || !dpi.y()) { repr->removeAttribute("inkscape:export-xdpi"); repr->removeAttribute("inkscape:export-ydpi"); } else { repr->setAttributeSvgDouble("inkscape:export-xdpi", dpi.x()); repr->setAttributeSvgDouble("inkscape:export-ydpi", dpi.y()); } } // For debugging: Print SP tree structure. void SPObject::recursivePrintTree( unsigned level ) { if (level == 0) { std::cout << "SP Object Tree" << std::endl; } std::cout << "SP: "; for (unsigned i = 0; i < level; ++i) { std::cout << " "; } std::cout << (getId()?getId():"No object id") << " clone: " << std::boolalpha << (bool)cloned << " hrefcount: " << hrefcount << std::endl; for (auto& child: children) { child.recursivePrintTree(level + 1); } } // Function to allow tracing of program flow through SPObject and derived classes. // To trace function, add at entrance ('in' = true) and exit of function ('in' = false). void SPObject::objectTrace( std::string const &text, bool in, unsigned flags ) { if( in ) { for (unsigned i = 0; i < indent_level; ++i) { std::cout << " "; } std::cout << text << ":" << " entrance: " << (id?id:"null") // << " uflags: " << uflags // << " mflags: " << mflags // << " flags: " << flags << std::endl; ++indent_level; } else { --indent_level; for (unsigned i = 0; i < indent_level; ++i) { std::cout << " "; } std::cout << text << ":" << " exit: " << (id?id:"null") // << " uflags: " << uflags // << " mflags: " << mflags // << " flags: " << flags << std::endl; } } std::ostream &operator<<(std::ostream &out, const SPObject &o) { out << (o.getId()?o.getId():"No ID") << " cloned: " << std::boolalpha << (bool)o.cloned << " ref: " << o.refCount << " href: " << o.hrefcount << " total href: " << o._total_hrefcount; return out; } /* 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 :