1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * SVG <text> and <tspan> implementation
4  *
5  * Author:
6  *   Lauris Kaplinski <lauris@kaplinski.com>
7  *   bulia byak <buliabyak@users.sf.net>
8  *   Jon A. Cruz <jon@joncruz.org>
9  *   Abhishek Sharma
10  *
11  * Copyright (C) 1999-2002 Lauris Kaplinski
12  * Copyright (C) 2000-2001 Ximian, Inc.
13  *
14  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
15  */
16 
17 /*
18  * fixme:
19  *
20  * These subcomponents should not be items, or alternately
21  * we have to invent set of flags to mark, whether standard
22  * attributes are applicable to given item (I even like this
23  * idea somewhat - Lauris)
24  *
25  */
26 
27 #include <2geom/affine.h>
28 #include <libnrtype/FontFactory.h>
29 #include <libnrtype/font-instance.h>
30 
31 #include <glibmm/i18n.h>
32 #include <glibmm/regex.h>
33 
34 #include "svg/svg.h"
35 #include "display/drawing-text.h"
36 #include "attributes.h"
37 #include "document.h"
38 #include "preferences.h"
39 #include "desktop.h"
40 #include "desktop-style.h"
41 #include "sp-namedview.h"
42 #include "inkscape.h"
43 #include "xml/quote.h"
44 #include "mod360.h"
45 
46 #include "sp-title.h"
47 #include "sp-desc.h"
48 #include "sp-rect.h"
49 #include "sp-text.h"
50 
51 #include "sp-shape.h"
52 #include "sp-textpath.h"
53 #include "sp-tref.h"
54 #include "sp-tspan.h"
55 #include "sp-flowregion.h"
56 
57 #include "text-editing.h"
58 
59 // For SVG 2 text flow
60 #include "livarot/Path.h"
61 #include "livarot/Shape.h"
62 #include "display/curve.h"
63 
64 /*#####################################################
65 #  SPTEXT
66 #####################################################*/
SPText()67 SPText::SPText() : SPItem() {
68 }
69 
~SPText()70 SPText::~SPText()
71 {
72     if (css) {
73         sp_repr_css_attr_unref(css);
74     }
75 };
76 
build(SPDocument * doc,Inkscape::XML::Node * repr)77 void SPText::build(SPDocument *doc, Inkscape::XML::Node *repr) {
78     this->readAttr(SPAttr::X);
79     this->readAttr(SPAttr::Y);
80     this->readAttr(SPAttr::DX);
81     this->readAttr(SPAttr::DY);
82     this->readAttr(SPAttr::ROTATE);
83 
84     // textLength and friends
85     this->readAttr(SPAttr::TEXTLENGTH);
86     this->readAttr(SPAttr::LENGTHADJUST);
87     SPItem::build(doc, repr);
88     css = nullptr;
89     this->readAttr(SPAttr::SODIPODI_LINESPACING);    // has to happen after the styles are read
90 }
91 
release()92 void SPText::release() {
93     SPItem::release();
94 }
95 
set(SPAttr key,const gchar * value)96 void SPText::set(SPAttr key, const gchar* value) {
97     //std::cout << "SPText::set: " << sp_attribute_name( key ) << ": " << (value?value:"Null") << std::endl;
98 
99     if (this->attributes.readSingleAttribute(key, value, style, &viewport)) {
100         this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
101     } else {
102         switch (key) {
103             case SPAttr::SODIPODI_LINESPACING:
104                 // convert deprecated tag to css... but only if 'line-height' missing.
105                 if (value && !this->style->line_height.set) {
106                     this->style->line_height.set = TRUE;
107                     this->style->line_height.inherit = FALSE;
108                     this->style->line_height.normal = FALSE;
109                     this->style->line_height.unit = SP_CSS_UNIT_PERCENT;
110                     this->style->line_height.value = this->style->line_height.computed = sp_svg_read_percentage (value, 1.0);
111                 }
112                 // Remove deprecated attribute
113                 this->removeAttribute("sodipodi:linespacing");
114 
115                 this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
116                 break;
117 
118             default:
119                 SPItem::set(key, value);
120                 break;
121         }
122     }
123 }
124 
child_added(Inkscape::XML::Node * rch,Inkscape::XML::Node * ref)125 void SPText::child_added(Inkscape::XML::Node *rch, Inkscape::XML::Node *ref) {
126     SPItem::child_added(rch, ref);
127 
128     this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
129 }
130 
remove_child(Inkscape::XML::Node * rch)131 void SPText::remove_child(Inkscape::XML::Node *rch) {
132     SPItem::remove_child(rch);
133 
134     this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG);
135 }
136 
137 
update(SPCtx * ctx,guint flags)138 void SPText::update(SPCtx *ctx, guint flags) {
139 
140     unsigned childflags = (flags & SP_OBJECT_MODIFIED_CASCADE);
141     if (flags & SP_OBJECT_MODIFIED_FLAG) {
142         childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
143     }
144 
145     // Create temporary list of children
146     std::vector<SPObject *> l;
147     for (auto& child: children) {
148         sp_object_ref(&child, this);
149         l.push_back(&child);
150     }
151 
152     for (auto child:l) {
153         if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
154             /* fixme: Do we need transform? */
155             child->updateDisplay(ctx, childflags);
156         }
157         sp_object_unref(child, this);
158     }
159 
160     // update ourselves after updating children
161     SPItem::update(ctx, flags);
162 
163     if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG |
164                   SP_OBJECT_CHILD_MODIFIED_FLAG |
165                   SP_TEXT_LAYOUT_MODIFIED_FLAG   ) )
166     {
167 
168         SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx);
169 
170         double const w = ictx->viewport.width();
171         double const h = ictx->viewport.height();
172         double const em = style->font_size.computed;
173         double const ex = 0.5 * em;  // fixme: get x height from pango or libnrtype.
174 
175         attributes.update( em, ex, w, h );
176 
177         // Set inline_size computed value if necessary (i.e. if unit is %).
178         if (has_inline_size()) {
179             if (style->inline_size.unit == SP_CSS_UNIT_PERCENT) {
180                 if (is_horizontal()) {
181                     style->inline_size.computed = style->inline_size.value * ictx->viewport.width();
182                 } else {
183                     style->inline_size.computed = style->inline_size.value * ictx->viewport.height();
184                 }
185             }
186         }
187 
188         /* fixme: It is not nice to have it here, but otherwise children content changes does not work */
189         /* fixme: Even now it may not work, as we are delayed */
190         /* fixme: So check modification flag everywhere immediate state is used */
191         this->rebuildLayout();
192 
193         Geom::OptRect paintbox = this->geometricBounds();
194 
195         for (SPItemView* v = this->display; v != nullptr; v = v->next) {
196             Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
197             this->_clearFlow(g);
198             g->setStyle(this->style, this->parent->style);
199             // pass the bbox of this as paintbox (used for paintserver fills)
200             this->layout.show(g, paintbox);
201         }
202     }
203 }
204 
modified(guint flags)205 void SPText::modified(guint flags) {
206 //	SPItem::onModified(flags);
207 
208     guint cflags = (flags & SP_OBJECT_MODIFIED_CASCADE);
209 
210     if (flags & SP_OBJECT_MODIFIED_FLAG) {
211         cflags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
212     }
213 
214     // FIXME: all that we need to do here is to call setStyle, to set the changed
215     // style, but there's no easy way to access the drawing glyphs or texts corresponding to a
216     // text this. Therefore we do here the same as in _update, that is, destroy all items
217     // and create new ones. This is probably quite wasteful.
218     if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) {
219         Geom::OptRect paintbox = this->geometricBounds();
220 
221         for (SPItemView* v = this->display; v != nullptr; v = v->next) {
222             Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
223             this->_clearFlow(g);
224             g->setStyle(this->style, this->parent->style);
225             this->layout.show(g, paintbox);
226         }
227     }
228 
229     // Create temporary list of children
230     std::vector<SPObject *> l;
231     for (auto& child: children) {
232         sp_object_ref(&child, this);
233         l.push_back(&child);
234     }
235 
236     for (auto child:l) {
237         if (cflags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
238             child->emitModified(cflags);
239         }
240         sp_object_unref(child, this);
241     }
242 }
243 
write(Inkscape::XML::Document * xml_doc,Inkscape::XML::Node * repr,guint flags)244 Inkscape::XML::Node *SPText::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) {
245     if (flags & SP_OBJECT_WRITE_BUILD) {
246         if (!repr) {
247             repr = xml_doc->createElement("svg:text");
248         }
249 
250         std::vector<Inkscape::XML::Node *> l;
251 
252         for (auto& child: children) {
253             if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) {
254                 continue;
255             }
256 
257             Inkscape::XML::Node *crepr = nullptr;
258 
259             if (SP_IS_STRING(&child)) {
260                 crepr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str());
261             } else {
262                 crepr = child.updateRepr(xml_doc, nullptr, flags);
263             }
264 
265             if (crepr) {
266                 l.push_back(crepr);
267             }
268         }
269 
270         for (auto i=l.rbegin();i!=l.rend();++i) {
271             repr->addChild(*i, nullptr);
272             Inkscape::GC::release(*i);
273         }
274     } else {
275         for (auto& child: children) {
276             if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) {
277                 continue;
278             }
279 
280             if (SP_IS_STRING(&child)) {
281                 child.getRepr()->setContent(SP_STRING(&child)->string.c_str());
282             } else {
283                 child.updateRepr(flags);
284             }
285         }
286     }
287 
288     this->attributes.writeTo(repr);
289 
290     SPItem::write(xml_doc, repr, flags);
291 
292     return repr;
293 }
294 
295 
bbox(Geom::Affine const & transform,SPItem::BBoxType type) const296 Geom::OptRect SPText::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const {
297     Geom::OptRect bbox = SP_TEXT(this)->layout.bounds(transform);
298 
299     // FIXME this code is incorrect
300     if (bbox && type == SPItem::VISUAL_BBOX && !this->style->stroke.isNone()) {
301         double scale = transform.descrim();
302         bbox->expandBy(0.5 * this->style->stroke_width.computed * scale);
303     }
304 
305     return bbox;
306 }
307 
show(Inkscape::Drawing & drawing,unsigned,unsigned)308 Inkscape::DrawingItem* SPText::show(Inkscape::Drawing &drawing, unsigned /*key*/, unsigned /*flags*/) {
309     Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing);
310     flowed->setPickChildren(false);
311     flowed->setStyle(this->style, this->parent->style);
312 
313     // pass the bbox of the text object as paintbox (used for paintserver fills)
314     this->layout.show(flowed, this->geometricBounds());
315 
316     return flowed;
317 }
318 
319 
hide(unsigned int key)320 void SPText::hide(unsigned int key) {
321     for (SPItemView* v = this->display; v != nullptr; v = v->next) {
322         if (v->key == key) {
323             Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem);
324             this->_clearFlow(g);
325         }
326     }
327 }
328 
displayName() const329 const char* SPText::displayName() const {
330     if (has_inline_size()) {
331         return _("Auto-wrapped text");
332     } else if (has_shape_inside()) {
333         return _("Text in-a-shape");
334     } else {
335         return _("Text");
336     }
337 }
338 
description() const339 gchar* SPText::description() const {
340 
341     SPStyle *style = this->style;
342 
343     char *n = xml_quote_strdup(style->font_family.value());
344 
345     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
346     int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
347     Inkscape::Util::Quantity q = Inkscape::Util::Quantity(style->font_size.computed, "px");
348     q.quantity *= this->i2doc_affine().descrim();
349     Glib::ustring xs = q.string(sp_style_get_css_unit_string(unit));
350 
351     char const *trunc = "";
352     Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this);
353 
354     if (layout && layout->inputTruncated()) {
355         trunc = _(" [truncated]");
356     }
357 
358     char *ret = ( SP_IS_TEXT_TEXTPATH(this)
359       ? g_strdup_printf(_("on path%s (%s, %s)"), trunc, n, xs.c_str())
360       : g_strdup_printf(_("%s (%s, %s)"),        trunc, n, xs.c_str()) );
361     return ret;
362 }
363 
snappoints(std::vector<Inkscape::SnapCandidatePoint> & p,Inkscape::SnapPreferences const * snapprefs) const364 void SPText::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const {
365     if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) {
366         // Choose a point on the baseline for snapping from or to, with the horizontal position
367         // of this point depending on the text alignment (left vs. right)
368         Inkscape::Text::Layout const *layout = te_get_layout(this);
369 
370         if (layout != nullptr && layout->outputExists()) {
371             std::optional<Geom::Point> pt = layout->baselineAnchorPoint();
372 
373             if (pt) {
374                 p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR);
375             }
376         }
377     }
378 }
379 
hide_shape_inside()380 void SPText::hide_shape_inside()
381 {
382     SPText *text = dynamic_cast<SPText *>(this);
383     SPStyle *item_style = this->style;
384     if (item_style && text && item_style->shape_inside.set) {
385         SPCSSAttr *css_unset = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
386         css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
387         sp_repr_css_unset_property(css_unset, "shape-inside");
388         sp_repr_css_attr_unref(css_unset);
389         this->changeCSS(css_unset, "style");
390     } else {
391         css = nullptr;
392     }
393 }
394 
show_shape_inside()395 void SPText::show_shape_inside()
396 {
397     SPText *text = dynamic_cast<SPText *>(this);
398     if (text && css) {
399         this->changeCSS(css, "style");
400     }
401 }
402 
set_transform(Geom::Affine const & xform)403 Geom::Affine SPText::set_transform(Geom::Affine const &xform) {
404     // See if 'shape-inside' has rectangle
405     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
406     if (prefs->getBool("/tools/text/use_svg2", true)) {
407         if (style->shape_inside.set) {
408             return xform;
409         }
410     }
411     // we cannot optimize textpath because changing its fontsize will break its match to the path
412 
413     if (SP_IS_TEXT_TEXTPATH (this)) {
414         if (!this->_optimizeTextpathText) {
415             return xform;
416         } else {
417             this->_optimizeTextpathText = false;
418         }
419     }
420 
421     // we cannot optimize text with textLength because it may show different size than specified
422     if (this->attributes.getTextLength()->_set)
423         return xform;
424 
425     if (this->style && this->style->inline_size.set)
426         return xform;
427 
428     /* This function takes care of scaling & translation only, we return whatever parts we can't
429        handle. */
430 
431 // TODO: pjrm tried to use fontsize_expansion(xform) here and it works for text in that font size
432 // is scaled more intuitively when scaling non-uniformly; however this necessitated using
433 // fontsize_expansion instead of expansion in other places too, where it was not appropriate
434 // (e.g. it broke stroke width on copy/pasting of style from horizontally stretched to vertically
435 // stretched shape). Using fontsize_expansion only here broke setting the style via font
436 // dialog. This needs to be investigated further.
437     double const ex = xform.descrim();
438     if (ex == 0) {
439         return xform;
440     }
441 
442     Geom::Affine ret(Geom::Affine(xform).withoutTranslation());
443     ret[0] /= ex;
444     ret[1] /= ex;
445     ret[2] /= ex;
446     ret[3] /= ex;
447 
448     // Adjust x/y, dx/dy
449     this->_adjustCoordsRecursive (this, xform * ret.inverse(), ex);
450 
451     // Adjust font size
452     this->_adjustFontsizeRecursive (this, ex);
453 
454     // Adjust stroke width
455     this->adjust_stroke_width_recursive (ex);
456 
457     // Adjust pattern fill
458     this->adjust_pattern(xform * ret.inverse());
459 
460     // Adjust gradient fill
461     this->adjust_gradient(xform * ret.inverse());
462 
463     return ret;
464 }
465 
print(SPPrintContext * ctx)466 void SPText::print(SPPrintContext *ctx) {
467     Geom::OptRect pbox, bbox, dbox;
468     pbox = this->geometricBounds();
469     bbox = this->desktopVisualBounds();
470     dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions());
471 
472     Geom::Affine const ctm (this->i2dt_affine());
473 
474     this->layout.print(ctx,pbox,dbox,bbox,ctm);
475 }
476 
477 /*
478  * Member functions
479  */
480 
_buildLayoutInit()481 void SPText::_buildLayoutInit()
482 {
483 
484     layout.strut.reset();
485     layout.wrap_mode = Inkscape::Text::Layout::WRAP_NONE; // Default to SVG 1.1
486 
487     if (style) {
488 
489         // Strut
490         font_instance *font = font_factory::Default()->FaceFromStyle( style );
491         if (font) {
492             font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight);
493             font->Unref();
494         }
495         layout.strut *= style->font_size.computed;
496         if (style->line_height.normal ) {
497             layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL );
498         } else if (style->line_height.unit == SP_CSS_UNIT_NONE) {
499             layout.strut.computeEffective( style->line_height.computed );
500         } else {
501             if( style->font_size.computed > 0.0 ) {
502                 layout.strut.computeEffective( style->line_height.computed/style->font_size.computed );
503             }
504         }
505 
506 
507         // To do: follow SPItem clip_ref/mask_ref code
508         if (style->shape_inside.set ) {
509 
510             layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE;
511 
512             // Find union of all exclusion shapes
513             Shape *exclusion_shape = nullptr;
514             if(style->shape_subtract.set) {
515                 exclusion_shape = _buildExclusionShape();
516             }
517 
518             // Find inside shape curves
519             for (auto *href : style->shape_inside.hrefs) {
520                 auto shape = href->getObject();
521 
522                     if ( shape ) {
523 
524                         // This code adapted from sp-flowregion.cpp: GetDest()
525                         if (!shape->curve()) {
526                             shape->set_shape();
527                         }
528                         SPCurve const *curve = shape->curve();
529 
530                         if ( curve ) {
531                             Path *temp = new Path;
532                             Path *padded = new Path;
533                             temp->LoadPathVector( curve->get_pathvector(), shape->transform, true );
534                             if( style->shape_padding.set ) {
535                                 // std::cout << "  padding: " << style->shape_padding.computed << std::endl;
536                                 temp->OutsideOutline ( padded, style->shape_padding.computed, join_round, butt_straight, 20.0 );
537                             } else {
538                                 // std::cout << "  no padding" << std::endl;
539                                 padded->Copy( temp );
540                             }
541                             padded->Convert( 0.25 );  // Convert to polyline
542                             Shape* sh = new Shape;
543                             padded->Fill( sh, 0 );
544                             // for( unsigned i = 0; i < temp->pts.size(); ++i ) {
545                             //   std::cout << " ........ " << temp->pts[i].p << std::endl;
546                             // }
547                             // std::cout << " ...... shape: " << sh->numberOfPoints() << std::endl;
548                             Shape *uncross = new Shape;
549                             uncross->ConvertToShape( sh );
550 
551                             // Subtract exclusion shape
552                             if(style->shape_subtract.set) {
553                                 Shape *copy = new Shape;
554                                 if (exclusion_shape && exclusion_shape->hasEdges()) {
555                                     copy->Booleen(uncross, const_cast<Shape*>(exclusion_shape), bool_op_diff);
556                                 } else {
557                                     copy->Copy(uncross);
558                                 }
559                                 layout.appendWrapShape( copy );
560                                 continue;
561                             }
562 
563                             layout.appendWrapShape( uncross );
564 
565                             delete temp;
566                             delete padded;
567                             delete sh;
568                             // delete uncross;
569                         } else {
570                             std::cerr << "SPText::_buildLayoutInit(): Failed to get curve." << std::endl;
571                         }
572                 }
573             }
574             delete exclusion_shape;
575 
576         } else if (has_inline_size()) {
577 
578             layout.wrap_mode = Inkscape::Text::Layout::WRAP_INLINE_SIZE;
579 
580             // If both shape_inside and inline_size are set, shape_inside wins out.
581 
582             // We construct a rectangle with one dimension set by the computed value of 'inline-size'
583             // and the other dimension set to infinity. Text is laid out starting at the 'x' and 'y'
584             // attribute values. This is handled elsewhere.
585 
586             Geom::OptRect opt_frame = get_frame();
587             Geom::Rect frame = *opt_frame;
588 
589             Shape *shape = new Shape;
590             shape->Reset();
591             int v0 = shape->AddPoint(frame.corner(0));
592             int v1 = shape->AddPoint(frame.corner(1));
593             int v2 = shape->AddPoint(frame.corner(2));
594             int v3 = shape->AddPoint(frame.corner(3));
595             shape->AddEdge(v0, v1);
596             shape->AddEdge(v1, v2);
597             shape->AddEdge(v2, v3);
598             shape->AddEdge(v3, v0);
599             Shape *uncross = new Shape;
600             uncross->ConvertToShape( shape );
601 
602             layout.appendWrapShape( uncross );
603 
604             delete shape;
605 
606         } else if (style->white_space.value == SP_CSS_WHITE_SPACE_PRE     ||
607                    style->white_space.value == SP_CSS_WHITE_SPACE_PREWRAP ||
608                    style->white_space.value == SP_CSS_WHITE_SPACE_PRELINE ) {
609             layout.wrap_mode = Inkscape::Text::Layout::WRAP_WHITE_SPACE;
610         }
611 
612     } // if (style)
613 }
614 
_buildLayoutInput(SPObject * object,Inkscape::Text::Layout::OptionalTextTagAttrs const & parent_optional_attrs,unsigned parent_attrs_offset,bool in_textpath)615 unsigned SPText::_buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath)
616 {
617     unsigned length = 0;
618     unsigned child_attrs_offset = 0;
619     Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs;
620 
621     // Per SVG spec, an object with 'display:none' doesn't contribute to text layout.
622     if (object->style->display.computed == SP_CSS_DISPLAY_NONE) {
623         return 0;
624     }
625 
626     SPText*  text_object  = dynamic_cast<SPText*>(object);
627     SPTSpan* tspan_object = dynamic_cast<SPTSpan*>(object);
628     SPTRef*  tref_object  = dynamic_cast<SPTRef*>(object);
629     SPTextPath* textpath_object = dynamic_cast<SPTextPath*>(object);
630 
631     if (text_object) {
632 
633         bool use_xy = true;
634         bool use_dxdyrotate = true;
635 
636         // SVG 2 Text wrapping.
637         if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE ||
638             layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
639             use_xy = false;
640             use_dxdyrotate = false;
641         }
642 
643         text_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate);
644 
645         // SVG 2 Text wrapping
646         if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
647 
648             // For horizontal text:
649             //   'x' is used to calculate the left/right edges of the rectangle but is not
650             //   needed later. If not deleted here, it will cause an incorrect positioning
651             //   of the first line.
652             //   'y' is used to determine where the first line box is located and is needed
653             //   during the output stage.
654             // For vertical text:
655             //   Follow above but exchange 'x' and 'y'.
656             // The SVG 2 spec currently says use the 'x' and 'y' from the <text> element,
657             // if not defined in the <text> element, use the 'x' and 'y' from the first child.
658             // We only look at the <text> element. (Doing otherwise means tracking if
659             // we've found 'x' and 'y' and then creating the Shape at the end.)
660             if (is_horizontal()) {
661                 // Horizontal text
662                 SVGLength* y = _getFirstYLength();
663                 if (y) {
664                     optional_attrs.y.push_back(*y);
665                 } else {
666                     std::cerr << "SPText::_buildLayoutInput: No 'y' attribute value with horizontal 'inline-size'!" << std::endl;
667                 }
668             } else {
669                 // Vertical text
670                 SVGLength* x = _getFirstXLength();
671                 if (x) {
672                     optional_attrs.x.push_back(*x);
673                 } else {
674                     std::cerr << "SPText::_buildLayoutInput: No 'x' attribute value with vertical 'inline-size'!" << std::endl;
675                 }
676             }
677         }
678 
679         // set textLength on the entire layout, see note in TNG-Layout.h
680         if (text_object->attributes.getTextLength()->_set) {
681             layout.textLength._set = true;
682             layout.textLength.value    = text_object->attributes.getTextLength()->value;
683             layout.textLength.computed = text_object->attributes.getTextLength()->computed;
684             layout.textLength.unit     = text_object->attributes.getTextLength()->unit;
685             layout.lengthAdjust = (Inkscape::Text::Layout::LengthAdjust) text_object->attributes.getLengthAdjust();
686         }
687     }
688 
689     else if (tspan_object) {
690 
691         // x, y attributes are stripped from some tspans marked with role="line" as we do our own line layout.
692         // This should be checked carefully, as it can undo line layout in imported SVG files.
693         bool use_xy = !in_textpath &&
694             (tspan_object->role == SP_TSPAN_ROLE_UNSPECIFIED || !tspan_object->attributes.singleXYCoordinates());
695         bool use_dxdyrotate = true;
696 
697         // SVG 2 Text wrapping: see comment above.
698         if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE ||
699             layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) {
700             use_xy = false;
701             use_dxdyrotate = false;
702         }
703 
704         tspan_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate);
705 
706         if (tspan_object->role != SP_TSPAN_ROLE_UNSPECIFIED) {
707             // We are doing line wrapping using sodipodi:role="line". New lines have been stripped.
708 
709             // Insert paragraph break before text if not first tspan.
710             SPObject *prev_object = object->getPrev();
711             if (prev_object && dynamic_cast<SPTSpan*>(prev_object)) {
712                 if (!layout.inputExists()) {
713                     // Add an object to store style, needed even if there is no text. When does this happen?
714                     layout.appendText("", prev_object->style, prev_object, &optional_attrs);
715                 }
716                 layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, prev_object);
717             }
718 
719             // Create empty span to store info (any non-empty tspan with sodipodi:role="line" has a child).
720             if (!object->hasChildren()) {
721                 layout.appendText("", object->style, object, &optional_attrs);
722             }
723 
724             length++;     // interpreting line breaks as a character for the purposes of x/y/etc attributes
725                           // is a liberal interpretation of the svg spec, but a strict reading would mean
726                           // that if the first line is empty the second line would take its place at the
727                           // start position. Very confusing.
728                           // SVG 2 clarifies, attributes are matched to unicode input characters so line
729                           // breaks do match to an x/y/etc attribute.
730             child_attrs_offset--;
731         }
732     }
733 
734     else if (tref_object) {
735         tref_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, true, true);
736     }
737 
738     else if (textpath_object) {
739         in_textpath = true; // This should be made local so we can mix normal text with textpath per SVG 2.
740         textpath_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, false, true);
741         optional_attrs.x.clear(); // Hmm, you can use x with horizontal text. So this is probably wrong.
742         optional_attrs.y.clear();
743     }
744 
745     else {
746         optional_attrs = parent_optional_attrs;
747         child_attrs_offset = parent_attrs_offset;
748     }
749 
750     // Recurse
751     for (auto& child: object->children) {
752         SPString *str = dynamic_cast<SPString *>(&child);
753         if (str) {
754             Glib::ustring const &string = str->string;
755             // std::cout << "  Appending: >" << string << "<" << std::endl;
756             layout.appendText(string, object->style, &child, &optional_attrs, child_attrs_offset + length);
757             length += string.length();
758         } else if (!sp_repr_is_meta_element(child.getRepr())) {
759             /*      ^^^^ XML Tree being directly used here while it shouldn't be.*/
760             length += _buildLayoutInput(&child, optional_attrs, child_attrs_offset + length, in_textpath);
761         }
762     }
763 
764     return length;
765 }
766 
_buildExclusionShape() const767 Shape* SPText::_buildExclusionShape() const
768 {
769     std::unique_ptr<Shape> result(new Shape()); // Union of all exclusion shapes
770     std::unique_ptr<Shape> shape_temp(new Shape());
771 
772     for (auto *href : style->shape_subtract.hrefs) {
773         auto shape = href->getObject();
774 
775             if ( shape ) {
776                 // This code adapted from sp-flowregion.cpp: GetDest()
777                 if (!shape->curve()) {
778                     shape->set_shape();
779                 }
780                 SPCurve const *curve = shape->curve();
781 
782                 if ( curve ) {
783                     Path *temp = new Path;
784                     Path *margin = new Path;
785                     temp->LoadPathVector( curve->get_pathvector(), shape->transform, true );
786 
787                     if( shape->style->shape_margin.set ) {
788                         temp->OutsideOutline ( margin, -shape->style->shape_margin.computed, join_round, butt_straight, 20.0 );
789                     } else {
790                         margin->Copy( temp );
791                     }
792 
793                     margin->Convert( 0.25 );  // Convert to polyline
794                     Shape* sh = new Shape;
795                     margin->Fill( sh, 0 );
796 
797                     Shape *uncross = new Shape;
798                     uncross->ConvertToShape( sh );
799 
800                     if (result->hasEdges()) {
801                         shape_temp->Booleen(result.get(), uncross, bool_op_union);
802                         std::swap(result, shape_temp);
803                     } else {
804                         result->Copy(uncross);
805                     }
806                 }
807             }
808     }
809     return result.release();
810 }
811 
812 
813 // SVG requires one to use the first x/y value found on a child element if x/y not given on text
814 // element. TODO: Recurse.
815 SVGLength*
_getFirstXLength()816 SPText::_getFirstXLength()
817 {
818     SVGLength* x = attributes.getFirstXLength();
819 
820     if (!x) {
821         for (auto& child: children) {
822             if (SP_IS_TSPAN(&child)) {
823                 SPTSpan *tspan = SP_TSPAN(&child);
824                 x = tspan->attributes.getFirstXLength();
825                 break;
826             }
827         }
828     }
829 
830     return x;
831 }
832 
833 
834 SVGLength*
_getFirstYLength()835 SPText::_getFirstYLength()
836 {
837     SVGLength* y = attributes.getFirstYLength();
838 
839     if (!y) {
840         for (auto& child: children) {
841             if (SP_IS_TSPAN(&child)) {
842                 SPTSpan *tspan = SP_TSPAN(&child);
843                 y = tspan->attributes.getFirstYLength();
844                 break;
845             }
846         }
847     }
848 
849     return y;
850 }
851 
getNormalizedBpath() const852 std::unique_ptr<SPCurve> SPText::getNormalizedBpath() const
853 {
854     return layout.convertToCurves();
855 }
856 
rebuildLayout()857 void SPText::rebuildLayout()
858 {
859     layout.clear();
860     _buildLayoutInit();
861 
862     Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs;
863     _buildLayoutInput(this, optional_attrs, 0, false);
864 
865     layout.calculateFlow();
866 
867     for (auto& child: children) {
868         if (SP_IS_TEXTPATH(&child)) {
869             SPTextPath const *textpath = SP_TEXTPATH(&child);
870             if (textpath->originalPath != nullptr) {
871 #if DEBUG_TEXTLAYOUT_DUMPASTEXT
872                 g_print("%s", layout.dumpAsText().c_str());
873 #endif
874                 layout.fitToPathAlign(textpath->startOffset, *textpath->originalPath);
875             }
876         }
877     }
878 #if DEBUG_TEXTLAYOUT_DUMPASTEXT
879     g_print("%s", layout.dumpAsText().c_str());
880 #endif
881 
882     // set the x,y attributes on role:line spans
883     for (auto& child: children) {
884         if (SP_IS_TSPAN(&child)) {
885             SPTSpan *tspan = SP_TSPAN(&child);
886             if ((tspan->role != SP_TSPAN_ROLE_UNSPECIFIED)
887                  && tspan->attributes.singleXYCoordinates() ) {
888                 Inkscape::Text::Layout::iterator iter = layout.sourceToIterator(tspan);
889                 Geom::Point anchor_point = layout.chunkAnchorPoint(iter);
890                 tspan->attributes.setFirstXY(anchor_point);
891                 // repr needs to be updated but if we do it here we get a loop.
892             }
893         }
894     }
895 }
896 
897 
_adjustFontsizeRecursive(SPItem * item,double ex,bool is_root)898 void SPText::_adjustFontsizeRecursive(SPItem *item, double ex, bool is_root)
899 {
900     SPStyle *style = item->style;
901 
902     if (style && !Geom::are_near(ex, 1.0)) {
903         if (!style->font_size.set && is_root) {
904             style->font_size.set = true;
905         }
906         style->font_size.type = SP_FONT_SIZE_LENGTH;
907         style->font_size.computed *= ex;
908         style->letter_spacing.computed *= ex;
909         style->word_spacing.computed *= ex;
910         if (style->line_height.unit != SP_CSS_UNIT_NONE &&
911             style->line_height.unit != SP_CSS_UNIT_PERCENT &&
912             style->line_height.unit != SP_CSS_UNIT_EM &&
913             style->line_height.unit != SP_CSS_UNIT_EX) {
914             // No unit on 'line-height' property has special behavior.
915             style->line_height.computed *= ex;
916         }
917         item->updateRepr();
918     }
919 
920     for(auto& o: item->children) {
921         if (SP_IS_ITEM(&o))
922             _adjustFontsizeRecursive(SP_ITEM(&o), ex, false);
923     }
924 }
925 
926 void
remove_newlines_recursive(SPObject * object,bool is_svg2)927 remove_newlines_recursive(SPObject* object, bool is_svg2)
928 {
929     // Replace '\n' by space.
930     SPString* string = dynamic_cast<SPString *>(object);
931     if (string) {
932         static Glib::RefPtr<Glib::Regex> r = Glib::Regex::create("\n+");
933         string->string = r->replace(string->string, 0, " ", (Glib::RegexMatchFlags)0);
934         string->getRepr()->setContent(string->string.c_str());
935     }
936 
937     for (auto child : object->childList(false)) {
938         remove_newlines_recursive(child, is_svg2);
939     }
940 
941     // Add space at end of a line if line is created by sodipodi:role="line".
942     SPTSpan* tspan = dynamic_cast<SPTSpan *>(object);
943     if (tspan                             &&
944         tspan->role == SP_TSPAN_ROLE_LINE &&
945         tspan->getNext() != nullptr       &&  // Don't add space at end of last line.
946         !is_svg2) {                           // SVG2 uses newlines, should not have sodipodi:role.
947 
948         std::vector<SPObject *> children = tspan->childList(false);
949 
950         // Find last string (could be more than one if there is tspan in the middle of a tspan).
951         for (auto it = children.rbegin(); it != children.rend(); ++it) {
952             SPString* string = dynamic_cast<SPString *>(*it);
953             if (string) {
954                 string->string += ' ';
955                 string->getRepr()->setContent(string->string.c_str());
956                 break;
957             }
958         }
959     }
960 }
961 
962 // Prepare multi-line text for putting on path.
963 void
remove_newlines()964 SPText::remove_newlines()
965 {
966     remove_newlines_recursive(this, has_shape_inside() || has_inline_size());
967     style->inline_size.clear();
968     style->shape_inside.clear();
969     updateRepr();
970 }
971 
_adjustCoordsRecursive(SPItem * item,Geom::Affine const & m,double ex,bool is_root)972 void SPText::_adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root)
973 {
974     if (SP_IS_TSPAN(item))
975         SP_TSPAN(item)->attributes.transform(m, ex, ex, is_root);
976               // it doesn't matter if we change the x,y for role=line spans because we'll just overwrite them anyway
977     else if (SP_IS_TEXT(item))
978         SP_TEXT(item)->attributes.transform(m, ex, ex, is_root);
979     else if (SP_IS_TEXTPATH(item))
980         SP_TEXTPATH(item)->attributes.transform(m, ex, ex, is_root);
981     else if (SP_IS_TREF(item)) {
982         SP_TREF(item)->attributes.transform(m, ex, ex, is_root);
983     } else {
984         g_warning("element is not text");
985 	return;
986     }
987 
988     for(auto& o: item->children) {
989         if (SP_IS_ITEM(&o))
990             _adjustCoordsRecursive(SP_ITEM(&o), m, ex, false);
991     }
992 }
993 
994 
_clearFlow(Inkscape::DrawingGroup * in_arena)995 void SPText::_clearFlow(Inkscape::DrawingGroup *in_arena)
996 {
997     in_arena->clearChildren();
998 }
999 
1000 
1001 /** Remove 'x' and 'y' values on children (lines) or they will be interpreted as absolute positions
1002  * when 'inline-size' is removed.
1003  */
remove_svg11_fallback()1004 void SPText::remove_svg11_fallback() {
1005     for (auto& child:  children) {
1006         child.removeAttribute("x");
1007         child.removeAttribute("y");
1008     }
1009 }
1010 
1011 /** Convert new lines in 'inline-size' text to tspans with sodipodi:role="tspan".
1012  *  Note sodipodi:role="tspan" will be removed in the future!
1013  */
newline_to_sodipodi()1014 void SPText::newline_to_sodipodi() {
1015 
1016     // New lines can come anywhere, we must search character-by-character.
1017     auto it = layout.begin();
1018     while (it != layout.end()) {
1019         if (layout.characterAt(it) == '\n') {
1020 
1021             // Delete newline ('\n').
1022             iterator_pair pair;
1023             auto it_end = it;
1024             it_end.nextCharacter();
1025             sp_te_delete (this, it, it_end, pair);
1026             it = pair.first;
1027 
1028             // Insert newline (sodipodi:role="line").
1029             it = sp_te_insert_line(this, it);
1030         }
1031 
1032         it.nextCharacter();
1033         layout.validateIterator(&it);
1034     }
1035 }
1036 
1037 /** Convert tspans with sodipodi:role="tspans" to '\n'.
1038  *  Note sodipodi:role="tspan" will be removed in the future!
1039  */
sodipodi_to_newline()1040 void SPText::sodipodi_to_newline() {
1041 
1042     // tspans with sodipodi:role="line" are only direct children of a <text> element.
1043     for (auto child : childList(false)) {
1044         auto tspan = dynamic_cast<SPTSpan *>(child);  // Could have <desc> or <title>.
1045         if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) {
1046 
1047             // Remove sodipodi:role attribute.
1048             tspan->removeAttribute("sodipodi:role");
1049             tspan->updateRepr();
1050 
1051             // Insert '/n' if not last line.
1052             // This may screw up dx, dy, rotate attribute counting but... SVG 2 text cannot have these values.
1053             if (tspan != lastChild()) {
1054                 tspan->style->white_space.computed = SP_CSS_WHITE_SPACE_PRE; // Set so '\n' is not immediately stripped out before CSS recascaded!
1055                 auto last_child = tspan->lastChild();
1056                 auto last_string = dynamic_cast<SPString *>(last_child);
1057                 if (last_string) {
1058                     // Add '\n' to string.
1059                     last_string->string += "\n";
1060                     last_string->updateRepr();
1061                 } else {
1062                     // Insert new string with '\n'.
1063                     auto tspan_node = tspan->getRepr();
1064                     auto xml_doc = tspan_node->document();
1065                     tspan_node->appendChild(xml_doc->createTextNode("\n"));
1066                 }
1067             }
1068         }
1069     }
1070 }
1071 
is_horizontal() const1072 bool SPText::is_horizontal() const
1073 {
1074     unsigned mode = style->writing_mode.computed;
1075     return (mode == SP_CSS_WRITING_MODE_LR_TB || mode == SP_CSS_WRITING_MODE_RL_TB);
1076 }
1077 
has_inline_size() const1078 bool SPText::has_inline_size() const
1079 {
1080     // If inline size is '0' it is as if it is not set.
1081     return (style->inline_size.set && style->inline_size.value != 0);
1082 }
1083 
has_shape_inside() const1084 bool SPText::has_shape_inside() const
1085 {
1086     return (style->shape_inside.set);
1087 }
1088 
1089 // Gets rectangle defined by <text> x, y and inline-size ("infinite" in one direction).
get_frame()1090 Geom::OptRect SPText::get_frame()
1091 {
1092     Geom::OptRect opt_frame;
1093     Geom::Rect frame;
1094 
1095     if (has_inline_size()) {
1096         double inline_size = style->inline_size.computed;
1097         //unsigned mode      = style->writing_mode.computed;
1098         unsigned anchor    = style->text_anchor.computed;
1099         unsigned direction = style->direction.computed;
1100 
1101         if (is_horizontal()) {
1102             // horizontal
1103             frame = Geom::Rect::from_xywh(attributes.firstXY()[Geom::X], -100000, inline_size, 200000);
1104             if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
1105                 frame *= Geom::Translate (-inline_size/2.0, 0 );
1106             } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END  ) ||
1107                         (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
1108                 frame *= Geom::Translate (-inline_size, 0);
1109             }
1110         } else {
1111             // vertical
1112             frame = Geom::Rect::from_xywh(-100000, attributes.firstXY()[Geom::Y], 200000, inline_size);
1113             if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
1114                 frame *= Geom::Translate (0, -inline_size/2.0);
1115             } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
1116                 frame *= Geom::Translate (0, -inline_size);
1117             }
1118         }
1119 
1120         opt_frame = frame;
1121 
1122     } else {
1123         // See if 'shape-inside' has rectangle
1124         Inkscape::XML::Node* rectangle = get_first_rectangle();
1125 
1126         if (rectangle) {
1127             double x = 0.0;
1128             double y = 0.0;
1129             double width = 0.0;
1130             double height = 0.0;
1131             sp_repr_get_double (rectangle, "x",      &x);
1132             sp_repr_get_double (rectangle, "y",      &y);
1133             sp_repr_get_double (rectangle, "width",  &width);
1134             sp_repr_get_double (rectangle, "height", &height);
1135             frame = Geom::Rect::from_xywh( x, y, width, height);
1136             opt_frame = frame;
1137         }
1138     }
1139 
1140     return opt_frame;
1141 }
1142 
1143 // Find the node of the first rectangle (if it exists) in 'shape-inside'.
get_first_rectangle()1144 Inkscape::XML::Node* SPText::get_first_rectangle()
1145 {
1146     if (style->shape_inside.set) {
1147 
1148         for (auto *href : style->shape_inside.hrefs) {
1149             auto *shape = href->getObject();
1150             if (dynamic_cast<SPRect*>(shape)) {
1151                 auto *item = shape->getRepr();
1152                 g_return_val_if_fail(item, nullptr);
1153                 assert(strncmp("svg:rect", item->name(), 8) == 0);
1154                 return item;
1155             }
1156         }
1157     }
1158 
1159     return nullptr;
1160 }
1161 
1162 /**
1163  * Get the first shape reference which affects the position and layout of
1164  * this text item. This can be either a shape-inside or a textPath referenced
1165  * shape. If this text does not depend on any other shape, then return NULL.
1166  */
get_first_shape_dependency()1167 SPItem *SPText::get_first_shape_dependency()
1168 {
1169     if (style->shape_inside.set) {
1170         for (auto *href : style->shape_inside.hrefs) {
1171             return href->getObject();
1172         }
1173     } else if (auto textpath = dynamic_cast<SPTextPath *>(firstChild())) {
1174         return sp_textpath_get_path_item(textpath);
1175     }
1176 
1177     return nullptr;
1178 }
1179 
create_text_with_inline_size(SPDesktop * desktop,Geom::Point p0,Geom::Point p1)1180 SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1)
1181 {
1182     SPDocument *doc = desktop->getDocument();
1183 
1184     Inkscape::XML::Document *xml_doc = doc->getReprDoc();
1185     Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text");
1186     text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
1187 
1188     SPText *text_object = dynamic_cast<SPText *>(desktop->currentLayer()->appendChildRepr(text_repr));
1189     g_assert(text_object != nullptr);
1190 
1191     // Invert coordinate system?
1192     p0 *= desktop->dt2doc();
1193     p1 *= desktop->dt2doc();
1194 
1195     // Pixels to user units
1196     p0 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
1197     p1 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse();
1198 
1199     sp_repr_set_svg_double( text_repr, "x", p0[Geom::X]);
1200     sp_repr_set_svg_double( text_repr, "y", p0[Geom::Y]);
1201 
1202     double inline_size = p1[Geom::X] - p0[Geom::X];
1203 
1204     text_object->style->inline_size.setDouble( inline_size );
1205     text_object->style->inline_size.set = true;
1206 
1207     Inkscape::XML::Node *text_node = xml_doc->createTextNode("");
1208     text_repr->appendChild(text_node);
1209 
1210     SPItem *item = dynamic_cast<SPItem *>(desktop->currentLayer());
1211     g_assert(item != nullptr);
1212 
1213     // text_object->transform = item->i2doc_affine().inverse();
1214 
1215     text_object->updateRepr();
1216 
1217     Inkscape::GC::release(text_repr);
1218     Inkscape::GC::release(text_node);
1219 
1220     return text_object;
1221 }
1222 
create_text_with_rectangle(SPDesktop * desktop,Geom::Point p0,Geom::Point p1)1223 SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1)
1224 {
1225     SPDocument *doc = desktop->getDocument();
1226     auto const parent = dynamic_cast<SPItem *>(desktop->currentLayer());
1227     assert(parent);
1228 
1229     Inkscape::XML::Document *xml_doc = doc->getReprDoc();
1230     Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text");
1231     text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create
1232     text_repr->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(parent->i2doc_affine().inverse()));
1233 
1234     SPText *text_object = dynamic_cast<SPText *>(desktop->currentLayer()->appendChildRepr(text_repr));
1235     g_assert(text_object != nullptr);
1236 
1237     // Invert coordinate system?
1238     p0 *= desktop->dt2doc();
1239     p1 *= desktop->dt2doc();
1240 
1241     // Create rectangle
1242     Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
1243     sp_repr_set_svg_double( rect_repr, "x", p0[Geom::X]);
1244     sp_repr_set_svg_double( rect_repr, "y", p0[Geom::Y]);
1245     sp_repr_set_svg_double( rect_repr, "width",  abs(p1[Geom::X]-p0[Geom::X]));
1246     sp_repr_set_svg_double( rect_repr, "height", abs(p1[Geom::Y]-p0[Geom::Y]));
1247 
1248     // Find defs, if does not exist, create.
1249     Inkscape::XML::Node *defs_repr = sp_repr_lookup_name (xml_doc->root(), "svg:defs");
1250     if (defs_repr == nullptr) {
1251         defs_repr = xml_doc->createElement("svg:defs");
1252         xml_doc->root()->addChild(defs_repr, nullptr);
1253     }
1254     else Inkscape::GC::anchor(defs_repr);
1255 
1256     // Add rectangle to defs.
1257     defs_repr->addChild(rect_repr, nullptr);
1258 
1259     // Apply desktop style (do before adding "shape-inside").
1260     sp_desktop_apply_style_tool(desktop, text_repr, "/tools/text", true);
1261     SPCSSAttr *css = sp_repr_css_attr(text_repr, "style" );
1262     sp_repr_css_set_property (css, "white-space", "pre");  // Respect new lines.
1263 
1264     // Link rectangle to text
1265     std::string value("url(#");
1266     value += rect_repr->attribute("id");
1267     value += ")";
1268     sp_repr_css_set_property (css, "shape-inside", value.c_str());
1269     sp_repr_css_set(text_repr, css, "style");
1270 
1271     sp_repr_css_attr_unref(css);
1272 
1273     /* Create <tspan> */
1274     Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan");
1275     rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan?
1276     Inkscape::XML::Node *text_node = xml_doc->createTextNode("");
1277     rtspan->appendChild(text_node);
1278     text_repr->appendChild(rtspan);
1279 
1280     SPItem *item = dynamic_cast<SPItem *>(desktop->currentLayer());
1281     g_assert(item != nullptr);
1282 
1283     Inkscape::GC::release(rtspan);
1284     Inkscape::GC::release(text_repr);
1285     Inkscape::GC::release(text_node);
1286     Inkscape::GC::release(defs_repr);
1287     Inkscape::GC::release(rect_repr);
1288 
1289     return text_object;
1290 }
1291 
1292 /*
1293  * TextTagAttributes implementation
1294  */
1295 
1296 // Not used.
1297 // void TextTagAttributes::readFrom(Inkscape::XML::Node const *node)
1298 // {
1299 //     readSingleAttribute(SPAttr::X, node->attribute("x"));
1300 //     readSingleAttribute(SPAttr::Y, node->attribute("y"));
1301 //     readSingleAttribute(SPAttr::DX, node->attribute("dx"));
1302 //     readSingleAttribute(SPAttr::DY, node->attribute("dy"));
1303 //     readSingleAttribute(SPAttr::ROTATE, node->attribute("rotate"));
1304 //     readSingleAttribute(SPAttr::TEXTLENGTH, node->attribute("textLength"));
1305 //     readSingleAttribute(SPAttr::LENGTHADJUST, node->attribute("lengthAdjust"));
1306 // }
1307 
readSingleAttribute(SPAttr key,gchar const * value,SPStyle const * style,Geom::Rect const * viewport)1308 bool TextTagAttributes::readSingleAttribute(SPAttr key, gchar const *value, SPStyle const *style, Geom::Rect const *viewport)
1309 {
1310     // std::cout << "TextTagAttributes::readSingleAttribute: key: " << key
1311     //           << "  value: " << (value?value:"Null") << std::endl;
1312     std::vector<SVGLength> *attr_vector;
1313     bool update_x = false;
1314     bool update_y = false;
1315     switch (key) {
1316         case SPAttr::X:      attr_vector = &attributes.x;  update_x = true; break;
1317         case SPAttr::Y:      attr_vector = &attributes.y;  update_y = true; break;
1318         case SPAttr::DX:     attr_vector = &attributes.dx; update_x = true; break;
1319         case SPAttr::DY:     attr_vector = &attributes.dy; update_y = true; break;
1320         case SPAttr::ROTATE: attr_vector = &attributes.rotate; break;
1321         case SPAttr::TEXTLENGTH:
1322             attributes.textLength.readOrUnset(value);
1323             return true;
1324             break;
1325         case SPAttr::LENGTHADJUST:
1326             attributes.lengthAdjust = (value && !strcmp(value, "spacingAndGlyphs")?
1327                                         Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS :
1328                                         Inkscape::Text::Layout::LENGTHADJUST_SPACING); // default is "spacing"
1329             return true;
1330             break;
1331         default: return false;
1332     }
1333 
1334     // FIXME: sp_svg_length_list_read() amalgamates repeated separators. This prevents unset values.
1335     *attr_vector = sp_svg_length_list_read(value);
1336 
1337     if( (update_x || update_y) && style != nullptr && viewport != nullptr ) {
1338         double const w = viewport->width();
1339         double const h = viewport->height();
1340         double const em = style->font_size.computed;
1341         double const ex = em * 0.5;
1342         for(auto & it : *attr_vector) {
1343             if( update_x )
1344                 it.update( em, ex, w );
1345             if( update_y )
1346                 it.update( em, ex, h );
1347         }
1348     }
1349     return true;
1350 }
1351 
writeTo(Inkscape::XML::Node * node) const1352 void TextTagAttributes::writeTo(Inkscape::XML::Node *node) const
1353 {
1354     writeSingleAttributeVector(node, "x", attributes.x);
1355     writeSingleAttributeVector(node, "y", attributes.y);
1356     writeSingleAttributeVector(node, "dx", attributes.dx);
1357     writeSingleAttributeVector(node, "dy", attributes.dy);
1358     writeSingleAttributeVector(node, "rotate", attributes.rotate);
1359 
1360     writeSingleAttributeLength(node, "textLength", attributes.textLength);
1361 
1362     if (attributes.textLength._set) {
1363         if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) {
1364             node->setAttribute("lengthAdjust", "spacing");
1365         } else if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) {
1366             node->setAttribute("lengthAdjust", "spacingAndGlyphs");
1367         }
1368     }
1369 }
1370 
update(double em,double ex,double w,double h)1371 void TextTagAttributes::update( double em, double ex, double w, double h )
1372 {
1373     for(auto & it : attributes.x) {
1374         it.update( em, ex, w );
1375     }
1376     for(auto & it : attributes.y) {
1377         it.update( em, ex, h );
1378     }
1379     for(auto & it : attributes.dx) {
1380         it.update( em, ex, w );
1381     }
1382     for(auto & it : attributes.dy) {
1383         it.update( em, ex, h );
1384     }
1385 }
1386 
writeSingleAttributeLength(Inkscape::XML::Node * node,gchar const * key,const SVGLength & length)1387 void TextTagAttributes::writeSingleAttributeLength(Inkscape::XML::Node *node, gchar const *key, const SVGLength &length)
1388 {
1389     if (length._set) {
1390         node->setAttribute(key, length.write());
1391     } else
1392         node->removeAttribute(key);
1393 }
1394 
writeSingleAttributeVector(Inkscape::XML::Node * node,gchar const * key,std::vector<SVGLength> const & attr_vector)1395 void TextTagAttributes::writeSingleAttributeVector(Inkscape::XML::Node *node, gchar const *key, std::vector<SVGLength> const &attr_vector)
1396 {
1397     if (attr_vector.empty())
1398         node->removeAttribute(key);
1399     else {
1400         Glib::ustring string;
1401 
1402         // FIXME: this has no concept of unset values because sp_svg_length_list_read() can't read them back in
1403         for (auto it : attr_vector) {
1404             if (!string.empty()) string += ' ';
1405             string += it.write();
1406         }
1407         node->setAttributeOrRemoveIfEmpty(key, string);
1408     }
1409 }
1410 
singleXYCoordinates() const1411 bool TextTagAttributes::singleXYCoordinates() const
1412 {
1413     return attributes.x.size() <= 1 && attributes.y.size() <= 1;
1414 }
1415 
anyAttributesSet() const1416 bool TextTagAttributes::anyAttributesSet() const
1417 {
1418     return !attributes.x.empty() || !attributes.y.empty() || !attributes.dx.empty() || !attributes.dy.empty() || !attributes.rotate.empty();
1419 }
1420 
firstXY() const1421 Geom::Point TextTagAttributes::firstXY() const
1422 {
1423     Geom::Point point;
1424     if (attributes.x.empty()) point[Geom::X] = 0.0;
1425     else point[Geom::X] = attributes.x[0].computed;
1426     if (attributes.y.empty()) point[Geom::Y] = 0.0;
1427     else point[Geom::Y] = attributes.y[0].computed;
1428     return point;
1429 }
1430 
setFirstXY(Geom::Point & point)1431 void TextTagAttributes::setFirstXY(Geom::Point &point)
1432 {
1433     SVGLength zero_length;
1434     zero_length = 0.0;
1435 
1436     if (attributes.x.empty())
1437         attributes.x.resize(1, zero_length);
1438     if (attributes.y.empty())
1439         attributes.y.resize(1, zero_length);
1440     attributes.x[0] = point[Geom::X];
1441     attributes.y[0] = point[Geom::Y];
1442 }
1443 
getFirstXLength()1444 SVGLength* TextTagAttributes::getFirstXLength()
1445 {
1446     if (!attributes.x.empty()) {
1447         return &attributes.x[0];
1448     } else {
1449         return nullptr;
1450     }
1451 }
1452 
getFirstYLength()1453 SVGLength* TextTagAttributes::getFirstYLength()
1454 {
1455     if (!attributes.y.empty()) {
1456         return &attributes.y[0];
1457     } else {
1458         return nullptr;
1459     }
1460 }
1461 
1462 // Instance of TextTagAttributes contains attributes as defined by text/tspan element.
1463 // output: What will be sent to the rendering engine.
1464 // parent_attrs: Attributes collected from all ancestors.
1465 // parent_attrs_offset: Where this element fits into the parent_attrs.
1466 // copy_xy: Should this elements x, y attributes contribute to output (can preserve set values but not use them... kind of strange).
1467 // copy_dxdxrotate: Should this elements dx, dy, rotate attributes contribute to output.
mergeInto(Inkscape::Text::Layout::OptionalTextTagAttrs * output,Inkscape::Text::Layout::OptionalTextTagAttrs const & parent_attrs,unsigned parent_attrs_offset,bool copy_xy,bool copy_dxdyrotate) const1468 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
1469 {
1470     mergeSingleAttribute(&output->x,      parent_attrs.x,      parent_attrs_offset, copy_xy ? &attributes.x : nullptr);
1471     mergeSingleAttribute(&output->y,      parent_attrs.y,      parent_attrs_offset, copy_xy ? &attributes.y : nullptr);
1472     mergeSingleAttribute(&output->dx,     parent_attrs.dx,     parent_attrs_offset, copy_dxdyrotate ? &attributes.dx : nullptr);
1473     mergeSingleAttribute(&output->dy,     parent_attrs.dy,     parent_attrs_offset, copy_dxdyrotate ? &attributes.dy : nullptr);
1474     mergeSingleAttribute(&output->rotate, parent_attrs.rotate, parent_attrs_offset, copy_dxdyrotate ? &attributes.rotate : nullptr);
1475     if (attributes.textLength._set) { // only from current node, this is not inherited from parent
1476         output->textLength.value = attributes.textLength.value;
1477         output->textLength.computed = attributes.textLength.computed;
1478         output->textLength.unit = attributes.textLength.unit;
1479         output->textLength._set = attributes.textLength._set;
1480         output->lengthAdjust = attributes.lengthAdjust;
1481     }
1482 }
1483 
mergeSingleAttribute(std::vector<SVGLength> * output_list,std::vector<SVGLength> const & parent_list,unsigned parent_offset,std::vector<SVGLength> const * overlay_list)1484 void TextTagAttributes::mergeSingleAttribute(std::vector<SVGLength> *output_list, std::vector<SVGLength> const &parent_list, unsigned parent_offset, std::vector<SVGLength> const *overlay_list)
1485 {
1486     output_list->clear();
1487     if (overlay_list == nullptr) {
1488         if (parent_list.size() > parent_offset)
1489         {
1490             output_list->reserve(parent_list.size() - parent_offset);
1491             std::copy(parent_list.begin() + parent_offset, parent_list.end(), std::back_inserter(*output_list));
1492         }
1493     } else {
1494         output_list->reserve(std::max((int)parent_list.size() - (int)parent_offset, (int)overlay_list->size()));
1495         unsigned overlay_offset = 0;
1496         while (parent_offset < parent_list.size() || overlay_offset < overlay_list->size()) {
1497             SVGLength const *this_item;
1498             if (overlay_offset < overlay_list->size()) {
1499                 this_item = &(*overlay_list)[overlay_offset];
1500                 overlay_offset++;
1501                 parent_offset++;
1502             } else {
1503                 this_item = &parent_list[parent_offset];
1504                 parent_offset++;
1505             }
1506             output_list->push_back(*this_item);
1507         }
1508     }
1509 }
1510 
erase(unsigned start_index,unsigned n)1511 void TextTagAttributes::erase(unsigned start_index, unsigned n)
1512 {
1513     if (n == 0) return;
1514     if (!singleXYCoordinates()) {
1515         eraseSingleAttribute(&attributes.x, start_index, n);
1516         eraseSingleAttribute(&attributes.y, start_index, n);
1517     }
1518     eraseSingleAttribute(&attributes.dx, start_index, n);
1519     eraseSingleAttribute(&attributes.dy, start_index, n);
1520     eraseSingleAttribute(&attributes.rotate, start_index, n);
1521 }
1522 
eraseSingleAttribute(std::vector<SVGLength> * attr_vector,unsigned start_index,unsigned n)1523 void TextTagAttributes::eraseSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n)
1524 {
1525     if (attr_vector->size() <= start_index) return;
1526     if (attr_vector->size() <= start_index + n)
1527         attr_vector->erase(attr_vector->begin() + start_index, attr_vector->end());
1528     else
1529         attr_vector->erase(attr_vector->begin() + start_index, attr_vector->begin() + start_index + n);
1530 }
1531 
insert(unsigned start_index,unsigned n)1532 void TextTagAttributes::insert(unsigned start_index, unsigned n)
1533 {
1534     if (n == 0) return;
1535     if (!singleXYCoordinates()) {
1536         insertSingleAttribute(&attributes.x, start_index, n, true);
1537         insertSingleAttribute(&attributes.y, start_index, n, true);
1538     }
1539     insertSingleAttribute(&attributes.dx, start_index, n, false);
1540     insertSingleAttribute(&attributes.dy, start_index, n, false);
1541     insertSingleAttribute(&attributes.rotate, start_index, n, false);
1542 }
1543 
insertSingleAttribute(std::vector<SVGLength> * attr_vector,unsigned start_index,unsigned n,bool is_xy)1544 void TextTagAttributes::insertSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n, bool is_xy)
1545 {
1546     if (attr_vector->size() <= start_index) return;
1547     SVGLength zero_length;
1548     zero_length = 0.0;
1549     attr_vector->insert(attr_vector->begin() + start_index, n, zero_length);
1550     if (is_xy) {
1551         double begin = start_index == 0 ? (*attr_vector)[start_index + n].computed : (*attr_vector)[start_index - 1].computed;
1552         double diff = ((*attr_vector)[start_index + n].computed - begin) / n;   // n tested for nonzero in insert()
1553         for (unsigned i = 0 ; i < n ; i++)
1554             (*attr_vector)[start_index + i] = begin + diff * i;
1555     }
1556 }
1557 
split(unsigned index,TextTagAttributes * second)1558 void TextTagAttributes::split(unsigned index, TextTagAttributes *second)
1559 {
1560     if (!singleXYCoordinates()) {
1561         splitSingleAttribute(&attributes.x, index, &second->attributes.x, false);
1562         splitSingleAttribute(&attributes.y, index, &second->attributes.y, false);
1563     }
1564     splitSingleAttribute(&attributes.dx, index, &second->attributes.dx, true);
1565     splitSingleAttribute(&attributes.dy, index, &second->attributes.dy, true);
1566     splitSingleAttribute(&attributes.rotate, index, &second->attributes.rotate, true);
1567 }
1568 
splitSingleAttribute(std::vector<SVGLength> * first_vector,unsigned index,std::vector<SVGLength> * second_vector,bool trimZeros)1569 void TextTagAttributes::splitSingleAttribute(std::vector<SVGLength> *first_vector, unsigned index, std::vector<SVGLength> *second_vector, bool trimZeros)
1570 {
1571     second_vector->clear();
1572     if (first_vector->size() <= index) return;
1573     second_vector->resize(first_vector->size() - index);
1574     std::copy(first_vector->begin() + index, first_vector->end(), second_vector->begin());
1575     first_vector->resize(index);
1576     if (trimZeros)
1577         while (!first_vector->empty() && (!first_vector->back()._set || first_vector->back().value == 0.0))
1578             first_vector->resize(first_vector->size() - 1);
1579 }
1580 
join(TextTagAttributes const & first,TextTagAttributes const & second,unsigned second_index)1581 void TextTagAttributes::join(TextTagAttributes const &first, TextTagAttributes const &second, unsigned second_index)
1582 {
1583     if (second.singleXYCoordinates()) {
1584         attributes.x = first.attributes.x;
1585         attributes.y = first.attributes.y;
1586     } else {
1587         joinSingleAttribute(&attributes.x, first.attributes.x, second.attributes.x, second_index);
1588         joinSingleAttribute(&attributes.y, first.attributes.y, second.attributes.y, second_index);
1589     }
1590     joinSingleAttribute(&attributes.dx, first.attributes.dx, second.attributes.dx, second_index);
1591     joinSingleAttribute(&attributes.dy, first.attributes.dy, second.attributes.dy, second_index);
1592     joinSingleAttribute(&attributes.rotate, first.attributes.rotate, second.attributes.rotate, second_index);
1593 }
1594 
joinSingleAttribute(std::vector<SVGLength> * dest_vector,std::vector<SVGLength> const & first_vector,std::vector<SVGLength> const & second_vector,unsigned second_index)1595 void TextTagAttributes::joinSingleAttribute(std::vector<SVGLength> *dest_vector, std::vector<SVGLength> const &first_vector, std::vector<SVGLength> const &second_vector, unsigned second_index)
1596 {
1597     if (second_vector.empty())
1598         *dest_vector = first_vector;
1599     else {
1600         dest_vector->resize(second_index + second_vector.size());
1601         if (first_vector.size() < second_index) {
1602             std::copy(first_vector.begin(), first_vector.end(), dest_vector->begin());
1603             SVGLength zero_length;
1604             zero_length = 0.0;
1605             std::fill(dest_vector->begin() + first_vector.size(), dest_vector->begin() + second_index, zero_length);
1606         } else
1607             std::copy(first_vector.begin(), first_vector.begin() + second_index, dest_vector->begin());
1608         std::copy(second_vector.begin(), second_vector.end(), dest_vector->begin() + second_index);
1609     }
1610 }
1611 
transform(Geom::Affine const & matrix,double scale_x,double scale_y,bool extend_zero_length)1612 void TextTagAttributes::transform(Geom::Affine const &matrix, double scale_x, double scale_y, bool extend_zero_length)
1613 {
1614     SVGLength zero_length;
1615     zero_length = 0.0;
1616 
1617     /* edge testcases for this code:
1618        1) moving text elements whose position is done entirely with transform="...", no x,y attributes
1619        2) unflowing multi-line flowtext then moving it (it has x but not y)
1620     */
1621     unsigned points_count = std::max(attributes.x.size(), attributes.y.size());
1622     if (extend_zero_length && points_count < 1)
1623         points_count = 1;
1624     for (unsigned i = 0 ; i < points_count ; i++) {
1625         Geom::Point point;
1626         if (i < attributes.x.size()) point[Geom::X] = attributes.x[i].computed;
1627         else point[Geom::X] = 0.0;
1628         if (i < attributes.y.size()) point[Geom::Y] = attributes.y[i].computed;
1629         else point[Geom::Y] = 0.0;
1630         point *= matrix;
1631         if (i < attributes.x.size())
1632             attributes.x[i] = point[Geom::X];
1633         else if (point[Geom::X] != 0.0 && extend_zero_length) {
1634             attributes.x.resize(i + 1, zero_length);
1635             attributes.x[i] = point[Geom::X];
1636         }
1637         if (i < attributes.y.size())
1638             attributes.y[i] = point[Geom::Y];
1639         else if (point[Geom::Y] != 0.0 && extend_zero_length) {
1640             attributes.y.resize(i + 1, zero_length);
1641             attributes.y[i] = point[Geom::Y];
1642         }
1643     }
1644     for (auto & it : attributes.dx)
1645         it = it.computed * scale_x;
1646     for (auto & it : attributes.dy)
1647         it = it.computed * scale_y;
1648 }
1649 
getDx(unsigned index)1650 double TextTagAttributes::getDx(unsigned index)
1651 {
1652     if( attributes.dx.empty()) {
1653         return 0.0;
1654     }
1655     if( index < attributes.dx.size() ) {
1656         return attributes.dx[index].computed;
1657     } else {
1658         return 0.0; // attributes.dx.back().computed;
1659     }
1660 }
1661 
1662 
getDy(unsigned index)1663 double TextTagAttributes::getDy(unsigned index)
1664 {
1665     if( attributes.dy.empty() ) {
1666         return 0.0;
1667     }
1668     if( index < attributes.dy.size() ) {
1669         return attributes.dy[index].computed;
1670     } else {
1671         return 0.0; // attributes.dy.back().computed;
1672     }
1673 }
1674 
1675 
addToDx(unsigned index,double delta)1676 void TextTagAttributes::addToDx(unsigned index, double delta)
1677 {
1678     SVGLength zero_length;
1679     zero_length = 0.0;
1680 
1681     if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length);
1682     attributes.dx[index] = attributes.dx[index].computed + delta;
1683 }
1684 
addToDy(unsigned index,double delta)1685 void TextTagAttributes::addToDy(unsigned index, double delta)
1686 {
1687     SVGLength zero_length;
1688     zero_length = 0.0;
1689 
1690     if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length);
1691     attributes.dy[index] = attributes.dy[index].computed + delta;
1692 }
1693 
addToDxDy(unsigned index,Geom::Point const & adjust)1694 void TextTagAttributes::addToDxDy(unsigned index, Geom::Point const &adjust)
1695 {
1696     SVGLength zero_length;
1697     zero_length = 0.0;
1698 
1699     if (adjust[Geom::X] != 0.0) {
1700         if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length);
1701         attributes.dx[index] = attributes.dx[index].computed + adjust[Geom::X];
1702     }
1703     if (adjust[Geom::Y] != 0.0) {
1704         if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length);
1705         attributes.dy[index] = attributes.dy[index].computed + adjust[Geom::Y];
1706     }
1707 }
1708 
getRotate(unsigned index)1709 double TextTagAttributes::getRotate(unsigned index)
1710 {
1711     if( attributes.rotate.empty() ) {
1712         return 0.0;
1713     }
1714     if( index < attributes.rotate.size() ) {
1715         return attributes.rotate[index].computed;
1716     } else {
1717         return attributes.rotate.back().computed;
1718     }
1719 }
1720 
1721 
addToRotate(unsigned index,double delta)1722 void TextTagAttributes::addToRotate(unsigned index, double delta)
1723 {
1724     SVGLength zero_length;
1725     zero_length = 0.0;
1726 
1727     if (attributes.rotate.size() < index + 2) {
1728         if (attributes.rotate.empty())
1729             attributes.rotate.resize(index + 2, zero_length);
1730         else
1731             attributes.rotate.resize(index + 2, attributes.rotate.back());
1732     }
1733     attributes.rotate[index] = mod360(attributes.rotate[index].computed + delta);
1734 }
1735 
1736 
setRotate(unsigned index,double angle)1737 void TextTagAttributes::setRotate(unsigned index, double angle)
1738 {
1739     SVGLength zero_length;
1740     zero_length = 0.0;
1741 
1742     if (attributes.rotate.size() < index + 2) {
1743         if (attributes.rotate.empty())
1744             attributes.rotate.resize(index + 2, zero_length);
1745         else
1746             attributes.rotate.resize(index + 2, attributes.rotate.back());
1747     }
1748     attributes.rotate[index] = mod360(angle);
1749 }
1750 
1751 
1752 /*
1753   Local Variables:
1754   mode:c++
1755   c-file-style:"stroustrup"
1756   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1757   indent-tabs-mode:nil
1758   fill-column:99
1759   End:
1760 */
1761 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1762