# HG changeset patch
# User Pantxo Diribarne <pantxo.diribarne@gmail.com>
# Date 1510913010 -3600
#      Fri Nov 17 11:03:30 2017 +0100
# Node ID caaa6cabcb382b95340bbf11edb163c4bee54ad5
# Parent  9c47eedc44e214eb40b3b879f5f10e005ae03773
Add SVG convertion option for printing to PDF and raster outputs (bug #52193)

* print.m (docstring): Mark Gnuplot only devices with "*". For ps formats
  document the limited text support. Add "pdfcrop" device (no surrounding page)
  and remove "pdfwrite" from the examples of ghostscript devices.
  (print): in warnings, replace "print:" namespace by "octave:print:".
  Only change grid lines transparency for "ps2write" device. Add svgconvert_cmd
  field to opt structure
  (svgconvert): new function to build the svg convertion command

* __print_parse_opts__.m: In warnings, replace "print:" namespace by
  "octave:print:". Add svgconvert_binary to arg_st structure.
  (__svgconv_binary__): New function to locate the svg conversion binary either
  using OCTAVE_ARCHLIBDIR environment variable or in Octave's archlibdir.

* __opengl_print__.m: For other ghostscript devices than ps2write, first convert
 SVG to PDF and pass the PDF to ghostscript instead of an EPS.

* src/octave-svgconvert.cc: New file with the command line converter code.

* src/module.mk: Build converter based AMCOND_BUILD_QT_GUI condition. Setup
  related SOURCE, CPPFLAGS, LDADD, LDFLAGS variables with Qt headers and
  libraries.

* m4/acinclude.m4: Add QtXml to necessary Qt modules. 

diff -r 9c47eedc44e2 -r caaa6cabcb38 m4/acinclude.m4
--- a/m4/acinclude.m4	Mon Oct 08 09:16:29 2018 +0200
+++ b/m4/acinclude.m4	Fri Nov 17 11:03:30 2017 +0100
@@ -1974,10 +1974,10 @@
   ## Check for Qt libraries
   case "$qt_version" in
     4)
-      QT_MODULES="QtCore QtGui QtNetwork QtOpenGL QtHelp"
+      QT_MODULES="QtCore QtGui QtNetwork QtOpenGL QtHelp QtXml"
     ;;
     5)
-      QT_MODULES="Qt5Core Qt5Gui Qt5Network Qt5OpenGL Qt5PrintSupport Qt5Help"
+      QT_MODULES="Qt5Core Qt5Gui Qt5Network Qt5OpenGL Qt5PrintSupport Qt5Help Qt5Xml"
     ;;
     *)
       AC_MSG_ERROR([Unrecognized Qt version $qt_version])
diff -r 9c47eedc44e2 -r caaa6cabcb38 scripts/plot/util/print.m
--- a/scripts/plot/util/print.m	Mon Oct 08 09:16:29 2018 +0200
+++ b/scripts/plot/util/print.m	Fri Nov 17 11:03:30 2017 +0100
@@ -74,6 +74,18 @@
 ## orientation specified.  This option is equivalent to changing the figure's
 ## @qcode{"paperorientation"} property.
 ##
+## @item -svgconvert
+##   For OpenGL based toolkits, this option adds support for printing
+## arbitrary characters and fonts in PDF and raster formats (by
+## default those formats inherit the limitations of the EPS format).
+##
+## This option also avoids some antialiasing artifacts in patch and surface
+## objects rendering.  Finally it adds support for printing transparent
+## line, patch and surface objects.
+##
+## Caution: -svgconvert may lead to innacurate rendering of image
+## objects
+##
 ## @item  -TextAlphaBits=@var{n}
 ## @itemx -GraphicsAlphaBits=@var{n}
 ##   Octave is able to produce output for various printers, bitmaps, and
@@ -84,7 +96,8 @@
 ##
 ## @item -d@var{device}
 ##   The available output format is specified by the option @var{device}, and
-## is one of:
+## is one of (devices marked with a "*" are available only with Gnuplot
+## toolkit):
 ##
 ##   @table @code
 ##   @item  ps
