# HG changeset patch # User Markus Mützel # Date 1674993307 -3600 # Sun Jan 29 12:55:07 2023 +0100 # Node ID 094166909d154553f780b16617f71c0278521675 # Parent 5e9bfd2c11b50642ede3e99827f5544ddb812734 print: Add option to merge only subsequent triangles with SVG toolchain (bug #63646). Trying to merge all triangles that are sharing an edge might take a long time. But that might be necessary to avoid hairlines in figures containing patch or surface graphics objects. Allow selecting if no, all, or only subsequent triangles sharing an edge should be merged into a polygon. * src/octave-svgconvert.cc (main): Distinguish between merging no (0), all (2), or only consecutive (1) triangles into polygons. (octave_polygon::reconstruct): Skip numerically expensive part of trying to merge all polygons that are sharing an edge unless it was selected. * scripts/plot/util/print.m: Document new options "-polymerge", "-nopolymerge", and "-polymerge-all" for polygon merging with the SVG toolchain. (svgconvert): Call "octave-svgconvert" with the selected polygon merge mode. * scripts/plot/private/__print_parge_opts__.m: Select default value and parse input for new options. diff -r 5e9bfd2c11b5 -r 094166909d15 scripts/plot/util/print.m --- a/scripts/plot/util/print.m Fri Jan 27 14:27:30 2023 -0800 +++ b/scripts/plot/util/print.m Sun Jan 29 12:55:07 2023 +0100 @@ -157,6 +157,20 @@ ## Caution: If Octave was built against Qt version earlier than 5.13, ## @option{-svgconvert} may lead to inaccurate rendering of image objects. ## +## @item -polymerge +## @itemx -nopolymerge +## @itemx -polymerge-all +## When using the SVG based backend @option{-svgconvert}, faces are rendered +## as triangles. In some cases, some viewers might display fine lines where +## those triangles share an edge. These options control whether all triangles +## that share edges are merged into polygons (@option{-polymerge-all} which +## might take some time for graphics consisting of many triangles -- including +## line markers), only consecutive polygons are merged (@option{-polymerge}), +## or no triangles are merged at all (@option{-no-polymerge}). By default, +## only consecutive triangles sharing an edge are merged, unless the printed +## figure contains patch or surface graphics objects in which case all +## triangles that are sharing an edge are merged. +## ## @item -portrait ## @itemx -landscape ## Specify the orientation of the plot for printed output. @@ -1160,7 +1174,8 @@ cmd = sprintf ('%s - %%s %3.2f "%s" %d "%%s"', ... undo_string_escapes (opts.svgconvert_binary), ... get (0, "screenpixelsperinch"), ... - undo_string_escapes (fullfile (fontdir, "FreeSans.otf")), 1); + undo_string_escapes (fullfile (fontdir, "FreeSans.otf")), + opts.polymerge); if (opts.debug) fprintf ("svgconvert command: '%s'\n", cmd); diff -r 5e9bfd2c11b5 -r 094166909d15 scripts/plot/util/private/__print_parse_opts__.m --- a/scripts/plot/util/private/__print_parse_opts__.m Fri Jan 27 14:27:30 2023 -0800 +++ b/scripts/plot/util/private/__print_parse_opts__.m Sun Jan 29 12:55:07 2023 +0100 @@ -59,6 +59,7 @@ arg_st.ghostscript.antialiasing_textalphabits = 4; arg_st.ghostscript.antialiasing_graphicsalphabits = 1; arg_st.lpr_binary = __quote_path__ (__find_binary__ ("lpr")); + arg_st.polymerge = 1; arg_st.name = ""; arg_st.orientation = ""; arg_st.pstoedit_binary = __quote_path__ (__find_binary__ ("pstoedit")); @@ -88,6 +89,11 @@ varargin(1) = []; endif + if (! isempty (findall (arg_st.figure, "type", "patch", ... + "-or", "type", "surface"))) + arg_st.polymerge = 2; + endif + for i = 1:numel (varargin) if (! ischar (varargin{i}) && ! iscellstr (varargin{i})) error ("print: input arguments must be a graphics handle or strings."); @@ -127,6 +133,12 @@ arg_st.svgconvert = true; elseif (strcmp (arg, "-nosvgconvert")) arg_st.svgconvert = false; + elseif (strcmp (arg, "-polymerge")) + arg_st.polymerge = 1; + elseif (strcmp (arg, "-nopolymerge")) + arg_st.polymerge = 0; + elseif (strcmp (arg, "-polymerge-all")) + arg_st.polymerge = 2; elseif (strcmp (arg, "-textspecial")) arg_st.special_flag = "textspecial"; elseif (strcmp (arg, "-fillpage")) diff -r 5e9bfd2c11b5 -r 094166909d15 src/octave-svgconvert.cc --- a/src/octave-svgconvert.cc Fri Jan 27 14:27:30 2023 -0800 +++ b/src/octave-svgconvert.cc Sun Jan 29 12:55:07 2023 +0100 @@ -161,7 +161,7 @@ void reset () { m_polygons.clear (); } - QList reconstruct () + QList reconstruct (int reconstruct_level) { if (m_polygons.isEmpty ()) return QList (); @@ -171,74 +171,77 @@ for (auto it = m_polygons.begin (); it != m_polygons.end (); it++) unused.push_back (false); - bool tryagain = (m_polygons.count () > 1); - - while (tryagain) + if (reconstruct_level > 1) { - tryagain = false; - for (auto ii = 0; ii < m_polygons.count (); ii++) + bool tryagain = (m_polygons.count () > 1); + + while (tryagain) { - if (! unused[ii]) + tryagain = false; + for (auto ii = 0; ii < m_polygons.count (); ii++) { - QPolygonF polygon = m_polygons[ii]; - for (auto jj = ii+1; jj < m_polygons.count (); jj++) + if (! unused[ii]) { - if (! unused[jj]) + QPolygonF polygon = m_polygons[ii]; + for (auto jj = ii+1; jj < m_polygons.count (); jj++) { - QPolygonF newpoly = mergepoly (polygon, m_polygons[jj]); - if (newpoly.count ()) + if (! unused[jj]) { - polygon = newpoly; - m_polygons[ii] = newpoly; - unused[jj] = true; - tryagain = true; + 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]; + // 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 del; + while (tryagain && polygon.count () > 4) + { + tryagain = false; + QVector 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); + 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); + } - del.push_front (jj+1); - } - - for (auto idx : del) - polygon.remove (idx); + for (auto idx : del) + polygon.remove (idx); - if (del.count ()) - tryagain = true; + if (del.count ()) + tryagain = true; + } + m_polygons[ii] = polygon; } - 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 + // 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 retval; for (int ii = 0; ii < m_polygons.count (); ii++) @@ -749,7 +752,7 @@ parent_elt.removeChild (orig.at (ii)); } -void reconstruct_polygons (QDomElement& parent_elt) +void reconstruct_polygons (QDomElement& parent_elt, int reconstruct_level) { QDomNodeList nodes = parent_elt.childNodes (); QColor current_color; @@ -787,7 +790,8 @@ if (color != current_color) { // Reconstruct the previous series of triangles - QList polygons = current_polygon.reconstruct (); + QList polygons + = current_polygon.reconstruct (reconstruct_level); collection.push_back (QPair,QList > (replaced_nodes, polygons)); @@ -806,19 +810,20 @@ { if (current_polygon.count ()) { - QList polygons = current_polygon.reconstruct (); + QList polygons = current_polygon.reconstruct (reconstruct_level); collection.push_back (QPair,QList > (replaced_nodes, polygons)); replaced_nodes.clear (); current_polygon.reset (); } - reconstruct_polygons (elt); + reconstruct_polygons (elt, reconstruct_level); } } // Finish collection.push_back (QPair,QList > - (replaced_nodes, current_polygon.reconstruct ())); + (replaced_nodes, + current_polygon.reconstruct (reconstruct_level))); for (int ii = 0; ii < collection.count (); ii++) replace_polygons (parent_elt, collection[ii].first, collection[ii].second); @@ -876,7 +881,10 @@ * 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\ +* reconstruct: specify whether to reconstruct triangle to polygons\n\ + 0: no reconstruction (merging) of polygons\n\ + 1: merge consecutive triangles if they share an edge\n\ + 2: merge all triangles that share edges (might take a long time)\n\ * outfile: output file name\n"; if (strcmp (argv[1], "-h") == 0) @@ -977,8 +985,9 @@ } // Do basic polygons reconstruction - if (QString (argv[5]).toInt ()) - reconstruct_polygons (root); + int reconstruct_level = QString (argv[5]).toInt (); + if (reconstruct_level) + reconstruct_polygons (root, reconstruct_level); // Add custom properties to SVG add_custom_properties (root);