-
Notifications
You must be signed in to change notification settings - Fork 560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SVG text failing to render #759
Comments
Determined this occurs when |
Did you ever translate that Python code into Swift? Looking to load these Verovio SVGs into Macaw and don’t want to reinvent the wheel. |
Happy to share. Here are the relevant snippets (may require edits to adapt to your project). Swift (older implementation)import KissXML
class VerovioRenderer {
// [...]
private func transformSvgSymbol(_ svg: String) -> String {
let svgXml = try! XMLDocument(xmlString: svg, options: 0)
var useConfigs: Dictionary<String, Set<String>> = [:]
for node in try! svgXml.nodes(forXPath: "//*[local-name()=\"use\"]") {
if node.kind == XMLElementKind,
let elem = node as? XMLElement,
let hrefAttr = elem.attribute(forName: "xlink:href"),
let oldHref = hrefAttr.stringValue,
let width = elem.attribute(forName: "width")?.stringValue?.trimmingCharacters(in: .letters),
let height = elem.attribute(forName: "height")?.stringValue?.trimmingCharacters(in: .letters)
{
let elemConfig = "\(width)-\(height)"
let newHref = "\(oldHref)-\(elemConfig)"
hrefAttr.stringValue = newHref
elem.removeAttribute(forName: "width")
elem.removeAttribute(forName: "height")
let symbolId = String(oldHref.dropFirst())
if useConfigs[symbolId] == nil {
useConfigs[symbolId] = []
}
useConfigs[symbolId]!.insert(elemConfig)
}
}
var defs: [XMLElement] = []
let transformRe = try! NSRegularExpression(
pattern: "scale\\((\\d+),\\s*(-?\\d+)\\)",
options: []
)
let viewBoxRe = try! NSRegularExpression(
pattern: "\\d+\\s+\\d+\\s+(\\d+)\\s+(\\d+)",
options: []
)
for node in try! svgXml.nodes(forXPath: "//*[local-name()=\"symbol\"]") {
if node.kind == XMLElementKind,
let elem = node as? XMLElement,
let symbolId = elem.attribute(forName: "id")?.stringValue,
let viewBox = elem.attribute(forName: "viewBox")?.stringValue,
let pathElem = elem.elements(forName: "path").first,
let transform = pathElem.attribute(forName: "transform")?.stringValue,
let coords = pathElem.attribute(forName: "d")?.stringValue,
let parsedTransform = transformRe.firstMatch(in: transform, range: NSRange(transform.startIndex..., in: transform)),
let parsedViewBox = viewBoxRe.firstMatch(in: viewBox, range: NSRange(viewBox.startIndex..., in: viewBox)),
let viewBoxWidth = Double(getCaptureGroup(str: viewBox, match: parsedViewBox, index: 1)),
let viewBoxHeight = Double(getCaptureGroup(str: viewBox, match: parsedViewBox, index: 2)),
let transformX = Double(getCaptureGroup(str: transform, match: parsedTransform, index: 1)),
let transformY = Double(getCaptureGroup(str: transform, match: parsedTransform, index: 2))
{
for elemConfig in useConfigs[symbolId] ?? [] {
let parsedConfig = elemConfig.split(separator: "-")
let width = Double(parsedConfig[0])!
let height = Double(parsedConfig[1])!
let newPathElem = XMLNode.element(withName: "path") as! XMLElement
newPathElem.addAttribute(withName: "id", stringValue: "\(symbolId)-\(elemConfig)")
newPathElem.addAttribute(withName: "transform", stringValue: "scale(\(transformX * width / viewBoxWidth),\(transformY * height / viewBoxHeight))")
newPathElem.addAttribute(withName: "d", stringValue: coords)
defs.append(newPathElem)
}
}
}
if let root = svgXml.rootElement(),
let innerSvg = root.elements(forName: "svg").first,
let viewBox = innerSvg.attribute(forName: "viewBox")?.stringValue
{
root.elements(forName: "defs").first?.setChildren(defs)
root.addAttribute(withName: "viewBox", stringValue: viewBox)
root.removeAttribute(forName: "width")
root.removeAttribute(forName: "height")
}
// Remove nested tspan.
for node in try! svgXml.nodes(forXPath: "//*[local-name()=\"text\"]") {
if node.kind == XMLElementKind,
let elem = node as? XMLElement
{
let textNodes = recurseGetTextNodes(elem, parentAttr: [:])
if textNodes.count > 0 {
elem.setChildren(textNodes)
}
}
}
return removeInnerSvg(xml: svgXml.xmlString)
}
private func getCaptureGroup(str: String, match: NSTextCheckingResult, index: Int) -> String {
if let range = Range(match.range(at: index), in: str) {
return String(str[range])
}
return ""
}
private func recurseGetTextNodes(
_ elem: XMLElement,
parentAttr: Dictionary<String, String>
) -> [XMLNode] {
let children = elem.elements(forName: "tspan")
guard children.count > 0 else {
if let elemText = elem.stringValue, !elemText.isEmpty {
let textNode = XMLNode.element(withName: "tspan") as! XMLElement
textNode.setChildren([XMLNode.text(withStringValue: elemText) as! XMLNode])
for (attr, attrValue) in parentAttr {
textNode.addAttribute(withName: attr, stringValue: attrValue)
}
return [textNode]
}
return []
}
var output: [XMLNode] = []
for child in children {
let textNodes = recurseGetTextNodes(
child,
parentAttr: mergeTextAttributes(node: child, parentAttr: parentAttr)
)
output.append(contentsOf: textNodes)
}
return output
}
private func mergeTextAttributes(node: XMLElement, parentAttr: Dictionary<String, String>) -> Dictionary<String, String> {
let knownAttrs = ["x", "y", "font-family", "font-size", "font-style", "text-anchor", "class"]
var output: Dictionary<String, String> = [:]
for attr in knownAttrs {
if let attrValue = node.attribute(forName: attr)?.stringValue ?? parentAttr[attr] {
output[attr] = attrValue
}
}
return output
}
private func removeInnerSvg(xml input: String) -> String {
var xml = input
// For some reason, the XML library crashes when trying to detach
// nodes to push up in the hierarchy. Promote elements from
// inner nested svg element.
let svgOpenTagRe = try! NSRegularExpression(
pattern: "<svg[^>]*>",
options: []
)
let openTagMatches = svgOpenTagRe.matches(
in: xml,
options: [],
range: NSRange(xml.startIndex..., in: xml)
)
guard openTagMatches.count == 2,
let innerOpenTag = Range(openTagMatches[1].range(at: 0), in: xml)
else {
return input
}
xml.removeSubrange(innerOpenTag)
guard let innerCloseTag = xml.range(of: "</svg>") else {
return input
}
xml.removeSubrange(innerCloseTag)
return xml
}
} C++ (currently use this; probably fixes some issues with the Swift version, though cannot recall what)#include "VrvSvgFilter.h"
#include "pugixml.hpp"
#include <algorithm>
#include <cstring>
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <vector>
template <class UnaryPredicate>
inline static void vrvSvgTrim(std::string &s, UnaryPredicate p)
{
s.erase(std::find_if(s.rbegin(), s.rend(), p).base(), s.end());
s.erase(s.begin(), std::find_if(s.begin(), s.end(), p));
}
/**
* Remove alphabetical characters from both ends of the string.
*/
inline static void vrvSvgTrimLetters(std::string &s)
{
vrvSvgTrim(s, [](int c) { return !std::isalpha(c); });
}
#ifndef ANDROID
/**
* Verovio has an svg element as a child of the root svg. Flatten it out.
*
* Required for iOS renderer to work; breaks layout for Android.
*/
static void removeInnerSvg(const pugi::xml_document &svgXml)
{
pugi::xml_node root = svgXml.first_child();
pugi::xml_node innerSvg = root.child("svg");
// Promote required attributes.
root.append_attribute("viewBox") = innerSvg.attribute("viewBox").value();
root.remove_attribute("width");
root.remove_attribute("height");
// Promote children.
for (pugi::xml_node node = innerSvg.first_child(); node;
node = innerSvg.first_child()) {
root.append_move(node);
}
root.remove_child(innerSvg);
}
#endif
/**
* Merge whitelisted attributes from the element with the supplied mapping.
*/
static std::map<std::string, std::string> mergeTextAttributes(
pugi::xml_node child,
std::map<std::string, std::string> parentAttr)
{
std::vector<std::string> knownAttrs{
"x",
"y",
"font-family",
"font-size",
"font-style",
"text-anchor",
"class"};
std::map<std::string, std::string> output;
for (const std::string &attr : knownAttrs) {
pugi::xml_attribute childAttr = child.attribute(attr.c_str());
if (!childAttr.empty()) {
output[attr] = childAttr.value();
} else if (parentAttr.find(attr) != parentAttr.end()) {
// Attribute not in child? Take forwarded from parent, if exists.
output[attr] = parentAttr[attr];
}
}
return output;
}
/**
* Populate a flattened tspan element.
*/
static int appendFlatTspan(
pugi::xml_node flatTextNode,
pugi::xml_node elem,
std::map<std::string, std::string> newAttrs)
{
// Try to merge nodes.
pugi::xml_node prevTspan = flatTextNode.last_child();
if (std::strcmp(prevTspan.name(), "tspan") == 0) {
int attrMatchCount = 0;
for (pugi::xml_attribute attr : prevTspan.attributes()) {
auto itr = newAttrs.find(attr.name());
if (itr != newAttrs.end() && itr->second == attr.value()) {
attrMatchCount += 1;
} else {
attrMatchCount = -1;
break;
}
}
if (newAttrs.size() == attrMatchCount) {
// Previous node has the same attributes. Append text children
// nodes instead of wrapping in a new tspan.
std::string combinedText(prevTspan.text().get());
combinedText += elem.text().get();
prevTspan.remove_children();
prevTspan.append_child(pugi::node_pcdata)
.set_value(combinedText.c_str());
return 0;
}
}
// Append new node.
pugi::xml_node textNode = flatTextNode.append_child("tspan");
textNode.append_child(pugi::node_pcdata).set_value(elem.text().get());
for (const auto &attr : newAttrs) {
textNode.append_attribute(attr.first.c_str()) = attr.second.c_str();
}
return 1;
}
/**
* Recursively reduce nested tspan elements to a single layer.
*
* @param flatTextNode
* Target node within which to create new elements.
* @param elem
* tspan element to flatten.
*
* @return
* Number of nodes created.
*/
static int recurseFlattenTextNode(
pugi::xml_node flatTextNode,
pugi::xml_node elem,
const std::map<std::string, std::string> &parentAttr)
{
int numAdded = 0;
bool hasChild = false;
for (pugi::xml_node child : elem.children("tspan")) {
hasChild = true;
numAdded += recurseFlattenTextNode(
flatTextNode,
child,
mergeTextAttributes(child, parentAttr));
}
if (!hasChild && !elem.text().empty()) {
numAdded += appendFlatTspan(flatTextNode, elem, parentAttr);
}
return numAdded;
}
/**
* Recursively reduce nested tspan elements to a single layer.
*/
static void removeNestedTspan(const pugi::xml_document &svgXml)
{
for (pugi::xpath_node selectedNode :
svgXml.select_nodes("//*[local-name()=\"text\"]")) {
pugi::xml_node node = selectedNode.node();
pugi::xml_node flatTextNode =
node.parent().insert_copy_after(node, node);
flatTextNode.remove_children();
if (recurseFlattenTextNode(
flatTextNode,
node,
std::map<std::string, std::string>()) > 0) {
node.parent().remove_child(node);
} else {
node.parent().remove_child(flatTextNode);
}
}
}
/**
* Set text font.
*/
static void styleVerseText(const pugi::xml_document &svgXml)
{
pugi::xml_node style = svgXml.first_child().append_child("style");
style.append_attribute("type") = "text/css";
#ifdef ANDROID
style.text() = ".verse .text { font-family: LiberationSerif; }";
#else
// CSS selector support for iOS renderer is limited.
style.text() = ".text { font-family: LiberationSerif; }";
#endif
}
/**
* Convert svg symbol defs to path defs.
*/
std::string transformSvgSymbol(
const std::string &annotation,
const std::string &svg,
int pageNo)
{
pugi::xml_document svgXml;
pugi::xml_parse_result parseResult = svgXml.load_string(svg.c_str());
if (parseResult.status != pugi::status_ok) {
return parseResult.description();
}
// Find symbol usages.
std::map<std::string, std::set<std::string>> useConfigs;
for (pugi::xpath_node selectedNode :
svgXml.select_nodes("//*[local-name()=\"use\"]")) {
pugi::xml_node node = selectedNode.node();
pugi::xml_attribute hrefAttr = node.attribute("xlink:href");
std::string oldHref(hrefAttr.value());
std::string width(node.attribute("width").value());
vrvSvgTrimLetters(width);
std::string height(node.attribute("height").value());
vrvSvgTrimLetters(height);
// Retarget to a path def.
std::string elemConfig(width + '-' + height);
std::string newHref(oldHref + '-' + elemConfig);
hrefAttr.set_value(newHref.c_str());
node.remove_attribute("width");
node.remove_attribute("height");
std::string symbolId(oldHref.substr(1));
useConfigs[symbolId].insert(elemConfig);
}
// Create path defs.
pugi::xml_node defs = svgXml.first_child().child("defs");
std::regex transformRe(R"(scale\((\d+),\s*(-?\d+)\))");
std::regex viewBoxRe(R"(\d+\s+\d+\s+(\d+)\s+(\d+))");
for (pugi::xpath_node selectedNode :
svgXml.select_nodes("//*[local-name()=\"symbol\"]")) {
pugi::xml_node node = selectedNode.node();
std::string symbolId(node.attribute("id").value());
std::string viewBox(node.attribute("viewBox").value());
pugi::xml_node pathElem = node.child("path");
std::string transform(pathElem.attribute("transform").value());
std::string coords(pathElem.attribute("d").value());
// Remove symbol def.
node.parent().remove_child(node);
// Parse attributes.
std::smatch parsedTransform;
if (!std::regex_search(transform, parsedTransform, transformRe)) {
continue;
}
std::smatch parsedViewBox;
if (!std::regex_search(viewBox, parsedViewBox, viewBoxRe)) {
continue;
}
double transformX = std::stod(parsedTransform[1]);
double transformY = std::stod(parsedTransform[2]);
double viewBoxWidth = std::stod(parsedViewBox[1]);
double viewBoxHeight = std::stod(parsedViewBox[2]);
// Create def nodes for each required transform of the symbol.
for (const std::string &elemConfig : useConfigs[symbolId]) {
size_t dashIdx = elemConfig.find('-');
double width = std::stod(elemConfig.substr(0, dashIdx));
double height = std::stod(elemConfig.substr(dashIdx + 1));
pugi::xml_node newPathElem = defs.append_child("path");
newPathElem.append_attribute("id") =
(symbolId + '-' + elemConfig).c_str();
std::string transformAttr("scale(");
transformAttr += std::to_string(transformX * width / viewBoxWidth);
transformAttr += ',';
transformAttr +=
std::to_string(transformY * height / viewBoxHeight);
transformAttr += ')';
newPathElem.append_attribute("transform") = transformAttr.c_str();
newPathElem.append_attribute("d") = coords.c_str();
}
}
removeNestedTspan(svgXml);
#ifndef ANDROID
removeInnerSvg(svgXml);
#endif
styleVerseText(svgXml);
std::ostringstream result;
svgXml.save(result);
return result.str();
} |
You're the coolest! I just used the C++ version as with Verovio I've got a mountain of it in the project already. Thanks for hooking me up! |
The attached svg (music generated by Verovio and transformed by rism-digital/verovio#332 (comment), since Macaw does not appear to handle
symbol
elements) renders correctly except that all text is missing.Is Macaw supposed to handle
tspan
svg elements?iosTransform6.svg.zip
The text was updated successfully, but these errors were encountered: