Skip to content

Commit

Permalink
[layouts][FEATURE] Don't force the whole layout to be rasterized
Browse files Browse the repository at this point in the history
when exporting to PDF

If an individual layout item needs rasterisation in order to
be exported correctly, it can now be individually rasterised
without forcing every other item to also be rasterised.

This allows exports to PDF keeping as much as possible as vectors,
e.g. a map with layer opacity won't force labels, scalebars, etc
to be rasterised too.

To accompany this, a new "Always export as vectors" checkbox
was added to layout properties. If checked, this will force
the export to keep items as vectors, even when it causes the
output to look different to layouts.

Fixes #7885
  • Loading branch information
nyalldawson committed Dec 17, 2017
1 parent 91179f1 commit b992e87
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 40 deletions.
1 change: 1 addition & 0 deletions python/core/layout/qgslayoutcontext.sip
Expand Up @@ -27,6 +27,7 @@ class QgsLayoutContext : QObject
FlagOutlineOnly,
FlagAntialiasing,
FlagUseAdvancedEffects,
FlagForceVectorOutput,
};
typedef QFlags<QgsLayoutContext::Flag> Flags;

Expand Down
13 changes: 12 additions & 1 deletion python/core/layout/qgslayoutexporter.sip
Expand Up @@ -212,7 +212,18 @@ Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.

bool rasterizeWholeImage;
%Docstring
Set to true to force whole layout to be rasterized while exporting
Set to true to force whole layout to be rasterized while exporting.

This option is mutually exclusive with forceVectorOutput.
%End

bool forceVectorOutput;
%Docstring
Set to true to force vector object exports, even when the resultant appearance will differ
from the layout. If false, some items may be rasterized in order to maintain their
correct appearance in the output.

This option is mutually exclusive with rasterizeWholeImage.
%End

QgsLayoutContext::Flags flags;
Expand Down
10 changes: 10 additions & 0 deletions python/core/layout/qgslayoutitem.sip
Expand Up @@ -827,6 +827,16 @@ Sets whether the item should be excluded from composer exports and prints.

Subclasses should ensure that implemented overrides of this method
also check the base class result.

.. seealso:: :py:func:`requiresRasterization()`
:rtype: bool
%End

virtual bool requiresRasterization() const;
%Docstring
Returns true if the item is drawn in such a way that forces the whole layout
to be rasterised when exporting to vector formats.
.. seealso:: :py:func:`containsAdvancedEffects()`
:rtype: bool
%End

Expand Down
2 changes: 2 additions & 0 deletions python/core/layout/qgslayoutitempicture.sip
Expand Up @@ -296,6 +296,8 @@ Forces a recalculation of the picture's frame size

virtual void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties );

virtual bool containsAdvancedEffects() const;


signals:
void pictureRotationChanged( double newRotation );
Expand Down
53 changes: 47 additions & 6 deletions src/app/layout/qgslayoutdesignerdialog.cpp
Expand Up @@ -1552,9 +1552,14 @@ void QgsLayoutDesignerDialog::exportToPdf()
showWmsPrintingWarning();
}

if ( containsAdvancedEffects() )
if ( requiresRasterization() )
{
showAdvancedEffectsWarning();
showRasterizationWarning();
}

if ( containsAdvancedEffects() && ( mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) )
{
showForceVectorWarning();
}

QgsSettings settings;
Expand Down Expand Up @@ -1602,7 +1607,8 @@ void QgsLayoutDesignerDialog::exportToPdf()
QApplication::setOverrideCursor( Qt::BusyCursor );

QgsLayoutExporter::PdfExportSettings pdfSettings;
pdfSettings.rasteriseWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool();
pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool();
pdfSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();

QgsLayoutExporter exporter( mLayout );
switch ( exporter.exportToPdf( outputFileName, pdfSettings ) )
Expand Down Expand Up @@ -1783,6 +1789,19 @@ void QgsLayoutDesignerDialog::showWmsPrintingWarning()
}
}

