Skip to content

Commit

Permalink
Merge pull request #5897 from nyalldawson/layout_next
Browse files Browse the repository at this point in the history
Misc layout fixes
  • Loading branch information
nyalldawson committed Dec 18, 2017
2 parents 332c57d + 200669a commit a5f7f41
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 14 deletions.
5 changes: 5 additions & 0 deletions python/core/layout/qgslayoutguidecollection.sip
Expand Up @@ -206,6 +206,11 @@ Resets all other pages' guides to match the guides from the specified ``sourcePa
void update();
%Docstring
Updates the position (and visibility) of all guide line items.
%End

QList< QgsLayoutGuide * > guides();
%Docstring
Returns a list of all guides contained in the collection.
%End

QList< QgsLayoutGuide * > guides( Qt::Orientation orientation, int page = -1 );
Expand Down
10 changes: 10 additions & 0 deletions python/core/qgsmultirenderchecker.sip
Expand Up @@ -84,6 +84,16 @@ Default value is 0.

:param colorTolerance: The maximum difference for each color component
including alpha to be considered correct.
%End

void setSizeTolerance( int xTolerance, int yTolerance );
%Docstring
Sets the largest allowable difference in size between the rendered and the expected image.

:param xTolerance: x tolerance in pixels
:param yTolerance: y tolerance in pixels

.. versionadded:: 3.0
%End

bool runTest( const QString &testName, unsigned int mismatchCount = 0 );
Expand Down
6 changes: 4 additions & 2 deletions src/app/layout/qgslayoutdesignerdialog.cpp
Expand Up @@ -1529,11 +1529,12 @@ void QgsLayoutDesignerDialog::exportToRaster()
if ( imageDlg.antialiasing() )
settings.flags |= QgsLayoutContext::FlagAntialiasing;

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

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

QFileInfo fi( outputFileName );
QgsLayoutExporter exporter( mLayout );
switch ( exporter.exportToPdf( outputFileName, pdfSettings ) )
{
case QgsLayoutExporter::Success:
{
mMessageBar->pushMessage( tr( "Export layout" ),
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( outputFileName ).toString(), outputFileName ),
tr( "Successfully exported layout to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ),
QgsMessageBar::INFO, 0 );
break;
}
Expand Down
39 changes: 31 additions & 8 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -20,6 +20,7 @@
#include "qgslayoutpagecollection.h"
#include "qgsogrutils.h"
#include "qgspaintenginehack.h"
#include "qgslayoutguidecollection.h"
#include <QImageWriter>
#include <QSize>

Expand Down Expand Up @@ -48,6 +49,34 @@ class LayoutContextPreviewSettingRestorer
bool mPreviousSetting = false;
};

class LayoutGuideHider
{
public:

LayoutGuideHider( QgsLayout *layout )
: mLayout( layout )
{
const QList< QgsLayoutGuide * > guides = mLayout->guides().guides();
for ( QgsLayoutGuide *guide : guides )
{
mPrevVisibility.insert( guide, guide->item()->isVisible() );
guide->item()->setVisible( false );
}
}

~LayoutGuideHider()
{
for ( auto it = mPrevVisibility.constBegin(); it != mPrevVisibility.constEnd(); ++it )
{
it.key()->item()->setVisible( it.value() );
}
}

private:
QgsLayout *mLayout = nullptr;
QHash< QgsLayoutGuide *, bool > mPrevVisibility;
};

///@endcond PRIVATE

QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
Expand Down Expand Up @@ -150,18 +179,12 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
( void )cacheRestorer;
LayoutContextPreviewSettingRestorer restorer( mLayout );
( void )restorer;

#if 0 //TODO
setSnapLinesVisible( false );
#endif
LayoutGuideHider guideHider( mLayout );
( void ) guideHider;

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

mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region );

#if 0 // TODO
setSnapLinesVisible( true );
#endif
}

QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const
Expand Down
9 changes: 7 additions & 2 deletions src/core/layout/qgslayoutguidecollection.cpp
Expand Up @@ -89,7 +89,7 @@ void QgsLayoutGuide::update()
}
else
{
mLineItem->setLine( 0, layoutPos, mPage->rect().width(), layoutPos );
mLineItem->setLine( 0, layoutPos + mPage->y(), mPage->rect().width(), layoutPos + mPage->y() );
mLineItem->setVisible( showGuide );
}

