Skip to content

Commit a5f7f41

Browse files
authoredDec 18, 2017
Merge pull request #5897 from nyalldawson/layout_next
Misc layout fixes
2 parents 332c57d + 200669a commit a5f7f41

File tree

13 files changed

+218
-14
lines changed

13 files changed

+218
-14
lines changed
 

‎python/core/layout/qgslayoutguidecollection.sip

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ Resets all other pages' guides to match the guides from the specified ``sourcePa
206206
void update();
207207
%Docstring
208208
Updates the position (and visibility) of all guide line items.
209+
%End
210+
211+
QList< QgsLayoutGuide * > guides();
212+
%Docstring
213+
Returns a list of all guides contained in the collection.
209214
%End
210215

211216
QList< QgsLayoutGuide * > guides( Qt::Orientation orientation, int page = -1 );

‎python/core/qgsmultirenderchecker.sip

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ Default value is 0.
8484

8585
:param colorTolerance: The maximum difference for each color component
8686
including alpha to be considered correct.
87+
%End
88+
89+
void setSizeTolerance( int xTolerance, int yTolerance );
90+
%Docstring
91+
Sets the largest allowable difference in size between the rendered and the expected image.
92+
93+
:param xTolerance: x tolerance in pixels
94+
:param yTolerance: y tolerance in pixels
95+
96+
.. versionadded:: 3.0
8797
%End
8898

8999
bool runTest( const QString &testName, unsigned int mismatchCount = 0 );

‎src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,11 +1529,12 @@ void QgsLayoutDesignerDialog::exportToRaster()
15291529
if ( imageDlg.antialiasing() )
15301530
settings.flags |= QgsLayoutContext::FlagAntialiasing;
15311531

1532+
QFileInfo fi( fileNExt.first );
15321533
switch ( exporter.exportToImage( fileNExt.first, settings ) )
15331534
{
15341535
case QgsLayoutExporter::Success:
15351536
mMessageBar->pushMessage( tr( "Export layout" ),
1536-
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fileNExt.first ).toString(), fileNExt.first ),
1537+
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), fileNExt.first ),
15371538
QgsMessageBar::INFO, 0 );
15381539
break;
15391540

@@ -1630,13 +1631,14 @@ void QgsLayoutDesignerDialog::exportToPdf()
16301631
// force a refresh, to e.g. update data defined properties, tables, etc
16311632
mLayout->refresh();
16321633

1634+
QFileInfo fi( outputFileName );
16331635
QgsLayoutExporter exporter( mLayout );
16341636
switch ( exporter.exportToPdf( outputFileName, pdfSettings ) )
16351637
{
16361638
case QgsLayoutExporter::Success:
16371639
{
16381640
mMessageBar->pushMessage( tr( "Export layout" ),
1639-
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( outputFileName ).toString(), outputFileName ),
1641+
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ),
16401642
QgsMessageBar::INFO, 0 );
16411643
break;
16421644
}

‎src/core/layout/qgslayoutexporter.cpp

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "qgslayoutpagecollection.h"
2121
#include "qgsogrutils.h"
2222
#include "qgspaintenginehack.h"
23+
#include "qgslayoutguidecollection.h"
2324
#include <QImageWriter>
2425
#include <QSize>
2526

@@ -48,6 +49,34 @@ class LayoutContextPreviewSettingRestorer
4849
bool mPreviousSetting = false;
4950
};
5051

52+
class LayoutGuideHider
53+
{
54+
public:
55+
56+
LayoutGuideHider( QgsLayout *layout )
57+
: mLayout( layout )
58+
{
59+
const QList< QgsLayoutGuide * > guides = mLayout->guides().guides();
60+
for ( QgsLayoutGuide *guide : guides )
61+
{
62+
mPrevVisibility.insert( guide, guide->item()->isVisible() );
63+
guide->item()->setVisible( false );
64+
}
65+
}
66+
67+
~LayoutGuideHider()
68+
{
69+
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
70+
{
71+
it.key()->item()->setVisible( it.value() );
72+
}
73+
}
74+
75+
private:
76+
QgsLayout *mLayout = nullptr;
77+
QHash< QgsLayoutGuide *, bool > mPrevVisibility;
78+
};
79+
5180
///@endcond PRIVATE
5281