@@ -92,14 +105,19 @@
 ##   @itemx psc
 ##   @itemx psc2
 ##     PostScript (level 1 and 2, mono and color).  The OpenGL-based toolkits
-## always generate PostScript level 3.0.
+## always generate PostScript level 3.0 and have a limited support for
+## text.
 ##
 ##   @item  eps
 ##   @itemx eps2
 ##   @itemx epsc
 ##   @itemx epsc2
 ##     Encapsulated PostScript (level 1 and 2, mono and color).  The
-## OpenGL-based toolkits always generate PostScript level 3.0.
+## OpenGL-based toolkits always generate PostScript level 3.0 and have a
+## limited support for text.  Only the set of ASCII characters may be
+## used and the only supported fonts are the base postscript fonts:
+## Helvetica (the default), Times, Courier and their variants (bold or
+## italic).  Any other font will be replaced by Helvetica.
 ##
 ##   @item  pslatex
 ##   @itemx epslatex
@@ -125,31 +143,30 @@
 ## braces).  The @samp{pdflatex} device, and any of the @samp{standalone}
 ## formats, are not available with the Gnuplot toolkit.
 ##
-##   @item  epscairo
-##   @itemx pdfcairo
-##   @itemx epscairolatex
-##   @itemx pdfcairolatex
-##   @itemx epscairolatexstandalone
-##   @itemx pdfcairolatexstandalone
-##     Generate Cairo-based output when using the Gnuplot graphics toolkit.
-## The @samp{epscairo} and @samp{pdfcairo} devices are synonymous with
-## the @samp{epsc} device.  The @LaTeX{} variants generate a @LaTeX{} file,
-## @file{@var{filename}.tex}, for the text portions of a plot, and an image
-## file, @file{@var{filename}.(eps|pdf)}, for the graph portion of the plot.
-## The @samp{standalone} variants behave as described for
+##   @item  epscairo*
+##   @itemx pdfcairo*
+##   @itemx epscairolatex*
+##   @itemx pdfcairolatex*
+##   @itemx epscairolatexstandalone*
+##   @itemx pdfcairolatexstandalone*
+##     Generate Cairo based output.  The @samp{epscairo} and
+## @samp{pdfcairo} devices are synonymous with the @samp{epsc} device.
+## The @LaTeX{} variants generate a @LaTeX{} file,
+## @file{@var{filename}.tex}, for the text portions of a plot, and an
+## image file, @file{@var{filename}.(eps|pdf)}, for the graph portion of
+## the plot.  The @samp{standalone} variants behave as described for
 ## @samp{epslatexstandalone} above.
 ##
 ##   @item  ill
 ##   @itemx @nospell{aifm}
 ##     Adobe Illustrator (obsolete for Gnuplot versions > 4.2)
 ##
-##   @item canvas
-##     Javascript-based drawing on HTML5 canvas viewable in a web browser
-## (only available for the Gnuplot graphics toolkit).
+##   @item canvas*
+##     Javascript-based drawing on HTML5 canvas viewable in a web browser.
 ##
-##   @item  cdr
-##   @itemx @nospell{corel}
-##     @nospell{CorelDraw}
+##   @item  cdr*
+##   @itemx @nospell{corel*}
+##     CorelDraw.
 ##
 ##   @item cgm
 ##     Computer Graphics Metafile, Version 1, ANSI X3.122-1986
@@ -168,9 +185,8 @@
 ## whether the special flag should be set for the text in the figure.
 ## (default is @option{-textnormal})
 ##
-##   @item gif
-##     GIF image
-## (only available for the Gnuplot graphics toolkit).
+##   @item gif*
+##     GIF image.
 ##
 ##   @item hpgl
 ##     HP plotter language
@@ -179,10 +195,9 @@
 ##   @itemx jpeg
 ##     JPEG image
 ##
-##   @item latex
-##   @itemx eepic
-##     @LaTeX{} picture environment and extended picture environment
-## (only available for the Gnuplot graphics toolkit).
+##   @item latex*
+##   @itemx eepic*
+##     @LaTeX{} picture environment and extended picture environment.
 ##
 ##   @item mf
 ##     Metafont
@@ -194,7 +209,9 @@
 ##     PBMplus
 ##
 ##   @item pdf
