GNU Octave 10.1.0
A high-level interpreted language, primarily intended for numerical computations, mostly compatible with Matlab
 
Loading...
Searching...
No Matches
octave-svgconvert.cc
Go to the documentation of this file.
1////////////////////////////////////////////////////////////////////////
2//
3// Copyright (C) 2017-2025 The Octave Project Developers
4//
5// See the file COPYRIGHT.md in the top-level directory of this
6// distribution or <https://octave.org/copyright/>.
7//
8// This file is part of Octave.
9//
10// Octave is free software: you can redistribute it and/or modify it
11// under the terms of the GNU General Public License as published by
12// the Free Software Foundation, either version 3 of the License, or
13// (at your option) any later version.
14//
15// Octave is distributed in the hope that it will be useful, but
16// WITHOUT ANY WARRANTY; without even the implied warranty of
17// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18// GNU General Public License for more details.
19//
20// You should have received a copy of the GNU General Public License
21// along with Octave; see the file COPYING. If not, see
22// <https://www.gnu.org/licenses/>.
23//
24////////////////////////////////////////////////////////////////////////
25
26#if defined (HAVE_CONFIG_H)
27# include "config.h"
28#endif
29
30#include <cstdio>
31#include <iostream>
32
33#if defined (OCTAVE_USE_WINDOWS_API)
34# include <vector>
35# include <locale>
36# include <codecvt>
37#endif
38
39#include <QtCore>
40#include <QtXml>
41
42#include <QApplication>
43#include <QFontDatabase>
44#include <QImage>
45#include <QPainter>
46#include <QPrinter>
47#include <QRegularExpression>
48
49// Include a set of path rendering functions extracted from Qt-5.12 source
50#include "octave-qsvghandler.h"
51
52// Render to pdf
53class pdfpainter : public QPainter
54{
55public:
56
57 pdfpainter () = delete;
58
59 pdfpainter (QString fname, QRectF sz)
60 : m_printer ()
61 {
62 // Printer settings
63 m_printer.setOutputFormat (QPrinter::PdfFormat);
64 m_printer.setFontEmbeddingEnabled (true);
65 m_printer.setOutputFileName (fname);
66 m_printer.setFullPage (true);
67 m_printer.setPageSize (QPageSize (sz.size (), QPageSize::Point,
68 QString ("custom"),
69 QPageSize::ExactMatch));
70
71 // Painter settings
72 begin (&m_printer);
73 setWindow (sz.toRect ());
74 }
75
76 OCTAVE_DISABLE_COPY_MOVE (pdfpainter)
77
78 ~pdfpainter () { end (); }
79
80private:
81
82 QPrinter m_printer;
83};
84
85// String conversion functions+QVector<double> qstr2vectorf (QString str)
86QVector<double>
87qstr2vectorf (QString str)
88{
89 QVector<double> pts;
90 QStringList coords = str.split (",");
91 for (auto& p : coords)
92 pts.append (p.toDouble ());
93
94 return pts;
95}
96
97// FIXME: What's the difference between qstr2vectorf and qstr2vectord?
98// Can one be called from the other to avoid code duplication, or deleted?
99QVector<double>
100qstr2vectord (QString str)
101{
102 QVector<double> pts;
103 QStringList coords = str.split (",");
104 for (auto& p : coords)
105 pts.append (p.toDouble ());
106
107 return pts;
108}
109
110QVector<QPointF>
111qstr2ptsvector (QString str)
112{
113 QVector<QPointF> pts;
114 str = str.trimmed ();
115 str.replace (" ", ",");
116 QStringList coords = str.split (",");
117 for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 2)
118 {
119 QPointF pt ((*p).toDouble (), (*(p+1)).toDouble ());
120 pts.append (pt);
121 }
122 return pts;
123}
124
125// FIXME: What's the difference between qstr2ptsvector and qstr2ptsvectord?
126// Can one be called from the other to avoid code duplication, or deleted?
127QVector<QPoint>
128qstr2ptsvectord (QString str)
129{
130 QVector<QPoint> pts;
131 str = str.trimmed ();
132 str.replace (" ", ",");
133 QStringList coords = str.split (",");
134 for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 2)
135 {
136 QPoint pt ((*p).toDouble (), (*(p+1)).toDouble ());
137 pts.append (pt);
138 }
139 return pts;
140}
141
142// Extract field arguments in a style-like string, e.g. "bla field(1,34,56) bla"
143QString
144get_field (QString str, QString field)
145{
146 QRegularExpression rx (field + "\\(([^\\)]*)\\)");
147 QRegularExpressionMatch match = rx.match (str);
148 QString retval = match.captured (1);
149
150 return retval;
151}
152
153// Polygon reconstruction class
154class octave_polygon
155{
156public:
157
158 octave_polygon (QPolygonF p)
159 { m_polygons.push_back (p); }
160
161 OCTAVE_DEFAULT_CONSTRUCT_COPY_MOVE_DELETE (octave_polygon)
162
163 int count () const
164 { return m_polygons.count (); }
165
166 void reset ()
167 { m_polygons.clear (); }
168
169 QList<QPolygonF> reconstruct (int reconstruct_level)
170 {
171 if (m_polygons.isEmpty ())
172 return QList<QPolygonF> ();
173 else if (reconstruct_level < 2)
174 return m_polygons;
175
176 // Once a polygon has been merged to another, it is marked unsuded
177 QVector<bool> unused (m_polygons.count (), false);
178
179 bool tryagain = (m_polygons.count () > 1);
180
181 while (tryagain)
182 {
183 tryagain = false;
184 for (auto ii = 0; ii < m_polygons.count (); ii++)
185 {
186 if (! unused[ii])
187 {
188 QPolygonF polygon = m_polygons[ii];
189 for (auto jj = ii+1; jj < m_polygons.count (); jj++)
190 {
191 if (! unused[jj])
192 {
193 QPolygonF newpoly = mergepoly (polygon, m_polygons[jj]);
194 if (newpoly.count ())
195 {
196 polygon = newpoly;
197 m_polygons[ii] = newpoly;
198 unused[jj] = true;
199 tryagain = true;
200 }
201 }
202 }
203 }
204 }
205 }
206
207 // Try to remove cracks in polygons
208 for (auto ii = 0; ii < m_polygons.count (); ii++)
209 {
210 QPolygonF polygon = m_polygons[ii];
211 tryagain = ! unused[ii];
212
213 while (tryagain && polygon.count () > 4)
214 {
215 tryagain = false;
216 QVector<int> del;
217
218 for (auto jj = 1; jj < (polygon.count () - 1); jj++)
219 if (polygon[jj-1] == polygon[jj+1])
220 {
221 if (! del.contains (jj))
222 del.push_front (jj);
223
224 del.push_front (jj+1);
225 }
226
227 for (auto idx : del)
228 polygon.remove (idx);
229
230 if (del.count ())
231 tryagain = true;
232 }
233 m_polygons[ii] = polygon;
234 }
235
236 // FIXME: There may still be residual cracks, we should do something like
237 // resetloop = 2;
238 // while (resetloop)
239 // currface = shift (currface, 1);
240 // if (currface(1) == currface(3))
241 // currface([2 3]) = [];
242 // resetloop = 2;
243 // else
244 // resetloop--;
245 // endif
246 // endwhile
247
248 QList<QPolygonF> retval;
249 for (int ii = 0; ii < m_polygons.count (); ii++)
250 {
251 QPolygonF polygon = m_polygons[ii];
252 if (! unused[ii] && polygon.count () > 2)
253 retval.push_back (polygon);
254 }
255
256 return retval;
257 }
258
259 static inline
260 bool eq (QPointF p1, QPointF p2)
261 {
262 return ((qAbs (p1.x () - p2.x ())
263 <= 0.00001 * qMin (qAbs (p1.x ()), qAbs (p2.x ())))
264 && (qAbs (p1.y () - p2.y ())
265 <= 0.00001 * qMin (qAbs (p1.y ()), qAbs (p2.y ()))));
266 }
267
268 static
269 QPolygonF mergepoly (QPolygonF poly1, QPolygonF poly2)
270 {
271 // Close polygon contour
272 poly1.push_back (poly1[0]);
273 poly2.push_back (poly2[0]);
274
275 for (int ii = 0; ii < (poly1.size () - 1); ii++)
276 {
277 for (int jj = 0; jj < (poly2.size () - 1); jj++)
278 {
279 bool forward = (eq (poly1[ii], poly2[jj])
280 && eq (poly1[ii+1], poly2[jj+1]));
281 bool backward = ! forward && (eq (poly1[ii], poly2[jj+1])
282 && eq (poly1[ii+1], poly2[jj]));
283
284 if (forward || backward)
285 {
286 // Unclose contour
287 poly1.pop_back ();
288 poly2.pop_back ();
289
290 QPolygonF merged;
291 for (int kk = 0; kk < (ii+1); kk++)
292 merged.push_back (poly1[kk]);
293
294 // Shift vertices and eliminate the common edge
295 std::rotate (poly2.begin (), poly2.begin () + jj, poly2.end ());
296 poly2.erase (poly2.begin ());
297 poly2.erase (poly2.begin ());
298
299 if (forward)
300 for (int kk = poly2.size (); kk > 0; kk--)
301 merged.push_back (poly2[kk-1]);
302 else
303 for (int kk = 0; kk < poly2.size (); kk++)
304 merged.push_back (poly2[kk]);
305
306 for (int kk = ii+1; kk < poly1.size (); kk++)
307 merged.push_back (poly1[kk]);
308
309 // Return row vector
310 QPolygonF out (merged.size ());
311 for (int kk = 0; kk < merged.size (); kk++)
312 out[kk] = merged[kk];
313
314 return out;
315 }
316 }
317 }
318 return QPolygonF ();
319 }
320
321 void add (QPolygonF p)
322 {
323 if (m_polygons.count () == 0)
324 m_polygons.push_back (p);
325 else
326 {
327 QPolygonF tmp = mergepoly (m_polygons.back (), p);
328 if (tmp.count ())
329 m_polygons.back () = tmp;
330 else
331 m_polygons.push_back (p);
332 }
333 }
334
335private:
336 QList<QPolygonF> m_polygons;
337};
338
339void
340draw (QDomElement& parent_elt, pdfpainter& painter)
341{
342 QDomNodeList nodes = parent_elt.childNodes ();
343
344 static QString clippath_id;
345 static QMap< QString, QVector<QPoint>> clippath;
346
347 // tspan elements must have access to the font and position extracted from
348 // their parent text element
349 static QFont font;
350 static double dx = 0, dy = 0;
351
352 // Store path defined in <defs> in a map
353 static bool in_defs = false;
354 static QMap< QString, QPainterPath> path_map;
355
356 for (int i = 0; i < nodes.count (); i++)
357 {
358 QDomNode node = nodes.at (i);
359 if (! node.isElement ())
360 continue;
361
362 QDomElement elt = node.toElement ();
363
364 if (elt.tagName () == "clipPath")
365 {
366 clippath_id = "#" + elt.attribute ("id");
367 draw (elt, painter);
368 clippath_id = QString ();
369 }
370 else if (elt.tagName () == "g")
371 {
372 QString str = elt.attribute ("font-family");
373 if (! str.isEmpty ())
374 {
375 // Font
376 font = QFont (str);
377
378 str = elt.attribute ("font-weight");
379 if (! str.isEmpty () && str != "normal")
380 font.setWeight (QFont::Bold);
381
382 str = elt.attribute ("font-style");
383 if (! str.isEmpty () && str != "normal")
384 font.setStyle (QFont::StyleItalic);
385
386 str = elt.attribute ("font-size");
387 if (! str.isEmpty ())
388 font.setPixelSize (str.toDouble ());
389
390 painter.setFont (font);
391
392 // Translation and rotation
393 painter.save ();
394 str = get_field (elt.attribute ("transform"), "translate");
395 if (! str.isEmpty ())
396 {
397 QStringList trans = str.split (",");
398 dx = trans[0].toDouble ();
399 dy = trans[1].toDouble ();
400
401 str = get_field (elt.attribute ("transform"), "rotate");
402 if (! str.isEmpty ())
403 {
404 QStringList rot = str.split (",");
405 painter.translate (dx+rot[1].toDouble (),
406 dy+rot[2].toDouble ());
407 painter.rotate (rot[0].toDouble ());
408 dx = rot[1].toDouble ();
409 dy = rot[2].toDouble ();
410 }
411 else
412 {
413 painter.translate (dx, dy);
414 dx = 0;
415 dy = 0;
416 }
417 }
418
419 draw (elt, painter);
420 painter.restore ();
421 }
422 else
423 {
424 bool current_clipstate = painter.hasClipping ();
425 QRegion current_clippath = painter.clipRegion ();
426
427 str = elt.attribute ("clip-path");
428 if (! str.isEmpty ())
429 {
430 QVector<QPoint> pts = clippath[get_field (str, "url")];
431 if (! pts.isEmpty ())
432 {
433 painter.setClipRegion (QRegion (QPolygon (pts)));
434 painter.setClipping (true);
435 }
436 }
437
438 // Fill color
439 str = get_field (elt.attribute ("fill"), "rgb");
440 if (! str.isEmpty ())
441 {
442 QStringList clist = str.split (",");
443 painter.setBrush (QColor (clist[0].toInt (),
444 clist[1].toInt (),
445 clist[2].toInt ()));
446 }
447
448 // Transform
449 str = elt.attribute ("transform");
450 painter.save ();
451 if (! str.isEmpty ())
452 {
453#if HAVE_QSTRINGVIEW
454 QStringView tf {str};
455#else
456 QStringRef tf {&str};
457#endif
458 QTransform tform =
459 parseTransformationMatrix (tf) * painter.transform ();
460 painter.setTransform (tform);
461 }
462
463 draw (elt, painter);
464
465 // Restore previous clipping settings
466 painter.restore ();
467 painter.setClipRegion (current_clippath);
468 painter.setClipping (current_clipstate);
469 }
470 }
471 else if (elt.tagName () == "defs")
472 {
473 in_defs = true;
474 draw (elt, painter);
475 in_defs = false;
476 }
477 else if (elt.tagName () == "path")
478 {
479 // Store QPainterPath for latter use
480 QString id = elt.attribute ("id");
481 if (! id.isEmpty ())
482 {
483 QString d = elt.attribute ("d");
484
485 if (! d.isEmpty ())
486 {
487#if HAVE_QSTRINGVIEW
488 QStringView data {d};
489#else
490 QStringRef data {&d};
491#endif
492 QPainterPath path;
493 if (! parsePathDataFast (data, path))
494 continue; // Something went wrong, pass
495 else if (path.isEmpty ())
496 std::cout << "Empty path for data:"
497 << d.toStdString () << std::endl;
498 else if (in_defs)
499 path_map["#" + id] = path;
500 else
501 painter.drawPath (path);
502
503
504 if (path_map["#" + id].isEmpty ())
505 std::cout << "Empty path for data:"
506 << d.toStdString () << std::endl;
507 }
508 }
509 }
510 else if (elt.tagName () == "use")
511 {
512 painter.setPen (Qt::NoPen);
513
514 QString str = elt.attribute ("xlink:href");
515 if (! str.isEmpty () && str.size () > 2)
516 {
517 QPainterPath path = path_map[str];
518 if (! path.isEmpty ())
519 {
520 str = elt.attribute ("x");
521 double x = elt.attribute ("x").toDouble ();
522 str = elt.attribute ("y");
523 double y = elt.attribute ("y").toDouble ();
524 painter.translate (x, y);
525 painter.drawPath (path);
526 painter.translate (-x, -y);
527 }
528 }
529 }
530 else if (elt.tagName () == "text")
531 {
532 // Font
533 QFont saved_font (font);
534
535 QString str = elt.attribute ("font-family");
536 if (! str.isEmpty ())
537 {
538 // QFont::setFamily() doesn't seem to work properly in Qt6
539 // (see bug #66306). Use the QFont constructor to update
540 // the current font.
541 font = QFont (str, -1, font.weight (),
542 font.style () == QFont::StyleItalic);
543 font.setPixelSize (saved_font.pixelSize ());
544 }
545
546 str = elt.attribute ("font-weight");
547 if (! str.isEmpty ())
548 {
549 if (str != "normal")
550 font.setWeight (QFont::Bold);
551 else
552 font.setWeight (QFont::Normal);
553 }
554
555 str = elt.attribute ("font-style");
556 if (! str.isEmpty ())
557 {
558 if (str != "normal")
559 font.setStyle (QFont::StyleItalic);
560 else
561 font.setStyle (QFont::StyleNormal);
562 }
563
564 str = elt.attribute ("font-size");
565 if (! str.isEmpty ())
566 font.setPixelSize (str.toDouble ());
567
568 painter.setFont (font);
569
570 // Color is specified in rgb
571 str = get_field (elt.attribute ("fill"), "rgb");
572 if (! str.isEmpty ())
573 {
574 QStringList clist = str.split (",");
575 painter.setPen (QColor (clist[0].toInt (), clist[1].toInt (),
576 clist[2].toInt ()));
577 }
578
579 QStringList xx = elt.attribute ("x").split (" ");
580 int y = elt.attribute ("y").toInt ();
581 str = elt.text ();
582 if (! str.isEmpty ())
583 {
584 int ii = 0;
585 for (const QString& s : xx)
586 if (ii < str.size ())
587 painter.drawText (s.toInt ()-dx, y-dy, str.at (ii++));
588 }
589
590 draw (elt, painter);
591 font = saved_font;
592 }
593 else if (elt.tagName () == "polyline")
594 {
595 // Color
596 QColor c (elt.attribute ("stroke"));
597 QString str = elt.attribute ("stroke-opacity");
598 if (! str.isEmpty () && str.toDouble () != 1.0
599 && str.toDouble () >= 0.0)
600 c.setAlphaF (str.toDouble ());
601
602 QPen pen;
603 pen.setColor (c);
604
605 // Line properties
606 str = elt.attribute ("stroke-width");
607 if (! str.isEmpty ())
608 {
609 double w = str.toDouble ();
610 if (w > 0)
611 pen.setWidthF (w);
612 }
613
614 str = elt.attribute ("stroke-linecap");
615 pen.setCapStyle (Qt::SquareCap);
616 if (str == "round")
617 pen.setCapStyle (Qt::RoundCap);
618 else if (str == "butt")
619 pen.setCapStyle (Qt::FlatCap);
620
621 str = elt.attribute ("stroke-linejoin");
622 pen.setJoinStyle (Qt::MiterJoin);
623 if (str == "round")
624 pen.setJoinStyle (Qt::RoundJoin);
625 else if (str == "bevel")
626 pen.setJoinStyle (Qt::BevelJoin);
627
628 str = elt.attribute ("stroke-dasharray");
629 pen.setStyle (Qt::SolidLine);
630 if (! str.isEmpty ())
631 {
632 QVector<double> pat = qstr2vectord (str);
633 if (pat.count () != 2 || pat[1] != 0)
634 {
635 // Express pattern in linewidth units
636 for (auto& p : pat)
637 p /= pen.widthF ();
638
639 pen.setDashPattern (pat);
640 }
641 }
642
643 painter.setPen (pen);
644 painter.drawPolyline (qstr2ptsvector (elt.attribute ("points")));
645 }
646 else if (elt.tagName () == "image")
647 {
648 // Images are represented as a base64 stream of png formatted data
649 QString href_att = elt.attribute ("xlink:href");
650 QString prefix ("data:image/png;base64,");
651 QByteArray data
652 = QByteArray::fromBase64 (href_att.mid (prefix.length ()).toLatin1 ());
653 QImage img;
654 if (img.loadFromData (data, "PNG"))
655 {
656 QRect pos(elt.attribute ("x").toInt (),
657 elt.attribute ("y").toInt (),
658 elt.attribute ("width").toInt (),
659 elt.attribute ("height").toInt ());
660
661 // Translate
662 painter.save ();
663 QString str = get_field (elt.attribute ("transform"), "matrix");
664 if (! str.isEmpty ())
665 {
666 QVector<double> m = qstr2vectorf (str);
667 QTransform tform(m[0], m[1], m[2],
668 m[3], m[4], m[5]);
669 painter.setTransform (tform);
670 }
671
672 painter.setRenderHint (QPainter::Antialiasing, false);
673#if defined (HAVE_QPAINTER_RENDERHINT_LOSSLESS)
674 painter.setRenderHint (QPainter::LosslessImageRendering);
675#endif
676 painter.drawImage (pos, img);
677 painter.setRenderHint (QPainter::Antialiasing, true);
678 painter.restore ();
679 }
680 }
681 else if (elt.tagName () == "rect")
682 {
683 // Position
684 double x = elt.attribute ("x").toDouble ();
685 double y = elt.attribute ("y").toDouble ();
686
687 // Size
688 double wd = elt.attribute ("width").toDouble ();
689 double hg = elt.attribute ("height").toDouble ();
690
691 // Color
692 QColor saved_color = painter.brush ().color ();
693
694 QString str = elt.attribute ("fill");
695 if (! str.isEmpty ())
696 painter.setBrush (QColor (str));
697
698 painter.setPen (Qt::NoPen);
699
700 painter.drawRect (QRectF (x, y, wd, hg));
701
702 if (! str.isEmpty ())
703 painter.setBrush (saved_color);
704 }
705 else if (elt.tagName () == "polygon")
706 {
707 if (! clippath_id.isEmpty ())
708 clippath[clippath_id] = qstr2ptsvectord (elt.attribute ("points"));
709 else
710 {
711 QString str = elt.attribute ("fill");
712 if (! str.isEmpty ())
713 {
714 QColor color (str);
715
716 str = elt.attribute ("fill-opacity");
717 if (! str.isEmpty () && str.toDouble () != 1.0
718 && str.toDouble () >= 0.0)
719 color.setAlphaF (str.toDouble ());
720
721 QPolygonF p (qstr2ptsvector (elt.attribute ("points")));
722
723 if (p.count () > 2)
724 {
725 painter.setBrush (color);
726 painter.setPen (Qt::NoPen);
727
728 painter.setRenderHint (QPainter::Antialiasing, false);
729 painter.drawPolygon (p);
730 painter.setRenderHint (QPainter::Antialiasing, true);
731 }
732 }
733 }
734 }
735 }
736}
737
738// Append a list of reconstructed child polygons to a QDomElement and remove
739// the original nodes
740
741void
742replace_polygons (QDomElement& parent_elt, QList<QDomNode> orig,
743 QList<QPolygonF> polygons)
744{
745 if (! orig.count () || (orig.count () == polygons.count ()))
746 return;
747
748 QDomNode last = orig.last ();
749 for (int ii = 0; ii < polygons.count (); ii++)
750 {
751 QPolygonF polygon = polygons[ii];
752
753 QDomNode node = last.cloneNode ();
754
755 QString pts;
756
757 for (int jj = 0; jj < polygon.count (); jj++)
758 {
759 pts += QString ("%1,%2 ").arg (polygon[jj].x ())
760 .arg (polygon[jj].y ());
761 }
762
763 node.toElement ().setAttribute ("points", pts.trimmed ());
764
765 if (! last.isNull ())
766 last = parent_elt.insertAfter (node, last);
767 }
768
769 for (int ii = 0; ii < orig.count (); ii++)
770 parent_elt.removeChild (orig.at (ii));
771}
772
773void
774reconstruct_polygons (QDomElement& parent_elt, int reconstruct_level)
775{
776 QDomNodeList nodes = parent_elt.childNodes ();
777 QColor current_color;
778 QList<QDomNode> replaced_nodes;
779 octave_polygon current_polygon;
780
781 // Collection of child nodes to be removed and polygons to be added
782 QList< QPair<QList<QDomNode>, QList<QPolygonF>>> collection;
783
784 for (int ii = 0; ii < nodes.count (); ii++)
785 {
786 QDomNode node = nodes.at (ii);
787 if (! node.isElement ())
788 continue;
789
790 QDomElement elt = node.toElement ();
791
792 if (elt.tagName () == "polygon")
793 {
794 QString str = elt.attribute ("fill");
795 if (! str.isEmpty ())
796 {
797 QColor color (str);
798 str = elt.attribute ("fill-opacity");
799 if (! str.isEmpty ())
800 {
801 double alpha = str.toDouble ();
802 if (alpha != 1.0 && alpha >= 0.0)
803 color.setAlphaF (alpha);
804 }
805
806 if (! current_polygon.count ())
807 current_color = color;
808
809 if (color != current_color)
810 {
811 // Reconstruct the previous series of triangles
812 QList<QPolygonF> polygons
813 = current_polygon.reconstruct (reconstruct_level);
814 collection.push_back (QPair<QList<QDomNode>, QList<QPolygonF>>
815 (replaced_nodes, polygons));
816
817 replaced_nodes.clear ();
818 current_polygon.reset ();
819
820 current_color = color;
821 }
822
823 QPolygonF p (qstr2ptsvector (elt.attribute ("points")));
824 current_polygon.add (p);
825 replaced_nodes.push_back (node);
826 }
827 }
828 else
829 {
830 if (current_polygon.count ())
831 {
832 QList<QPolygonF> polygons = current_polygon.reconstruct (reconstruct_level);
833 collection.push_back (QPair<QList<QDomNode>, QList<QPolygonF>>
834 (replaced_nodes, polygons));
835 replaced_nodes.clear ();
836 current_polygon.reset ();
837 }
838 reconstruct_polygons (elt, reconstruct_level);
839 }
840 }
841
842 // Finish
843 collection.push_back (QPair<QList<QDomNode>, QList<QPolygonF>>
844 (replaced_nodes,
845 current_polygon.reconstruct (reconstruct_level)));
846
847 for (int ii = 0; ii < collection.count (); ii++)
848 replace_polygons (parent_elt, collection[ii].first, collection[ii].second);
849}
850
851void
852add_custom_properties (QDomElement& parent_elt)
853{
854 QDomNodeList nodes = parent_elt.childNodes ();
855
856 for (int ii = 0; ii < nodes.count (); ii++)
857 {
858 QDomNode node = nodes.at (ii);
859 if (! node.isElement ())
860 continue;
861
862 QDomElement elt = node.toElement ();
863
864 if (elt.tagName () == "image")
865 elt.setAttribute ("image-rendering", "optimizeSpeed");
866 else
868 }
869
870}
871
872#if defined (OCTAVE_USE_WINDOWS_API) && defined (_UNICODE)
873extern "C"
874int
875wmain (int argc, wchar_t **wargv)
876{
877 static char **argv = new char * [argc + 1];
878 std::vector<std::string> argv_str;
879
880 // convert wide character strings to multibyte UTF-8 strings
881 std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> wchar_conv;
882 for (int i_arg = 0; i_arg < argc; i_arg++)
883 argv_str.push_back (wchar_conv.to_bytes (wargv[i_arg]));
884
885 // Get pointers to C strings not before vector is stable.
886 for (int i_arg = 0; i_arg < argc; i_arg++)
887 argv[i_arg] = &argv_str[i_arg][0];
888 argv[argc] = nullptr;
889
890#else
891int
892main (int argc, char **argv)
893{
894#endif
895 const char *doc = "See \"octave-svgconvert -h\"";
896 const char *help = "Usage:\n\
897octave-svgconvert infile fmt dpi font reconstruct outfile\n\n\
898Convert svg file to pdf, or svg. All arguments are mandatory:\n\
899* infile: input svg file or \"-\" to indicate that the input svg file should be \
900read from stdin\n\
901* fmt: format of the output file. May be one of pdf or svg\n\
902* dpi: device dependent resolution in screen pixel per inch\n\
903* font: specify a file name for the default FreeSans font\n\
904* reconstruct: specify whether to reconstruct triangle to polygons\n\
905 0: no reconstruction (merging) of polygons\n\
906 1: merge consecutive triangles if they share an edge\n\
907 2: merge all triangles that share edges (might take a long time)\n\
908* outfile: output file name\n";
909
910 if (strcmp (argv[1], "-h") == 0)
911 {
912 std::cout << help;
913 return 0;
914 }
915 else if (argc != 7)
916 {
917 std::cerr << help;
918 return -1;
919 }
920
921 // Open svg file
922 QFile file;
923 if (strcmp (argv[1], "-") != 0)
924 {
925 // Read from file
926 file.setFileName (argv[1]);
927 if (! file.open (QIODevice::ReadOnly | QIODevice::Text))
928 {
929 std::cerr << "Unable to open file " << argv[1] << "\n";
930 std::cerr << help;
931 return -1;
932 }
933 }
934 else
935 {
936 // Read from stdin
937 if (! file.open (stdin, QIODevice::ReadOnly | QIODevice::Text))
938 {
939 std::cerr << "Unable to read from stdin\n";
940 std::cerr << doc;
941 return -1;
942 }
943 }
944
945 // Create a DOM document and load the svg file
946 QDomDocument document;
947#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
948 QDomDocument::ParseResult parseResult = document.setContent (&file);
949 if (! parseResult)
950 {
951 std::cerr << "Failed to parse XML contents" << std::endl
952 << "in line " << parseResult.errorLine
953 << " column " << parseResult.errorColumn << ":" << std::endl
954 << parseResult.errorMessage.toStdString () << std::endl;
955 file.close ();
956 return -1;
957 }
958#else
959 QString msg;
960 if (! document.setContent (&file, false, &msg))
961 {
962 std::cerr << "Failed to parse XML contents" << std::endl
963 << msg.toStdString () << std::endl;
964 file.close ();
965 return -1;
966 }
967#endif
968
969 file.close ();
970
971 // Format
972 if (strcmp (argv[2], "pdf") != 0 && strcmp (argv[2], "svg") != 0)
973 {
974 std::cerr << "Unhandled output file format " << argv[2] << "\n";
975 std::cerr << doc;
976 return -1;
977 }
978
979 // Resolution (Currently unused). Keep the DPI argument in case
980 // we implement raster outputs.
981 // double dpi = QString (argv[3]).toDouble ();
982
983 // Get the viewport from the root element
984 QDomElement root = document.firstChildElement();
985 double x0, y0, dx, dy;
986 QString s = root.attribute ("viewBox");
987 QTextStream (&s) >> x0 >> y0 >> dx >> dy;
988 QRectF vp (x0, y0, dx, dy);
989
990 // Setup application and add default FreeSans font if needed
991 QApplication a (argc, argv);
992
993 // When printing to PDF we may need the default FreeSans font
994 if (! strcmp (argv[2], "pdf"))
995 {
996 QFont font ("FreeSans");
997 if (! font.exactMatch ())
998 {
999 QString fontpath (argv[4]);
1000 if (! fontpath.isEmpty ())
1001 {
1002 int id = QFontDatabase::addApplicationFont (fontpath);
1003 if (id < 0)
1004 std::cerr << "warning: print: "
1005 "Unable to add default font to database\n";
1006 }
1007 else
1008 std::cerr << "warning: print: FreeSans font not found\n";
1009 }
1010 }
1011
1012 // First render in a temporary file
1013 QTemporaryFile fout;
1014 if (! fout.open ())
1015 {
1016 std::cerr << "Could not open temporary file\n";
1017 return -1;
1018 }
1019
1020 // Do basic polygons reconstruction
1021 int reconstruct_level = QString (argv[5]).toInt ();
1022 if (reconstruct_level)
1023 reconstruct_polygons (root, reconstruct_level);
1024
1025 // Add custom properties to SVG
1026 add_custom_properties (root);
1027
1028 // Draw
1029 if (! strcmp (argv[2], "pdf"))
1030 {
1031 // PDF painter
1032 pdfpainter painter (fout.fileName (), vp);
1033
1034 draw (root, painter);
1035 }
1036 else
1037 {
1038 // Return modified svg document
1039 QTextStream out (&fout);
1040#if HAVE_QTEXTSTREAM_SETENCODING
1041 out.setEncoding (QStringConverter::Utf8);
1042#else
1043 out.setCodec ("UTF-8");
1044#endif
1045 out << document.toByteArray ();
1046 }
1047
1048 // Delete output file before writing with new data
1049 if (QFile::exists (argv[6]))
1050 if (! QFile::remove (argv[6]))
1051 {
1052 std::cerr << "Unable to replace existing file " << argv[6] << "\n";
1053 return -1;
1054 }
1055
1056 fout.copy (argv[6]);
1057
1058 return 0;
1059}
program main
F77_RET_T const F77_DBLE const F77_DBLE F77_DBLE * d
F77_RET_T const F77_DBLE * x
bool strcmp(const T &str_a, const T &str_b)
Octave string utility functions.
void reconstruct_polygons(QDomElement &parent_elt, int reconstruct_level)
QString get_field(QString str, QString field)
QVector< double > qstr2vectord(QString str)
void replace_polygons(QDomElement &parent_elt, QList< QDomNode > orig, QList< QPolygonF > polygons)
QVector< QPoint > qstr2ptsvectord(QString str)
QVector< double > qstr2vectorf(QString str)
QVector< QPointF > qstr2ptsvector(QString str)
void add_custom_properties(QDomElement &parent_elt)
void draw(QDomElement &parent_elt, pdfpainter &painter)