Expand All @@ -102,7 +102,7 @@ void QgsLayoutGuide::update()
}
else
{
mLineItem->setLine( layoutPos, 0, layoutPos, mPage->rect().height() );
mLineItem->setLine( layoutPos, mPage->y(), layoutPos, mPage->y() + mPage->rect().height() );
mLineItem->setVisible( showGuide );
}

Expand Down Expand Up @@ -467,6 +467,11 @@ void QgsLayoutGuideCollection::update()
}
}

QList<QgsLayoutGuide *> QgsLayoutGuideCollection::guides()
{
return mGuides;
}

QList<QgsLayoutGuide *> QgsLayoutGuideCollection::guides( Qt::Orientation orientation, int page )
{
QList<QgsLayoutGuide *> res;
Expand Down
5 changes: 5 additions & 0 deletions src/core/layout/qgslayoutguidecollection.h
Expand Up @@ -235,6 +235,11 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel, public
*/
void update();

/**
* Returns a list of all guides contained in the collection.
*/
QList< QgsLayoutGuide * > guides();

/**
* Returns the list of guides contained in the collection with the specified
* \a orientation and on a matching \a page.
Expand Down
4 changes: 3 additions & 1 deletion src/core/layout/qgslayoutpagecollection.cpp
Expand Up @@ -95,6 +95,7 @@ void QgsLayoutPageCollection::reflow()
currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + spaceBetweenPages();
p.setY( currentY );
}
mLayout->guides().update();
mLayout->updateBounds();
emit changed();
}
Expand Down Expand Up @@ -193,7 +194,8 @@ int QgsLayoutPageCollection::predictPageNumberForPoint( QPointF point ) const

QgsLayoutItemPage *QgsLayoutPageCollection::pageAtPoint( QPointF point ) const
{
Q_FOREACH ( QGraphicsItem *item, mLayout->items( point ) )
const QList< QGraphicsItem * > items = mLayout->items( point );
for ( QGraphicsItem *item : items )
{
if ( item->type() == QgsLayoutItemRegistry::LayoutPage )
{
Expand Down
1 change: 1 addition & 0 deletions src/core/qgsmultirenderchecker.cpp
Expand Up @@ -55,6 +55,7 @@ bool QgsMultiRenderChecker::runTest( const QString &testName, unsigned int misma
QgsRenderChecker checker;
checker.enableDashBuffering( true );
checker.setColorTolerance( mColorTolerance );
checker.setSizeTolerance( mMaxSizeDifferenceX, mMaxSizeDifferenceY );
checker.setControlPathPrefix( mControlPathPrefix );
checker.setControlPathSuffix( suffix );
checker.setControlName( mControlName );
Expand Down
10 changes: 10 additions & 0 deletions src/core/qgsmultirenderchecker.h
Expand Up @@ -94,6 +94,14 @@ class CORE_EXPORT QgsMultiRenderChecker
*/
void setColorTolerance( unsigned int colorTolerance ) { mColorTolerance = colorTolerance; }

/**
* Sets the largest allowable difference in size between the rendered and the expected image.
* \param xTolerance x tolerance in pixels
* \param yTolerance y tolerance in pixels
* \since QGIS 3.0
*/
void setSizeTolerance( int xTolerance, int yTolerance ) { mMaxSizeDifferenceX = xTolerance; mMaxSizeDifferenceY = yTolerance; }

/**
* Test using renderer to generate the image to be compared.
*
Expand Down Expand Up @@ -134,6 +142,8 @@ class CORE_EXPORT QgsMultiRenderChecker
QString mControlName;
QString mControlPathPrefix;
unsigned int mColorTolerance = 0;
int mMaxSizeDifferenceX = 0;
int mMaxSizeDifferenceY = 0;
QgsMapSettings mMapSettings;
};

Expand Down
114 changes: 113 additions & 1 deletion tests/src/python/test_qgslayoutexporter.py
Expand Up @@ -17,24 +17,80 @@
import tempfile
import shutil
import os
import subprocess

from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter,
QgsLayout,
QgsProject,
QgsMargins,
QgsLayoutItemShape,
QgsLayoutGuide,
QgsRectangle,
QgsLayoutItemPage,
QgsLayoutItemMap,
QgsLayoutPoint,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsSimpleFillSymbolLayer,
QgsFillSymbol)
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
from qgis.PyQt.QtGui import QImage, QPainter

from qgis.testing import start_app, unittest

from utilities import getExecutablePath

# PDF-to-image utility
# look for Poppler w/ Cairo, then muPDF
# * Poppler w/ Cairo renders correctly
# * Poppler w/o Cairo does not always correctly render vectors in PDF to image
# * muPDF renders correctly, but sightly shifts colors
for util in [
'pdftocairo',
# 'mudraw',
]:
PDFUTIL = getExecutablePath(util)
if PDFUTIL:
break

# noinspection PyUnboundLocalVariable
if not PDFUTIL:
raise Exception('PDF-to-image utility not found on PATH: '
'install Poppler (with Cairo)')


def pdfToPng(pdf_file_path, rendered_file_path, page, dpi=96):
if PDFUTIL.strip().endswith('pdftocairo'):
filebase = os.path.join(
os.path.dirname(rendered_file_path),
os.path.splitext(os.path.basename(rendered_file_path))[0]
)
call = [
PDFUTIL, '-png', '-singlefile', '-r', str(dpi),
'-x', '0', '-y', '0', '-f', str(page), '-l', str(page),
pdf_file_path, filebase
]
elif PDFUTIL.strip().endswith('mudraw'):
call = [
PDFUTIL, '-c', 'rgba',
'-r', str(dpi), '-f', str(page), '-l', str(page),
# '-b', '8',
'-o', rendered_file_path, pdf_file_path
]
else:
return False, ''

print("exportToPdf call: {0}".format(' '.join(call)))
try:
subprocess.check_call(call)
except subprocess.CalledProcessError as e:
assert False, ("exportToPdf failed!\n"
"cmd: {0}\n"
"returncode: {1}\n"
"message: {2}".format(e.cmd, e.returncode, e.message))


start_app()


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

def checkImage(self, name, reference_image, rendered_image):
def checkImage(self, name, reference_image, rendered_image, size_tolerance=0):
checker = QgsMultiRenderChecker()
checker.setControlPathPrefix("layout_exporter")
checker.setControlName("expected_layoutexporter_" + reference_image)
checker.setRenderedImage(rendered_image)
checker.setColorTolerance(2)
checker.setSizeTolerance(size_tolerance, size_tolerance)
result = checker.runTest(name, 20)
self.report += checker.report()
print((self.report))
Expand Down Expand Up @@ -134,6 +191,10 @@ def testRenderRegion(self):
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()

# add a guide, to ensure it is not included in export
g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters), l.pageCollection().page(0))
l.guides().addGuide(g1)

# add some items
item1 = QgsLayoutItemShape(l)
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
Expand Down Expand Up @@ -278,6 +339,57 @@ def testExportToImage(self):
page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path))

def testExportToPdf(self):
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()

# add a second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
l.pageCollection().addPage(page2)

# add some items
item1 = QgsLayoutItemShape(l)
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150))
fill = QgsSimpleFillSymbolLayer()
fill_symbol = QgsFillSymbol()
fill_symbol.changeSymbolLayer(0, fill)
fill.setColor(Qt.green)
fill.setStrokeStyle(Qt.NoPen)
item1.setSymbol(fill_symbol)
l.addItem(item1)

item2 = QgsLayoutItemShape(l)
item2.attemptSetSceneRect(QRectF(10, 20, 100, 150))
item2.attemptMove(QgsLayoutPoint(10, 20), page=1)
fill = QgsSimpleFillSymbolLayer()
fill_symbol = QgsFillSymbol()
fill_symbol.changeSymbolLayer(0, fill)
fill.setColor(Qt.cyan)
fill.setStrokeStyle(Qt.NoPen)
item2.setSymbol(fill_symbol)
l.addItem(item2)

exporter = QgsLayoutExporter(l)
# setup settings
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 80
settings.rasterizeWholeImage = False
settings.forceVectorOutput = False

pdf_file_path = os.path.join(self.basetestpath, 'test_exporttopdfdpi.pdf')
self.assertEqual(exporter.exportToPdf(pdf_file_path, settings), QgsLayoutExporter.Success)
self.assertTrue(os.path.exists(pdf_file_path))

rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi.png')
dpi = 80
pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1)
rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttopdfdpi2.png')
pdfToPng(pdf_file_path, rendered_page_2, dpi=dpi, page=2)

self.assertTrue(self.checkImage('exporttopdfdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1))
self.assertTrue(self.checkImage('exporttopdfdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1))

def testExportWorldFile(self):
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()
Expand Down

0 comments on commit a5f7f41

Please sign in to comment.