-##     Portable Document Format
+##   @itemx pdfcrop
+##     Portable document format. The pdfcrop device removes the default
+## surrounding page.
 ##
 ##   @item svg
 ##     Scalable Vector Graphics
@@ -204,12 +221,11 @@
 ##     TIFF image, compressed
 ##
 ##   @item  tikz
-##   @itemx tikzstandalone
+##   @itemx tikzstandalone*
 ##     Generate a @LaTeX{} file using PGF/TikZ format.  The OpenGL-based
 ## toolkits create a PGF file while Gnuplot creates a TikZ file.  The
 ## @samp{tikzstandalone} device produces a @LaTeX{} document which includes the
-## TikZ file (@samp{tikzstandalone} and is only available for the Gnuplot
-## graphics toolkit).
+## TikZ file.
 ##   @end table
 ##
 ##   If the device is omitted, it is inferred from the file extension,
@@ -220,9 +236,6 @@
 ## Some examples are;
 ##
 ##   @table @code
-##   @item pdfwrite
-##     Produces pdf output from eps
-##
 ##   @item ljet2p
 ##     HP LaserJet @nospell{IIP}
 ##
@@ -346,6 +359,7 @@
   opts.latex_standalone = @latex_standalone;
   opts.lpr_cmd = @lpr;
   opts.epstool_cmd = @epstool;
+  opts.svgconvert_cmd = @svgconvert;
 
   if (isempty (opts.figure) || ! isfigure (opts.figure))
     error ("print: no figure to print");
@@ -406,11 +420,13 @@
       nfig += 1;
     endfor
 
-    ## FIXME: line transparency is only handled for svg output when
-    ## using gl2ps. For other formats, switch grid lines to light gray
+    ## FIXME: line transparency is not handled for postscript output when
+    ## using opengl toolkits. For ps formats, switch grid lines to light gray
     ## so that the image output approximately matches on-screen experience.
     hax = findall (opts.figure, "type", "axes");
-    if (! strcmp (tk, "gnuplot") && ! strcmp (opts.devopt, "svg"))
+    if (! strcmp (tk, "gnuplot")
+        && ((! opts.svgconvert && ! strcmp (opts.devopt, "svg"))
+            || (opts.svgconvert && strcmp (opts.devopt, "ps2write"))))
       for n = 1:numel (hax)
         if (strcmp (get (hax(n), "gridcolormode"), "auto"))
           props(end+1).h = hax(n);
@@ -594,7 +610,8 @@
     for n = 1:numel (opts.unlink)
       [status, output] = unlink (opts.unlink{n});
       if (status != 0)
-        warning ("print.m: %s, '%s'", output, opts.unlink{n});
+        warning ("octave:print:unlinkerror", ...
+                 "print.m: %s, '%s'", output, opts.unlink{n});
       endif
     endfor
   end_unwind_protect
@@ -662,7 +679,7 @@
   endif
 
   if (! isempty (opts.preview) && opts.tight_flag)