5382
QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
@@ -150,18 +179,12 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
150179
( void )cacheRestorer;
151180
LayoutContextPreviewSettingRestorer restorer( mLayout );
152181
( void )restorer;
153-
154-
#if 0 //TODO
155-
setSnapLinesVisible( false );
156-
#endif
182+
LayoutGuideHider guideHider( mLayout );
183+
( void ) guideHider;
157184

158185
painter->setRenderHint( QPainter::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing );
159186

160187
mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region );
161-
162-
#if 0 // TODO
163-
setSnapLinesVisible( true );
164-
#endif
165188
}
166189

167190
QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const

‎src/core/layout/qgslayoutguidecollection.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ void QgsLayoutGuide::update()
8989
}
9090
else
9191
{
92-
mLineItem->setLine( 0, layoutPos, mPage->rect().width(), layoutPos );
92+
mLineItem->setLine( 0, layoutPos + mPage->y(), mPage->rect().width(), layoutPos + mPage->y() );
9393
mLineItem->setVisible( showGuide );
9494
}
9595

@@ -102,7 +102,7 @@ void QgsLayoutGuide::update()
102102
}
103103
else
104104
{
105-
mLineItem->setLine( layoutPos, 0, layoutPos, mPage->rect().height() );
105+
mLineItem->setLine( layoutPos, mPage->y(), layoutPos, mPage->y() + mPage->rect().height() );
106106
mLineItem->setVisible( showGuide );
107107
}
108108

@@ -467,6 +467,11 @@ void QgsLayoutGuideCollection::update()
467467
}
468468
}
469469

