// SPDX-License-Identifier: GPL-2.0-or-later /* * Text commands * * Authors: * bulia byak * Jon A. Cruz * Abhishek Sharma * * Copyright (C) 2004 authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include #include "desktop.h" #include "document-undo.h" #include "document.h" #include "inkscape.h" #include "message-stack.h" #include "text-chemistry.h" #include "text-editing.h" #include "object/sp-flowdiv.h" #include "object/sp-flowregion.h" #include "object/sp-flowtext.h" #include "object/sp-rect.h" #include "object/sp-textpath.h" #include "object/sp-tspan.h" #include "style.h" #include "ui/icon-names.h" #include "xml/repr.h" using Inkscape::DocumentUndo; static SPItem * text_or_flowtext_in_selection(Inkscape::Selection *selection) { auto items = selection->items(); for(auto i=items.begin();i!=items.end();++i){ if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) return *i; } return nullptr; } static SPItem * shape_in_selection(Inkscape::Selection *selection) { auto items = selection->items(); for(auto i=items.begin();i!=items.end();++i){ if (SP_IS_SHAPE(*i)) return *i; } return nullptr; } void text_put_on_path() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (!desktop) return; Inkscape::Selection *selection = desktop->getSelection(); SPItem *text = text_or_flowtext_in_selection(selection); SPItem *shape = shape_in_selection(selection); Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); if (!text || !shape || boost::distance(selection->items()) != 2) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a text and a path to put text on path.")); return; } if (SP_IS_TEXT_TEXTPATH(text)) { desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("This text object is already put on a path. Remove it from the path first. Use Shift+D to look up its path.")); return; } if (SP_IS_RECT(shape)) { // rect is the only SPShape which is not yet, and thus SVG forbids us from putting text on it desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("You cannot put text on a rectangle in this version. Convert rectangle to path first.")); return; } // if a flowed text is selected, convert it to a regular text object if (SP_IS_FLOWTEXT(text)) { if (!SP_FLOWTEXT(text)->layout.outputExists()) { desktop->getMessageStack()-> flash(Inkscape::WARNING_MESSAGE, _("The flowed text(s) must be visible in order to be put on a path.")); } Inkscape::XML::Node *repr = SP_FLOWTEXT(text)->getAsText(); if (!repr) return; Inkscape::XML::Node *parent = text->getRepr()->parent(); parent->appendChild(repr); SPItem *new_item = (SPItem *) desktop->getDocument()->getObjectByRepr(repr); new_item->doWriteTransform(text->transform); new_item->updateRepr(); Inkscape::GC::release(repr); text->deleteObject(); // delete the original flowtext desktop->getDocument()->ensureUpToDate(); selection->clear(); text = new_item; // point to the new text } if (auto textitem = dynamic_cast(text)) { // Replace any new lines (including sodipodi:role="line") by spaces. textitem->remove_newlines(); } Inkscape::Text::Layout const *layout = te_get_layout(text); Inkscape::Text::Layout::Alignment text_alignment = layout->paragraphAlignment(layout->begin()); // remove transform from text, but recursively scale text's fontsize by the expansion SP_TEXT(text)->_adjustFontsizeRecursive (text, text->transform.descrim()); text->removeAttribute("transform"); // make a list of text children std::vector text_reprs; for(auto& o: text->children) { text_reprs.push_back(o.getRepr()); } // create textPath and put it into the text Inkscape::XML::Node *textpath = xml_doc->createElement("svg:textPath"); // reference the shape gchar *href_str = g_strdup_printf("#%s", shape->getRepr()->attribute("id")); textpath->setAttribute("xlink:href", href_str); g_free(href_str); if (text_alignment == Inkscape::Text::Layout::RIGHT) { textpath->setAttribute("startOffset", "100%"); } else if (text_alignment == Inkscape::Text::Layout::CENTER) { textpath->setAttribute("startOffset", "50%"); } text->getRepr()->addChild(textpath, nullptr); for (auto i=text_reprs.rbegin();i!=text_reprs.rend();++i) { // Make a copy of each text child Inkscape::XML::Node *copy = (*i)->duplicate(xml_doc); // We cannot have multiline in textpath, so remove line attrs from tspans if (!strcmp(copy->name(), "svg:tspan")) { copy->removeAttribute("sodipodi:role"); copy->removeAttribute("x"); copy->removeAttribute("y"); } // remove the old repr from under text text->getRepr()->removeChild(*i); // put its copy into under textPath textpath->addChild(copy, nullptr); // fixme: copy id } // x/y are useless with textpath, and confuse Batik 1.5 text->removeAttribute("x"); text->removeAttribute("y"); DocumentUndo::done(desktop->getDocument(), _("Put text on path"), INKSCAPE_ICON("draw-text")); } void text_remove_from_path() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; Inkscape::Selection *selection = desktop->getSelection(); if (selection->isEmpty()) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a text on path to remove it from path.")); return; } bool did = false; auto items = selection->items(); for(auto i=items.begin();i!=items.end();++i){ SPObject *obj = *i; if (SP_IS_TEXT_TEXTPATH(obj)) { SPObject *tp = obj->firstChild(); did = true; sp_textpath_to_text(tp); } } if (!did) { desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("No texts-on-paths in the selection.")); } else { DocumentUndo::done(desktop->getDocument(), _("Remove text from path"), INKSCAPE_ICON("draw-text")); std::vector vec(selection->items().begin(), selection->items().end()); selection->setList(vec); // reselect to update statusbar description } } static void text_remove_all_kerns_recursively(SPObject *o) { o->removeAttribute("dx"); o->removeAttribute("dy"); o->removeAttribute("rotate"); // if x contains a list, leave only the first value gchar const *x = o->getRepr()->attribute("x"); if (x) { gchar **xa_space = g_strsplit(x, " ", 0); gchar **xa_comma = g_strsplit(x, ",", 0); if (xa_space && *xa_space && *(xa_space + 1)) { o->setAttribute("x", *xa_space); } else if (xa_comma && *xa_comma && *(xa_comma + 1)) { o->setAttribute("x", *xa_comma); } g_strfreev(xa_space); g_strfreev(xa_comma); } for (auto& i: o->children) { text_remove_all_kerns_recursively(&i); i.requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); } } //FIXME: must work with text selection void text_remove_all_kerns() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; Inkscape::Selection *selection = desktop->getSelection(); if (selection->isEmpty()) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select text(s) to remove kerns from.")); return; } bool did = false; auto items = selection->items(); for(auto i=items.begin();i!=items.end();++i){ SPObject *obj = *i; if (!SP_IS_TEXT(obj) && !SP_IS_TSPAN(obj) && !SP_IS_FLOWTEXT(obj)) { continue; } text_remove_all_kerns_recursively(obj); obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); did = true; } if (!did) { desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("Select text(s) to remove kerns from.")); } else { DocumentUndo::done(desktop->getDocument(), _("Remove manual kerns"), INKSCAPE_ICON("draw-text")); } } void text_flow_shape_subtract() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (!desktop) return; SPDocument *doc = desktop->getDocument(); Inkscape::Selection *selection = desktop->getSelection(); SPItem *text = text_or_flowtext_in_selection(selection); if (SP_IS_TEXT(text)) { // Make list of all shapes. Glib::ustring shapes; auto items = selection->items(); for (auto item : items) { if (SP_IS_SHAPE(item)) { if (!shapes.empty()) shapes += " "; shapes += item->getUrl(); } } // Set 'shape-subtract' property. text->style->shape_subtract.read(shapes.c_str()); text->updateRepr(); DocumentUndo::done(doc, _("Flow text subtract shape"), INKSCAPE_ICON("draw-text")); } else { // SVG 1.2 Flowed Text desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Subtraction not available for SVG 1.2 Flowed text.")); } } void text_flow_into_shape() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (!desktop) return; SPDocument *doc = desktop->getDocument(); Inkscape::XML::Document *xml_doc = doc->getReprDoc(); Inkscape::Selection *selection = desktop->getSelection(); SPItem *text = text_or_flowtext_in_selection(selection); SPItem *shape = shape_in_selection(selection); if (!text || !shape || boost::distance(selection->items()) < 2) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a text and one or more paths or shapes to flow text.")); return; } Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (prefs->getBool("/tools/text/use_svg2", true)) { // SVG 2 Text if (SP_IS_TEXT(text)) { // Make list of all shapes. Glib::ustring shapes; auto items = selection->items(); for (auto item : items) { if (SP_IS_SHAPE(item)) { if (!shapes.empty()) { shapes += " "; } else { // can only take one shape into account for transform compensation // compensate transform auto const new_transform = i2i_affine(item->parent, text->parent); auto const ex = text->transform.descrim() / new_transform.descrim(); static_cast(text)->_adjustFontsizeRecursive(text, ex); text->transform = new_transform; } shapes += item->getUrl(); } } // Set 'shape-inside' property. text->style->shape_inside.read(shapes.c_str()); text->style->white_space.read("pre"); // Respect new lines. text->updateRepr(); DocumentUndo::done(doc, _("Flow text into shape"), INKSCAPE_ICON("draw-text")); } } else { if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) { // remove transform from text, but recursively scale text's fontsize by the expansion auto ex = i2i_affine(text, shape->parent).descrim(); SP_TEXT(text)->_adjustFontsizeRecursive(text, ex); text->removeAttribute("transform"); } Inkscape::XML::Node *root_repr = xml_doc->createElement("svg:flowRoot"); root_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create root_repr->setAttribute("style", text->getRepr()->attribute("style")); // fixme: transfer style attrs too shape->parent->getRepr()->appendChild(root_repr); SPObject *root_object = doc->getObjectByRepr(root_repr); g_return_if_fail(SP_IS_FLOWTEXT(root_object)); Inkscape::XML::Node *region_repr = xml_doc->createElement("svg:flowRegion"); root_repr->appendChild(region_repr); SPObject *object = doc->getObjectByRepr(region_repr); g_return_if_fail(SP_IS_FLOWREGION(object)); /* Add clones */ auto items = selection->items(); for(auto i=items.begin();i!=items.end();++i){ SPItem *item = *i; if (SP_IS_SHAPE(item)){ Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); clone->setAttribute("x", "0"); clone->setAttribute("y", "0"); gchar *href_str = g_strdup_printf("#%s", item->getRepr()->attribute("id")); clone->setAttribute("xlink:href", href_str); g_free(href_str); // add the new clone to the region region_repr->appendChild(clone); } } if (SP_IS_TEXT(text)) { // flow from text, as string Inkscape::XML::Node *para_repr = xml_doc->createElement("svg:flowPara"); root_repr->appendChild(para_repr); object = doc->getObjectByRepr(para_repr); g_return_if_fail(SP_IS_FLOWPARA(object)); Inkscape::Text::Layout const *layout = te_get_layout(text); Glib::ustring text_ustring = sp_te_get_string_multiline(text, layout->begin(), layout->end()); Inkscape::XML::Node *text_repr = xml_doc->createTextNode(text_ustring.c_str()); // FIXME: transfer all formatting! and convert newlines into flowParas! para_repr->appendChild(text_repr); Inkscape::GC::release(para_repr); Inkscape::GC::release(text_repr); } else { // reflow an already flowed text, preserving paras for(auto& o: text->children) { if (SP_IS_FLOWPARA(&o)) { Inkscape::XML::Node *para_repr = o.getRepr()->duplicate(xml_doc); root_repr->appendChild(para_repr); object = doc->getObjectByRepr(para_repr); g_return_if_fail(SP_IS_FLOWPARA(object)); Inkscape::GC::release(para_repr); } } } text->deleteObject(true); DocumentUndo::done(doc, _("Flow text into shape"), INKSCAPE_ICON("draw-text")); desktop->getSelection()->set(SP_ITEM(root_object)); Inkscape::GC::release(root_repr); Inkscape::GC::release(region_repr); } } void text_unflow () { SPDesktop *desktop = SP_ACTIVE_DESKTOP; if (!desktop) return; SPDocument *doc = desktop->getDocument(); Inkscape::XML::Document *xml_doc = doc->getReprDoc(); Inkscape::Selection *selection = desktop->getSelection(); if (!text_or_flowtext_in_selection(selection) || boost::distance(selection->items()) < 1) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a flowed text to unflow it.")); return; } std::vector new_objs; std::vector old_objs; auto items = selection->items(); for (auto i : items) { SPFlowtext *flowtext = dynamic_cast(i); SPText *text = dynamic_cast(i); if (flowtext) { // we discard transform when unflowing, but we must preserve expansion which is visible as // font size multiplier double ex = (flowtext->transform).descrim(); Glib::ustring text_string = sp_te_get_string_multiline(flowtext); if (text_string.empty()) { // flowtext is empty continue; } /* Create */ Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create /* Set style */ rtext->setAttribute("style", flowtext->getRepr()->attribute("style")); // fixme: transfer style attrs too; // and from descendants Geom::OptRect bbox = flowtext->geometricBounds(flowtext->i2doc_affine()); if (bbox) { Geom::Point xy = bbox->min(); rtext->setAttributeSvgDouble("x", xy[Geom::X]); rtext->setAttributeSvgDouble("y", xy[Geom::Y]); } /* Create */ Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? rtext->addChild(rtspan, nullptr); Inkscape::XML::Node *text_repr = xml_doc->createTextNode(text_string.c_str()); // FIXME: transfer all formatting!!! rtspan->appendChild(text_repr); flowtext->parent->getRepr()->appendChild(rtext); SPObject *text_object = doc->getObjectByRepr(rtext); // restore the font size multiplier from the flowtext's transform SPText *text = SP_TEXT(text_object); text->_adjustFontsizeRecursive(text, ex); new_objs.push_back((SPItem *)text_object); old_objs.push_back(flowtext); Inkscape::GC::release(rtext); Inkscape::GC::release(rtspan); Inkscape::GC::release(text_repr); } else if (text) { if (text->has_shape_inside()) { auto old_point = text->getBaselinePoint(); Inkscape::XML::Node *rtext = text->getRepr(); // Position unflowed text near shape. Geom::OptRect bbox = text->geometricBounds(text->i2doc_affine()); if (bbox) { Geom::Point xy = bbox->min(); rtext->setAttributeSvgDouble("x", xy[Geom::X]); rtext->setAttributeSvgDouble("y", xy[Geom::Y]); } // Remove 'shape-inside' property. SPCSSAttr *css = sp_repr_css_attr(rtext, "style"); sp_repr_css_unset_property(css, "shape-inside"); sp_repr_css_change(rtext, css, "style"); sp_repr_css_attr_unref(css); // We'll leave tspans alone other than stripping 'x' and 'y' (this will preserve // styling). // We'll also remove temporarily 'sodipodi:role' (which shouldn't be // necessary later). for (auto j : text->childList(false)) { SPTSpan* tspan = dynamic_cast(j); if (tspan) { tspan->getRepr()->removeAttribute("x"); tspan->getRepr()->removeAttribute("y"); tspan->getRepr()->removeAttribute("sodipodi:role"); } } // Reposition the text so the baselines don't change. text->rebuildLayout(); auto new_point = text->getBaselinePoint(); if (old_point && new_point) { auto move = Geom::Translate(*old_point - *new_point) * text->transform; text->doWriteTransform(move, &move, false); } } } } // For flowtext objects. if (new_objs.size() != 0) { // Update selection selection->clear(); reverse(new_objs.begin(), new_objs.end()); selection->setList(new_objs); // Delete old objects for (auto i : old_objs) { i->deleteObject(true); } } DocumentUndo::done(doc, _("Unflow flowed text"), INKSCAPE_ICON("draw-text")); } void flowtext_to_text() { SPDesktop *desktop = SP_ACTIVE_DESKTOP; Inkscape::Selection *selection = desktop->getSelection(); if (selection->isEmpty()) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select flowed text(s) to convert.")); return; } bool did = false; bool ignored = false; std::vector reprs; std::vector items(selection->items().begin(), selection->items().end()); for(auto item : items){ if (!SP_IS_FLOWTEXT(item)) continue; if (!SP_FLOWTEXT(item)->layout.outputExists()) { ignored = true; continue; } Inkscape::XML::Node *repr = SP_FLOWTEXT(item)->getAsText(); if (!repr) break; did = true; Inkscape::XML::Node *parent = item->getRepr()->parent(); parent->addChild(repr, item->getRepr()); SPItem *new_item = reinterpret_cast(desktop->getDocument()->getObjectByRepr(repr)); new_item->doWriteTransform(item->transform); new_item->updateRepr(); Inkscape::GC::release(repr); item->deleteObject(); reprs.push_back(repr); } if (did) { DocumentUndo::done(desktop->getDocument(), _("Convert flowed text to text"), INKSCAPE_ICON("text-convert-to-regular")); selection->setReprList(reprs); } else if (ignored) { // no message for (did && ignored) because it is immediately overwritten desktop->getMessageStack()-> flash(Inkscape::ERROR_MESSAGE, _("Flowed text(s) must be visible in order to be converted.")); } else { desktop->getMessageStack()-> flash(Inkscape::ERROR_MESSAGE, _("No flowed text(s) to convert in the selection.")); } } Glib::ustring text_relink_shapes_str(gchar const *prop, std::map const &old_to_new) { std::vector shapes_url = Glib::Regex::split_simple(" ", prop); Glib::ustring res; for (auto shape_url : shapes_url) { if (shape_url.compare(0, 5, "url(#") != 0 || shape_url.compare(shape_url.size() - 1, 1, ")") != 0) { std::cerr << "text_relink_shapes_str: Invalid shape value: " << shape_url << std::endl; } else { auto old_id = shape_url.substr(5, shape_url.size() - 6); auto find_it = old_to_new.find(old_id); if (find_it != old_to_new.end()) { res.append("url(#").append(find_it->second).append(") "); } else { std::cerr << "Failed to replace reference " << old_id << std::endl; } } } // remove trailing space if (!res.empty()) { assert(res.raw().back() == ' '); res.resize(res.size() - 1); } return res; } /* 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 :