Skip to content

Commit f54476c

Browse files
committed
[FEATURE] Export processing models as PDF/SVG
1 parent 4603eb0 commit f54476c

File tree

4 files changed

+112
-19
lines changed

4 files changed

+112
-19
lines changed

python/plugins/processing/modeler/ModelerDialog.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
import os
3232

3333
from qgis.PyQt import uic
34-
from qgis.PyQt.QtCore import Qt, QRectF, QMimeData, QPoint, QPointF, QSettings, QByteArray, QSize, pyqtSignal
34+
from qgis.PyQt.QtCore import Qt, QRectF, QMimeData, QPoint, QPointF, QSettings, QByteArray, QSize, QSizeF, pyqtSignal
3535
from qgis.PyQt.QtWidgets import QGraphicsView, QTreeWidget, QMessageBox, QFileDialog, QTreeWidgetItem, QSizePolicy, QMainWindow
3636
from qgis.PyQt.QtGui import QIcon, QImage, QPainter
37+
from qgis.PyQt.QtSvg import QSvgGenerator
38+
from qgis.PyQt.QtPrintSupport import QPrinter
3739
from qgis.core import QgsApplication
3840
from qgis.gui import QgsMessageBar
3941
from processing.core.ProcessingConfig import ProcessingConfig
@@ -213,6 +215,8 @@ def _mimeDataAlgorithm(items):
213215
self.mActionSave.triggered.connect(self.save)
214216
self.mActionSaveAs.triggered.connect(self.saveAs)
215217
self.mActionExportImage.triggered.connect(self.exportAsImage)
218+
self.mActionExportPdf.triggered.connect(self.exportAsPdf)
219+
self.mActionExportSvg.triggered.connect(self.exportAsSvg)
216220
self.mActionExportPython.triggered.connect(self.exportAsPython)
217221
self.mActionEditHelp.triggered.connect(self.editHelp)
218222
self.mActionRun.triggered.connect(self.runModel)
@@ -287,6 +291,7 @@ def saveAs(self):
287291
self.saveModel(True)
288292

289293
def exportAsImage(self):
294+
self.repaintModel(controls=False)
290295
filename, fileFilter = QFileDialog.getSaveFileName(self,
291296
self.tr('Save Model As Image'), '',
292297
self.tr('PNG files (*.png *.PNG)'))
@@ -296,23 +301,79 @@ def exportAsImage(self):
296301
if not filename.lower().endswith('.png'):
297302
filename += '.png'
298303

299-
totalRect = QRectF(0, 0, 1, 1)
300-
for item in list(self.scene.items()):
301-
totalRect = totalRect.united(item.sceneBoundingRect())
304+
totalRect = self.scene.itemsBoundingRect()
302305
totalRect.adjust(-10, -10, 10, 10)
306+
imgRect = QRectF(0, 0, totalRect.width(), totalRect.height())
303307

304308
img = QImage(totalRect.width(), totalRect.height(),
305309
QImage.Format_ARGB32_Premultiplied)
306310
img.fill(Qt.white)
307311
painter = QPainter()
308312
painter.setRenderHint(QPainter.Antialiasing)
309313
painter.begin(img)
310-
self.scene.render(painter, totalRect, totalRect)
314+
self.scene.render(painter, imgRect, totalRect)
311315
painter.end()
312316

313317
img.save(filename)
314318

