Mercurial > octave
view src/octave-svgconvert.cc @ 29100:014d2710dcd5
__pltopt__.m: Remove ancient code supporting numeric values for colors in LineSpec.
* __pltopt__.m: Remove support for marker '@'. Remove numeric support for
colors in LineSpec. Both of these are undocumented and have not been
supported in Matlab for years. Rewrite BIST test not to test for this
feature.
author | Rik <rik@octave.org> |
---|---|
date | Thu, 26 Nov 2020 07:53:09 -0800 |
parents | 6e8d5c99263c |
children | 0a5b15007766 |
line wrap: on
line source
//////////////////////////////////////////////////////////////////////// // // Copyright (C) 2017-2020 The Octave Project Developers // // See the file COPYRIGHT.md in the top-level directory of this // distribution or <https://octave.org/copyright/>. // // This file is part of Octave. // // Octave is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // Octave is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Octave; see the file COPYING. If not, see // <http://www.gnu.org/licenses/>. // //////////////////////////////////////////////////////////////////////// #include <iostream> #include <QtCore> #include <QtXml> #include <QApplication> #include <QFontDatabase> #include <QImage> #include <QPainter> #include <QPrinter> #include <QRegExp> class pdfpainter : public QPainter { public: pdfpainter (QString fname, QRectF sizepix, double dpi) : m_fname (fname), m_sizef (sizepix), m_dpi (dpi), m_printer () { double scl = get_scale (); m_sizef.setWidth (m_sizef.width () * scl); m_sizef.setHeight (m_sizef.height () * scl); // Printer settings m_printer.setOutputFormat (QPrinter::PdfFormat); m_printer.setFontEmbeddingEnabled (true); m_printer.setOutputFileName (get_fname ()); m_printer.setFullPage (true); m_printer.setPaperSize (get_rectf ().size (), QPrinter::DevicePixel); // Painter settings begin (&m_printer); setViewport (get_rect ()); scale (get_scale (), get_scale ()); } ~pdfpainter (void) { } QString get_fname (void) const { return m_fname; } QRectF get_rectf (void) const { return m_sizef; } QRect get_rect (void) const { return m_sizef.toRect (); } double get_scale (void) const { return m_dpi / 72.0; } void finish (void) { end (); } private: QString m_fname; QRectF m_sizef; double m_dpi; QPrinter m_printer; }; // String conversion functions QVector<double> qstr2vectorf (QString str) { QVector<double> pts; QStringList coords = str.split (","); for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 1) { double pt = (*p).toDouble (); pts.append (pt); } return pts; } QVector<double> qstr2vectord (QString str) { QVector<double> pts; QStringList coords = str.split (","); for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 1) { double pt = (*p).toDouble (); pts.append (pt); } return pts; } QVector<QPointF> qstr2ptsvector (QString str) { QVector<QPointF> pts; str = str.trimmed (); str.replace (" ", ","); QStringList coords = str.split (","); for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 2) { QPointF pt ((*p).toDouble (), (*(p+1)).toDouble ()); pts.append (pt); } return pts; } QVector<QPoint> qstr2ptsvectord (QString str) { QVector<QPoint> pts; str = str.trimmed (); str.replace (" ", ","); QStringList coords = str.split (","); for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 2) { QPoint pt ((*p).toDouble (), (*(p+1)).toDouble ()); pts.append (pt); } return pts; } // Extract field arguments in a style-like string, e.g. "bla field(1,34,56) bla" QString get_field (QString str, QString field) { QString retval; QRegExp rx (field + "\\(([^\\)]*)\\)"); int pos = 0; pos = rx.indexIn (str, pos); if (pos > -1) retval = rx.cap (1); return retval; } // Polygon reconstruction class class octave_polygon { public: octave_polygon (void) { } octave_polygon (QPolygonF p) { m_polygons.push_back (p); } ~octave_polygon (void) { } int count (void) const { return m_polygons.count (); } void reset (void) { m_polygons.clear (); } QList<QPolygonF> reconstruct (void) { if (m_polygons.isEmpty ()) return QList<QPolygonF> (); // Once a polygon has been merged to another, it is marked unsuded QVector<bool> unused; for (auto it = m_polygons.begin (); it != m_polygons.end (); it++) unused.push_back (false); bool tryagain = (m_polygons.count () > 1); while (tryagain) { tryagain = false; for (auto ii = 0; ii < m_polygons.count (); ii++) { if (! unused[ii]) { QPolygonF polygon = m_polygons[ii]; for (auto jj = ii+1; jj < m_polygons.count (); jj++) { if (! unused[jj]) { QPolygonF newpoly = mergepoly (polygon, m_polygons[jj]); if (newpoly.count ()) { polygon = newpoly; m_polygons[ii] = newpoly; unused[jj] = true; tryagain = true; } } } } } } // Try to remove cracks in polygons for (auto ii = 0; ii < m_polygons.count (); ii++) { QPolygonF polygon = m_polygons[ii]; tryagain = ! unused[ii]; while (tryagain && polygon.count () > 4) { tryagain = false; QVector<int> del; for (auto jj = 1; jj < (polygon.count () - 1); jj++) if (polygon[jj-1] == polygon[jj+1]) { if (! del.contains (jj)) del.push_front (jj); del.push_front (jj+1); } for (auto idx : del) polygon.remove (idx); if (del.count ()) tryagain = true; } m_polygons[ii] = polygon; } // FIXME: There may still be residual cracks, we should do something like // resetloop = 2; // while (resetloop) // currface = shift (currface, 1); // if (currface(1) == currface(3)) // currface([2 3]) = []; // resetloop = 2; // else // resetloop--; // endif // endwhile QList<QPolygonF> retval; for (int ii = 0; ii < m_polygons.count (); ii++) { QPolygonF polygon = m_polygons[ii]; if (! unused[ii] && polygon.count () > 2) retval.push_back (polygon); } return retval; } static inline bool eq (QPointF p1, QPointF p2) { return ((qAbs (p1.x () - p2.x ()) <= 0.00001 * qMin (qAbs (p1.x ()), qAbs (p2.x ()))) && (qAbs (p1.y () - p2.y ()) <= 0.00001 * qMin (qAbs (p1.y ()), qAbs (p2.y ())))); } static QPolygonF mergepoly (QPolygonF poly1, QPolygonF poly2) { // Close polygon contour poly1.push_back (poly1[0]); poly2.push_back (poly2[0]); for (int ii = 0; ii < (poly1.size () - 1); ii++) { for (int jj = 0; jj < (poly2.size () - 1); jj++) { bool forward = (eq (poly1[ii], poly2[jj]) && eq (poly1[ii+1], poly2[jj+1])); bool backward = ! forward && (eq (poly1[ii], poly2[jj+1]) && eq (poly1[ii+1], poly2[jj])); if (forward || backward) { // Unclose contour poly1.pop_back (); poly2.pop_back (); QPolygonF merged; for (int kk = 0; kk < (ii+1); kk++) merged.push_back (poly1[kk]); // Shift vertices and eliminate the common edge std::rotate (poly2.begin (), poly2.begin () + jj, poly2.end ()); poly2.erase (poly2.begin ()); poly2.erase (poly2.begin ()); if (forward) for (int kk = poly2.size (); kk > 0; kk--) merged.push_back (poly2[kk-1]); else for (int kk = 0; kk < poly2.size (); kk++) merged.push_back (poly2[kk]); for (int kk = ii+1; kk < poly1.size (); kk++) merged.push_back (poly1[kk]); // Return row vector QPolygonF out (merged.size ()); for (int kk = 0; kk < merged.size (); kk++) out[kk] = merged[kk]; return out; } } } return QPolygonF (); } void add (QPolygonF p) { if (m_polygons.count () == 0) m_polygons.push_back (p); else { QPolygonF tmp = mergepoly (m_polygons.back (), p); if (tmp.count ()) m_polygons.back () = tmp; else m_polygons.push_back (p); } } private: QList<QPolygonF> m_polygons; }; void draw (QDomElement& parent_elt, pdfpainter& painter) { QDomNodeList nodes = parent_elt.childNodes (); static QString clippath_id; static QMap< QString, QVector<QPoint> > clippath; // tspan elements must have access to the font and position extracted from // their parent text element static QFont font; static double dx = 0, dy = 0; for (int i = 0; i < nodes.count (); i++) { QDomNode node = nodes.at (i); if (! node.isElement ()) continue; QDomElement elt = node.toElement (); if (elt.tagName () == "clipPath") { clippath_id = "#" + elt.attribute ("id"); draw (elt, painter); clippath_id = QString (); } else if (elt.tagName () == "g") { bool current_clipstate = painter.hasClipping (); QRegion current_clippath = painter.clipRegion (); QString str = elt.attribute ("clip-path"); if (! str.isEmpty ()) { QVector<QPoint> pts = clippath[get_field (str, "url")]; if (! pts.isEmpty ()) { painter.setClipRegion (QRegion (QPolygon (pts))); painter.setClipping (true); } } draw (elt, painter); // Restore previous clipping settings painter.setClipRegion (current_clippath); painter.setClipping (current_clipstate); } else if (elt.tagName () == "text") { // Font font = QFont (); QString str = elt.attribute ("font-family"); if (! str.isEmpty ()) font.setFamily (elt.attribute ("font-family")); str = elt.attribute ("font-weight"); if (! str.isEmpty () && str != "normal") font.setWeight (QFont::Bold); str = elt.attribute ("font-style"); if (! str.isEmpty () && str != "normal") font.setStyle (QFont::StyleItalic); str = elt.attribute ("font-size"); if (! str.isEmpty ()) font.setPixelSize (str.toDouble ()); painter.setFont (font); // Translation and rotation painter.save (); str = get_field (elt.attribute ("transform"), "translate"); if (! str.isEmpty ()) { QStringList trans = str.split (","); dx = trans[0].toDouble (); dy = trans[1].toDouble (); str = get_field (elt.attribute ("transform"), "rotate"); if (! str.isEmpty ()) { QStringList rot = str.split (","); painter.translate (dx+rot[1].toDouble (), dy+rot[2].toDouble ()); painter.rotate (rot[0].toDouble ()); dx = rot[1].toDouble (); dy = rot[2].toDouble (); } else { painter.translate (dx, dy); dx = 0; dy = 0; } } draw (elt, painter); painter.restore (); } else if (elt.tagName () == "tspan") { // Font QFont saved_font (font); QString str = elt.attribute ("font-family"); if (! str.isEmpty ()) font.setFamily (elt.attribute ("font-family")); str = elt.attribute ("font-weight"); if (! str.isEmpty ()) { if (str != "normal") font.setWeight (QFont::Bold); else font.setWeight (QFont::Normal); } str = elt.attribute ("font-style"); if (! str.isEmpty ()) { if (str != "normal") font.setStyle (QFont::StyleItalic); else font.setStyle (QFont::StyleNormal); } str = elt.attribute ("font-size"); if (! str.isEmpty ()) font.setPixelSize (str.toDouble ()); painter.setFont (font); // Color is specified in rgb str = get_field (elt.attribute ("fill"), "rgb"); if (! str.isEmpty ()) { QStringList clist = str.split (","); painter.setPen (QColor (clist[0].toInt (), clist[1].toInt (), clist[2].toInt ())); } QStringList xx = elt.attribute ("x").split (" "); int y = elt.attribute ("y").toInt (); str = elt.text (); if (! str.isEmpty ()) { int ii = 0; foreach (QString s, xx) if (ii < str.size ()) painter.drawText (s.toInt ()-dx, y-dy, str.at (ii++)); } draw (elt, painter); font = saved_font; } else if (elt.tagName () == "polyline") { // Color QColor c (elt.attribute ("stroke")); QString str = elt.attribute ("stroke-opacity"); if (! str.isEmpty () && str.toDouble () != 1.0 && str.toDouble () >= 0.0) c.setAlphaF (str.toDouble ()); QPen pen; pen.setColor (c); // Line properties str = elt.attribute ("stroke-width"); if (! str.isEmpty ()) { double w = str.toDouble () * painter.get_scale (); if (w > 0) pen.setWidthF (w / painter.get_scale ()); } str = elt.attribute ("stroke-linecap"); pen.setCapStyle (Qt::SquareCap); if (str == "round") pen.setCapStyle (Qt::RoundCap); else if (str == "butt") pen.setCapStyle (Qt::FlatCap); str = elt.attribute ("stroke-linejoin"); pen.setJoinStyle (Qt::MiterJoin); if (str == "round") pen.setJoinStyle (Qt::RoundJoin); else if (str == "bevel") pen.setJoinStyle (Qt::BevelJoin); str = elt.attribute ("stroke-dasharray"); pen.setStyle (Qt::SolidLine); if (! str.isEmpty ()) { QVector<double> pat = qstr2vectord (str); if (pat.count () != 2 || pat[1] != 0) { // Express pattern in linewidth units for (auto& p : pat) p /= pen.widthF (); pen.setDashPattern (pat); } } painter.setPen (pen); painter.drawPolyline (qstr2ptsvector (elt.attribute ("points"))); } else if (elt.tagName () == "image") { // Images are represented as a base64 stream of png formatted data QString href_att = elt.attribute ("xlink:href"); QString prefix ("data:image/png;base64,"); QByteArray data = QByteArray::fromBase64 (href_att.mid (prefix.length ()).toLatin1 ()); QImage img; if (img.loadFromData (data, "PNG")) { QRect pos(elt.attribute ("x").toInt (), elt.attribute ("y").toInt (), elt.attribute ("width").toInt (), elt.attribute ("height").toInt ()); // Translate painter.save (); QString str = get_field (elt.attribute ("transform"), "matrix"); if (! str.isEmpty ()) { QVector<double> m = qstr2vectorf (str); double scl = painter.get_scale (); QTransform tform(m[0]*scl, m[1]*scl, m[2]*scl, m[3]*scl, m[4]*scl, m[5]*scl); painter.setTransform (tform); } painter.setRenderHint (QPainter::Antialiasing, false); painter.drawImage (pos, img); painter.setRenderHint (QPainter::Antialiasing, true); painter.restore (); } } else if (elt.tagName () == "polygon") { if (! clippath_id.isEmpty ()) clippath[clippath_id] = qstr2ptsvectord (elt.attribute ("points")); else { QString str = elt.attribute ("fill"); if (! str.isEmpty ()) { QColor color (str); str = elt.attribute ("fill-opacity"); if (! str.isEmpty () && str.toDouble () != 1.0 && str.toDouble () >= 0.0) color.setAlphaF (str.toDouble ()); QPolygonF p (qstr2ptsvector (elt.attribute ("points"))); if (p.count () > 2) { painter.setBrush (color); painter.setPen (Qt::NoPen); painter.setRenderHint (QPainter::Antialiasing, false); painter.drawPolygon (p); painter.setRenderHint (QPainter::Antialiasing, true); } } } } } } // Append a list of reconstructed child polygons to a QDomElement and remove // the original nodes void replace_polygons (QDomElement& parent_elt, QList<QDomNode> orig, QList<QPolygonF> polygons) { if (! orig.count () || (orig.count () == polygons.count ())) return; QDomNode last = orig.last (); for (int ii = 0; ii < polygons.count (); ii++) { QPolygonF polygon = polygons[ii]; QDomNode node = last.cloneNode (); QString pts; for (int jj = 0; jj < polygon.count (); jj++) { pts += QString ("%1,%2 ").arg (polygon[jj].x ()) .arg (polygon[jj].y ()); } node.toElement ().setAttribute ("points", pts.trimmed ()); if (! last.isNull ()) last = parent_elt.insertAfter (node, last); } for (int ii = 0; ii < orig.count (); ii++) parent_elt.removeChild (orig.at (ii)); } void reconstruct_polygons (QDomElement& parent_elt) { QDomNodeList nodes = parent_elt.childNodes (); QColor current_color; QList<QDomNode> replaced_nodes; octave_polygon current_polygon; // Collection of child nodes to be removed and polygons to be added QList< QPair<QList<QDomNode>,QList<QPolygonF> > > collection; for (int ii = 0; ii < nodes.count (); ii++) { QDomNode node = nodes.at (ii); if (! node.isElement ()) continue; QDomElement elt = node.toElement (); if (elt.tagName () == "polygon") { QString str = elt.attribute ("fill"); if (! str.isEmpty ()) { QColor color (str); str = elt.attribute ("fill-opacity"); if (! str.isEmpty ()) { double alpha = str.toDouble (); if (alpha != 1.0 && str.toDouble () >= 0.0) color.setAlphaF (alpha); } if (! current_polygon.count ()) current_color = color; if (color != current_color) { // Reconstruct the previous series of triangle QList<QPolygonF> polygons = current_polygon.reconstruct (); collection.push_back (QPair<QList<QDomNode>,QList<QPolygonF> > (replaced_nodes, polygons)); replaced_nodes.clear (); current_polygon.reset (); current_color = color; } QPolygonF p (qstr2ptsvector (elt.attribute ("points"))); current_polygon.add (p); replaced_nodes.push_back (node); } } else { if (current_polygon.count ()) { QList<QPolygonF> polygons = current_polygon.reconstruct (); collection.push_back (QPair<QList<QDomNode>,QList<QPolygonF> > (replaced_nodes, polygons)); replaced_nodes.clear (); current_polygon.reset (); } reconstruct_polygons (elt); } } // Finish collection.push_back (QPair<QList<QDomNode>,QList<QPolygonF> > (replaced_nodes, current_polygon.reconstruct ())); for (int ii = 0; ii < collection.count (); ii++) replace_polygons (parent_elt, collection[ii].first, collection[ii].second); } int main(int argc, char *argv[]) { const char *doc = "See \"octave-svgconvert -h\""; const char *help = "Usage:\n\ octave-svgconvert infile fmt dpi font reconstruct outfile\n\n\ Convert svg file to pdf, or svg. All arguments are mandatory:\n\ * infile: input svg file or \"-\" to indicate that the input svg file should be \ read from stdin\n\ * fmt: format of the output file. May be one of pdf or svg\n\ * dpi: device dependent resolution in screen pixel per inch\n\ * font: specify a file name for the default FreeSans font\n\ * reconstruct: specify whether to reconstruct triangle to polygons (0 or 1)\n\ * outfile: output file name\n"; if (strcmp (argv[1], "-h") == 0) { std::cout << help; return 0; } else if (argc != 7) { std::cerr << help; return -1; } // Open svg file QFile file; if (strcmp (argv[1], "-") != 0) { // Read from file file.setFileName (argv[1]); if (! file.open (QIODevice::ReadOnly | QIODevice::Text)) { std::cerr << "Unable to open file " << argv[1] << "\n"; std::cerr << help; return -1; } } else { // Read from stdin if (! file.open (stdin, QIODevice::ReadOnly | QIODevice::Text)) { std::cerr << "Unable read from stdin\n"; std::cerr << doc; return -1; } } // Create a DOM document and load the svg file QDomDocument document; QString msg; if (! document.setContent (&file, false, &msg)) { std::cerr << "Failed to parse XML contents" << std::endl << msg.toStdString (); std::cerr << doc; file.close(); return -1; } file.close (); // Format if (strcmp (argv[2], "pdf") != 0 && strcmp (argv[2], "svg") != 0) { std::cerr << "Unhandled output file format " << argv[2] << "\n"; std::cerr << doc; return -1; } // Resolution double dpi = QString (argv[3]).toDouble (); if (dpi <= 0.0) { std::cerr << "DPI must be positive\n"; return -1; } // Get the viewport from the root element QDomElement root = document.firstChildElement(); double x0, y0, dx, dy; QString s = root.attribute ("viewBox"); QTextStream (&s) >> x0 >> y0 >> dx >> dy; QRectF vp (x0, y0, dx, dy); // Setup application and add default FreeSans font if needed QApplication a (argc, argv); // When printing to PDF we may need the default FreeSans font if (! strcmp (argv[2], "pdf")) { QFont font ("FreeSans"); if (! font.exactMatch ()) { QString fontpath (argv[4]); if (! fontpath.isEmpty ()) { int id = QFontDatabase::addApplicationFont (fontpath); if (id < 0) std::cerr << "warning: print: " "Unable to add default font to database\n"; } else std::cerr << "warning: print: FreeSans font not found\n"; } } // First render in a temporary file QTemporaryFile fout; if (! fout.open ()) { std::cerr << "Could not open temporary file\n"; return -1; } // Do basic polygons reconstruction if (QString (argv[5]).toInt ()) reconstruct_polygons (root); // Draw if (! strcmp (argv[2], "pdf")) { // PDF painter pdfpainter painter (fout.fileName (), vp, dpi); draw (root, painter); painter.finish (); } else { // Return modified svg document QTextStream out (&fout); out.setCodec ("UTF-8"); out << document.toByteArray (); } // Delete output file before writing with new data if (QFile::exists (argv[6])) if (! QFile::remove (argv[6])) { std::cerr << "Unable to replace existing file " << argv[6] << "\n"; return -1; } fout.copy (argv[6]); return 0; }