Merge pull request #5897 from nyalldawson/layout_next

Misc layout fixes
This commit is contained in:
Nyall Dawson 2017-12-18 19:41:00 +11:00 committed by GitHub
commit a5f7f410a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 218 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import sip
import tempfile
import shutil
import os
import subprocess
from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter,
@ -24,10 +25,13 @@ from qgis.core import (QgsMultiRenderChecker,
QgsProject,
QgsMargins,
QgsLayoutItemShape,
QgsLayoutGuide,
QgsRectangle,
QgsLayoutItemPage,
QgsLayoutItemMap,
QgsLayoutPoint,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsSimpleFillSymbolLayer,
QgsFillSymbol)
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt
@ -35,6 +39,58 @@ 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 @@ class TestQgsLayoutExporter(unittest.TestCase):
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
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 @@ class TestQgsLayoutExporter(unittest.TestCase):
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()

View File

@ -65,6 +65,11 @@ class TestQgsLayoutGuide(unittest.TestCase):
p = QgsProject()
l = QgsLayout(p)
l.initializeDefaults() # add a page
# add a second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
l.pageCollection().addPage(page2)
g = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
g.setLayout(l)
g.update()
@ -85,6 +90,19 @@ class TestQgsLayoutGuide(unittest.TestCase):
self.assertEqual(g.item().line().y2(), 15)
self.assertEqual(g.layoutPosition(), 15)
# guide on page2
g1 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(1))
g1.setLayout(l)
g1.update()
g1.setPosition(QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters))
g1.update()
self.assertTrue(g1.item().isVisible())
self.assertEqual(g1.item().line().x1(), 0)
self.assertEqual(g1.item().line().y1(), 235)
self.assertEqual(g1.item().line().x2(), 148)
self.assertEqual(g1.item().line().y2(), 235)
self.assertEqual(g1.layoutPosition(), 235)
# vertical guide
g2 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
g2.setLayout(l)
@ -109,6 +127,17 @@ class TestQgsLayoutGuide(unittest.TestCase):
g.update()
self.assertFalse(g.item().isVisible())
# guide on page2
g3 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(1))
g3.setLayout(l)
g3.update()
self.assertTrue(g3.item().isVisible())
self.assertEqual(g3.item().line().x1(), 50)
self.assertEqual(g3.item().line().y1(), 220)
self.assertEqual(g3.item().line().x2(), 50)
self.assertEqual(g3.item().line().y2(), 430)
self.assertEqual(g3.layoutPosition(), 50)
def testCollection(self):
p = QgsProject()
l = QgsLayout(p)