315319
self.bar.pushMessage("", "Model was correctly exported as image", level=QgsMessageBar.SUCCESS, duration=5)
320+
self.repaintModel(controls=True)
321+
322+
def exportAsPdf(self):
323+
self.repaintModel(controls=False)
324+
filename, fileFilter = QFileDialog.getSaveFileName(self,
325+
self.tr('Save Model As PDF'), '',
326+
self.tr('SVG files (*.pdf *.PDF)'))
327+
if not filename:
328+
return
329+
330+
if not filename.lower().endswith('.pdf'):
331+
filename += '.pdf'
332+
333+
totalRect = self.scene.itemsBoundingRect()
334+
totalRect.adjust(-10, -10, 10, 10)
335+
printerRect = QRectF(0, 0, totalRect.width(), totalRect.height())
336+
337+
printer = QPrinter()
338+
printer.setOutputFormat(QPrinter.PdfFormat)
339+
printer.setOutputFileName(filename)
340+
printer.setPaperSize(QSizeF(printerRect.width(), printerRect.height()), QPrinter.DevicePixel)
341+
printer.setFullPage(True)
342+
343+
painter = QPainter(printer)
344+
self.scene.render(painter, printerRect, totalRect)
345+
painter.end()
346+
347+
self.bar.pushMessage("", "Model was correctly exported as PDF", level=QgsMessageBar.SUCCESS, duration=5)
348+
self.repaintModel(controls=True)
349+
350+
def exportAsSvg(self):
351+
self.repaintModel(controls=False)
352+
filename, fileFilter = QFileDialog.getSaveFileName(self,
353+
self.tr('Save Model As SVG'), '',
354+
self.tr('SVG files (*.svg *.SVG)'))
355+
if not filename:
356+
return
357+
358+
if not filename.lower().endswith('.svg'):
359+
filename += '.svg'
360+
361+
totalRect = self.scene.itemsBoundingRect()
362+
totalRect.adjust(-10, -10, 10, 10)
363+
svgRect = QRectF(0, 0, totalRect.width(), totalRect.height())
364+
365+
svg = QSvgGenerator()
366+
svg.setFileName(filename)
367+
svg.setSize(QSize(totalRect.width(), totalRect.height()))
368+
svg.setViewBox(svgRect)
369+
svg.setTitle(self.alg.name)
370+
371+
painter = QPainter(svg)
372+
self.scene.render(painter, svgRect, totalRect)
373+
painter.end()
374+
375+
self.bar.pushMessage("", "Model was correctly exported as SVG", level=QgsMessageBar.SUCCESS, duration=5)
376+
self.repaintModel(controls=True)
316377

317378
def exportAsPython(self):
318379
filename, filter = QFileDialog.getSaveFileName(self,
@@ -399,11 +460,11 @@ def openModel(self):
399460
self.tr('The selected model could not be loaded.\n'
400461
'See the log for more information.'))
401462

402-
def repaintModel(self):
463+
def repaintModel(self, controls=True):
403464
self.scene = ModelerScene()
404465
self.scene.setSceneRect(QRectF(0, 0, ModelerAlgorithm.CANVAS_SIZE,
405466
ModelerAlgorithm.CANVAS_SIZE))
406-
self.scene.paintModel(self.alg)
467+
self.scene.paintModel(self.alg, controls)
407468
self.view.setScene(self.scene)
408469

409470
def addInput(self):

python/plugins/processing/modeler/ModelerGraphicItem.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ class ModelerGraphicItem(QGraphicsItem):
4545
BOX_HEIGHT = 30
4646
BOX_WIDTH = 200
4747

48-
def __init__(self, element, model):
48+
def __init__(self, element, model, controls):
4949
super(ModelerGraphicItem, self).__init__(None)
50+
self.controls = controls
5051
self.model = model
5152
self.element = element
5253
if isinstance(element, ModelerParameter):
@@ -73,7 +74,7 @@ def __init__(self, element, model):
7374
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
7475
self.setZValue(1000)
7576

76-
if not isinstance(element, ModelerOutput):
77+
if not isinstance(element, ModelerOutput) and controls:
7778
svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'edit.svg'))
7879
picture = QPicture()
7980
painter = QPainter(picture)
@@ -101,13 +102,15 @@ def __init__(self, element, model):
101102
if alg.parameters:
102103
pt = self.getLinkPointForParameter(-1)
103104
pt = QPointF(0, pt.y())
104-
self.inButton = FoldButtonGraphicItem(pt, self.foldInput, self.element.paramsFolded)
105-
self.inButton.setParentItem(self)
105+
if controls:
106+
self.inButton = FoldButtonGraphicItem(pt, self.foldInput, self.element.paramsFolded)
107+
self.inButton.setParentItem(self)
106108
if alg.outputs:
107109
pt = self.getLinkPointForOutput(-1)
108110
pt = QPointF(0, pt.y())
109-
self.outButton = FoldButtonGraphicItem(pt, self.foldOutput, self.element.outputsFolded)
110-
self.outButton.setParentItem(self)
111+
if controls:
112+
self.outButton = FoldButtonGraphicItem(pt, self.foldOutput, self.element.outputsFolded)
113+
self.outButton.setParentItem(self)
111114