-    warning ("print:previewandtight",
+    warning ("octave:print:previewandtight",
              "print.m: eps preview may not be combined with -tight");
   endif
   if (! isempty (opts.preview) || opts.tight_flag)
@@ -899,3 +916,28 @@
   endif
 
 endfunction
+
+function cmd = svgconvert (opts, devopt)
+
+  cmd = "";
+
+  if (nargin < 2)
+    devopt = opts.devopt;
+  endif
+
+  if (isempty (opts.svgconvert_binary))
+    warning ("octave:print:nosvgconvert", ...
+             ["print.m: unale to find octave-svgconvert, ", ...
+              "falling back to eps convertion"]);
+  else
+    def_font = fullfile (__octave_config_info__ ("datadir"), "font", ...
+                         "FreeSans.otf");
+    cmd = sprintf ("%s - %%s %3.2f %s %d %%s", opts.svgconvert_binary, ...
+                   get (0, "screenpixelsperinch"), def_font, 1);
+
+    if (opts.debug)
+      fprintf ("svgconvert command: '%s'\n", cmd);
+    endif
+  endif
+
+endfunction
diff -r 9c47eedc44e2 -r caaa6cabcb38 scripts/plot/util/private/__opengl_print__.m
--- a/scripts/plot/util/private/__opengl_print__.m	Mon Oct 08 09:16:29 2018 +0200
+++ b/scripts/plot/util/private/__opengl_print__.m	Fri Nov 17 11:03:30 2017 +0100
@@ -78,7 +78,15 @@
     case "svg"
       ## format GL2PS_SVG
       gl2ps_device = {"svg"};
-      pipeline = {sprintf("cat > %s", opts.name)};
+      svgcmd = "";
+      if (opts.svgconvert)
+        svgcmd = opts.svgconvert_cmd (opts, opts.ghostscript.device);
+      endif
+      if (! isempty (svgcmd))
+        pipeline = {sprintf(svgcmd, "svg", opts.name)};
+      else
+        pipeline = {sprintf("cat > %s", opts.name)};
+      endif
     case fig2dev_devices
       cmd_pstoedit = opts.pstoedit_cmd (opts, "fig");
       cmd_fig2dev = opts.fig2dev_cmd (opts, opts.devopt);
@@ -110,36 +118,63 @@
       pipeline = {sprintf("%s > %s", cmd, opts.name)};
     case {"corel", "gif"}
       error ("print:unsupporteddevice",
-             "print.m: %s output is not available for the FLTK graphics toolkit",
+             "print.m: %s output is not available for OpenGL graphics toolkits",
              upper (opts.devopt));
     case opts.ghostscript.device
-      opts.ghostscript.source = "-";
+      ## Except for postscript, use svg format and first convert to pdf
+      ## before going through ghostscript for final adjusments
+      svgcmd = "";
+      if (opts.svgconvert)
+        svgcmd = opts.svgconvert_cmd (opts, opts.ghostscript.device);
+      endif
+      dosvg = ! (strcmp (opts.devopt, "ps2write") || isempty (svgcmd));
+      if (! dosvg)
+        opts.ghostscript.source = "-";
+      else
+        tmp = tempname ();
+        opts.ghostscript.source = tmp;
+        opts.unlink = [opts.unlink tmp];
+        svgcmd = sprintf (svgcmd, "pdf", tmp);
+      endif
+
       opts.ghostscript.output = opts.name;
       if (opts.send_to_printer)
         opts.unlink(strcmp (opts.unlink, opts.ghostscript.output)) = [];
         opts.ghostscript.output = "-";
       endif
+
       [cmd_gs, cmd_cleanup] = __ghostscript__ (opts.ghostscript);
       if (opts.send_to_printer || isempty (opts.name))
         cmd_lpr = opts.lpr_cmd (opts);
         cmd = sprintf ("%s | %s", cmd_gs, cmd_lpr);
+      elseif (dosvg)
+        if (dos_shell)
+          cmd = sprintf ("%s & %s", svgcmd, cmd_gs);
+        else
+          cmd = sprintf ("%s ; %s", svgcmd, cmd_gs);
+        endif
       else
         cmd = sprintf ("%s", cmd_gs);
       endif
+
+      if (dosvg)
+        gl2ps_device = {"svg"};
+      else
+        gl2ps_device = {"eps"};
+      endif
+
       if (! isempty (cmd_cleanup))
-        gl2ps_device = {"eps"};
         if (dos_shell)
           pipeline = {sprintf("%s & %s", cmd, cmd_cleanup)};
         else
           pipeline = {sprintf("%s ; %s", cmd, cmd_cleanup)};
         endif
       else
-        gl2ps_device = {"eps"};
         pipeline = {cmd};
       endif
     otherwise
       error (sprintf ("print:no%soutput", opts.devopt),
-             "print.m: %s output is not available for GL2PS output",
+             "print.m: %s output is not available for OpenGL toolkits",
              upper (opts.devopt));
   endswitch
 
diff -r 9c47eedc44e2 -r caaa6cabcb38 scripts/plot/util/private/__print_parse_opts__.m
--- a/scripts/plot/util/private/__print_parse_opts__.m	Mon Oct 08 09:16:29 2018 +0200
+++ b/scripts/plot/util/private/__print_parse_opts__.m	Fri Nov 17 11:03:30 2017 +0100
@@ -60,6 +60,8 @@
   arg_st.printer = "";
   arg_st.send_to_printer = false;
   arg_st.special_flag = "textnormal";
+  arg_st.svgconvert = false;
+  arg_st.svgconvert_binary = __quote_path__ (__svgconv_binary__ ());
   arg_st.tight_flag = false;
   arg_st.use_color = 0; # 0=default, -1=mono, +1=color
 
@@ -100,6 +102,8 @@
       elseif (strcmp (arg, "-tight"))
         arg_st.loose = false;
         arg_st.tight_flag = true;
+      elseif (strcmp (arg, "-svgconvert"))
+        arg_st.svgconvert = true;
       elseif (strcmp (arg, "-textspecial"))
         arg_st.special_flag = "textspecial";
       elseif (any (strcmp (arg,
@@ -155,9 +159,12 @@
       elseif (length (arg) > 2 && arg(1:2) == "-f")
         arg_st.figure = str2double (arg(3:end));
       elseif (any (strcmp (arg, {"-painters", "-opengl"})))
-        warning ("print: '%s' accepted for Matlab compatibility, but is ignored", arg);
+        warning ("octave:ignoredargument", ...
+                 ["print: '%s' accepted for Matlab compatibility, ", ...
+                  "but is ignored"], arg);
       elseif (strcmp (arg, "-noui"))
-        warning ("print: option '-noui' not yet implemented");
+        warning ("octave:ignoredargument", ...
+                 "print: option '-noui' not yet implemented");
       elseif (length (arg) >= 1 && arg(1) == "-")
         error ("print: unknown option '%s'", arg);
       elseif (length (arg) > 0)
@@ -213,6 +220,8 @@
     arg_st.devopt = "jpeg";
   elseif (strcmp (arg_st.devopt, "tif"))
     arg_st.devopt = "tiff";
+  elseif (strcmp (arg_st.devopt, "pdfcrop"))
+    arg_st.devopt = "pdfwrite";
   endif
 
   persistent dev_list = {"aifm", "corel", "fig", "png", "jpeg", ...
@@ -221,7 +230,7 @@
               "psc2", "eps", "eps2", "epsc", "epsc2", ...
               "emf", "pdf", "pslatex", "epslatex", "epslatexstandalone", ...
               "pslatexstandalone", "pdflatexstandalone", ...
-              "pstex", "tiff", "tiffn" "tikz", "pcxmono", ...
+              "pstex", "tiff", "tiffn", "tikz", "tikzstandalone", "pcxmono", ...
               "pcx24b", "pcx256", "pcx16", "pgm", "pgmraw", ...
               "ppm", "ppmraw", "pdflatex", "texdraw", ...
               "epscairo", "pdfcairo", "pngcairo", "cairolatex", ...
@@ -236,7 +245,7 @@
               "ps", "eps", "eps", "eps", "eps", ...
               "emf", "pdf", "tex", "tex", "tex", ...
               "tex", "tex", ...
-              "ps", "tiff", "tiff", "tikz", "pcx", ...
+              "ps", "tiff", "tiff", "tikz", "tikz", "pcx", ...
               "pcx", "pcx", "pcx", "pgm", "pgm", ...
               "ppm", "ppm", "tex", "tex", ...
               "eps", "pdf", "png", "tex", ...
@@ -384,7 +393,9 @@
 
   if (warn_on_missing_ghostscript)
     if (isempty (arg_st.ghostscript.binary))
-      warning ("print:missing_gs", "print.m: Ghostscript binary is not available.  Only eps output is possible");
+      warning ("octave:print:missing_gs", ...
+               ["print.m: Ghostscript binary is not available.  ", ...
+                "Only eps output is possible"]);
     endif
     warn_on_missing_ghostscript = false;
   endif
@@ -467,7 +478,6 @@
 
 endfunction
 
-
 function gs = __ghostscript_binary__ ()
 
   persistent ghostscript_binary = "";
@@ -479,7 +489,7 @@
         || (! isempty (GSC) && file_in_path (getenv ("PATH"), GSC)))
       gs_binaries = {GSC};
     elseif (! isempty (GSC) && warn_on_bad_gsc)
-      warning ("print:badgscenv",
+      warning ("octave:print:badgscenv",
                "print.m: GSC environment variable not set properly");
       warn_on_bad_gsc = false;
       gs_binaries = {};
@@ -505,6 +515,32 @@
 
 endfunction
 
+function bin = __svgconv_binary__ ()
+
+  persistent binary = "";
+
+  if (isempty (binary))
+    bindir = getenv ("OCTAVE_ARCHLIBDIR");
+    if (isempty (bindir))
+      bindir = __octave_config_info__ ("archlibdir");
+    endif
+    
+    binary = fullfile (bindir, "octave-svgconvert");
+
+    if (! exist (binary, "file"))
+      if (! isunix () && exist ([binary, ".exe"], "file"))
+        ## Unix - Includes Mac OSX and Cygwin.
+        binary = [binary, ".exe"];
+      else
+        binary = "";
+      endif
+    endif
+  endif
+
+  bin = binary;
+
+endfunction
+
 function bin = __find_binary__ (binary)
 
   persistent data = struct ();
diff -r 9c47eedc44e2 -r caaa6cabcb38 src/module.mk
--- a/src/module.mk	Mon Oct 08 09:16:29 2018 +0200
+++ b/src/module.mk	Fri Nov 17 11:03:30 2017 +0100
@@ -49,6 +49,11 @@
   OCTAVE_INTERPRETER_TARGETS += %reldir%/octave-gui$(EXEEXT)
 endif
 
+if AMCOND_BUILD_QT_GUI
+  archlib_PROGRAMS += %reldir%/octave-svgconvert
+  OCTAVE_INTERPRETER_TARGETS += %reldir%/octave-svgconvert$(EXEEXT)
+endif
+
 OCTAVE_CORE_LIBS = \
   libinterp/liboctinterp.la \
   liboctave/liboctave.la \
@@ -115,6 +120,14 @@
   $(OCTAVE_GUI_LINK_OPTS) \
   $(WARN_LDFLAGS)
 
+%canon_reldir%_octave_svgconvert_SOURCES = %reldir%/octave-svgconvert.cc
+
+%canon_reldir%_octave_svgconvert_CPPFLAGS = $(QT_CPPFLAGS)
+
+%canon_reldir%_octave_svgconvert_LDADD = $(QT_LIBS)
+
+%canon_reldir%_octave_svgconvert_LDFLAGS = $(QT_LDFLAGS)
+
 %canon_reldir%_mkoctfile_SOURCES =
 
 nodist_%canon_reldir%_mkoctfile_SOURCES = %reldir%/mkoctfile.cc
diff -r 9c47eedc44e2 -r caaa6cabcb38 src/octave-svgconvert.cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/octave-svgconvert.cc	Fri Nov 17 11:03:30 2017 +0100
@@ -0,0 +1,850 @@
+/*
+Copyright (C) 2017 Pantxo Diribarne
+
+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 <QtMath>
+#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];
+        bool 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);
+
+          int sz = elt.attribute ("font-size").toInt ();
+          if (sz > 0)
+            font.setPixelSize (sz);
+
+          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);
+            }
+
+          int sz = elt.attribute ("font-size").toInt ();
+          if (sz > 0)
+            font.setPixelSize (sz);
+
+          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 properies
+          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 formated 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);
+
+                  QString 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 wether to reconstruct triangle to polygons (0 or 1)\n\
+* outfile: output file name\n";
+
+  if (argc != 7 || strcmp (argv[1], "-h") == 0)
+    {
+      std::cerr << help;
+      if (argc != 7)
+        return -1;
+      else
+        return 0;
+    }
+
+
+  // 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::cout << "Failed to parse XML contents" << std::endl
+                << msg.toStdString ();
+      std::cerr << doc;
+      file.close();
+      return -1;
+    }
+
+  // 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);
+
+  QFont font ("FreeSans");
+  int id = 0;
+  if (! font.exactMatch ())
+    {
+      QString fontname (argv[4]);
+      if (! fontname.isEmpty ())
+        {
+          id = QFontDatabase::addApplicationFont (fontname);
+          if (id < 0)
+            std::cout << "Unable to add default font to database\n";
+        }
+      else
+        std::cout << "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 << document.toString ();
+    }
+
+  // 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;
+}
