From f4ca09f9855c3ba4b4e15579552abad8566e722f Mon Sep 17 00:00:00 2001 From: Tomas Pavlicek Date: Fri, 7 Jun 2019 21:09:14 +0200 Subject: [PATCH] TechDraw: SVG export refactoring - All XML modifications via DOM --- src/Mod/TechDraw/App/CMakeLists.txt | 5 +- src/Mod/TechDraw/App/DrawSVGTemplate.cpp | 243 ++++++++-------- src/Mod/TechDraw/App/DrawSVGTemplate.h | 6 +- src/Mod/TechDraw/App/DrawTemplate.cpp | 5 - src/Mod/TechDraw/App/DrawTemplate.h | 1 - src/Mod/TechDraw/App/QDomNodeModel.cpp | 345 +++++++++++++++++++++++ src/Mod/TechDraw/App/QDomNodeModel.h | 41 +++ src/Mod/TechDraw/Gui/CMakeLists.txt | 1 + src/Mod/TechDraw/Gui/QGVPage.cpp | 142 ++++++++-- src/Mod/TechDraw/Gui/QGVPage.h | 2 +- 10 files changed, 620 insertions(+), 171 deletions(-) create mode 100644 src/Mod/TechDraw/App/QDomNodeModel.cpp create mode 100644 src/Mod/TechDraw/App/QDomNodeModel.h diff --git a/src/Mod/TechDraw/App/CMakeLists.txt b/src/Mod/TechDraw/App/CMakeLists.txt index d3faadadf3..784de108c8 100644 --- a/src/Mod/TechDraw/App/CMakeLists.txt +++ b/src/Mod/TechDraw/App/CMakeLists.txt @@ -17,6 +17,7 @@ include_directories( ${PYTHON_INCLUDE_DIRS} ${XercesC_INCLUDE_DIRS} ${QT_QTCORE_INCLUDE_DIR} + ${QT_QTXMLPATTERNS_INCLUDE_DIR} ) link_directories(${OCC_LIBRARY_DIR}) @@ -99,6 +100,8 @@ SET(Draw_SRCS DrawLeaderLine.h DrawRichAnno.cpp DrawRichAnno.h + QDomNodeModel.cpp + QDomNodeModel.h ) SET(TechDraw_SRCS @@ -177,7 +180,7 @@ endif(FREECAD_USE_PCH) add_library(TechDraw SHARED ${TechDraw_SRCS} ${Draw_SRCS} ${TechDrawAlgos_SRCS} ${Geometry_SRCS} ${Python_SRCS}) -target_link_libraries(TechDraw ${TechDrawLIBS};${QT_QTXML_LIBRARY};${TechDraw}) +target_link_libraries(TechDraw ${TechDrawLIBS};${QT_QTXML_LIBRARY};${QT_QTXMLPATTERNS_LIBRARY};${TechDraw}) ADD_CUSTOM_COMMAND(TARGET TechDraw POST_BUILD diff --git a/src/Mod/TechDraw/App/DrawSVGTemplate.cpp b/src/Mod/TechDraw/App/DrawSVGTemplate.cpp index 2e5a8fe959..0b94d7970d 100644 --- a/src/Mod/TechDraw/App/DrawSVGTemplate.cpp +++ b/src/Mod/TechDraw/App/DrawSVGTemplate.cpp @@ -29,6 +29,10 @@ #include #endif +#include +#include +#include "QDomNodeModel.h" + #include #include #include @@ -125,18 +129,18 @@ void DrawSVGTemplate::onChanged(const App::Property* prop) App::DocumentObjectExecReturn * DrawSVGTemplate::execute(void) { - std::string templValue = Template.getValue(); - if (templValue.empty()) + std::string templateFilename = Template.getValue(); + if (templateFilename.empty()) return App::DocumentObject::StdReturn; - Base::FileInfo fi(templValue); + Base::FileInfo fi(templateFilename); if (!fi.isReadable()) { // non-empty template value, but can't read file // if there is a old absolute template file set use a redirect fi.setFile(App::Application::getResourceDir() + "Mod/Drawing/Templates/" + fi.fileName()); // try the redirect if (!fi.isReadable()) { - Base::Console().Log("DrawPage::execute() not able to open %s!\n",Template.getValue()); + Base::Console().Log("DrawPage::execute() not able to open %s!\n", Template.getValue()); std::string error = std::string("Cannot open file ") + Template.getValue(); return new App::DocumentObjectExecReturn(error); } @@ -145,99 +149,81 @@ App::DocumentObjectExecReturn * DrawSVGTemplate::execute(void) if (std::string(PageResult.getValue()).empty()) //first time through? PageResult.setValue(fi.filePath().c_str()); - // open Template file - string line; - ifstream inTemplate (fi.filePath().c_str()); + QFile templateFile(QString::fromUtf8(fi.filePath().c_str())); + if (!templateFile.open(QIODevice::ReadOnly)) { + Base::Console().Log("DrawPage::execute() can't read template %s!\n", Template.getValue()); + std::string error = std::string("Cannot read file ") + Template.getValue(); + return new App::DocumentObjectExecReturn(error); + } - ostringstream copyTemplate; - string tempendl = "--endOfLine--"; + QDomDocument templateDocument; + if (!templateDocument.setContent(&templateFile)) { + Base::Console().Message("DrawPage::execute() - failed to parse file: %s\n", + Template.getValue()); + std::string error = std::string("Cannot parse file ") + Template.getValue(); + return new App::DocumentObjectExecReturn(error); + } - //inTemplate to copyTemplate - //remove DrawingContent comment line - //change line endings - //capture TitleBlock dimensions - while (getline(inTemplate,line)) + QXmlQuery query(QXmlQuery::XQuery10); + QDomNodeModel model(query.namePool(), templateDocument); + query.setFocus(QXmlItem(model.fromDomNode(templateDocument.documentElement()))); + + // XPath query to select all nodes whose parent + // has "freecad:editable" attribute + query.setQuery(QString::fromUtf8( + "declare default element namespace \"" SVG_NS_URI "\"; " + "declare namespace freecad=\"" FREECAD_SVG_NS_URI "\"; " + "//text[@freecad:editable]/tspan")); + + QXmlResultItems queryResult; + query.evaluateTo(&queryResult); + + std::map substitutions = EditableTexts.getValues(); + while (!queryResult.next().isNull()) { - // copy every line except the DrawingContent comment? - if(line.find("") == string::npos) { - // if not - write through - copyTemplate << line << tempendl; - } + QDomElement tspanTempl = model.toDomNode(queryResult.current().toNodeModelIndex()).toElement(); - //double t0, t1,t2,t3; - float t0, t1,t2,t3; - if(line.find("" - //coverity 151677 - blockDimensions = QRectF(t0, t1, t2 - t0, t3 - t1); - } + // Replace the editable text spans with new nodes holding actual values + std::map::iterator item = + substitutions.find(tspanTempl.parentNode().toElement() + .attribute(QString::fromUtf8("freecad:editable")).toStdString()); + if (item != substitutions.end()) { + QDomElement tspanActual = templateDocument.createElement(QString::fromUtf8("tspan")); + tspanActual.appendChild(templateDocument.createTextNode(QString::fromUtf8(item->second.c_str()))); - } - inTemplate.close(); + // Keep all spaces in the text node + tspanActual.setAttribute(QString::fromUtf8("xml:space"), QString::fromUtf8("preserve")); - string outfragment(copyTemplate.str()); - std::string newfragment = outfragment; - - // update EditableText SVG clauses with Property values - std::map subs = EditableTexts.getValues(); - - if (subs.size() > 0) { - boost::regex e1 ("(.*?)"); - string::const_iterator begin, end; - begin = outfragment.begin(); - end = outfragment.end(); - boost::match_results what; - - // Find editable texts - while (boost::regex_search(begin, end, what, e1)) { //search in outfragment - // if we have a replacement value for the text we've found - if (subs.count(what[1].str())) { - // change it to specified value - boost::regex e2 ("((.*?)()"); - newfragment = boost::regex_replace(newfragment, e2, "$1>" + subs[what[1].str()] + "$3"); //replace in newfragment - } - begin = what[0].second; + tspanTempl.parentNode().replaceChild(tspanActual, tspanTempl); } } - - // restoring linebreaks and saving the file - boost::regex e3 ("--endOfLine--"); - string fmt = "\\n"; - outfragment = boost::regex_replace(newfragment, e3, fmt); - - const QString qsOut = QString::fromStdString(outfragment); - QDomDocument doc(QString::fromLatin1("mydocument")); - - //if (!doc.setContent(&resultFile)) { - if (!doc.setContent(qsOut)) { - //setError(); //???? how/when does this get reset? - std::string errMsg = std::string("Invalid SVG syntax in ") + getNameInDocument() + std::string(" - Check EditableTexts"); - return new App::DocumentObjectExecReturn(errMsg); - } else { - // make a temp file for FileIncluded Property - string tempName = PageResult.getExchangeTempFile(); - ofstream outfinal(tempName.c_str()); - outfinal << outfragment; - outfinal.close(); - PageResult.setValue(tempName.c_str()); + string pageResultFilename = PageResult.getExchangeTempFile(); + QFile pageResult(QString::fromUtf8(pageResultFilename.c_str())); + if (pageResult.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream stream(&pageResult); + stream << templateDocument.toString(); + pageResult.close(); + PageResult.setValue(pageResultFilename.c_str()); + } + else { + Base::Console().Message("DrawPage::execute() - failed to open file for writing: %s\n", + pageResultFilename.c_str()); } // Calculate the dimensions of the page and store for retrieval - // Parse the document XML - QDomElement docElem = doc.documentElement(); - // Obtain the size of the SVG document by reading the document attributes + QDomElement docElement = templateDocument.documentElement(); Base::Quantity quantity; // Obtain the width - QString str = docElem.attribute(QString::fromLatin1("width")); + QString str = docElement.attribute(QString::fromLatin1("width")); quantity = Base::Quantity::parse(str); quantity.setUnit(Base::Unit::Length); Width.setValue(quantity.getValue()); - str = docElem.attribute(QString::fromLatin1("height")); + str = docElement.attribute(QString::fromLatin1("height")); quantity = Base::Quantity::parse(str); quantity.setUnit(Base::Unit::Length); @@ -250,14 +236,6 @@ App::DocumentObjectExecReturn * DrawSVGTemplate::execute(void) return TechDraw::DrawTemplate::execute(); } -void DrawSVGTemplate::getBlockDimensions(double &x, double &y, double &width, double &height) const -{ - x = blockDimensions.left(); - y = blockDimensions.bottom(); - width = blockDimensions.width(); - height = blockDimensions.height(); -} - double DrawSVGTemplate::getWidth() const { return Width.getValue(); @@ -268,58 +246,63 @@ double DrawSVGTemplate::getHeight() const return Height.getValue(); } - std::map DrawSVGTemplate::getEditableTextsFromTemplate() { - std::map eds; + std::map editables; - std::string temp = Template.getValue(); - if (!temp.empty()) { - Base::FileInfo tfi(temp); + std::string templateFilename = Template.getValue(); + if (templateFilename.empty()) { + return editables; + } + + Base::FileInfo tfi(templateFilename); + if (!tfi.isReadable()) { + // if there is a old absolute template file set use a redirect + tfi.setFile(App::Application::getResourceDir() + "Mod/Drawing/Templates/" + tfi.fileName()); + // try the redirect if (!tfi.isReadable()) { - // if there is a old absolute template file set use a redirect - tfi.setFile(App::Application::getResourceDir() + "Mod/Drawing/Templates/" + tfi.fileName()); - // try the redirect - if (!tfi.isReadable()) { - return eds; - } - } - string tline, tfrag; - ifstream tfile (tfi.filePath().c_str()); - while (getline (tfile,tline)) { - tfrag += tline; - tfrag += "--endOfLine--"; - } - tfile.close(); - //this catches all the tags: - //keep tagRegex in sync with Gui/QGISVGTemplate.cpp - boost::regex tagRegex ("]*freecad:editable=[^>]*)>[^<]*]*>([^<]*)"); - boost::regex nameRegex("freecad:editable=\"(.*?)\""); - boost::regex valueRegex("(.*?)"); - - string::const_iterator tbegin, tend; - tbegin = tfrag.begin(); - tend = tfrag.end(); - boost::match_results tagMatch; - boost::match_results nameMatch; - boost::match_results valueMatch; - while (boost::regex_search(tbegin, tend, tagMatch, tagRegex)) { - if ( boost::regex_search(tagMatch[0].first, tagMatch[0].second, nameMatch, nameRegex) && - boost::regex_search(tagMatch[0].first, tagMatch[0].second, valueMatch, valueRegex)) { - //found valid name/value pair - string name = nameMatch[1]; - string value = valueMatch[1]; - if (eds.count(name) > 0) { - //TODO: Throw or [better] change key - qDebug() << "Got duplicate value for key "< nodes whose parent + // has "freecad:editable" attribute + query.setQuery(QString::fromUtf8( + "declare default element namespace \"" SVG_NS_URI "\"; " + "declare namespace freecad=\"" FREECAD_SVG_NS_URI "\"; " + "//text[@freecad:editable]/tspan")); + + QXmlResultItems queryResult; + query.evaluateTo(&queryResult); + + while (!queryResult.next().isNull()) + { + QDomElement tspan = model.toDomNode(queryResult.current().toNodeModelIndex()).toElement(); + + // Takeover the names stored as attributes and the values stored as text items + editables[tspan.parentNode().toElement().attribute(QString::fromUtf8("freecad:editable")) + .toStdString()] = tspan.firstChild().nodeValue().toStdString(); + } + + return editables; } diff --git a/src/Mod/TechDraw/App/DrawSVGTemplate.h b/src/Mod/TechDraw/App/DrawSVGTemplate.h index a6ef39523a..dc4af3dd6c 100644 --- a/src/Mod/TechDraw/App/DrawSVGTemplate.h +++ b/src/Mod/TechDraw/App/DrawSVGTemplate.h @@ -29,6 +29,9 @@ #include #include "DrawTemplate.h" +#define SVG_NS_URI "http://www.w3.org/2000/svg" +#define FREECAD_SVG_NS_URI "http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace" + namespace TechDraw { @@ -61,7 +64,6 @@ public: double getWidth() const; double getHeight() const; - void getBlockDimensions(double &x, double &y, double &width, double &height) const; protected: void onChanged(const App::Property* prop); @@ -71,8 +73,6 @@ protected: * Also populates editableSvgIds */ std::map getEditableTextsFromTemplate(); - - QRectF blockDimensions; }; typedef App::FeaturePythonT DrawSVGTemplatePython; diff --git a/src/Mod/TechDraw/App/DrawTemplate.cpp b/src/Mod/TechDraw/App/DrawTemplate.cpp index fdc53b0f26..0b980085db 100644 --- a/src/Mod/TechDraw/App/DrawTemplate.cpp +++ b/src/Mod/TechDraw/App/DrawTemplate.cpp @@ -126,11 +126,6 @@ App::DocumentObjectExecReturn *DrawTemplate::execute(void) return App::DocumentObject::execute(); } -void DrawTemplate::getBlockDimensions(double & /*x*/, double & /*y*/, double & /*width*/, double & /*height*/) const -{ - throw Base::NotImplementedError("implement in virtual function"); -} - DrawPage* DrawTemplate::getParentPage() const { TechDraw::DrawPage* page = nullptr; diff --git a/src/Mod/TechDraw/App/DrawTemplate.h b/src/Mod/TechDraw/App/DrawTemplate.h index 32c2c73352..0654487a34 100644 --- a/src/Mod/TechDraw/App/DrawTemplate.h +++ b/src/Mod/TechDraw/App/DrawTemplate.h @@ -57,7 +57,6 @@ public: /// Returns template height in mm virtual double getHeight() const; - virtual void getBlockDimensions(double &x, double &y, double &width, double &height) const; virtual DrawPage* getParentPage() const; /** @name methods override Feature */ diff --git a/src/Mod/TechDraw/App/QDomNodeModel.cpp b/src/Mod/TechDraw/App/QDomNodeModel.cpp new file mode 100644 index 0000000000..6e1111b64f --- /dev/null +++ b/src/Mod/TechDraw/App/QDomNodeModel.cpp @@ -0,0 +1,345 @@ +// Copyright (c) 2011 Stanislaw Adaszewski, portions (c) 2019 Tomas Pavlicek +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Stanislaw Adaszewski nor the +// names of other contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL STANISLAW ADASZEWSKI BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +#include "QDomNodeModel.h" + +#include +#include +#include +#include +#include +#include + +class PrivateDomNodeWrapper: public QDomNode +{ +public: + PrivateDomNodeWrapper(const QDomNode& other): + QDomNode(other) + { + } + + PrivateDomNodeWrapper(QDomNodePrivate *otherImpl): + QDomNode(otherImpl) + { + } + + QDomNodePrivate* getImpl() + { + return impl; + } +}; + +QDomNodeModel::QDomNodeModel(QXmlNamePool pool, QDomDocument doc, bool parsedReadOnly): + m_Pool(pool), m_Doc(doc), m_ReadOnly(parsedReadOnly) +{ + +} + +QUrl QDomNodeModel::baseUri (const QXmlNodeModelIndex &) const +{ + // TODO: Not implemented. + return QUrl(); +} + +#include + +QXmlNodeModelIndex::DocumentOrder QDomNodeModel::compareOrder ( + const QXmlNodeModelIndex & ni1, + const QXmlNodeModelIndex & ni2 ) const +{ + QDomNode n1 = toDomNode(ni1); + QDomNode n2 = toDomNode(ni2); + + if (n1 == n2) + return QXmlNodeModelIndex::Is; + + if (m_ReadOnly) + { + int i1 = n1.lineNumber(); + int i2 = n2.lineNumber(); + + if (i1 < i2) + return QXmlNodeModelIndex::Precedes; + + if (i1 > i2) + return QXmlNodeModelIndex::Follows; + + i1 = n1.columnNumber(); + i2 = n2.columnNumber(); + + if (i1 < i2) + return QXmlNodeModelIndex::Precedes; + + if (i1 > i2) + return QXmlNodeModelIndex::Follows; + + return QXmlNodeModelIndex::Is; + } + + QVector p1(path(n1)); + QVector p2(path(n2)); + + if (p1.at(0) != p2.at(0)) + return QXmlNodeModelIndex::Is; // When root is not common, return obvious nonsense + + int s = p1.size() < p2.size() ? p1.size() : p2.size(); + for (int i = 1; i < s; ++i) + { + if (p1.at(i) != p2.at(i)) + { + QDomNode c = p1.at(i - 1).firstChild(); + while (!c.isNull()) + { + if (c == p1.at(i)) + return QXmlNodeModelIndex::Precedes; + if (c == p2.at(i)) + return QXmlNodeModelIndex::Follows; + + c = c.nextSibling(); + } + + return QXmlNodeModelIndex::Is; // Should be impossible! + } + } + + return QXmlNodeModelIndex::Is; // Should be impossible! +} + +QUrl QDomNodeModel::documentUri (const QXmlNodeModelIndex&) const +{ + // TODO: Not implemented. + return QUrl(); +} + +QXmlNodeModelIndex QDomNodeModel::elementById ( const QXmlName & id ) const +{ + return fromDomNode(m_Doc.elementById(id.toClarkName(m_Pool))); +} + +QXmlNodeModelIndex::NodeKind QDomNodeModel::kind ( const QXmlNodeModelIndex & ni ) const +{ + QDomNode n = toDomNode(ni); + if (n.isAttr()) + return QXmlNodeModelIndex::Attribute; + else if (n.isText()) + return QXmlNodeModelIndex::Text; + else if (n.isComment()) + return QXmlNodeModelIndex::Comment; + else if (n.isDocument()) + return QXmlNodeModelIndex::Document; + else if (n.isElement()) + return QXmlNodeModelIndex::Element; + else if (n.isProcessingInstruction()) + return QXmlNodeModelIndex::ProcessingInstruction; + + return (QXmlNodeModelIndex::NodeKind) 0; +} + +QXmlName QDomNodeModel::name ( const QXmlNodeModelIndex & ni ) const +{ + QDomNode n = toDomNode(ni); + + if (n.isAttr() || n.isElement()) { + if (!n.namespaceURI().isEmpty()) + return QXmlName(m_Pool, n.localName(), n.namespaceURI(), n.prefix()); + + QString p = n.prefix(); + QString t = n.nodeName(); + + if (p.isEmpty()) + { + int c = t.indexOf(QLatin1Char(':')); + if (c < 0) + p = QString::fromUtf8(""); + else + { + p = t.left(c); + t = t.mid(c + 1); + } + } + + QVector ns(namespaceBindings(ni)); + int x; + for (x = 0; x < ns.size(); ++x) + if (ns.at(x).prefix(m_Pool) == p) break; + + if (x < ns.size()) + return QXmlName(m_Pool, t, ns.at(x).namespaceUri(m_Pool), p); + } + + return QXmlName(m_Pool, n.nodeName(), QString(), QString()); +} + +QVector QDomNodeModel::namespaceBindings(const QXmlNodeModelIndex & ni ) const +{ + QDomNode n = toDomNode(ni); + + QVector res; + while (!n.isNull()) + { + QDomNamedNodeMap attrs = n.attributes(); + for (int i = 0; i < attrs.size(); ++i) + { + QString a = attrs.item(i).nodeName(); + + QString p; + if (a == QString::fromUtf8("xmlns")) + p = QString::fromUtf8(""); + else if (a.startsWith(QString::fromUtf8("xmlns:"))) + p = a.mid(6); + + if (!p.isNull()) + { + int x; + for (x = 0; x < res.size(); ++x) + if (res.at(x).prefix(m_Pool) == p) break; + + if (x >= res.size()) + res.append(QXmlName(m_Pool, QString::fromUtf8("xmlns"), attrs.item(i).nodeValue(), p)); + } + } + + n = n.parentNode(); + } + + return res; +} + +QVector QDomNodeModel::nodesByIdref(const QXmlName&) const +{ + // TODO: Not implemented. + return QVector(); +} + +QXmlNodeModelIndex QDomNodeModel::root ( const QXmlNodeModelIndex & ni ) const +{ + QDomNode n = toDomNode(ni); + while (!n.parentNode().isNull()) + n = n.parentNode(); + + return fromDomNode(n); +} + +QSourceLocation QDomNodeModel::sourceLocation(const QXmlNodeModelIndex&) const +{ + // TODO: Not implemented. + return QSourceLocation(); +} + +QString QDomNodeModel::stringValue ( const QXmlNodeModelIndex & ni ) const +{ + QDomNode n = toDomNode(ni); + + if (n.isProcessingInstruction()) + return n.toProcessingInstruction().data(); + else if (n.isText()) + return n.toText().data(); + else if (n.isComment()) + return n.toComment().data(); + else if (n.isElement()) + return n.toElement().text(); + else if (n.isDocument()) + return n.toDocument().documentElement().text(); + else if (n.isAttr()) + return n.toAttr().value(); + + return QString(); +} + +QVariant QDomNodeModel::typedValue ( const QXmlNodeModelIndex & ni ) const +{ + return qVariantFromValue(stringValue(ni)); +} + +QXmlNodeModelIndex QDomNodeModel::fromDomNode(const QDomNode &n) const +{ + if (n.isNull()) + return QXmlNodeModelIndex(); + + return createIndex(PrivateDomNodeWrapper(n).getImpl(), 0); +} + +QDomNode QDomNodeModel::toDomNode(const QXmlNodeModelIndex &ni) const +{ + return PrivateDomNodeWrapper((QDomNodePrivate*) ni.data()); +} + +QVector QDomNodeModel::path(const QDomNode &n) const +{ + QVector res; + QDomNode cur = n; + while (!cur.isNull()) + { + res.push_back(cur); + cur = cur.parentNode(); + } + + std::reverse(res.begin(), res.end()); + return res; +} + +int QDomNodeModel::childIndex(const QDomNode &n) const +{ + QDomNodeList children = n.parentNode().childNodes(); + for (int i = 0; i < children.size(); i++) + if (children.at(i) == n) + return i; + + return -1; +} + +QVector QDomNodeModel::attributes ( const QXmlNodeModelIndex & ni ) const +{ + QDomElement n = toDomNode(ni).toElement(); + QDomNamedNodeMap attrs = n.attributes(); + QVector res; + for (int i = 0; i < attrs.size(); i++) + { + res.push_back(fromDomNode(attrs.item(i))); + } + return res; +} + +QXmlNodeModelIndex QDomNodeModel::nextFromSimpleAxis ( SimpleAxis axis, const QXmlNodeModelIndex & ni) const +{ + QDomNode n = toDomNode(ni); + switch(axis) + { + case Parent: + return fromDomNode(n.parentNode()); + + case FirstChild: + return fromDomNode(n.firstChild()); + + case PreviousSibling: + return fromDomNode(n.previousSibling()); + + case NextSibling: + return fromDomNode(n.nextSibling()); + } + + return QXmlNodeModelIndex(); +} diff --git a/src/Mod/TechDraw/App/QDomNodeModel.h b/src/Mod/TechDraw/App/QDomNodeModel.h new file mode 100644 index 0000000000..4aa774e051 --- /dev/null +++ b/src/Mod/TechDraw/App/QDomNodeModel.h @@ -0,0 +1,41 @@ +#ifndef _QDOMNODEMODEL_H_ +#define _QDOMNODEMODEL_H_ + +#include +#include +#include + +class QDomNodeModel: public QAbstractXmlNodeModel +{ +public: + QDomNodeModel(QXmlNamePool, QDomDocument, bool parsedReadOnly = false); + QUrl baseUri ( const QXmlNodeModelIndex & n ) const; + QXmlNodeModelIndex::DocumentOrder compareOrder ( const QXmlNodeModelIndex & ni1, const QXmlNodeModelIndex & ni2 ) const; + QUrl documentUri ( const QXmlNodeModelIndex & n ) const; + QXmlNodeModelIndex elementById ( const QXmlName & id ) const; + QXmlNodeModelIndex::NodeKind kind ( const QXmlNodeModelIndex & ni ) const; + QXmlName name ( const QXmlNodeModelIndex & ni ) const; + QVector namespaceBindings ( const QXmlNodeModelIndex & n ) const; + QVector nodesByIdref ( const QXmlName & idref ) const; + QXmlNodeModelIndex root ( const QXmlNodeModelIndex & n ) const; + QSourceLocation sourceLocation ( const QXmlNodeModelIndex & index ) const; + QString stringValue ( const QXmlNodeModelIndex & n ) const; + QVariant typedValue ( const QXmlNodeModelIndex & node ) const; + +public: + QXmlNodeModelIndex fromDomNode (const QDomNode&) const; + QDomNode toDomNode(const QXmlNodeModelIndex &) const; + QVector path(const QDomNode&) const; + int childIndex(const QDomNode&) const; + +protected: + QVector attributes ( const QXmlNodeModelIndex & element ) const; + QXmlNodeModelIndex nextFromSimpleAxis ( SimpleAxis axis, const QXmlNodeModelIndex & origin) const; + +private: + mutable QXmlNamePool m_Pool; + mutable QDomDocument m_Doc; + bool m_ReadOnly; +}; + +#endif // _QDOMNODEMODEL_H_ diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 567de64c0b..91216d61a9 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -15,6 +15,7 @@ include_directories( ${OCC_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR} ${XercesC_INCLUDE_DIRS} + ${QT_QTXMLPATTERNS_INCLUDE_DIR} ) link_directories(${OCC_LIBRARY_DIR}) diff --git a/src/Mod/TechDraw/Gui/QGVPage.cpp b/src/Mod/TechDraw/Gui/QGVPage.cpp index 5e20ce9c23..b7cbd82c95 100644 --- a/src/Mod/TechDraw/Gui/QGVPage.cpp +++ b/src/Mod/TechDraw/Gui/QGVPage.cpp @@ -43,6 +43,9 @@ #include #endif +#include +#include + #include #include #include @@ -72,6 +75,7 @@ #include #include #include +#include #include "Rez.h" #include "QGIDrawingTemplate.h" @@ -740,16 +744,20 @@ void QGVPage::saveSvg(QString filename) docName; QSvgGenerator svgGen; - QTemporaryFile* tempFile = new QTemporaryFile();; - svgGen.setOutputDevice(tempFile); - svgGen.setSize(QSize((int) Rez::guiX(page->getPageWidth()), (int) Rez::guiX(page->getPageHeight()))); //expects pixels, gets mm + QTemporaryFile temporaryFile; + svgGen.setOutputDevice(&temporaryFile); + + // Set resolution in DPI. Use the actual one, i.e. Rez::guiX(inch) + svgGen.setResolution(Rez::guiX(25.4)); + + // Set size in pixels, which Qt recomputes using DPI to mm. + int pixelWidth = Rez::guiX(page->getPageWidth()); + int pixelHeight = Rez::guiX(page->getPageHeight()); + svgGen.setSize(QSize(pixelWidth, pixelHeight)); + //"By default this property is set to QSize(-1, -1), which indicates that the generator should not output // the width and height attributes of the element." >> but Inkscape won't read it without size info?? - svgGen.setViewBox(QRect(0, 0, Rez::guiX(page->getPageWidth()), Rez::guiX(page->getPageHeight()))); - - // Set resolution in DPI. To keep text dimensions as they are on screen, - // use the very same resolution the screen paint device reports. - svgGen.setResolution(MDIViewPage::getFromScene(scene())->logicalDpiY()); + svgGen.setViewBox(QRect(0, 0, pixelWidth, pixelHeight)); svgGen.setTitle(QObject::tr("FreeCAD SVG Export")); svgGen.setDescription(svgDescription); @@ -760,6 +768,17 @@ void QGVPage::saveSvg(QString filename) m_vpPage->setFrameState(false); m_vpPage->setTemplateMarkers(false); toggleHatch(false); + + // Here we temporarily hide the page template, because Qt would otherwise convert the SVG template + // texts into series of paths, making the later document edits practically unfeasible. + // We will insert the SVG template ourselves in the final XML postprocessing operation. + QGISVGTemplate *svgTemplate = dynamic_cast(pageTemplate); + bool templateVisible = false; + if (svgTemplate) { + templateVisible = svgTemplate->isVisible(); + svgTemplate->hide(); + } + refreshViews(); viewport()->repaint(); @@ -778,56 +797,119 @@ void QGVPage::saveSvg(QString filename) m_vpPage->setFrameState(saveState); m_vpPage->setTemplateMarkers(saveState); toggleHatch(true); + if (templateVisible) { + svgTemplate->show(); + } + refreshViews(); viewport()->repaint(); - tempFile->close(); - postProcessXml(tempFile, filename, pageName); + temporaryFile.close(); + postProcessXml(temporaryFile, filename, pageName); } -void QGVPage::postProcessXml(QTemporaryFile* tempFile, QString fileName, QString pageName) +void QGVPage::postProcessXml(QTemporaryFile& temporaryFile, QString fileName, QString pageName) { - QDomDocument doc(QString::fromUtf8("SvgDoc")); - QFile file(tempFile->fileName()); + QDomDocument exportDoc(QString::fromUtf8("SvgDoc")); + QFile file(temporaryFile.fileName()); if (!file.open(QIODevice::ReadOnly)) { Base::Console().Message("QGVPage::ppsvg - tempfile open error\n"); return; } - if (!doc.setContent(&file)) { + if (!exportDoc.setContent(&file)) { Base::Console().Message("QGVPage::ppsvg - xml error\n"); file.close(); return; } file.close(); - QDomElement docElem = doc.documentElement(); //root + QDomElement exportDocElem = exportDoc.documentElement(); //root - QDomNode n = docElem.firstChild(); - bool firstGroupFound = false; - QString groupTag = QString::fromUtf8("g"); - QDomElement e; - while(!n.isNull()) { - e = n.toElement(); // try to convert the node to an element. - if(!e.isNull()) { - if (!firstGroupFound) { - if (e.tagName() == groupTag) { - firstGroupFound = true; - break; + QXmlQuery query(QXmlQuery::XQuery10); + QDomNodeModel model(query.namePool(), exportDoc); + query.setFocus(QXmlItem(model.fromDomNode(exportDocElem))); + + // XPath query to select first node as direct element descendant + query.setQuery(QString::fromUtf8( + "declare default element namespace \"" SVG_NS_URI "\"; " + "declare namespace freecad=\"" FREECAD_SVG_NS_URI "\"; " + "/svg/g[1]")); + + QXmlResultItems queryResult; + query.evaluateTo(&queryResult); + + // Insert Freecad SVG namespace into namespace declarations + exportDocElem.setAttribute(QString::fromUtf8("xmlns:freecad"), + QString::fromUtf8(FREECAD_SVG_NS_URI)); + + // Set the first group's id to page name + QDomElement g; + if (!queryResult.next().isNull()) { + g = model.toDomNode(queryResult.current().toNodeModelIndex()).toElement(); + g.setAttribute(QString::fromUtf8("id"), pageName); + } + + // Now insert our template + QGISVGTemplate *svgTemplate = dynamic_cast(pageTemplate); + if (svgTemplate) { + DrawSVGTemplate *drawTemplate = svgTemplate->getSVGTemplate(); + if (drawTemplate) { + QFile templateResultFile(QString::fromUtf8(drawTemplate->PageResult.getValue())); + if (templateResultFile.open(QIODevice::ReadOnly)) { + QDomDocument templateResultDoc(QString::fromUtf8("SvgDoc")); + if (templateResultDoc.setContent(&templateResultFile)) { + QDomElement templateDocElem = templateResultDoc.documentElement(); + + // Insert the template into a new group with id set to template name + QDomElement groupWrapper = exportDoc.createElement(QString::fromUtf8("g")); + Base::FileInfo fi(drawTemplate->Template.getValue()); + groupWrapper.setAttribute(QString::fromUtf8("id"), + QString::fromUtf8(fi.fileName().c_str())); + groupWrapper.setAttribute(QString::fromUtf8("style"), + QString::fromUtf8("stroke: none;")); + + // Scale the template group correctly + groupWrapper.setAttribute(QString::fromUtf8("transform"), + QString().sprintf("scale(%f, %f)", Rez::guiX(1.0), Rez::guiX(1.0))); + + // Finally, transfer all template document child nodes under the wrapper group + while (!templateDocElem.firstChild().isNull()) { + groupWrapper.appendChild(templateDocElem.firstChild()); + } + + if (!g.isNull()) { + g.insertBefore(groupWrapper, QDomNode()); + } + else { + exportDocElem.insertBefore(groupWrapper, QDomNode()); + } } } } - n = n.nextSibling(); } - e.setAttribute(QString::fromUtf8("id"),pageName); + // As icing on the cake, get rid of the empty 's Qt SVG generator painting inserts. + // XPath query to select any element anywhere with no child nodes whatsoever + query.setQuery(QString::fromUtf8( + "declare default element namespace \"" SVG_NS_URI "\"; " + "declare namespace freecad=\"" FREECAD_SVG_NS_URI "\"; " + "//g[not(*)]")); + + query.evaluateTo(&queryResult); + while (!queryResult.next().isNull()) { + g = model.toDomNode(queryResult.current().toNodeModelIndex()).toElement(); + g.parentNode().removeChild(g); + } + + // Time to save our product QFile outFile( fileName ); if( !outFile.open( QIODevice::WriteOnly | QIODevice::Text ) ) { Base::Console().Message("QGVP::ppxml - failed to open file for writing: %s\n",qPrintable(fileName) ); } + QTextStream stream( &outFile ); - stream << doc.toString(); + stream << exportDoc.toString(); outFile.close(); - delete tempFile; } void QGVPage::paintEvent(QPaintEvent *event) diff --git a/src/Mod/TechDraw/Gui/QGVPage.h b/src/Mod/TechDraw/Gui/QGVPage.h index 4611354f5e..52904916ac 100644 --- a/src/Mod/TechDraw/Gui/QGVPage.h +++ b/src/Mod/TechDraw/Gui/QGVPage.h @@ -115,7 +115,7 @@ public: /// Renders the page to SVG with filename. void saveSvg(QString filename); - void postProcessXml(QTemporaryFile* tempFile, QString filename, QString pagename); + void postProcessXml(QTemporaryFile& tempFile, QString filename, QString pagename); /* int balloonIndex;*/