bool QgsLayoutDesignerDialog::requiresRasterization() const
{
QList< QgsLayoutItem *> items;
mLayout->layoutItems( items );

for ( QgsLayoutItem *currentItem : qgis::as_const( items ) )
{
if ( currentItem->requiresRasterization() )
return true;
}
return false;
}

bool QgsLayoutDesignerDialog::containsAdvancedEffects() const
{
QList< QgsLayoutItem *> items;
Expand All @@ -1796,10 +1815,11 @@ bool QgsLayoutDesignerDialog::containsAdvancedEffects() const
return false;
}

void QgsLayoutDesignerDialog::showAdvancedEffectsWarning()
void QgsLayoutDesignerDialog::showRasterizationWarning()
{
bool rasterize = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool();
if ( rasterise )

if ( mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool() ||
mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() )
return;

QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false );
Expand All @@ -1817,6 +1837,27 @@ void QgsLayoutDesignerDialog::showAdvancedEffectsWarning()
delete m;
}

void QgsLayoutDesignerDialog::showForceVectorWarning()
{
QgsSettings settings;
if ( settings.value( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), false, QgsSettings::App ).toBool() )
return;

QgsMessageViewer *m = new QgsMessageViewer( this, QgsGuiUtils::ModalDialogFlags, false );
m->setWindowTitle( tr( "Force Vector" ) );
m->setMessage( tr( "This layout has the \"Always export as vectors\" option enabled, but the layout contains effects such as blend modes or vector layer transparency, which cannot be printed as vectors. The generated file will differ from the layout contents." ), QgsMessageOutput::MessageText );
m->setCheckBoxText( tr( "Never show this message again" ) );
m->setCheckBoxState( Qt::Unchecked );
m->setCheckBoxVisible( true );
m->showMessage( true );

if ( m->checkBoxState() == Qt::Checked )
{
settings.setValue( QStringLiteral( "LayoutDesigner/hideForceVectorWarning" ), true, QgsSettings::App );
}
delete m;
}

void QgsLayoutDesignerDialog::selectItems( const QList<QgsLayoutItem *> items )
{
for ( QGraphicsItem *item : items )
Expand Down
5 changes: 4 additions & 1 deletion src/app/layout/qgslayoutdesignerdialog.h
Expand Up @@ -371,10 +371,13 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
void showWmsPrintingWarning();

//! True if the layout contains advanced effects, such as blend modes
bool requiresRasterization() const;

bool containsAdvancedEffects() const;

//! Displays a warning because of incompatibility between blend modes and QPrinter
void showAdvancedEffectsWarning();
void showRasterizationWarning();
void showForceVectorWarning();
};

#endif // QGSLAYOUTDESIGNERDIALOG_H
Expand Down
29 changes: 29 additions & 0 deletions src/app/layout/qgslayoutpropertieswidget.cpp
Expand Up @@ -59,6 +59,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout
connect( mGenerateWorldFileCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::worldFileToggled );

connect( mRasterizeCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::rasteriseToggled );
connect( mForceVectorCheckBox, &QCheckBox::toggled, this, &QgsLayoutPropertiesWidget::forceVectorToggled );

mTopMarginSpinBox->setValue( topMargin );
mMarginUnitsComboBox->linkToWidget( mTopMarginSpinBox );
Expand Down Expand Up @@ -93,6 +94,19 @@ void QgsLayoutPropertiesWidget::updateGui()

bool rasterise = mLayout->customProperty( QStringLiteral( "rasterise" ), false ).toBool();
whileBlocking( mRasterizeCheckBox )->setChecked( rasterise );

bool forceVectors = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool();
whileBlocking( mForceVectorCheckBox )->setChecked( forceVectors );

if ( rasterise )
{
mForceVectorCheckBox->setChecked( false );
mForceVectorCheckBox->setEnabled( false );
}
else
{
mForceVectorCheckBox->setEnabled( true );
}
}