470+
QList<QgsLayoutGuide *> QgsLayoutGuideCollection::guides()
471+
{
472+
return mGuides;
473+
}
474+
470475
QList<QgsLayoutGuide *> QgsLayoutGuideCollection::guides( Qt::Orientation orientation, int page )
471476
{
472477
QList<QgsLayoutGuide *> res;

‎src/core/layout/qgslayoutguidecollection.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel, public
235235
*/
236236
void update();
237237

238+
/**
239+
* Returns a list of all guides contained in the collection.
240+
*/
241+
QList< QgsLayoutGuide * > guides();
242+
238243
/**
239244
* Returns the list of guides contained in the collection with the specified
240245
* \a orientation and on a matching \a page.

‎src/core/layout/qgslayoutpagecollection.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ void QgsLayoutPageCollection::reflow()
9595
currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + spaceBetweenPages();
9696
p.setY( currentY );
9797
}
98+
mLayout->guides().update();
9899
mLayout->updateBounds();
99100
emit changed();
100101
}
@@ -193,7 +194,8 @@ int QgsLayoutPageCollection::predictPageNumberForPoint( QPointF point ) const
193194

194195
QgsLayoutItemPage *QgsLayoutPageCollection::pageAtPoint( QPointF point ) const
195196
{
196-
Q_FOREACH ( QGraphicsItem *item, mLayout->items( point ) )
197+
const QList< QGraphicsItem * > items = mLayout->items( point );
198+
for ( QGraphicsItem *item : items )
197199
{
198200
if ( item->type() == QgsLayoutItemRegistry::LayoutPage )
199201
{

‎src/core/qgsmultirenderchecker.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ bool QgsMultiRenderChecker::runTest( const QString &testName, unsigned int misma
5555
QgsRenderChecker checker;
5656
checker.enableDashBuffering( true );
5757
checker.setColorTolerance( mColorTolerance );
58+
checker.setSizeTolerance( mMaxSizeDifferenceX, mMaxSizeDifferenceY );
5859
checker.setControlPathPrefix( mControlPathPrefix );
5960
checker.setControlPathSuffix( suffix );
6061
checker.setControlName( mControlName );

‎src/core/qgsmultirenderchecker.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ class CORE_EXPORT QgsMultiRenderChecker
9494
*/
9595
void setColorTolerance( unsigned int colorTolerance ) { mColorTolerance = colorTolerance; }
9696

97+
/**
98+
* Sets the largest allowable difference in size between the rendered and the expected image.
99+
* \param xTolerance x tolerance in pixels
100+
* \param yTolerance y tolerance in pixels
101+
* \since QGIS 3.0
102+
*/
103+
void setSizeTolerance( int xTolerance, int yTolerance ) { mMaxSizeDifferenceX = xTolerance; mMaxSizeDifferenceY = yTolerance; }
104+
97105
/**
98106
* Test using renderer to generate the image to be compared.
99107
*
@@ -134,6 +142,8 @@ class CORE_EXPORT QgsMultiRenderChecker
134142
QString mControlName;
135143
QString mControlPathPrefix;
136144
unsigned int mColorTolerance = 0;
145+
int mMaxSizeDifferenceX = 0;
146+
int mMaxSizeDifferenceY = 0;
137147
QgsMapSettings mMapSettings;
138148
};
139149

‎tests/src/python/test_qgslayoutexporter.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,80 @@
1717
import tempfile
1818
import shutil
1919
import os
20+
import subprocess
2021

2122
from qgis.core import (QgsMultiRenderChecker,
2223
QgsLayoutExporter,
2324
QgsLayout,
2425
QgsProject,
2526
QgsMargins,
2627
QgsLayoutItemShape,
28+
QgsLayoutGuide,
2729
QgsRectangle,
2830
QgsLayoutItemPage,
2931
QgsLayoutItemMap,
3032
QgsLayoutPoint,
33+
QgsLayoutMeasurement,
34+
QgsUnitTypes,
3135
QgsSimpleFillSymbolLayer,
3236
QgsFillSymbol)
3337
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
3438
from qgis.PyQt.QtGui import QImage, QPainter
3539

3640
from qgis.testing import start_app, unittest
3741

42+
from utilities import getExecutablePath
43+
44+
# PDF-to-image utility
45+
# look for Poppler w/ Cairo, then muPDF
46+
# * Poppler w/ Cairo renders correctly
47+
# * Poppler w/o Cairo does not always correctly render vectors in PDF to image
48+
# * muPDF renders correctly, but sightly shifts colors
49+
for util in [
50+
'pdftocairo',
51+
# 'mudraw',
52+
]:
53+
PDFUTIL = getExecutablePath(util)
54+
if PDFUTIL:
55+
break
56+
57+
# noinspection PyUnboundLocalVariable
58+
if not PDFUTIL:
59+
raise Exception('PDF-to-image utility not found on PATH: '
60+
'install Poppler (with Cairo)')
61+
62+
63+
def pdfToPng(pdf_file_path, rendered_file_path, page, dpi=96):
64+
if PDFUTIL.strip().endswith('pdftocairo'):
65+
filebase = os.path.join(
66+
os.path.dirname(rendered_file_path),
67+
os.path.splitext(os.path.basename(rendered_file_path))[0]
68+
)
69+
call = [
70+
PDFUTIL, '-png', '-singlefile', '-r', str(dpi),
71+
'-x', '0', '-y', '0', '-f', str(page), '-l', str(page),
72+
pdf_file_path, filebase
73+
]
74+
elif PDFUTIL.strip().endswith('mudraw'):
75+
call = [
76+
PDFUTIL, '-c', 'rgba',
77+
'-r', str(dpi), '-f', str(page), '-l', str(page),
78+
# '-b', '8',
79+
'-o', rendered_file_path, pdf_file_path
80+
]
81+
else:
82+
return False, ''
83+
84+
print("exportToPdf call: {0}".format(' '.join(call)))
85+
try:
86+
subprocess.check_call(call)
87+
except subprocess.CalledProcessError as e:
88+
assert False, ("exportToPdf failed!\n"
89+
"cmd: {0}\n"
90+
"returncode: {1}\n"
91+
"message: {2}".format(e.cmd, e.returncode, e.message))
92+
93+
3894
start_app()
3995

4096

@@ -54,12 +110,13 @@ def tearDown(self):
54110
with open(report_file_path, 'a') as report_file:
55111
report_file.write(self.report)
56112

57-
def checkImage(self, name, reference_image, rendered_image):
113+
def checkImage(self, name, reference_image, rendered_image, size_tolerance=0):
58114
checker = QgsMultiRenderChecker()
59115
checker.setControlPathPrefix("layout_exporter")
60116
checker.setControlName("expected_layoutexporter_" + reference_image)
61117
checker.setRenderedImage(rendered_image)
62118
checker.setColorTolerance(2)
119+
checker.setSizeTolerance(size_tolerance, size_tolerance)
63120
result = checker.runTest(name, 20)
64121
self.report += checker.report()
65122
print((self.report))
@@ -134,6 +191,10 @@ def testRenderRegion(self):
134191
l = QgsLayout(QgsProject.instance())
135192
l.initializeDefaults()
136193

194+
# add a guide, to ensure it is not included in export
195+
g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters), l.pageCollection().page(0))
196+
l.guides().addGuide(g1)
197+
137198
# add some items
138199
item1 = QgsLayoutItemShape(l)
139200
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
@@ -278,6 +339,57 @@ def testExportToImage(self):
278339
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
279340
self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))
280341

342+
def testExportToPdf(self):
343+
l = QgsLayout(QgsProject.instance())
344+
l.initializeDefaults()
345+
346+
# add a second page
347+
page2 = QgsLayoutItemPage(l)
348+
page2.setPageSize('A5')
349+
l.pageCollection().addPage(page2)
350+
351+
# add some items
352+
item1 = QgsLayoutItemShape(l)
353+
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
354+
fill = QgsSimpleFillSymbolLayer()
355+
fill_symbol = QgsFillSymbol()
356+
fill_symbol.changeSymbolLayer(0, fill)
357+
fill.setColor(Qt.green)
358+
fill.setStrokeStyle(Qt.NoPen)
359+
item1.setSymbol(fill_symbol)
360+
l.addItem(item1)
361+
362+
item2 = QgsLayoutItemShape(l)
363+
item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
364+
item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
365+
fill = QgsSimpleFillSymbolLayer()
366+
fill_symbol = QgsFillSymbol()
367+
fill_symbol.changeSymbolLayer(0, fill)
368+
fill.setColor(Qt.cyan)
369+
fill.setStrokeStyle(Qt.NoPen)
370+
item2.setSymbol(fill_symbol)
371+
l.addItem(item2)
372+
373+
exporter = QgsLayoutExporter(l)
374+
# setup settings
375+
settings = QgsLayoutExporter.PdfExportSettings()
376+
settings.dpi = 80
377+
settings.rasterizeWholeImage = False
378+
settings.forceVectorOutput = False
379+
380+
pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdfdpi.pdf')
381+
self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
382+
self.assertTrue(os.path.exists(pdf_file_path))
383+
384+
rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi.png')
385+
dpi = 80
386+
pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1)
387+
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttopdfdpi2.png')
388+
pdfToPng(pdf_file_path, rendered_page_2, dpi=dpi, page=2)
389+
390+
self.assertTrue(self.checkImage('exporttopdfdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
391+
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))
392+
281393
def testExportWorldFile(self):
282394
l = QgsLayout(QgsProject.instance())
283395
l.initializeDefaults()

‎tests/src/python/test_qgslayoutguides.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def testUpdateGuide(self):
6565
p = QgsProject()
6666
l = QgsLayout(p)
6767
l.initializeDefaults() # add a page
68+
# add a second page
69+
page2 = QgsLayoutItemPage(l)
70+
page2.setPageSize('A5')
71+
l.pageCollection().addPage(page2)
72+
6873
g = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
6974
g.setLayout(l)
7075
g.update()
@@ -85,6 +90,19 @@ def testUpdateGuide(self):
8590
self.assertEqual(g.item().line().y2(), 15)
8691
self.assertEqual(g.layoutPosition(), 15)
8792

93+
# guide on page2
94+
g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(1))
95+
g1.setLayout(l)
96+
g1.update()
97+
g1.setPosition(QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters))
98+
g1.update()
99+
self.assertTrue(g1.item().isVisible())
100+
self.assertEqual(g1.item().line().x1(), 0)
101+
self.assertEqual(g1.item().line().y1(), 235)
102+
self.assertEqual(g1.item().line().x2(), 148)
103+
self.assertEqual(g1.item().line().y2(), 235)
104+
self.assertEqual(g1.layoutPosition(), 235)
105+
88106
# vertical guide
89107
g2 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
90108
g2.setLayout(l)
@@ -109,6 +127,17 @@ def testUpdateGuide(self):
109127
g.update()
110128
self.assertFalse(g.item().isVisible())
111129

130+
# guide on page2
131+
g3 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(1))
132+
g3.setLayout(l)
133+
g3.update()
134+
self.assertTrue(g3.item().isVisible())
135+
self.assertEqual(g3.item().line().x1(), 50)
136+
self.assertEqual(g3.item().line().y1(), 220)
137+
self.assertEqual(g3.item().line().x2(), 50)
138+
self.assertEqual(g3.item().line().y2(), 430)
139+
self.assertEqual(g3.layoutPosition(), 50)
140+
112141
def testCollection(self):
113142
p = QgsProject()
114143
l = QgsLayout(p)

0 commit comments

Comments
 (0)
Please sign in to comment.