112115
def foldInput(self, folded):
113116
self.element.paramsFolded = folded
@@ -237,12 +240,15 @@ def paint(self, painter, option, widget=None):
237240
ModelerGraphicItem.BOX_HEIGHT + 2)
238241
color = QColor(172, 196, 114)
239242
outline = QColor(90, 140, 90)
243+
selected = QColor(90, 140, 90)
240244
if isinstance(self.element, ModelerParameter):
241245
color = QColor(238, 242, 131)
242246
outline = QColor(234, 226, 118)
247+
selected = QColor(151, 153, 83)
243248
elif isinstance(self.element, Algorithm):
244249
color = Qt.white
245250
outline = Qt.gray
251+
selected = Qt.gray
246252
painter.setPen(QPen(outline, 1))
247253
painter.setBrush(QBrush(color, Qt.SolidPattern))
248254
painter.drawRect(rect)
@@ -255,7 +261,7 @@ def paint(self, painter, option, widget=None):
255261
painter.setPen(QPen(Qt.gray))
256262
text = text + "\n(deactivated)"
257263
elif self.isSelected():
258-
painter.setPen(QPen(outline))
264+
painter.setPen(QPen(selected))
259265
fm = QFontMetricsF(font)
260266
text = self.getAdjustedText(self.text)
261267
h = fm.ascent()
@@ -379,7 +385,7 @@ def paint(self, painter, option, widget=None):
379385
rect = QRectF(pt.x(), pt.y(), self.WIDTH, self.HEIGHT)
380386
if self.isIn:
381387
painter.setPen(QPen(Qt.transparent, 1))
382-
painter.setBrush(QBrush(Qt.lightGray,
388+
painter.setBrush(QBrush(QColor(55, 55, 55, 33),
383389
Qt.SolidPattern))
384390
else:
385391
painter.setPen(QPen(Qt.transparent, 1))

python/plugins/processing/modeler/ModelerScene.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ def getItemsFromParamValue(self, value):
7878
items.append((self.algItems[value.alg], i))
7979
return items
8080

81-
def paintModel(self, model):
81+
def paintModel(self, model, controls=True):
8282
self.model = model
8383
# Inputs
8484
for inp in list(model.inputs.values()):
85-
item = ModelerGraphicItem(inp, model)
85+
item = ModelerGraphicItem(inp, model, controls)
8686
item.setFlag(QGraphicsItem.ItemIsMovable, True)
8787
item.setFlag(QGraphicsItem.ItemIsSelectable, True)
8888
self.addItem(item)
@@ -91,7 +91,7 @@ def paintModel(self, model):
9191

9292
# We add the algs
9393
for alg in list(model.algs.values()):
94-
item = ModelerGraphicItem(alg, model)
94+
item = ModelerGraphicItem(alg, model, controls)
9595
item.setFlag(QGraphicsItem.ItemIsMovable, True)
9696
item.setFlag(QGraphicsItem.ItemIsSelectable, True)
9797
self.addItem(item)
@@ -131,7 +131,7 @@ def paintModel(self, model):
131131
for key in outputs:
132132
out = outputs[key]
133133
if out is not None:
134-
item = ModelerGraphicItem(out, model)
134+
item = ModelerGraphicItem(out, model, controls)
135135
item.setFlag(QGraphicsItem.ItemIsMovable, True)
136136
item.setFlag(QGraphicsItem.ItemIsSelectable, True)
137137
self.addItem(item)

python/plugins/processing/ui/DlgModeler.ui

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@
310310
<addaction name="mActionSaveAs"/>
311311
<addaction name="separator"/>
312312
<addaction name="mActionExportImage"/>
313+
<addaction name="mActionExportPdf"/>
314+
<addaction name="mActionExportSvg"/>
313315
<addaction name="mActionExportPython"/>
314316
<addaction name="separator"/>
315317
<addaction name="mActionEditHelp"/>
@@ -373,6 +375,30 @@
373375
<string>Export as image</string>
374376
</property>
375377
</action>
378+
<action name="mActionExportPdf">
379+
<property name="icon">
380+
<iconset resource="../../images/images.qrc">
381+
<normaloff>:/images/themes/default/mActionSaveAsPDF.svg</normaloff>:/images/themes/default/mActionSaveAsPDF.svg</iconset>
382+
</property>
383+
<property name="text">
384+
<string>Export as PDF...</string>
385+
</property>
386+
<property name="toolTip">
387+
<string>Export as PDF</string>
388+
</property>
389+
</action>
390+
<action name="mActionExportSvg">
391+
<property name="icon">
392+
<iconset resource="../../images/images.qrc">
393+
<normaloff>:/images/themes/default/mActionSaveAsSVG.svg</normaloff>:/images/themes/default/mActionSaveAsSVG.svg</iconset>
394+
</property>
395+
<property name="text">
396+
<string>Export as SVG...</string>
397+
</property>
398+
<property name="toolTip">
399+
<string>Export as SVG</string>
400+
</property>
401+
</action>
376402
<action name="mActionExportPython">
377403
<property name="icon">
378404
<iconset resource="../../images/images.qrc">

0 commit comments

Comments
 (0)