view src/octave-svgconvert.cc @ 30564:796f54d4ddbf stable

update Octave Project Developers copyright for the new year In files that have the "Octave Project Developers" copyright notice, update for 2021. In all .txi and .texi files except gpl.txi and gpl.texi in the doc/liboctave and doc/interpreter directories, change the copyright to "Octave Project Developers", the same as used for other source files. Update copyright notices for 2022 (not done since 2019). For gpl.txi and gpl.texi, change the copyright notice to be "Free Software Foundation, Inc." and leave the date at 2007 only because this file only contains the text of the GPL, not anything created by the Octave Project Developers. Add Paul Thomas to contributors.in.
author John W. Eaton <jwe@octave.org>
date Tue, 28 Dec 2021 18:22:40 -0500
parents c23dafed5ffd
children bb9d776eafac
line wrap: on
line source

////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2017-2022 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
// <https://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////

#if defined (HAVE_CONFIG_H)
#  include "config.h"
#endif

#include <iostream>

#if defined (OCTAVE_USE_WINDOWS_API)
#  include <vector>
#  include <locale>
#  include <codecvt>
#endif

#include <QtCore>
#include <QtXml>

#include <QApplication>
#include <QFontDatabase>
#include <QImage>
#include <QPainter>
#include <QPrinter>
#include <QRegExp>

// Include a set of path rendering functions extracted from Qt-5.12 source
#include "octave-qsvghandler.h"

// Render to pdf
class pdfpainter : public QPainter
{
public:
  pdfpainter (QString fname, QRectF sz)
      :  m_printer ()
  {
    // Printer settings
    m_printer.setOutputFormat (QPrinter::PdfFormat);
    m_printer.setFontEmbeddingEnabled (true);
    m_printer.setOutputFileName (fname);
    m_printer.setFullPage (true);
#if defined (HAVE_QPRINTER_SETPAGESIZE)
    m_printer.setPageSize (QPageSize (sz.size (), QPageSize::Point,
                                      QString ("custom"),
                                      QPageSize::ExactMatch));
#else
    m_printer.setPaperSize (sz.size (), QPrinter::Point);
#endif

    // Painter settings
    begin (&m_printer);
    setWindow (sz.toRect ());
  }

  ~pdfpainter (void) { end (); }

private:
  QPrinter m_printer;
};

// String conversion functions+QVector<double> qstr2vectorf (QString str)
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;

  // Store path defined in <defs> in a map
  static bool in_defs = false;
  static QMap< QString, QPainterPath> path_map;

  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")
        {
          QString str = elt.attribute ("font-family");
          if (! str.isEmpty ())
            {
              // Font
              font = QFont ();
              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
            {
              bool current_clipstate = painter.hasClipping ();
              QRegion current_clippath = painter.clipRegion ();

              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);
                    }
                }

              // Fill color
              str = get_field (elt.attribute ("fill"), "rgb");
              if (! str.isEmpty ())
                {
                  QStringList clist = str.split (",");
                  painter.setBrush (QColor (clist[0].toInt (),
                                            clist[1].toInt (),
                                            clist[2].toInt ()));
                }

              // Transform
              str = elt.attribute ("transform");
              painter.save ();
              if (! str.isEmpty ())
                {
                  QStringRef tf (&str);
                  QTransform  tform =
                    parseTransformationMatrix (tf) * painter.transform ();
                  painter.setTransform (tform);
                }

              draw (elt, painter);

              // Restore previous clipping settings
              painter.restore  ();
              painter.setClipRegion (current_clippath);
              painter.setClipping (current_clipstate);
            }
        }
      else if (elt.tagName () == "defs")
        {
          in_defs = true;
          draw (elt, painter);
          in_defs = false;
        }
      else if (elt.tagName () == "path")
        {
          // Store QPainterPath for latter use
          QString id = elt.attribute ("id");
          if (! id.isEmpty ())
            {
              QString d = elt.attribute ("d");

              if (! d.isEmpty ())
                {
                  QStringRef data (&d);
                  QPainterPath path;
                  if (! parsePathDataFast (data, path))
                    continue; // Something went wrong, pass
                  else if (path.isEmpty ())
                    std::cout << "Empty path for data:"
                              << d.toStdString () << std::endl;
                  else if (in_defs)
                    path_map["#" + id] = path;
                  else
                    painter.drawPath (path);


                  if (path_map["#" + id].isEmpty ())
                    std::cout << "Empty path for data:"
                              << d.toStdString () << std::endl;
                }
            }
        }
      else if (elt.tagName () == "use")
        {
          painter.setPen (Qt::NoPen);

          QString str = elt.attribute ("xlink:href");
          if (! str.isEmpty () && str.size () > 2)
            {
              QPainterPath path = path_map[str];
              if (! path.isEmpty ())
                {
                  str = elt.attribute ("x");
                  double x = elt.attribute ("x").toDouble ();
                  str = elt.attribute ("y");
                  double y = elt.attribute ("y").toDouble ();
                  painter.translate (x, y);
                  painter.drawPath (path);
                  painter.translate (-x, -y);
                }
            }
        }
      else if (elt.tagName () == "text")
        {
          // 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 ();
              if (w > 0)
                pen.setWidthF (w);
            }

          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);
                  QTransform tform(m[0], m[1], m[2],
                                   m[3], m[4], m[5]);
                  painter.setTransform (tform);
                }

              painter.setRenderHint (QPainter::Antialiasing, false);
              painter.drawImage (pos, img);
              painter.setRenderHint (QPainter::Antialiasing, true);
              painter.restore  ();
            }
        }
      else if (elt.tagName () == "rect")
        {
          // Color
          QColor col (Qt::black);
          QString str = elt.attribute ("fill");
          if (! str.isEmpty ())
            col = QColor (str);

          // Position
          double x = elt.attribute ("x").toDouble ();
          double y = elt.attribute ("y").toDouble ();

          // Size
          double wd = elt.attribute ("width").toDouble ();
          double hg = elt.attribute ("height").toDouble ();

          painter.setBrush (col);
          painter.setPen (Qt::NoPen);

          painter.drawRect (QRectF (x, y, wd, hg));
        }
      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 && alpha >= 0.0)
                    color.setAlphaF (alpha);
                }

              if (! current_polygon.count ())
                current_color = color;

              if (color != current_color)
                {
                  // Reconstruct the previous series of triangles
                  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);
}

#if defined (OCTAVE_USE_WINDOWS_API) && defined (_UNICODE)
extern "C"
int
wmain (int argc, wchar_t **wargv)
{
  static char **argv = new char * [argc + 1];
  std::vector<std::string> argv_str;

  // convert wide character strings to multibyte UTF-8 strings
  std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> wchar_conv;
  for (int i_arg = 0; i_arg < argc; i_arg++)
    argv_str.push_back (wchar_conv.to_bytes (wargv[i_arg]));

  // Get pointers to C strings not before vector is stable.
  for (int i_arg = 0; i_arg < argc; i_arg++)
    argv[i_arg] = &argv_str[i_arg][0];
  argv[argc] = nullptr;

#else
int
main (int argc, char **argv)
{
#endif
  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 to 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::endl;
      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 (Currently unused). Keep the DPI argument in case
  // we implement raster outputs.
  // double dpi = QString (argv[3]).toDouble ();

  // 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);

      draw (root, painter);
    }
  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;
}