void QgsLayoutPropertiesWidget::updateSnappingElements()
Expand Down Expand Up @@ -197,6 +211,21 @@ void QgsLayoutPropertiesWidget::worldFileToggled()
void QgsLayoutPropertiesWidget::rasteriseToggled()
{
mLayout->setCustomProperty( QStringLiteral( "rasterise" ), mRasterizeCheckBox->isChecked() );

if ( mRasterizeCheckBox->isChecked() )
{
mForceVectorCheckBox->setChecked( false );
mForceVectorCheckBox->setEnabled( false );
}
else
{
mForceVectorCheckBox->setEnabled( true );
}
}

void QgsLayoutPropertiesWidget::forceVectorToggled()
{
mLayout->setCustomProperty( QStringLiteral( "forceVector" ), mForceVectorCheckBox->isChecked() );
}

void QgsLayoutPropertiesWidget::blockSignals( bool block )
Expand Down
1 change: 1 addition & 0 deletions src/app/layout/qgslayoutpropertieswidget.h
Expand Up @@ -47,6 +47,7 @@ class QgsLayoutPropertiesWidget: public QgsPanelWidget, private Ui::QgsLayoutWid
void dpiChanged( int value );
void worldFileToggled();
void rasteriseToggled();
void forceVectorToggled();

private:

Expand Down
1 change: 1 addition & 0 deletions src/core/layout/qgslayoutcontext.h
Expand Up @@ -45,6 +45,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject
FlagOutlineOnly = 1 << 2, //!< Render items as outlines only.
FlagAntialiasing = 1 << 3, //!< Use antialiasing when drawing items.
FlagUseAdvancedEffects = 1 << 4, //!< Enable advanced effects such as blend modes.
FlagForceVectorOutput = 1 << 5, //!< Force output in vector format where possible, even if items require rasterization to keep their correct appearance.
};
Q_DECLARE_FLAGS( Flags, Flag )

Expand Down
6 changes: 4 additions & 2 deletions src/core/layout/qgslayoutexporter.cpp
Expand Up @@ -310,7 +310,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
// If we are not printing as raster, temporarily disable advanced effects
// as QPrinter does not support composition modes and can result
// in items missing from the output
mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, settings.rasteriseWholeImage );
mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput );

mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput );

QPrinter printer;
preparePrintAsPdf( printer, filePath );
Expand All @@ -322,7 +324,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f
return PrintError;
}

ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasteriseWholeImage );
ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage );
p.end();

#if 0//TODO
Expand Down
15 changes: 14 additions & 1 deletion src/core/layout/qgslayoutexporter.h
Expand Up @@ -216,9 +216,22 @@ class CORE_EXPORT QgsLayoutExporter
//! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used.
double dpi = -1;

//! Set to true to force whole layout to be rasterized while exporting
/**
* Set to true to force whole layout to be rasterized while exporting.
*
* This option is mutually exclusive with forceVectorOutput.
*/
bool rasterizeWholeImage = false;

/**
* Set to true to force vector object exports, even when the resultant appearance will differ
* from the layout. If false, some items may be rasterized in order to maintain their
* correct appearance in the output.
*
* This option is mutually exclusive with rasterizeWholeImage.
*/
bool forceVectorOutput = false;

/**
* Layout context flags, which control how the export will be created.
*/
Expand Down
55 changes: 39 additions & 16 deletions src/core/layout/qgslayoutitem.cpp
Expand Up @@ -242,18 +242,32 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it
return;
}

double destinationDpi = itemStyle->matrix.m11() * 25.4;
bool previewRender = !mLayout || mLayout->context().isPreviewRender();
double destinationDpi = previewRender ? itemStyle->matrix.m11() * 25.4 : mLayout->context().dpi();
bool useImageCache = false;
bool forceRasterOutput = containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) );

