From 5c5af59fdf46afb91177152685e18b9a72a29ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Se=C5=86ko?= Date: Fri, 5 Sep 2025 21:43:48 +0300 Subject: [PATCH 1/2] Implement text to path for svg fonts. --- src/libnrtype/Layout-TNG-Output.cpp | 32 +++++ src/libnrtype/Layout-TNG.h | 2 + src/libnrtype/font-instance.cpp | 9 ++ src/libnrtype/font-instance.h | 2 + src/path-chemistry.cpp | 190 ++++++++++++++++------------ 5 files changed, 154 insertions(+), 81 deletions(-) diff --git a/src/libnrtype/Layout-TNG-Output.cpp b/src/libnrtype/Layout-TNG-Output.cpp index d79f59448e..4796d65ea4 100644 --- a/src/libnrtype/Layout-TNG-Output.cpp +++ b/src/libnrtype/Layout-TNG-Output.cpp @@ -848,6 +848,38 @@ Geom::PathVector Layout::convertToCurves(iterator const &from_glyph, iterator co return curve; } +Geom::PathVector Layout::convertToSVG(iterator const &from_glyph, iterator const &to_glyph, + std::vector> &svgOut) const +{ + Geom::PathVector curve; + + for (int glyph_index = from_glyph._glyph_index; glyph_index < to_glyph._glyph_index; glyph_index++) { + Span const &span = _glyphs[glyph_index].span(this); + Geom::Affine glyph_matrix = _glyphs[glyph_index].transform(*this); + auto glyph_id = _glyphs[glyph_index].glyph; + auto &font = span.font; + auto glyph = font->LoadGlyph(glyph_id); + if (!glyph) { + continue; + } + if (font->FontHasSVG()) { + auto svg = font->GlyphSvg(glyph_id); + if (!svg.empty()) { + auto scale = 1.0 / font->GetDesignUnits(); + svgOut.push_back({std::move(svg), Geom::Scale(scale, -scale) * glyph_matrix}); + } + continue; + } + Geom::PathVector const *pathv = span.font->PathVector(glyph_id); + if (pathv) { + Geom::PathVector pathv_trans = *pathv * glyph_matrix; + pathvector_append(curve, std::move(pathv_trans)); + } + } + + return curve; +} + void Layout::transform(Geom::Affine const &transform) { // this is all massively oversimplified diff --git a/src/libnrtype/Layout-TNG.h b/src/libnrtype/Layout-TNG.h index 4ac69eb4ec..b7b025c8ee 100644 --- a/src/libnrtype/Layout-TNG.h +++ b/src/libnrtype/Layout-TNG.h @@ -415,6 +415,8 @@ public: outlines. */ Geom::PathVector convertToCurves(iterator const &from_glyph, iterator const &to_glyph) const; + Geom::PathVector convertToSVG(iterator const &from_glyph, iterator const &to_glyph, + std::vector> &svgOut) const; Geom::PathVector convertToCurves() const; /** Apply the given transform to all the output presently stored in diff --git a/src/libnrtype/font-instance.cpp b/src/libnrtype/font-instance.cpp index 58235e59ea..18232ad607 100644 --- a/src/libnrtype/font-instance.cpp +++ b/src/libnrtype/font-instance.cpp @@ -718,6 +718,15 @@ Inkscape::Pixbuf const *FontInstance::PixBuf(unsigned int glyph_id) return pixbuf; } +std::string FontInstance::GlyphSvg(unsigned int glyph_id) +{ + auto glyph_iter = data->openTypeSVGGlyphs.find(glyph_id); + if (glyph_iter == data->openTypeSVGGlyphs.end()) { + return ""; + } + return data->openTypeSVGData[glyph_iter->second.entry_index]; +} + double FontInstance::Advance(unsigned int glyph_id, bool vertical) { auto g = LoadGlyph(glyph_id); diff --git a/src/libnrtype/font-instance.h b/src/libnrtype/font-instance.h index 3fbc601a1e..a09b791675 100644 --- a/src/libnrtype/font-instance.h +++ b/src/libnrtype/font-instance.h @@ -83,6 +83,8 @@ public: // are lazy-loaded but immutable once loaded. They are guaranteed to be in Cairo pixel format. Inkscape::Pixbuf const *PixBuf(unsigned int glyph_id); + std::string GlyphSvg(unsigned int glyph_id); + // Horizontal advance if 'vertical' is false, vertical advance if true. double Advance(unsigned int glyph_id, bool vertical); diff --git a/src/path-chemistry.cpp b/src/path-chemistry.cpp index bc2760627a..f3732e8cc5 100644 --- a/src/path-chemistry.cpp +++ b/src/path-chemistry.cpp @@ -518,101 +518,130 @@ void Inkscape::convert_text_to_curves(SPDocument *doc) sp_item_list_to_curves(items, selected, to_select); } -Inkscape::XML::Node * -sp_selected_item_to_curved_repr(SPItem *item, guint32 /*text_grouping_policy*/) +Inkscape::XML::Node *sp_text_to_curve_repr(SPItem *item) { - if (!item) - return nullptr; - Inkscape::XML::Document *xml_doc = item->getRepr()->document(); + auto target_doc = item->document; + + // Special treatment for text: convert each glyph to separate path, then group the paths + auto layout = te_get_layout(item); + + // Save original text for accessibility. + Glib::ustring original_text = sp_te_get_string_multiline(item, layout->begin(), layout->end()); + + SPObject *prev_parent = nullptr; + std::vector> curves; + + Inkscape::XML::Node *result = xml_doc->createElement("svg:g"); + // temporary add the group to doc, some of the transformation logic expects object to be within document tree + item->parent->addChild(result); + SPItem *tmpParent = cast(target_doc->getObjectByRepr(result)); + tmpParent->set_i2d_affine(item->i2dt_affine()); + + bool need_group = false; + Inkscape::Text::Layout::iterator iter = layout->begin(); + while (iter != layout->end()) { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) + break; + + /* This glyph's style */ + SPObject *pos_obj = nullptr; + layout->getSourceOfCharacter(iter, &pos_obj); + if (!pos_obj) // no source for glyph, abort + break; + while (is(pos_obj) && pos_obj->parent) { + pos_obj = pos_obj->parent; // SPStrings don't have style + } - if (is(item) || is(item)) { + // get path from iter to iter_next: + std::vector> svgSnippets; + auto curve = layout->convertToSVG(iter, iter_next, svgSnippets); + iter = iter_next; // shift to next glyph - // Special treatment for text: convert each glyph to separate path, then group the paths - auto layout = te_get_layout(item); - - // Save original text for accessibility. - Glib::ustring original_text = sp_te_get_string_multiline(item, layout->begin(), layout->end()); - - SPObject *prev_parent = nullptr; - std::vector> curves; - - Inkscape::Text::Layout::iterator iter = layout->begin(); - do { - Inkscape::Text::Layout::iterator iter_next = iter; - iter_next.nextGlyph(); // iter_next is one glyph ahead from iter - if (iter == iter_next) - break; - - /* This glyph's style */ - SPObject *pos_obj = nullptr; - layout->getSourceOfCharacter(iter, &pos_obj); - if (!pos_obj) // no source for glyph, abort - break; - while (is(pos_obj) && pos_obj->parent) { - pos_obj = pos_obj->parent; // SPStrings don't have style - } + if (curve.empty() && svgSnippets.empty()) { // whitespace glyph? + continue; + } - // get path from iter to iter_next: - auto curve = layout->convertToCurves(iter, iter_next); - iter = iter_next; // shift to next glyph - if (curve.empty()) { // whitespace glyph? + for (auto &svgSnippet : svgSnippets) { + auto doc = SPDocument::createNewDocFromMem(svgSnippet.first, false); + if (!doc) { continue; } + auto transform = svgSnippet.second * item->i2doc_affine(); + target_doc->import(*doc, result, nullptr, transform, nullptr, SPDocument::ImportRoot::UngroupSingle, + SPDocument::ImportLayersMode::ToGroup); + need_group = true; + } - // Create a new path for each span to group glyphs into - // which preserves styles such as paint-order - if (!prev_parent || prev_parent != pos_obj) { - // Record the style for the dying tspan tree (see sp_style_merge_from_dying_parent in style.cpp) - auto style = pos_obj->style; - for (auto sp = pos_obj->parent; sp && sp != item; sp = sp->parent) { - style->merge(sp->style); - } - curves.emplace_back(curve, style); - } else { - for (auto &path : curve) { - curves.back().first.push_back(path); - } - } + if (curve.empty()) { + continue; + } - prev_parent = pos_obj; - if (iter == layout->end()) - break; - - } while (true); - - if (curves.empty()) - return nullptr; - - Inkscape::XML::Node *result = curves.size() > 1 ? xml_doc->createElement("svg:g") : nullptr; - SPStyle *result_style = new SPStyle(item->document); - - for (auto &[pathv, style] : curves) { - Glib::ustring glyph_style = style->writeIfDiff(item->style); - auto new_path = xml_doc->createElement("svg:path"); - new_path->setAttributeOrRemoveIfEmpty("style", glyph_style); - new_path->setAttribute("d", sp_svg_write_path(pathv)); - if (curves.size() == 1) { - result = new_path; - result_style->merge(style); - } else { - result->appendChild(new_path); - Inkscape::GC::release(new_path); + // Create a new path for each span to group glyphs into + // which preserves styles such as paint-order + if (!prev_parent || prev_parent != pos_obj) { + // Record the style for the dying tspan tree (see sp_style_merge_from_dying_parent in style.cpp) + auto style = pos_obj->style; + for (auto sp = pos_obj->parent; sp && sp != item; sp = sp->parent) { + style->merge(sp->style); + } + curves.emplace_back(curve, style); + } else { + for (auto &path : curve) { + curves.back().first.push_back(path); } } - result_style->merge(item->style); - Glib::ustring css = result_style->writeIfDiff(item->parent ? item->parent->style : nullptr); - delete result_style; + prev_parent = pos_obj; + } + result->parent()->removeChild(result); + + if (curves.empty() && !need_group) { + Inkscape::GC::release(result); + return nullptr; + } - Inkscape::copy_object_properties(result, item->getRepr()); - result->setAttributeOrRemoveIfEmpty("style", css); - result->setAttributeOrRemoveIfEmpty("transform", item->getRepr()->attribute("transform")); + auto result_style = std::make_unique(item->document); - if (!original_text.empty()) { - result->setAttribute("aria-label", original_text); + for (auto &[pathv, style] : curves) { + Glib::ustring glyph_style = style->writeIfDiff(item->style); + auto new_path = xml_doc->createElement("svg:path"); + new_path->setAttributeOrRemoveIfEmpty("style", glyph_style); + new_path->setAttribute("d", sp_svg_write_path(pathv)); + if (curves.size() == 1 && !need_group) { + Inkscape::GC::release(result); + result = new_path; + result_style->merge(style); + } else { + result->appendChild(new_path); + Inkscape::GC::release(new_path); } - return result; + } + + result_style->merge(item->style); + Glib::ustring css = result_style->writeIfDiff(item->parent ? item->parent->style : nullptr); + + Inkscape::copy_object_properties(result, item->getRepr()); + result->setAttributeOrRemoveIfEmpty("style", css); + result->setAttributeOrRemoveIfEmpty("transform", item->getRepr()->attribute("transform")); + + if (!original_text.empty()) { + result->setAttribute("aria-label", original_text); + } + return result; +} + +Inkscape::XML::Node *sp_selected_item_to_curved_repr(SPItem *item, guint32 /*text_grouping_policy*/) +{ + if (!item) + return nullptr; + + Inkscape::XML::Document *xml_doc = item->getRepr()->document(); + + if (is(item) || is(item)) { + return sp_text_to_curve_repr(item); } Geom::PathVector curve; @@ -646,7 +675,6 @@ sp_selected_item_to_curved_repr(SPItem *item, guint32 /*text_grouping_policy*/) return repr; } - void ObjectSet::pathReverse() { -- GitLab From fd2043821bbcbf479dcafd476387457f3b3dd093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C4=81rlis=20Se=C5=86ko?= Date: Sun, 7 Sep 2025 19:15:51 +0300 Subject: [PATCH 2/2] Add svg font to path test. Reference image for imagemagick compare should be the first argument instead of second. It affects chosen colorspace. Previously resulted in false negatives when comparing black and white output to colored reference. --- testfiles/cli_tests/CMakeLists.txt | 10 +++++++++- testfiles/cli_tests/check_output.sh | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/testfiles/cli_tests/CMakeLists.txt b/testfiles/cli_tests/CMakeLists.txt index 2ec3ba8877..f653f249b6 100644 --- a/testfiles/cli_tests/CMakeLists.txt +++ b/testfiles/cli_tests/CMakeLists.txt @@ -1011,7 +1011,15 @@ add_cli_test(action_test_multiline_anchoring PARAMETERS --actions=select-by-id:grouped_text$transform-translate:10,0$transform-translate:-10,0$export-filename:multiline-anchoring_out.svg$export-plain-svg$export-do EXPECTED_FILES multiline-anchoring_out.svg TEST_SCRIPT match_regex_fail.sh multiline-anchoring_out.svg "(x=\"[3-9][0-9]{2}\")|(x=\"[12][0-9]{3}\")|(y=\"[0-9]{2}\")|(y=\"1[0-9]{2}\")") - +# test text to path with SVG in OTF fonts, at the end of test erase all text elements to make sure it doesn't pass by +# rendering the nonconverted text. +add_cli_test(actions-svginot-text-topath + INPUT_FILENAME ../../rendering_tests/text-gzipped-svg-glyph.svg + PARAMETERS --actions=select-all$object-to-path$select-clear$select-by-element:text$delete + OUTPUT_FILENAME actions-svginot-text-topath-output.png + FUZZYREF_FILENAME ../../rendering_tests/expected_rendering/text-gzipped-svg-glyph.png + FUZZ_PERCENTAGE 3 +) # The export area type should be the last set value add_cli_test(actions-export-area-drawing-then-page diff --git a/testfiles/cli_tests/check_output.sh b/testfiles/cli_tests/check_output.sh index baae1ffc41..4dce1d7e4c 100755 --- a/testfiles/cli_tests/check_output.sh +++ b/testfiles/cli_tests/check_output.sh @@ -59,7 +59,7 @@ if [ -n "${REFERENCE_FILENAME}" ]; then fi # compare files - if ! compare ${FUZZ} -metric AE ${PNG_FILENAME} ${PNG_REFERENCE} ${PNG_COMPARE}; then + if ! compare ${FUZZ} -metric AE ${PNG_REFERENCE} ${PNG_FILENAME} ${PNG_COMPARE}; then echo && echo "Error: Comparison failed." exit 1 fi -- GitLab