summaryrefslogtreecommitdiffstats
path: root/src/object/sp-text.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/object/sp-text.cpp')
-rw-r--r--src/object/sp-text.cpp1765
1 files changed, 1765 insertions, 0 deletions
diff --git a/src/object/sp-text.cpp b/src/object/sp-text.cpp
new file mode 100644
index 0000000..61f898d
--- /dev/null
+++ b/src/object/sp-text.cpp
@@ -0,0 +1,1765 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * SVG <text> and <tspan> implementation
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * fixme:
+ *
+ * These subcomponents should not be items, or alternately
+ * we have to invent set of flags to mark, whether standard
+ * attributes are applicable to given item (I even like this
+ * idea somewhat - Lauris)
+ *
+ */
+
+#include <2geom/affine.h>
+#include <libnrtype/FontFactory.h>
+#include <libnrtype/font-instance.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include "svg/svg.h"
+#include "display/drawing-text.h"
+#include "attributes.h"
+#include "document.h"
+#include "preferences.h"
+#include "desktop.h"
+#include "desktop-style.h"
+#include "sp-namedview.h"
+#include "inkscape.h"
+#include "xml/quote.h"
+#include "mod360.h"
+
+#include "sp-title.h"
+#include "sp-desc.h"
+#include "sp-rect.h"
+#include "sp-text.h"
+
+#include "sp-shape.h"
+#include "sp-textpath.h"
+#include "sp-tref.h"
+#include "sp-tspan.h"
+#include "sp-flowregion.h"
+
+#include "text-editing.h"
+
+// For SVG 2 text flow
+#include "livarot/Path.h"
+#include "livarot/Shape.h"
+#include "display/curve.h"
+
+#include "layer-manager.h"
+
+/*#####################################################
+# SPTEXT
+#####################################################*/
+SPText::SPText() : SPItem() {
+}
+
+SPText::~SPText()
+{
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ }
+};
+
+void SPText::build(SPDocument *doc, Inkscape::XML::Node *repr) {
+ this->readAttr(SPAttr::X);
+ this->readAttr(SPAttr::Y);
+ this->readAttr(SPAttr::DX);
+ this->readAttr(SPAttr::DY);
+ this->readAttr(SPAttr::ROTATE);
+
+ // textLength and friends
+ this->readAttr(SPAttr::TEXTLENGTH);
+ this->readAttr(SPAttr::LENGTHADJUST);
+ SPItem::build(doc, repr);
+ css = nullptr;
+ this->readAttr(SPAttr::SODIPODI_LINESPACING); // has to happen after the styles are read
+}
+
+void SPText::release() {
+ SPItem::release();
+}
+
+void SPText::set(SPAttr key, const gchar* value) {
+ //std::cout << "SPText::set: " << sp_attribute_name( key ) << ": " << (value?value:"Null") << std::endl;
+
+ if (this->attributes.readSingleAttribute(key, value, style, &viewport)) {
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ } else {
+ switch (key) {
+ case SPAttr::SODIPODI_LINESPACING:
+ // convert deprecated tag to css... but only if 'line-height' missing.
+ if (value && !this->style->line_height.set) {
+ this->style->line_height.set = TRUE;
+ this->style->line_height.inherit = FALSE;
+ this->style->line_height.normal = FALSE;
+ this->style->line_height.unit = SP_CSS_UNIT_PERCENT;
+ this->style->line_height.value = this->style->line_height.computed = sp_svg_read_percentage (value, 1.0);
+ }
+ // Remove deprecated attribute
+ this->removeAttribute("sodipodi:linespacing");
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
+ break;
+
+ default:
+ SPItem::set(key, value);
+ break;
+ }
+ }
+}
+
+void SPText::child_added(Inkscape::XML::Node *rch, Inkscape::XML::Node *ref) {
+ SPItem::child_added(rch, ref);
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
+}
+
+void SPText::remove_child(Inkscape::XML::Node *rch) {
+ SPItem::remove_child(rch);
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
+}
+
+
+void SPText::update(SPCtx *ctx, guint flags) {
+
+ unsigned childflags = (flags & SP_OBJECT_MODIFIED_CASCADE);
+ if (flags & SP_OBJECT_MODIFIED_FLAG) {
+ childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
+ }
+
+ // Create temporary list of children
+ std::vector<SPObject *> l;
+ for (auto& child: children) {
+ sp_object_ref(&child, this);
+ l.push_back(&child);
+ }
+
+ for (auto child:l) {
+ if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ /* fixme: Do we need transform? */
+ child->updateDisplay(ctx, childflags);
+ }
+ sp_object_unref(child, this);
+ }
+
+ // update ourselves after updating children
+ SPItem::update(ctx, flags);
+
+ if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG |
+ SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_TEXT_LAYOUT_MODIFIED_FLAG ) )
+ {
+
+ SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx);
+
+ double const w = ictx->viewport.width();
+ double const h = ictx->viewport.height();
+ double const em = style->font_size.computed;
+ double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype.
+
+ attributes.update( em, ex, w, h );
+
+ // Set inline_size computed value if necessary (i.e. if unit is %).
+ if (has_inline_size()) {
+ if (style->inline_size.unit == SP_CSS_UNIT_PERCENT) {
+ if (is_horizontal()) {
+ style->inline_size.computed = style->inline_size.value * ictx->viewport.width();
+ } else {
+ style->inline_size.computed = style->inline_size.value * ictx->viewport.height();
+ }
+ }
+ }
+
+ /* fixme: It is not nice to have it here, but otherwise children content changes does not work */
+ /* fixme: Even now it may not work, as we are delayed */
+ /* fixme: So check modification flag everywhere immediate state is used */
+ this->rebuildLayout();
+
+ Geom::OptRect paintbox = this->geometricBounds();
+
+ for (SPItemView* v = this->display; v != nullptr; v = v->next) {
+ Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
+ this->_clearFlow(g);
+ g->setStyle(this->style, this->parent->style);
+ // pass the bbox of this as paintbox (used for paintserver fills)
+ this->layout.show(g, paintbox);
+ }
+ }
+}
+
+void SPText::modified(guint flags) {
+// SPItem::onModified(flags);
+
+ guint cflags = (flags & SP_OBJECT_MODIFIED_CASCADE);
+
+ if (flags & SP_OBJECT_MODIFIED_FLAG) {
+ cflags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
+ }
+
+ // FIXME: all that we need to do here is to call setStyle, to set the changed
+ // style, but there's no easy way to access the drawing glyphs or texts corresponding to a
+ // text this. Therefore we do here the same as in _update, that is, destroy all items
+ // and create new ones. This is probably quite wasteful.
+ if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) {
+ Geom::OptRect paintbox = this->geometricBounds();
+
+ for (SPItemView* v = this->display; v != nullptr; v = v->next) {
+ Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
+ this->_clearFlow(g);
+ g->setStyle(this->style, this->parent->style);
+ this->layout.show(g, paintbox);
+ }
+ }
+
+ // Create temporary list of children
+ std::vector<SPObject *> l;
+ for (auto& child: children) {
+ sp_object_ref(&child, this);
+ l.push_back(&child);
+ }
+
+ for (auto child:l) {
+ if (cflags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ child->emitModified(cflags);
+ }
+ sp_object_unref(child, this);
+ }
+}
+
+Inkscape::XML::Node *SPText::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) {
+ if (flags & SP_OBJECT_WRITE_BUILD) {
+ if (!repr) {
+ repr = xml_doc->createElement("svg:text");
+ // we preserve spaces in the text objects we create
+ repr->setAttribute("xml:space", "preserve");
+ }
+
+ std::vector<Inkscape::XML::Node *> l;
+
+ for (auto& child: children) {
+ if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) {
+ continue;
+ }
+
+ Inkscape::XML::Node *crepr = nullptr;
+
+ if (SP_IS_STRING(&child)) {
+ crepr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str());
+ } else {
+ crepr = child.updateRepr(xml_doc, nullptr, flags);
+ }
+
+ if (crepr) {
+ l.push_back(crepr);
+ }
+ }
+
+ for (auto i=l.rbegin();i!=l.rend();++i) {
+ repr->addChild(*i, nullptr);
+ Inkscape::GC::release(*i);
+ }
+ } else {
+ for (auto& child: children) {
+ if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) {
+ continue;
+ }
+
+ if (SP_IS_STRING(&child)) {
+ child.getRepr()->setContent(SP_STRING(&child)->string.c_str());
+ } else {
+ child.updateRepr(flags);
+ }
+ }
+ }
+
+ this->attributes.writeTo(repr);
+
+ SPItem::write(xml_doc, repr, flags);
+
+ return repr;
+}
+
+
+Geom::OptRect SPText::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const {
+ return this->layout.bounds(transform, type == SPItem::VISUAL_BBOX);
+}
+
+Inkscape::DrawingItem* SPText::show(Inkscape::Drawing &drawing, unsigned /*key*/, unsigned /*flags*/) {
+ Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing);
+ flowed->setPickChildren(false);
+ flowed->setStyle(this->style, this->parent->style);
+
+ // pass the bbox of the text object as paintbox (used for paintserver fills)
+ this->layout.show(flowed, this->geometricBounds());
+
+ return flowed;
+}
+
+
+void SPText::hide(unsigned int key) {
+ for (SPItemView* v = this->display; v != nullptr; v = v->next) {
+ if (v->key == key) {
+ Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
+ this->_clearFlow(g);
+ }
+ }
+}
+
+const char* SPText::typeName() const {
+ if (has_inline_size() || has_shape_inside())
+ return "text-flow";
+ return "text";
+}
+
+const char* SPText::displayName() const {
+ if (has_inline_size()) {
+ return _("Auto-wrapped text");
+ } else if (has_shape_inside()) {
+ return _("Text in-a-shape");
+ } else {
+ return _("Text");
+ }
+}
+
+gchar* SPText::description() const {
+
+ SPStyle *style = this->style;
+
+ char *n = xml_quote_strdup(style->font_family.value());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ Inkscape::Util::Quantity q = Inkscape::Util::Quantity(style->font_size.computed, "px");
+ q.quantity *= this->i2doc_affine().descrim();
+ Glib::ustring xs = q.string(sp_style_get_css_unit_string(unit));
+
+ char const *trunc = "";
+ Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this);
+
+ if (layout && layout->inputTruncated()) {
+ trunc = _(" [truncated]");
+ }
+
+ char *ret = ( SP_IS_TEXT_TEXTPATH(this)
+ ? g_strdup_printf(_("on path%s (%s, %s)"), trunc, n, xs.c_str())
+ : g_strdup_printf(_("%s (%s, %s)"), trunc, n, xs.c_str()) );
+ return ret;
+}
+
+void SPText::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const {
+ if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) {
+ // Choose a point on the baseline for snapping from or to, with the horizontal position
+ // of this point depending on the text alignment (left vs. right)
+ Inkscape::Text::Layout const *layout = te_get_layout(this);
+
+ if (layout != nullptr && layout->outputExists()) {
+ std::optional<Geom::Point> pt = layout->baselineAnchorPoint();
+
+ if (pt) {
+ p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR);
+ }
+ }
+ }
+}
+
+void SPText::hide_shape_inside()
+{
+ SPText *text = dynamic_cast<SPText *>(this);
+ SPStyle *item_style = this->style;
+ if (item_style && text && item_style->shape_inside.set) {
+ SPCSSAttr *css_unset = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
+ css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
+ sp_repr_css_unset_property(css_unset, "shape-inside");
+ sp_repr_css_attr_unref(css_unset);
+ this->changeCSS(css_unset, "style");
+ } else {
+ css = nullptr;
+ }
+}
+
+void SPText::show_shape_inside()
+{
+ SPText *text = dynamic_cast<SPText *>(this);
+ if (text && css) {
+ this->changeCSS(css, "style");
+ }
+}
+
+Geom::Affine SPText::set_transform(Geom::Affine const &xform) {
+ // See if 'shape-inside' has rectangle
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/text/use_svg2", true)) {
+ if (style->shape_inside.set) {
+ return xform;
+ }
+ }
+ // we cannot optimize textpath because changing its fontsize will break its match to the path
+
+ if (SP_IS_TEXT_TEXTPATH (this)) {
+ if (!this->_optimizeTextpathText) {
+ return xform;
+ } else {
+ this->_optimizeTextpathText = false;
+ }
+ }
+
+ // we cannot optimize text with textLength because it may show different size than specified
+ if (this->attributes.getTextLength()->_set)
+ return xform;
+
+ if (this->style && this->style->inline_size.set)
+ return xform;
+
+ /* This function takes care of scaling & translation only, we return whatever parts we can't
+ handle. */
+
+// TODO: pjrm tried to use fontsize_expansion(xform) here and it works for text in that font size
+// is scaled more intuitively when scaling non-uniformly; however this necessitated using
+// fontsize_expansion instead of expansion in other places too, where it was not appropriate
+// (e.g. it broke stroke width on copy/pasting of style from horizontally stretched to vertically
+// stretched shape). Using fontsize_expansion only here broke setting the style via font
+// dialog. This needs to be investigated further.
+ double const ex = xform.descrim();
+ if (ex == 0) {
+ return xform;
+ }
+
+ Geom::Affine ret(Geom::Affine(xform).withoutTranslation());
+ ret[0] /= ex;
+ ret[1] /= ex;
+ ret[2] /= ex;
+ ret[3] /= ex;
+
+ // Adjust x/y, dx/dy
+ this->_adjustCoordsRecursive (this, xform * ret.inverse(), ex);
+
+ // Adjust font size
+ this->_adjustFontsizeRecursive (this, ex);
+
+ // Adjust stroke width
+ this->adjust_stroke_width_recursive (ex);
+
+ // Adjust pattern fill
+ this->adjust_pattern(xform * ret.inverse());
+
+ // Adjust gradient fill
+ this->adjust_gradient(xform * ret.inverse());
+
+ return ret;
+}
+
+void SPText::print(SPPrintContext *ctx) {
+ Geom::OptRect pbox, bbox, dbox;
+ pbox = this->geometricBounds();
+ bbox = this->desktopVisualBounds();
+ dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions());
+
+ Geom::Affine const ctm (this->i2dt_affine());
+
+ this->layout.print(ctx,pbox,dbox,bbox,ctm);
+}
+
+/*
+ * Member functions
+ */
+
+void SPText::_buildLayoutInit()
+{
+
+ layout.strut.reset();
+ layout.wrap_mode = Inkscape::Text::Layout::WRAP_NONE; // Default to SVG 1.1
+
+ if (style) {
+
+ // Strut
+ font_instance *font = font_factory::Default()->FaceFromStyle( style );
+ if (font) {
+ font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight);
+ font->Unref();
+ }
+ layout.strut *= style->font_size.computed;
+ if (style->line_height.normal ) {
+ layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL );
+ } else if (style->line_height.unit == SP_CSS_UNIT_NONE) {
+ layout.strut.computeEffective( style->line_height.computed );
+ } else {
+ if( style->font_size.computed > 0.0 ) {
+ layout.strut.computeEffective( style->line_height.computed/style->font_size.computed );
+ }
+ }
+
+
+ // To do: follow SPItem clip_ref/mask_ref code
+ if (style->shape_inside.set ) {
+
+ layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE;
+
+ // Find union of all exclusion shapes
+ Shape *exclusion_shape = nullptr;
+ if(style->shape_subtract.set) {
+ exclusion_shape = getExclusionShape();
+ }
+
+ // Find inside shape curves
+ for (auto *href : style->shape_inside.hrefs) {
+ auto obj = href->getObject();
+ if (Shape *uncross = getInclusionShape(obj)) {
+ // Subtrack padding shape
+ auto padding = style->shape_padding.computed;
+ if (std::fabs(padding) > 1e-12) {
+ auto pad_shape = getInclusionShape(obj, true);
+ Shape *copy = new Shape;
+ copy->Booleen(uncross, pad_shape, padding > 0.0 ? bool_op_diff : bool_op_union);
+ delete uncross;
+ uncross = copy;
+ }
+ // Subtract exclusion shape
+ if (exclusion_shape && exclusion_shape->hasEdges()) {
+ Shape *copy = new Shape;
+ copy->Booleen(uncross, const_cast<Shape*>(exclusion_shape), bool_op_diff);
+ delete uncross;
+ uncross = copy;
+ }
+ layout.appendWrapShape(uncross);
+ } else {
+ std::cerr << "SPText::_buildLayoutInit(): Failed to get curve." << std::endl;
+ }
+ }
+ delete exclusion_shape;
+
+ } else if (has_inline_size()) {
+
+ layout.wrap_mode = Inkscape::Text::Layout::WRAP_INLINE_SIZE;
+
+ // If both shape_inside and inline_size are set, shape_inside wins out.
+
+ // We construct a rectangle with one dimension set by the computed value of 'inline-size'
+ // and the other dimension set to infinity. Text is laid out starting at the 'x' and 'y'
+ // attribute values. This is handled elsewhere.
+
+ Geom::OptRect opt_frame = get_frame();
+ Geom::Rect frame = *opt_frame;
+
+ Shape *shape = new Shape;
+ shape->Reset();
+ int v0 = shape->AddPoint(frame.corner(0));
+ int v1 = shape->AddPoint(frame.corner(1));
+ int v2 = shape->AddPoint(frame.corner(2));
+ int v3 = shape->AddPoint(frame.corner(3));
+ shape->AddEdge(v0, v1);
+ shape->AddEdge(v1, v2);
+ shape->AddEdge(v2, v3);
+ shape->AddEdge(v3, v0);
+ Shape *uncross = new Shape;
+ uncross->ConvertToShape( shape );
+
+ layout.appendWrapShape( uncross );
+
+ delete shape;
+
+ } else if (style->white_space.value == SP_CSS_WHITE_SPACE_PRE ||
+ style->white_space.value == SP_CSS_WHITE_SPACE_PREWRAP ||
+ style->white_space.value == SP_CSS_WHITE_SPACE_PRELINE ) {
+ layout.wrap_mode = Inkscape::Text::Layout::WRAP_WHITE_SPACE;
+ }
+
+ } // if (style)
+}
+
+unsigned SPText::_buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath)
+{
+ unsigned length = 0;
+ unsigned child_attrs_offset = 0;
+ Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs;
+
+ // Per SVG spec, an object with 'display:none' doesn't contribute to text layout.
+ if (object->style->display.computed == SP_CSS_DISPLAY_NONE) {
+ return 0;
+ }
+
+ SPText* text_object = dynamic_cast<SPText*>(object);
+ SPTSpan* tspan_object = dynamic_cast<SPTSpan*>(object);
+ SPTRef* tref_object = dynamic_cast<SPTRef*>(object);
+ SPTextPath* textpath_object = dynamic_cast<SPTextPath*>(object);
+
+ if (text_object) {
+
+ bool use_xy = true;
+ bool use_dxdyrotate = true;
+
+ // SVG 2 Text wrapping.
+ if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE ||
+ layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
+ use_xy = false;
+ use_dxdyrotate = false;
+ }
+
+ text_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate);
+
+ // SVG 2 Text wrapping
+ if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
+
+ // For horizontal text:
+ // 'x' is used to calculate the left/right edges of the rectangle but is not
+ // needed later. If not deleted here, it will cause an incorrect positioning
+ // of the first line.
+ // 'y' is used to determine where the first line box is located and is needed
+ // during the output stage.
+ // For vertical text:
+ // Follow above but exchange 'x' and 'y'.
+ // The SVG 2 spec currently says use the 'x' and 'y' from the <text> element,
+ // if not defined in the <text> element, use the 'x' and 'y' from the first child.
+ // We only look at the <text> element. (Doing otherwise means tracking if
+ // we've found 'x' and 'y' and then creating the Shape at the end.)
+ if (is_horizontal()) {
+ // Horizontal text
+ SVGLength* y = _getFirstYLength();
+ if (y) {
+ optional_attrs.y.push_back(*y);
+ } else {
+ std::cerr << "SPText::_buildLayoutInput: No 'y' attribute value with horizontal 'inline-size'!" << std::endl;
+ }
+ } else {
+ // Vertical text
+ SVGLength* x = _getFirstXLength();
+ if (x) {
+ optional_attrs.x.push_back(*x);
+ } else {
+ std::cerr << "SPText::_buildLayoutInput: No 'x' attribute value with vertical 'inline-size'!" << std::endl;
+ }
+ }
+ }
+
+ // set textLength on the entire layout, see note in TNG-Layout.h
+ if (text_object->attributes.getTextLength()->_set) {
+ layout.textLength._set = true;
+ layout.textLength.value = text_object->attributes.getTextLength()->value;
+ layout.textLength.computed = text_object->attributes.getTextLength()->computed;
+ layout.textLength.unit = text_object->attributes.getTextLength()->unit;
+ layout.lengthAdjust = (Inkscape::Text::Layout::LengthAdjust) text_object->attributes.getLengthAdjust();
+ }
+ }
+
+ else if (tspan_object) {
+
+ // x, y attributes are stripped from some tspans marked with role="line" as we do our own line layout.
+ // This should be checked carefully, as it can undo line layout in imported SVG files.
+ bool use_xy = !in_textpath &&
+ (tspan_object->role == SP_TSPAN_ROLE_UNSPECIFIED || !tspan_object->attributes.singleXYCoordinates());
+ bool use_dxdyrotate = true;
+
+ // SVG 2 Text wrapping: see comment above.
+ if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE ||
+ layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
+ use_xy = false;
+ use_dxdyrotate = false;
+ }
+
+ tspan_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate);
+
+ if (tspan_object->role != SP_TSPAN_ROLE_UNSPECIFIED) {
+ // We are doing line wrapping using sodipodi:role="line". New lines have been stripped.
+
+ // Insert paragraph break before text if not first tspan.
+ SPObject *prev_object = object->getPrev();
+ if (prev_object && dynamic_cast<SPTSpan*>(prev_object)) {
+ if (!layout.inputExists()) {
+ // Add an object to store style, needed even if there is no text. When does this happen?
+ layout.appendText("", prev_object->style, prev_object, &optional_attrs);
+ }
+ layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, prev_object);
+ }
+
+ // Create empty span to store info (any non-empty tspan with sodipodi:role="line" has a child).
+ if (!object->hasChildren()) {
+ layout.appendText("", object->style, object, &optional_attrs);
+ }
+
+ length++; // interpreting line breaks as a character for the purposes of x/y/etc attributes
+ // is a liberal interpretation of the svg spec, but a strict reading would mean
+ // that if the first line is empty the second line would take its place at the
+ // start position. Very confusing.
+ // SVG 2 clarifies, attributes are matched to unicode input characters so line
+ // breaks do match to an x/y/etc attribute.
+ child_attrs_offset--;
+ }
+ }
+
+ else if (tref_object) {
+ tref_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, true, true);
+ }
+
+ else if (textpath_object) {
+ in_textpath = true; // This should be made local so we can mix normal text with textpath per SVG 2.
+ textpath_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, false, true);
+ optional_attrs.x.clear(); // Hmm, you can use x with horizontal text. So this is probably wrong.
+ optional_attrs.y.clear();
+ }
+
+ else {
+ optional_attrs = parent_optional_attrs;
+ child_attrs_offset = parent_attrs_offset;
+ }
+
+ // Recurse
+ for (auto& child: object->children) {
+ SPString *str = dynamic_cast<SPString *>(&child);
+ if (str) {
+ Glib::ustring const &string = str->string;
+ // std::cout << " Appending: >" << string << "<" << std::endl;
+ layout.appendText(string, object->style, &child, &optional_attrs, child_attrs_offset + length);
+ length += string.length();
+ } else if (!sp_repr_is_meta_element(child.getRepr())) {
+ /* ^^^^ XML Tree being directly used here while it shouldn't be.*/
+ length += _buildLayoutInput(&child, optional_attrs, child_attrs_offset + length, in_textpath);
+ }
+ }
+
+ return length;
+}
+
+Shape* SPText::getExclusionShape() const
+{
+ std::unique_ptr<Shape> result(new Shape()); // Union of all exclusion shapes
+ std::unique_ptr<Shape> shape_temp(new Shape());
+
+ for (auto *href : style->shape_subtract.hrefs) {
+ auto shape = href->getObject();
+
+ if ( shape ) {
+ // This code adapted from sp-flowregion.cpp: GetDest()
+ if (!shape->curve()) {
+ shape->set_shape();
+ }
+ SPCurve const *curve = shape->curve();
+
+ if ( curve ) {
+ Path *temp = new Path;
+ Path *margin = new Path;
+ temp->LoadPathVector( curve->get_pathvector(), shape->transform, true );
+
+ if( shape->style->shape_margin.set ) {
+ temp->OutsideOutline ( margin, -shape->style->shape_margin.computed, join_round, butt_straight, 20.0 );
+ } else {
+ margin->Copy( temp );
+ }
+
+ margin->Convert( 0.25 ); // Convert to polyline
+ Shape* sh = new Shape;
+ margin->Fill( sh, 0 );
+
+ Shape *uncross = new Shape;
+ uncross->ConvertToShape( sh );
+
+ if (result->hasEdges()) {
+ shape_temp->Booleen(result.get(), uncross, bool_op_union);
+ std::swap(result, shape_temp);
+ } else {
+ result->Copy(uncross);
+ }
+ }
+ }
+ }
+ return result.release();
+}
+
+Shape* SPText::getInclusionShape(SPShape *shape, bool padding) const
+{
+ if (!shape || (padding && !style->shape_padding.set))
+ return nullptr;
+
+ if (!shape->curve()) {
+ shape->set_shape();
+ }
+ auto curve = shape->curve();
+ if (!curve)
+ return nullptr;
+
+ Path *temp = new Path;
+ temp->LoadPathVector(curve->get_pathvector(), shape->transform, true);
+ if (padding) {
+ Path *padded = new Path;
+ temp->Outline(padded, style->shape_padding.computed, join_round, butt_straight, 20.0);
+ delete temp;
+ temp = padded;
+ }
+ temp->ConvertWithBackData(1.0); // Convert to polyline
+ Shape* sh = new Shape;
+ temp->Fill( sh, 0 );
+ Shape *uncross = new Shape;
+ uncross->ConvertToShape( sh );
+
+ delete temp;
+ delete sh;
+ return uncross;
+}
+
+// SVG requires one to use the first x/y value found on a child element if x/y not given on text
+// element. TODO: Recurse.
+SVGLength*
+SPText::_getFirstXLength()
+{
+ SVGLength* x = attributes.getFirstXLength();
+
+ if (!x) {
+ for (auto& child: children) {
+ if (SP_IS_TSPAN(&child)) {
+ SPTSpan *tspan = SP_TSPAN(&child);
+ x = tspan->attributes.getFirstXLength();
+ break;
+ }
+ }
+ }
+
+ return x;
+}
+
+
+SVGLength*
+SPText::_getFirstYLength()
+{
+ SVGLength* y = attributes.getFirstYLength();
+
+ if (!y) {
+ for (auto& child: children) {
+ if (SP_IS_TSPAN(&child)) {
+ SPTSpan *tspan = SP_TSPAN(&child);
+ y = tspan->attributes.getFirstYLength();
+ break;
+ }
+ }
+ }
+
+ return y;
+}
+
+std::unique_ptr<SPCurve> SPText::getNormalizedBpath() const
+{
+ return layout.convertToCurves();
+}
+
+void SPText::rebuildLayout()
+{
+ layout.clear();
+ _buildLayoutInit();
+
+ Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs;
+ _buildLayoutInput(this, optional_attrs, 0, false);
+
+ layout.calculateFlow();
+
+ for (auto& child: children) {
+ if (SP_IS_TEXTPATH(&child)) {
+ SPTextPath const *textpath = SP_TEXTPATH(&child);
+ if (textpath->originalPath != nullptr) {
+#if DEBUG_TEXTLAYOUT_DUMPASTEXT
+ g_print("%s", layout.dumpAsText().c_str());
+#endif
+ layout.fitToPathAlign(textpath->startOffset, *textpath->originalPath);
+ }
+ }
+ }
+#if DEBUG_TEXTLAYOUT_DUMPASTEXT
+ g_print("%s", layout.dumpAsText().c_str());
+#endif
+
+ // set the x,y attributes on role:line spans
+ for (auto& child: children) {
+ if (SP_IS_TSPAN(&child)) {
+ SPTSpan *tspan = SP_TSPAN(&child);
+ if ((tspan->role != SP_TSPAN_ROLE_UNSPECIFIED)
+ && tspan->attributes.singleXYCoordinates() ) {
+ Inkscape::Text::Layout::iterator iter = layout.sourceToIterator(tspan);
+ Geom::Point anchor_point = layout.chunkAnchorPoint(iter);
+ tspan->attributes.setFirstXY(anchor_point);
+ // repr needs to be updated but if we do it here we get a loop.
+ }
+ }
+ }
+}
+
+
+void SPText::_adjustFontsizeRecursive(SPItem *item, double ex, bool is_root)
+{
+ SPStyle *style = item->style;
+
+ if (style && !Geom::are_near(ex, 1.0)) {
+ if (!style->font_size.set && is_root) {
+ style->font_size.set = true;
+ }
+ style->font_size.type = SP_FONT_SIZE_LENGTH;
+ style->font_size.computed *= ex;
+ style->letter_spacing.computed *= ex;
+ style->word_spacing.computed *= ex;
+ if (style->line_height.unit != SP_CSS_UNIT_NONE &&
+ style->line_height.unit != SP_CSS_UNIT_PERCENT &&
+ style->line_height.unit != SP_CSS_UNIT_EM &&
+ style->line_height.unit != SP_CSS_UNIT_EX) {
+ // No unit on 'line-height' property has special behavior.
+ style->line_height.computed *= ex;
+ }
+ item->updateRepr();
+ }
+
+ for(auto& o: item->children) {
+ if (SP_IS_ITEM(&o))
+ _adjustFontsizeRecursive(SP_ITEM(&o), ex, false);
+ }
+}
+
+/**
+ * Get the position of the baseline point for this text object.
+ */
+std::optional<Geom::Point> SPText::getBaselinePoint() const
+{
+ if (layout.outputExists()) {
+ return layout.baselineAnchorPoint();
+ }
+ return std::optional<Geom::Point>();
+}
+
+void
+remove_newlines_recursive(SPObject* object, bool is_svg2)
+{
+ // Replace '\n' by space.
+ SPString* string = dynamic_cast<SPString *>(object);
+ if (string) {
+ static Glib::RefPtr<Glib::Regex> r = Glib::Regex::create("\n+");
+ string->string = r->replace(string->string, 0, " ", (Glib::RegexMatchFlags)0);
+ string->getRepr()->setContent(string->string.c_str());
+ }
+
+ for (auto child : object->childList(false)) {
+ remove_newlines_recursive(child, is_svg2);
+ }
+
+ // Add space at end of a line if line is created by sodipodi:role="line".
+ SPTSpan* tspan = dynamic_cast<SPTSpan *>(object);
+ if (tspan &&
+ tspan->role == SP_TSPAN_ROLE_LINE &&
+ tspan->getNext() != nullptr && // Don't add space at end of last line.
+ !is_svg2) { // SVG2 uses newlines, should not have sodipodi:role.
+
+ std::vector<SPObject *> children = tspan->childList(false);
+
+ // Find last string (could be more than one if there is tspan in the middle of a tspan).
+ for (auto it = children.rbegin(); it != children.rend(); ++it) {
+ SPString* string = dynamic_cast<SPString *>(*it);
+ if (string) {
+ string->string += ' ';
+ string->getRepr()->setContent(string->string.c_str());
+ break;
+ }
+ }
+ }
+}
+
+// Prepare multi-line text for putting on path.
+void
+SPText::remove_newlines()
+{
+ remove_newlines_recursive(this, has_shape_inside() || has_inline_size());
+ style->inline_size.clear();
+ style->shape_inside.clear();
+ updateRepr();
+}
+
+void SPText::_adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root)
+{
+ if (SP_IS_TSPAN(item))
+ SP_TSPAN(item)->attributes.transform(m, ex, ex, is_root);
+ // it doesn't matter if we change the x,y for role=line spans because we'll just overwrite them anyway
+ else if (SP_IS_TEXT(item))
+ SP_TEXT(item)->attributes.transform(m, ex, ex, is_root);
+ else if (SP_IS_TEXTPATH(item))
+ SP_TEXTPATH(item)->attributes.transform(m, ex, ex, is_root);
+ else if (SP_IS_TREF(item)) {
+ SP_TREF(item)->attributes.transform(m, ex, ex, is_root);
+ } else {
+ g_warning("element is not text");
+ return;
+ }
+
+ for(auto& o: item->children) {
+ if (SP_IS_ITEM(&o))
+ _adjustCoordsRecursive(SP_ITEM(&o), m, ex, false);
+ }
+}
+
+
+void SPText::_clearFlow(Inkscape::DrawingGroup *in_arena)
+{
+ in_arena->clearChildren();
+}
+
+
+/** Remove 'x' and 'y' values on children (lines) or they will be interpreted as absolute positions
+ * when 'inline-size' is removed.
+ */
+void SPText::remove_svg11_fallback() {
+ for (auto& child: children) {
+ child.removeAttribute("x");
+ child.removeAttribute("y");
+ }
+}
+
+/** Convert new lines in 'inline-size' text to tspans with sodipodi:role="tspan".
+ * Note sodipodi:role="tspan" will be removed in the future!
+ */
+void SPText::newline_to_sodipodi() {
+
+ // New lines can come anywhere, we must search character-by-character.
+ auto it = layout.begin();
+ while (it != layout.end()) {
+ if (layout.characterAt(it) == '\n') {
+
+ // Delete newline ('\n').
+ iterator_pair pair;
+ auto it_end = it;
+ it_end.nextCharacter();
+ sp_te_delete (this, it, it_end, pair);
+ it = pair.first;
+
+ // Insert newline (sodipodi:role="line").
+ it = sp_te_insert_line(this, it);
+ }
+
+ it.nextCharacter();
+ layout.validateIterator(&it);
+ }
+}
+
+/** Convert tspans with sodipodi:role="tspans" to '\n'.
+ * Note sodipodi:role="tspan" will be removed in the future!
+ */
+void SPText::sodipodi_to_newline() {
+
+ // tspans with sodipodi:role="line" are only direct children of a <text> element.
+ for (auto child : childList(false)) {
+ auto tspan = dynamic_cast<SPTSpan *>(child); // Could have <desc> or <title>.
+ if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) {
+
+ // Remove sodipodi:role attribute.
+ tspan->removeAttribute("sodipodi:role");
+ tspan->updateRepr();
+
+ // Insert '/n' if not last line.
+ // This may screw up dx, dy, rotate attribute counting but... SVG 2 text cannot have these values.
+ if (tspan != lastChild()) {
+ tspan->style->white_space.computed = SP_CSS_WHITE_SPACE_PRE; // Set so '\n' is not immediately stripped out before CSS recascaded!
+ auto last_child = tspan->lastChild();
+ auto last_string = dynamic_cast<SPString *>(last_child);
+ if (last_string) {
+ // Add '\n' to string.
+ last_string->string += "\n";
+ last_string->updateRepr();
+ } else {
+ // Insert new string with '\n'.
+ auto tspan_node = tspan->getRepr();
+ auto xml_doc = tspan_node->document();
+ tspan_node->appendChild(xml_doc->createTextNode("\n"));
+ }
+ }
+ }
+ }
+}
+
+bool SPText::is_horizontal() const
+{
+ unsigned mode = style->writing_mode.computed;
+ return (mode == SP_CSS_WRITING_MODE_LR_TB || mode == SP_CSS_WRITING_MODE_RL_TB);
+}
+
+bool SPText::has_inline_size() const
+{
+ // If inline size is '0' it is as if it is not set.
+ return (style->inline_size.set && style->inline_size.value != 0);
+}
+
+bool SPText::has_shape_inside() const
+{
+ return (style->shape_inside.set);
+}
+
+// Gets rectangle defined by <text> x, y and inline-size ("infinite" in one direction).
+Geom::OptRect SPText::get_frame()
+{
+ Geom::OptRect opt_frame;
+ Geom::Rect frame;
+
+ if (has_inline_size()) {
+ double inline_size = style->inline_size.computed;
+ //unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ if (is_horizontal()) {
+ // horizontal
+ frame = Geom::Rect::from_xywh(attributes.firstXY()[Geom::X], -100000, inline_size, 200000);
+ if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ frame *= Geom::Translate (-inline_size/2.0, 0 );
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ frame *= Geom::Translate (-inline_size, 0);
+ }
+ } else {
+ // vertical
+ frame = Geom::Rect::from_xywh(-100000, attributes.firstXY()[Geom::Y], 200000, inline_size);
+ if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ frame *= Geom::Translate (0, -inline_size/2.0);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ frame *= Geom::Translate (0, -inline_size);
+ }
+ }
+
+ opt_frame = frame;
+
+ } else {
+ // See if 'shape-inside' has rectangle
+ Inkscape::XML::Node* rectangle = get_first_rectangle();
+
+ if (rectangle) {
+ double x = rectangle->getAttributeDouble("x", 0.0);
+ double y = rectangle->getAttributeDouble("y", 0.0);
+ double width = rectangle->getAttributeDouble("width", 0.0);
+ double height = rectangle->getAttributeDouble("height", 0.0);
+ frame = Geom::Rect::from_xywh(x, y, width, height);
+ opt_frame = frame;
+ }
+ }
+
+ return opt_frame;
+}
+
+// Find the node of the first rectangle (if it exists) in 'shape-inside'.
+Inkscape::XML::Node* SPText::get_first_rectangle()
+{
+ if (style->shape_inside.set) {
+
+ for (auto *href : style->shape_inside.hrefs) {
+ auto *shape = href->getObject();
+ if (dynamic_cast<SPRect*>(shape)) {
+ auto *item = shape->getRepr();
+ g_return_val_if_fail(item, nullptr);
+ assert(strncmp("svg:rect", item->name(), 8) == 0);
+ return item;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * Get the first shape reference which affects the position and layout of
+ * this text item. This can be either a shape-inside or a textPath referenced
+ * shape. If this text does not depend on any other shape, then return NULL.
+ */
+SPItem *SPText::get_first_shape_dependency()
+{
+ if (style->shape_inside.set) {
+ for (auto *href : style->shape_inside.hrefs) {
+ return href->getObject();
+ }
+ } else if (auto textpath = dynamic_cast<SPTextPath *>(firstChild())) {
+ return sp_textpath_get_path_item(textpath);
+ }
+
+ return nullptr;
+}
+
+SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1)
+{
+ SPDocument *doc = desktop->getDocument();
+
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text");
+ text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
+
+ auto layer = desktop->layerManager().currentLayer();
+ g_assert(layer != nullptr);
+
+ SPText *text_object = dynamic_cast<SPText *>(layer->appendChildRepr(text_repr));
+ g_assert(text_object != nullptr);
+
+ // Invert coordinate system?
+ p0 *= desktop->dt2doc();
+ p1 *= desktop->dt2doc();
+
+ // Pixels to user units
+ p0 *= layer->i2doc_affine().inverse();
+ p1 *= layer->i2doc_affine().inverse();
+
+ text_repr->setAttributeSvgDouble("x", p0[Geom::X]);
+ text_repr->setAttributeSvgDouble("y", p0[Geom::Y]);
+
+ double inline_size = p1[Geom::X] - p0[Geom::X];
+
+ text_object->style->inline_size.setDouble( inline_size );
+ text_object->style->inline_size.set = true;
+
+ Inkscape::XML::Node *text_node = xml_doc->createTextNode("");
+ text_repr->appendChild(text_node);
+
+ //text_object->transform = layer->i2doc_affine().inverse();
+ text_object->updateRepr();
+
+ Inkscape::GC::release(text_repr);
+ Inkscape::GC::release(text_node);
+
+ return text_object;
+}
+
+SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1)
+{
+ SPDocument *doc = desktop->getDocument();
+ auto const parent = desktop->layerManager().currentLayer();
+ assert(parent);
+
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+ Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text");
+ text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
+ text_repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(parent->i2doc_affine().inverse()));
+
+ SPText *text_object = dynamic_cast<SPText *>(parent->appendChildRepr(text_repr));
+ g_assert(text_object != nullptr);
+
+ // Invert coordinate system?
+ p0 *= desktop->dt2doc();
+ p1 *= desktop->dt2doc();
+
+ // Create rectangle
+ Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
+ rect_repr->setAttributeSvgDouble("x", p0[Geom::X]);
+ rect_repr->setAttributeSvgDouble("y", p0[Geom::Y]);
+ rect_repr->setAttributeSvgDouble("width", abs(p1[Geom::X]-p0[Geom::X]));
+ rect_repr->setAttributeSvgDouble("height", abs(p1[Geom::Y]-p0[Geom::Y]));
+
+ // Find defs, if does not exist, create.
+ Inkscape::XML::Node *defs_repr = sp_repr_lookup_name (xml_doc->root(), "svg:defs");
+ if (defs_repr == nullptr) {
+ defs_repr = xml_doc->createElement("svg:defs");
+ xml_doc->root()->addChild(defs_repr, nullptr);
+ }
+ else Inkscape::GC::anchor(defs_repr);
+
+ // Add rectangle to defs.
+ defs_repr->addChild(rect_repr, nullptr);
+
+ // Apply desktop style (do before adding "shape-inside").
+ sp_desktop_apply_style_tool(desktop, text_repr, "/tools/text", true);
+ SPCSSAttr *css = sp_repr_css_attr(text_repr, "style" );
+ sp_repr_css_set_property (css, "white-space", "pre"); // Respect new lines.
+
+ // Link rectangle to text
+ std::string value("url(#");
+ value += rect_repr->attribute("id");
+ value += ")";
+ sp_repr_css_set_property (css, "shape-inside", value.c_str());
+ sp_repr_css_set(text_repr, css, "style");
+
+ sp_repr_css_attr_unref(css);
+
+ /* Create <tspan> */
+ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
+ rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
+ Inkscape::XML::Node *text_node = xml_doc->createTextNode("");
+ rtspan->appendChild(text_node);
+ text_repr->appendChild(rtspan);
+
+ Inkscape::GC::release(rtspan);
+ Inkscape::GC::release(text_repr);
+ Inkscape::GC::release(text_node);
+ Inkscape::GC::release(defs_repr);
+ Inkscape::GC::release(rect_repr);
+
+ return text_object;
+}
+
+/*
+ * TextTagAttributes implementation
+ */
+
+// Not used.
+// void TextTagAttributes::readFrom(Inkscape::XML::Node const *node)
+// {
+// readSingleAttribute(SPAttr::X, node->attribute("x"));
+// readSingleAttribute(SPAttr::Y, node->attribute("y"));
+// readSingleAttribute(SPAttr::DX, node->attribute("dx"));
+// readSingleAttribute(SPAttr::DY, node->attribute("dy"));
+// readSingleAttribute(SPAttr::ROTATE, node->attribute("rotate"));
+// readSingleAttribute(SPAttr::TEXTLENGTH, node->attribute("textLength"));
+// readSingleAttribute(SPAttr::LENGTHADJUST, node->attribute("lengthAdjust"));
+// }
+
+bool TextTagAttributes::readSingleAttribute(SPAttr key, gchar const *value, SPStyle const *style, Geom::Rect const *viewport)
+{
+ // std::cout << "TextTagAttributes::readSingleAttribute: key: " << key
+ // << " value: " << (value?value:"Null") << std::endl;
+ std::vector<SVGLength> *attr_vector;
+ bool update_x = false;
+ bool update_y = false;
+ switch (key) {
+ case SPAttr::X: attr_vector = &attributes.x; update_x = true; break;
+ case SPAttr::Y: attr_vector = &attributes.y; update_y = true; break;
+ case SPAttr::DX: attr_vector = &attributes.dx; update_x = true; break;
+ case SPAttr::DY: attr_vector = &attributes.dy; update_y = true; break;
+ case SPAttr::ROTATE: attr_vector = &attributes.rotate; break;
+ case SPAttr::TEXTLENGTH:
+ attributes.textLength.readOrUnset(value);
+ return true;
+ break;
+ case SPAttr::LENGTHADJUST:
+ attributes.lengthAdjust = (value && !strcmp(value, "spacingAndGlyphs")?
+ Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS :
+ Inkscape::Text::Layout::LENGTHADJUST_SPACING); // default is "spacing"
+ return true;
+ break;
+ default: return false;
+ }
+
+ // FIXME: sp_svg_length_list_read() amalgamates repeated separators. This prevents unset values.
+ *attr_vector = sp_svg_length_list_read(value);
+
+ if( (update_x || update_y) && style != nullptr && viewport != nullptr ) {
+ double const w = viewport->width();
+ double const h = viewport->height();
+ double const em = style->font_size.computed;
+ double const ex = em * 0.5;
+ for(auto & it : *attr_vector) {
+ if( update_x )
+ it.update( em, ex, w );
+ if( update_y )
+ it.update( em, ex, h );
+ }
+ }
+ return true;
+}
+
+void TextTagAttributes::writeTo(Inkscape::XML::Node *node) const
+{
+ writeSingleAttributeVector(node, "x", attributes.x);
+ writeSingleAttributeVector(node, "y", attributes.y);
+ writeSingleAttributeVector(node, "dx", attributes.dx);
+ writeSingleAttributeVector(node, "dy", attributes.dy);
+ writeSingleAttributeVector(node, "rotate", attributes.rotate);
+
+ writeSingleAttributeLength(node, "textLength", attributes.textLength);
+
+ if (attributes.textLength._set) {
+ if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) {
+ node->setAttribute("lengthAdjust", "spacing");
+ } else if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) {
+ node->setAttribute("lengthAdjust", "spacingAndGlyphs");
+ }
+ }
+}
+
+void TextTagAttributes::update( double em, double ex, double w, double h )
+{
+ for(auto & it : attributes.x) {
+ it.update( em, ex, w );
+ }
+ for(auto & it : attributes.y) {
+ it.update( em, ex, h );
+ }
+ for(auto & it : attributes.dx) {
+ it.update( em, ex, w );
+ }
+ for(auto & it : attributes.dy) {
+ it.update( em, ex, h );
+ }
+}
+
+void TextTagAttributes::writeSingleAttributeLength(Inkscape::XML::Node *node, gchar const *key, const SVGLength &length)
+{
+ if (length._set) {
+ node->setAttribute(key, length.write());
+ } else
+ node->removeAttribute(key);
+}
+
+void TextTagAttributes::writeSingleAttributeVector(Inkscape::XML::Node *node, gchar const *key, std::vector<SVGLength> const &attr_vector)
+{
+ if (attr_vector.empty())
+ node->removeAttribute(key);
+ else {
+ Glib::ustring string;
+
+ // FIXME: this has no concept of unset values because sp_svg_length_list_read() can't read them back in
+ for (auto it : attr_vector) {
+ if (!string.empty()) string += ' ';
+ string += it.write();
+ }
+ node->setAttributeOrRemoveIfEmpty(key, string);
+ }
+}
+
+bool TextTagAttributes::singleXYCoordinates() const
+{
+ return attributes.x.size() <= 1 && attributes.y.size() <= 1;
+}
+
+bool TextTagAttributes::anyAttributesSet() const
+{
+ return !attributes.x.empty() || !attributes.y.empty() || !attributes.dx.empty() || !attributes.dy.empty() || !attributes.rotate.empty();
+}
+
+Geom::Point TextTagAttributes::firstXY() const
+{
+ Geom::Point point;
+ if (attributes.x.empty()) point[Geom::X] = 0.0;
+ else point[Geom::X] = attributes.x[0].computed;
+ if (attributes.y.empty()) point[Geom::Y] = 0.0;
+ else point[Geom::Y] = attributes.y[0].computed;
+ return point;
+}
+
+void TextTagAttributes::setFirstXY(Geom::Point &point)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (attributes.x.empty())
+ attributes.x.resize(1, zero_length);
+ if (attributes.y.empty())
+ attributes.y.resize(1, zero_length);
+ attributes.x[0] = point[Geom::X];
+ attributes.y[0] = point[Geom::Y];
+}
+
+SVGLength* TextTagAttributes::getFirstXLength()
+{
+ if (!attributes.x.empty()) {
+ return &attributes.x[0];
+ } else {
+ return nullptr;
+ }
+}
+
+SVGLength* TextTagAttributes::getFirstYLength()
+{
+ if (!attributes.y.empty()) {
+ return &attributes.y[0];
+ } else {
+ return nullptr;
+ }
+}
+
+// Instance of TextTagAttributes contains attributes as defined by text/tspan element.
+// output: What will be sent to the rendering engine.
+// parent_attrs: Attributes collected from all ancestors.
+// parent_attrs_offset: Where this element fits into the parent_attrs.
+// copy_xy: Should this elements x, y attributes contribute to output (can preserve set values but not use them... kind of strange).
+// copy_dxdxrotate: Should this elements dx, dy, rotate attributes contribute to output.
+void TextTagAttributes::mergeInto(Inkscape::Text::Layout::OptionalTextTagAttrs *output, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_attrs, unsigned parent_attrs_offset, bool copy_xy, bool copy_dxdyrotate) const
+{
+ mergeSingleAttribute(&output->x, parent_attrs.x, parent_attrs_offset, copy_xy ? &attributes.x : nullptr);
+ mergeSingleAttribute(&output->y, parent_attrs.y, parent_attrs_offset, copy_xy ? &attributes.y : nullptr);
+ mergeSingleAttribute(&output->dx, parent_attrs.dx, parent_attrs_offset, copy_dxdyrotate ? &attributes.dx : nullptr);
+ mergeSingleAttribute(&output->dy, parent_attrs.dy, parent_attrs_offset, copy_dxdyrotate ? &attributes.dy : nullptr);
+ mergeSingleAttribute(&output->rotate, parent_attrs.rotate, parent_attrs_offset, copy_dxdyrotate ? &attributes.rotate : nullptr);
+ if (attributes.textLength._set) { // only from current node, this is not inherited from parent
+ output->textLength.value = attributes.textLength.value;
+ output->textLength.computed = attributes.textLength.computed;
+ output->textLength.unit = attributes.textLength.unit;
+ output->textLength._set = attributes.textLength._set;
+ output->lengthAdjust = attributes.lengthAdjust;
+ }
+}
+
+void TextTagAttributes::mergeSingleAttribute(std::vector<SVGLength> *output_list, std::vector<SVGLength> const &parent_list, unsigned parent_offset, std::vector<SVGLength> const *overlay_list)
+{
+ output_list->clear();
+ if (overlay_list == nullptr) {
+ if (parent_list.size() > parent_offset)
+ {
+ output_list->reserve(parent_list.size() - parent_offset);
+ std::copy(parent_list.begin() + parent_offset, parent_list.end(), std::back_inserter(*output_list));
+ }
+ } else {
+ output_list->reserve(std::max((int)parent_list.size() - (int)parent_offset, (int)overlay_list->size()));
+ unsigned overlay_offset = 0;
+ while (parent_offset < parent_list.size() || overlay_offset < overlay_list->size()) {
+ SVGLength const *this_item;
+ if (overlay_offset < overlay_list->size()) {
+ this_item = &(*overlay_list)[overlay_offset];
+ overlay_offset++;
+ parent_offset++;
+ } else {
+ this_item = &parent_list[parent_offset];
+ parent_offset++;
+ }
+ output_list->push_back(*this_item);
+ }
+ }
+}
+
+void TextTagAttributes::erase(unsigned start_index, unsigned n)
+{
+ if (n == 0) return;
+ if (!singleXYCoordinates()) {
+ eraseSingleAttribute(&attributes.x, start_index, n);
+ eraseSingleAttribute(&attributes.y, start_index, n);
+ }
+ eraseSingleAttribute(&attributes.dx, start_index, n);
+ eraseSingleAttribute(&attributes.dy, start_index, n);
+ eraseSingleAttribute(&attributes.rotate, start_index, n);
+}
+
+void TextTagAttributes::eraseSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n)
+{
+ if (attr_vector->size() <= start_index) return;
+ if (attr_vector->size() <= start_index + n)
+ attr_vector->erase(attr_vector->begin() + start_index, attr_vector->end());
+ else
+ attr_vector->erase(attr_vector->begin() + start_index, attr_vector->begin() + start_index + n);
+}
+
+void TextTagAttributes::insert(unsigned start_index, unsigned n)
+{
+ if (n == 0) return;
+ if (!singleXYCoordinates()) {
+ insertSingleAttribute(&attributes.x, start_index, n, true);
+ insertSingleAttribute(&attributes.y, start_index, n, true);
+ }
+ insertSingleAttribute(&attributes.dx, start_index, n, false);
+ insertSingleAttribute(&attributes.dy, start_index, n, false);
+ insertSingleAttribute(&attributes.rotate, start_index, n, false);
+}
+
+void TextTagAttributes::insertSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n, bool is_xy)
+{
+ if (attr_vector->size() <= start_index) return;
+ SVGLength zero_length;
+ zero_length = 0.0;
+ attr_vector->insert(attr_vector->begin() + start_index, n, zero_length);
+ if (is_xy) {
+ double begin = start_index == 0 ? (*attr_vector)[start_index + n].computed : (*attr_vector)[start_index - 1].computed;
+ double diff = ((*attr_vector)[start_index + n].computed - begin) / n; // n tested for nonzero in insert()
+ for (unsigned i = 0 ; i < n ; i++)
+ (*attr_vector)[start_index + i] = begin + diff * i;
+ }
+}
+
+void TextTagAttributes::split(unsigned index, TextTagAttributes *second)
+{
+ if (!singleXYCoordinates()) {
+ splitSingleAttribute(&attributes.x, index, &second->attributes.x, false);
+ splitSingleAttribute(&attributes.y, index, &second->attributes.y, false);
+ }
+ splitSingleAttribute(&attributes.dx, index, &second->attributes.dx, true);
+ splitSingleAttribute(&attributes.dy, index, &second->attributes.dy, true);
+ splitSingleAttribute(&attributes.rotate, index, &second->attributes.rotate, true);
+}
+
+void TextTagAttributes::splitSingleAttribute(std::vector<SVGLength> *first_vector, unsigned index, std::vector<SVGLength> *second_vector, bool trimZeros)
+{
+ second_vector->clear();
+ if (first_vector->size() <= index) return;
+ second_vector->resize(first_vector->size() - index);
+ std::copy(first_vector->begin() + index, first_vector->end(), second_vector->begin());
+ first_vector->resize(index);
+ if (trimZeros)
+ while (!first_vector->empty() && (!first_vector->back()._set || first_vector->back().value == 0.0))
+ first_vector->resize(first_vector->size() - 1);
+}
+
+void TextTagAttributes::join(TextTagAttributes const &first, TextTagAttributes const &second, unsigned second_index)
+{
+ if (second.singleXYCoordinates()) {
+ attributes.x = first.attributes.x;
+ attributes.y = first.attributes.y;
+ } else {
+ joinSingleAttribute(&attributes.x, first.attributes.x, second.attributes.x, second_index);
+ joinSingleAttribute(&attributes.y, first.attributes.y, second.attributes.y, second_index);
+ }
+ joinSingleAttribute(&attributes.dx, first.attributes.dx, second.attributes.dx, second_index);
+ joinSingleAttribute(&attributes.dy, first.attributes.dy, second.attributes.dy, second_index);
+ joinSingleAttribute(&attributes.rotate, first.attributes.rotate, second.attributes.rotate, second_index);
+}
+
+void TextTagAttributes::joinSingleAttribute(std::vector<SVGLength> *dest_vector, std::vector<SVGLength> const &first_vector, std::vector<SVGLength> const &second_vector, unsigned second_index)
+{
+ if (second_vector.empty())
+ *dest_vector = first_vector;
+ else {
+ dest_vector->resize(second_index + second_vector.size());
+ if (first_vector.size() < second_index) {
+ std::copy(first_vector.begin(), first_vector.end(), dest_vector->begin());
+ SVGLength zero_length;
+ zero_length = 0.0;
+ std::fill(dest_vector->begin() + first_vector.size(), dest_vector->begin() + second_index, zero_length);
+ } else
+ std::copy(first_vector.begin(), first_vector.begin() + second_index, dest_vector->begin());
+ std::copy(second_vector.begin(), second_vector.end(), dest_vector->begin() + second_index);
+ }
+}
+
+void TextTagAttributes::transform(Geom::Affine const &matrix, double scale_x, double scale_y, bool extend_zero_length)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ /* edge testcases for this code:
+ 1) moving text elements whose position is done entirely with transform="...", no x,y attributes
+ 2) unflowing multi-line flowtext then moving it (it has x but not y)
+ */
+ unsigned points_count = std::max(attributes.x.size(), attributes.y.size());
+ if (extend_zero_length && points_count < 1)
+ points_count = 1;
+ for (unsigned i = 0 ; i < points_count ; i++) {
+ Geom::Point point;
+ if (i < attributes.x.size()) point[Geom::X] = attributes.x[i].computed;
+ else point[Geom::X] = 0.0;
+ if (i < attributes.y.size()) point[Geom::Y] = attributes.y[i].computed;
+ else point[Geom::Y] = 0.0;
+ point *= matrix;
+ if (i < attributes.x.size())
+ attributes.x[i] = point[Geom::X];
+ else if (point[Geom::X] != 0.0 && extend_zero_length) {
+ attributes.x.resize(i + 1, zero_length);
+ attributes.x[i] = point[Geom::X];
+ }
+ if (i < attributes.y.size())
+ attributes.y[i] = point[Geom::Y];
+ else if (point[Geom::Y] != 0.0 && extend_zero_length) {
+ attributes.y.resize(i + 1, zero_length);
+ attributes.y[i] = point[Geom::Y];
+ }
+ }
+ for (auto & it : attributes.dx)
+ it = it.computed * scale_x;
+ for (auto & it : attributes.dy)
+ it = it.computed * scale_y;
+}
+
+double TextTagAttributes::getDx(unsigned index)
+{
+ if( attributes.dx.empty()) {
+ return 0.0;
+ }
+ if( index < attributes.dx.size() ) {
+ return attributes.dx[index].computed;
+ } else {
+ return 0.0; // attributes.dx.back().computed;
+ }
+}
+
+
+double TextTagAttributes::getDy(unsigned index)
+{
+ if( attributes.dy.empty() ) {
+ return 0.0;
+ }
+ if( index < attributes.dy.size() ) {
+ return attributes.dy[index].computed;
+ } else {
+ return 0.0; // attributes.dy.back().computed;
+ }
+}
+
+
+void TextTagAttributes::addToDx(unsigned index, double delta)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length);
+ attributes.dx[index] = attributes.dx[index].computed + delta;
+}
+
+void TextTagAttributes::addToDy(unsigned index, double delta)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length);
+ attributes.dy[index] = attributes.dy[index].computed + delta;
+}
+
+void TextTagAttributes::addToDxDy(unsigned index, Geom::Point const &adjust)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (adjust[Geom::X] != 0.0) {
+ if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length);
+ attributes.dx[index] = attributes.dx[index].computed + adjust[Geom::X];
+ }
+ if (adjust[Geom::Y] != 0.0) {
+ if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length);
+ attributes.dy[index] = attributes.dy[index].computed + adjust[Geom::Y];
+ }
+}
+
+double TextTagAttributes::getRotate(unsigned index)
+{
+ if( attributes.rotate.empty() ) {
+ return 0.0;
+ }
+ if( index < attributes.rotate.size() ) {
+ return attributes.rotate[index].computed;
+ } else {
+ return attributes.rotate.back().computed;
+ }
+}
+
+
+void TextTagAttributes::addToRotate(unsigned index, double delta)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (attributes.rotate.size() < index + 2) {
+ if (attributes.rotate.empty())
+ attributes.rotate.resize(index + 2, zero_length);
+ else
+ attributes.rotate.resize(index + 2, attributes.rotate.back());
+ }
+ attributes.rotate[index] = mod360(attributes.rotate[index].computed + delta);
+}
+
+
+void TextTagAttributes::setRotate(unsigned index, double angle)
+{
+ SVGLength zero_length;
+ zero_length = 0.0;
+
+ if (attributes.rotate.size() < index + 2) {
+ if (attributes.rotate.empty())
+ attributes.rotate.resize(index + 2, zero_length);
+ else
+ attributes.rotate.resize(index + 2, attributes.rotate.back());
+ }
+ attributes.rotate[index] = mod360(angle);
+}
+
+
+/*
+ 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 :