if ( useImageCache )
if ( useImageCache || forceRasterOutput )
{
double widthInPixels = boundingRect().width() * itemStyle->matrix.m11();
double heightInPixels = boundingRect().height() * itemStyle->matrix.m11();
double widthInPixels = 0;
double heightInPixels = 0;

if ( previewRender )
{
widthInPixels = boundingRect().width() * itemStyle->matrix.m11();
heightInPixels = boundingRect().height() * itemStyle->matrix.m11();
}
else
{
double layoutUnitsToPixels = mLayout ? mLayout->convertFromLayoutUnits( 1, QgsUnitTypes::LayoutPixels ).length() : destinationDpi / 25.4;
widthInPixels = boundingRect().width() * layoutUnitsToPixels;
heightInPixels = boundingRect().height() * layoutUnitsToPixels;
}

// limit size of image for better performance
double scale = 1.0;
if ( widthInPixels > CACHE_SIZE_LIMIT || heightInPixels > CACHE_SIZE_LIMIT )
if ( previewRender && ( widthInPixels > CACHE_SIZE_LIMIT || heightInPixels > CACHE_SIZE_LIMIT ) )
{
double scale = 1.0;
if ( widthInPixels > heightInPixels )
{
scale = widthInPixels / CACHE_SIZE_LIMIT;
Expand All @@ -269,7 +283,7 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it
destinationDpi = destinationDpi / scale;
}

if ( !mItemCachedImage.isNull() && qgsDoubleNear( mItemCacheDpi, destinationDpi ) )
if ( previewRender && !mItemCachedImage.isNull() && qgsDoubleNear( mItemCacheDpi, destinationDpi ) )
{
// can reuse last cached image
QgsRenderContext context = QgsLayoutUtils::createRenderContextForMap( nullptr, painter, destinationDpi );
Expand All @@ -284,13 +298,11 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it
}
else
{
mItemCacheDpi = destinationDpi;

mItemCachedImage = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 );
mItemCachedImage.fill( Qt::transparent );
mItemCachedImage.setDotsPerMeterX( 1000 * destinationDpi * 25.4 );
mItemCachedImage.setDotsPerMeterY( 1000 * destinationDpi * 25.4 );
QPainter p( &mItemCachedImage );
QImage image = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 );
image.fill( Qt::transparent );
image.setDotsPerMeterX( 1000 * destinationDpi * 25.4 );
image.setDotsPerMeterY( 1000 * destinationDpi * 25.4 );
QPainter p( &image );

preparePainter( &p );
QgsRenderContext context = QgsLayoutUtils::createRenderContextForLayout( nullptr, &p, destinationDpi );
Expand All @@ -306,8 +318,14 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it
// scale painter from mm to dots
painter->scale( 1.0 / context.scaleFactor(), 1.0 / context.scaleFactor() );
painter->drawImage( boundingRect().x() * context.scaleFactor(),
boundingRect().y() * context.scaleFactor(), mItemCachedImage );
boundingRect().y() * context.scaleFactor(), image );
painter->restore();

if ( previewRender )
{
mItemCacheDpi = destinationDpi;
mItemCachedImage = image;
}
}
}
else
Expand Down Expand Up @@ -842,7 +860,12 @@ void QgsLayoutItem::setExcludeFromExports( bool exclude )

bool QgsLayoutItem::containsAdvancedEffects() const
{
return blendMode() != QPainter::CompositionMode_SourceOver;
return false;
}

bool QgsLayoutItem::requiresRasterization() const
{
return itemOpacity() < 1.0 || blendMode() != QPainter::CompositionMode_SourceOver;
}

double QgsLayoutItem::estimatedFrameBleed() const
Expand Down
9 changes: 9 additions & 0 deletions src/core/layout/qgslayoutitem.h
Expand Up @@ -748,9 +748,18 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
*
* Subclasses should ensure that implemented overrides of this method
* also check the base class result.
*
* \see requiresRasterization()
*/
virtual bool containsAdvancedEffects() const;

/**
* Returns true if the item is drawn in such a way that forces the whole layout
* to be rasterised when exporting to vector formats.
* \see containsAdvancedEffects()
*/
virtual bool requiresRasterization() const;

/**
* Returns the estimated amount the item's frame bleeds outside the item's
* actual rectangle. For instance, if the item has a 2mm frame stroke, then
Expand Down

0 comments on commit b992e87

Please sign in to comment.