Skip to content
Permalink
Browse files

Merge pull request #5897 from nyalldawson/layout_next

Misc layout fixes
  • Loading branch information
nyalldawson committed Dec 18, 2017
2 parents 332c57d + 200669a commit a5f7f410a11fb6ddcc2f91e641f56887cfe52d1a
@@ -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 );
@@ -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 );
@@ -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;

@@ -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;
}
@@ -20,6 +20,7 @@
#include "qgslayoutpagecollection.h"
#include "qgsogrutils.h"
#include "qgspaintenginehack.h"
#include "qgslayoutguidecollection.h"
#include <QImageWriter>
#include <QSize>

@@ -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 )
@@ -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
@@ -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 );
}

@@ -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 );
}

@@ -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;
@@ -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.
@@ -95,6 +95,7 @@ void QgsLayoutPageCollection::reflow()
currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + spaceBetweenPages();
p.setY( currentY );
}
mLayout->guides().update();
mLayout->updateBounds();
emit changed();
}
@@ -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 )
{
@@ -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 );
@@ -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.
*
@@ -134,6 +142,8 @@ class CORE_EXPORT QgsMultiRenderChecker
QString mControlName;
QString mControlPathPrefix;
unsigned int mColorTolerance = 0;
int mMaxSizeDifferenceX = 0;
int mMaxSizeDifferenceY = 0;
QgsMapSettings mMapSettings;
};

@@ -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()


@@ -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))
@@ -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))
@@ -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()

0 comments on commit a5f7f41

Please sign in to comment.
You can’t perform that action at this time.