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(); void update();
%Docstring %Docstring
Updates the position (and visibility) of all guide line items. 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 %End
QList< QgsLayoutGuide * > guides( Qt::Orientation orientation, int page = -1 ); 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 :param colorTolerance: The maximum difference for each color component
including alpha to be considered correct. 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 %End
bool runTest( const QString &testName, unsigned int mismatchCount = 0 ); bool runTest( const QString &testName, unsigned int mismatchCount = 0 );

View File

@ -1529,11 +1529,12 @@ void QgsLayoutDesignerDialog::exportToRaster()
if ( imageDlg.antialiasing() ) if ( imageDlg.antialiasing() )
settings.flags |= QgsLayoutContext::FlagAntialiasing; settings.flags |= QgsLayoutContext::FlagAntialiasing;
QFileInfo fi( fileNExt.first );
switch ( exporter.exportToImage( fileNExt.first, settings ) ) switch ( exporter.exportToImage( fileNExt.first, settings ) )
{ {
case QgsLayoutExporter::Success: case QgsLayoutExporter::Success:
mMessageBar->pushMessage( tr( "Export layout" ), 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 ); QgsMessageBar::INFO, 0 );
break; break;
@ -1630,13 +1631,14 @@ void QgsLayoutDesignerDialog::exportToPdf()
// force a refresh, to e.g. update data defined properties, tables, etc // force a refresh, to e.g. update data defined properties, tables, etc
mLayout->refresh(); mLayout->refresh();
QFileInfo fi( outputFileName );
QgsLayoutExporter exporter( mLayout ); QgsLayoutExporter exporter( mLayout );
switch ( exporter.exportToPdf( outputFileName, pdfSettings ) ) switch ( exporter.exportToPdf( outputFileName, pdfSettings ) )
{ {
case QgsLayoutExporter::Success: case QgsLayoutExporter::Success:
{ {
mMessageBar->pushMessage( tr( "Export layout" ), 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 ); QgsMessageBar::INFO, 0 );
break; break;
} }

View File

@ -20,6 +20,7 @@
#include "qgslayoutpagecollection.h" #include "qgslayoutpagecollection.h"
#include "qgsogrutils.h" #include "qgsogrutils.h"
#include "qgspaintenginehack.h" #include "qgspaintenginehack.h"
#include "qgslayoutguidecollection.h"
#include <QImageWriter> #include <QImageWriter>
#include <QSize> #include <QSize>
@ -48,6 +49,34 @@ class LayoutContextPreviewSettingRestorer
bool mPreviousSetting = false; 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 ///@endcond PRIVATE
QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout ) QgsLayoutExporter::QgsLayoutExporter( QgsLayout *layout )
@ -150,18 +179,12 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF &region )
( void )cacheRestorer; ( void )cacheRestorer;
LayoutContextPreviewSettingRestorer restorer( mLayout ); LayoutContextPreviewSettingRestorer restorer( mLayout );
( void )restorer; ( void )restorer;
LayoutGuideHider guideHider( mLayout );
#if 0 //TODO ( void ) guideHider;
setSnapLinesVisible( false );
#endif
painter->setRenderHint( QPainter::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing ); painter->setRenderHint( QPainter::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing );
mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region ); 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 QImage QgsLayoutExporter::renderRegionToImage( const QRectF &region, QSize imageSize, double dpi ) const

View File

@ -89,7 +89,7 @@ void QgsLayoutGuide::update()
} }
else else
{ {
mLineItem->setLine( 0, layoutPos, mPage->rect().width(), layoutPos ); mLineItem->setLine( 0, layoutPos + mPage->y(), mPage->rect().width(), layoutPos + mPage->y() );
mLineItem->setVisible( showGuide ); mLineItem->setVisible( showGuide );
} }
@ -102,7 +102,7 @@ void QgsLayoutGuide::update()
} }
else else
{ {
mLineItem->setLine( layoutPos, 0, layoutPos, mPage->rect().height() ); mLineItem->setLine( layoutPos, mPage->y(), layoutPos, mPage->y() + mPage->rect().height() );
mLineItem->setVisible( showGuide ); 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 *> QgsLayoutGuideCollection::guides( Qt::Orientation orientation, int page )
{ {
QList<QgsLayoutGuide *> res; QList<QgsLayoutGuide *> res;

View File

@ -235,6 +235,11 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel, public
*/ */
void update(); 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 * Returns the list of guides contained in the collection with the specified
* \a orientation and on a matching \a page. * \a orientation and on a matching \a page.

View File

@ -95,6 +95,7 @@ void QgsLayoutPageCollection::reflow()
currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + spaceBetweenPages(); currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + spaceBetweenPages();
p.setY( currentY ); p.setY( currentY );
} }
mLayout->guides().update();
mLayout->updateBounds(); mLayout->updateBounds();
emit changed(); emit changed();
} }
@ -193,7 +194,8 @@ int QgsLayoutPageCollection::predictPageNumberForPoint( QPointF point ) const
QgsLayoutItemPage *QgsLayoutPageCollection::pageAtPoint( 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 ) if ( item->type() == QgsLayoutItemRegistry::LayoutPage )
{ {

View File

@ -55,6 +55,7 @@ bool QgsMultiRenderChecker::runTest( const QString &testName, unsigned int misma
QgsRenderChecker checker; QgsRenderChecker checker;
checker.enableDashBuffering( true ); checker.enableDashBuffering( true );
checker.setColorTolerance( mColorTolerance ); checker.setColorTolerance( mColorTolerance );
checker.setSizeTolerance( mMaxSizeDifferenceX, mMaxSizeDifferenceY );
checker.setControlPathPrefix( mControlPathPrefix ); checker.setControlPathPrefix( mControlPathPrefix );
checker.setControlPathSuffix( suffix ); checker.setControlPathSuffix( suffix );
checker.setControlName( mControlName ); checker.setControlName( mControlName );

View File

@ -94,6 +94,14 @@ class CORE_EXPORT QgsMultiRenderChecker
*/ */
void setColorTolerance( unsigned int colorTolerance ) { mColorTolerance = colorTolerance; } 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. * Test using renderer to generate the image to be compared.
* *
@ -134,6 +142,8 @@ class CORE_EXPORT QgsMultiRenderChecker
QString mControlName; QString mControlName;
QString mControlPathPrefix; QString mControlPathPrefix;
unsigned int mColorTolerance = 0; unsigned int mColorTolerance = 0;
int mMaxSizeDifferenceX = 0;
int mMaxSizeDifferenceY = 0;
QgsMapSettings mMapSettings; QgsMapSettings mMapSettings;
}; };

View File

@ -17,6 +17,7 @@ import sip
import tempfile import tempfile
import shutil import shutil
import os import os
import subprocess
from qgis.core import (QgsMultiRenderChecker, from qgis.core import (QgsMultiRenderChecker,
QgsLayoutExporter, QgsLayoutExporter,
@ -24,10 +25,13 @@ from qgis.core import (QgsMultiRenderChecker,
QgsProject, QgsProject,
QgsMargins, QgsMargins,
QgsLayoutItemShape, QgsLayoutItemShape,
QgsLayoutGuide,
QgsRectangle, QgsRectangle,
QgsLayoutItemPage, QgsLayoutItemPage,
QgsLayoutItemMap, QgsLayoutItemMap,
QgsLayoutPoint, QgsLayoutPoint,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsSimpleFillSymbolLayer, QgsSimpleFillSymbolLayer,
QgsFillSymbol) QgsFillSymbol)
from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt 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 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() start_app()
@ -54,12 +110,13 @@ class TestQgsLayoutExporter(unittest.TestCase):
with open(report_file_path, 'a') as report_file: with open(report_file_path, 'a') as report_file:
report_file.write(self.report) 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 = QgsMultiRenderChecker()
checker.setControlPathPrefix("layout_exporter") checker.setControlPathPrefix("layout_exporter")
checker.setControlName("expected_layoutexporter_" + reference_image) checker.setControlName("expected_layoutexporter_" + reference_image)
checker.setRenderedImage(rendered_image) checker.setRenderedImage(rendered_image)
checker.setColorTolerance(2) checker.setColorTolerance(2)
checker.setSizeTolerance(size_tolerance, size_tolerance)
result = checker.runTest(name, 20) result = checker.runTest(name, 20)
self.report += checker.report() self.report += checker.report()
print((self.report)) print((self.report))
@ -134,6 +191,10 @@ class TestQgsLayoutExporter(unittest.TestCase):
l = QgsLayout(QgsProject.instance()) l = QgsLayout(QgsProject.instance())
l.initializeDefaults() 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 # add some items
item1 = QgsLayoutItemShape(l) item1 = QgsLayoutItemShape(l)
item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) 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') page2_path = os.path.join(self.basetestpath, 'test_exporttoimagesize_2.png')
self.assertTrue(self.checkImage('exporttoimagesize_page2', 'exporttoimagesize_page2', page2_path)) 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): def testExportWorldFile(self):
l = QgsLayout(QgsProject.instance()) l = QgsLayout(QgsProject.instance())
l.initializeDefaults() l.initializeDefaults()

View File

@ -65,6 +65,11 @@ class TestQgsLayoutGuide(unittest.TestCase):
p = QgsProject() p = QgsProject()
l = QgsLayout(p) l = QgsLayout(p)
l.initializeDefaults() # add a page 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 = QgsLayoutGuide(Qt.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
g.setLayout(l) g.setLayout(l)
g.update() g.update()
@ -85,6 +90,19 @@ class TestQgsLayoutGuide(unittest.TestCase):
self.assertEqual(g.item().line().y2(), 15) self.assertEqual(g.item().line().y2(), 15)
self.assertEqual(g.layoutPosition(), 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 # vertical guide
g2 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0)) g2 = QgsLayoutGuide(Qt.Vertical, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
g2.setLayout(l) g2.setLayout(l)
@ -109,6 +127,17 @@ class TestQgsLayoutGuide(unittest.TestCase):
g.update() g.update()
self.assertFalse(g.item().isVisible()) 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): def testCollection(self):
p = QgsProject() p = QgsProject()
l = QgsLayout(p) l = QgsLayout(p)