[layout][server] composer -> layout in getprint

This commit is contained in:
Alessandro Pasotti 2018-01-10 18:02:48 +01:00
parent 3df284fb64
commit eedb7c795f
21 changed files with 237 additions and 251 deletions

View File

@ -78,19 +78,24 @@
//for printing
#include "qgslayoutmanager.h"
#include "qgscomposition.h"
#include "qgscomposerarrow.h"
#include "qgscomposerlabel.h"
#include "qgscomposerlegend.h"
#include "qgscomposermap.h"
#include "qgscomposermapgrid.h"
#include "qgscomposerframe.h"
#include "qgscomposerhtml.h"
#include "qgscomposerpicture.h"
#include "qgscomposerscalebar.h"
#include "qgscomposershape.h"
#include "qgslayoutexporter.h"
#include "qgslayoutsize.h"
#include "qgslayoutmeasurement.h"
#include "qgsprintlayout.h"
#include "qgslayoutpagecollection.h"
#include "qgslayoutitempage.h"
#include "qgslayoutitemlabel.h"
#include "qgslayoutitemlegend.h"
#include "qgslayoutitemmap.h"
#include "qgslayoutitemmapgrid.h"
#include "qgslayoutframe.h"
#include "qgslayoutitemhtml.h"
#include "qgslayoutitempicture.h"
#include "qgslayoutitemscalebar.h"
#include "qgslayoutitemshape.h"
#include "qgsfeaturefilterprovidergroup.h"
#include "qgsogcutils.h"
#include "qgsunittypes.h"
#include <QBuffer>
#include <QPrinter>
#include <QSvgGenerator>
@ -295,7 +300,8 @@ namespace QgsWms
QList<QgsMapLayer *> layers;
QList<QgsWmsParametersLayer> params = mWmsParameters.layersParameters();
// create the output image
// create the output image (this is not really used but configureMapSettings
// needs it)
std::unique_ptr<QImage> image( new QImage() );
// configure map settings (background, DPI, ...)
@ -354,69 +360,85 @@ namespace QgsWms
mapSettings.setLayers( layers );
const QgsLayoutManager *lManager = mProject->layoutManager();
QDomDocument cDocument;
if ( !lManager->saveAsTemplate( templateName, cDocument ) )
QgsPrintLayout *sourceLayout( dynamic_cast<QgsPrintLayout *>( lManager->layoutByName( templateName ) ) );
if ( !sourceLayout )
{
throw QgsBadRequestException( QStringLiteral( "InvalidTemplate" ),
QStringLiteral( "Template '%1' is not known" ).arg( templateName ) );
}
QgsComposition *c = new QgsComposition( const_cast<QgsProject *>( mProject ) );
if ( !c->loadFromTemplate( cDocument, nullptr, false, true ) )
// Check that layout has at least one page
if ( sourceLayout->pageCollection()->pageCount() < 1 )
{
throw QgsBadRequestException( QStringLiteral( "InvalidTemplate" ),
QStringLiteral( "Template '%1' is not loaded correctly" ).arg( templateName ) );
QStringLiteral( "Template '%1' has no pages" ).arg( templateName ) );
}
configureComposition( c, mapSettings );
c->setPlotStyle( QgsComposition::Print );
std::unique_ptr<QgsPrintLayout> layout( sourceLayout->clone() );
QByteArray ba;
configurePrintLayout( layout.get(), mapSettings );
// Get the temporary output file
QTemporaryFile tempOutputFile( QStringLiteral( "XXXXXX.%1" ).arg( formatString.toLower() ) );
if ( !tempOutputFile.open() )
{
// let the caller handle this
return nullptr;
}
//SVG export without a running X-Server is a problem. See e.g. http://developer.qt.nokia.com/forums/viewthread/2038
if ( formatString.compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 )
{
c->setPlotStyle( QgsComposition::Print );
QSvgGenerator generator;
QBuffer svgBuffer( &ba );
generator.setOutputDevice( &svgBuffer );
int width = ( int )( c->paperWidth() * c->printResolution() / 25.4 ); //width in pixel
int height = ( int )( c->paperHeight() * c->printResolution() / 25.4 ); //height in pixel
generator.setSize( QSize( width, height ) );
generator.setResolution( c->printResolution() ); //because the rendering is done in mm, convert the dpi
QPainter p( &generator );
if ( c->printAsRaster() ) //embed one raster into the svg
// Settings for the layout exporter
QgsLayoutExporter::SvgExportSettings exportSettings;
if ( !mWmsParameters.dpi().isEmpty() )
{
QImage img = c->printPageAsRaster( 0 );
p.drawImage( QRect( 0, 0, width, height ), img, QRectF( 0, 0, img.width(), img.height() ) );
bool ok;
double dpi( mWmsParameters.dpi().toDouble( &ok ) );
if ( ok )
exportSettings.dpi = dpi;
}
else
{
c->renderPage( &p, 0 );
}
p.end();
QgsLayoutExporter exporter( layout.get() );
exporter.exportToSvg( tempOutputFile.fileName(), exportSettings );
}
else if ( formatString.compare( QLatin1String( "png" ), Qt::CaseInsensitive ) == 0 || formatString.compare( QLatin1String( "jpg" ), Qt::CaseInsensitive ) == 0 )
{
QImage image = c->printPageAsRaster( 0 ); //can only return the first page if pixmap is requested
QBuffer buffer( &ba );
buffer.open( QIODevice::WriteOnly );
image.save( &buffer, formatString.toLocal8Bit().data(), -1 );
// Settings for the layout exporter
QgsLayoutExporter::ImageExportSettings exportSettings;
// Get the dpi from input or use the default
double dpi( layout->renderContext().dpi( ) );
if ( !mWmsParameters.dpi().isEmpty() )
{
bool ok;
double _dpi = mWmsParameters.dpi().toDouble( &ok );
if ( ! ok )
dpi = _dpi;
}
exportSettings.dpi = dpi;
// Destination image size in px
QgsLayoutSize layoutSize( layout->pageCollection()->page( 0 )->sizeWithUnits() );
QgsLayoutMeasurement width( layout->convertFromLayoutUnits( layoutSize.width(), QgsUnitTypes::LayoutUnit::LayoutMillimeters ) );
QgsLayoutMeasurement height( layout->convertFromLayoutUnits( layoutSize.height(), QgsUnitTypes::LayoutUnit::LayoutMillimeters ) );
exportSettings.imageSize = QSize( ( int )( width.length() * dpi / 25.4 ), ( int )( height.length() * dpi / 25.4 ) );
// Export first page only (unless it's a pdf, see below)
exportSettings.pages.append( 0 );
QgsLayoutExporter exporter( layout.get() );
exporter.exportToImage( tempOutputFile.fileName(), exportSettings );
}
else if ( formatString.compare( QLatin1String( "pdf" ), Qt::CaseInsensitive ) == 0 )
{
QTemporaryFile tempFile;
if ( !tempFile.open() )
// Settings for the layout exporter
QgsLayoutExporter::PdfExportSettings exportSettings;
// TODO: handle size from input ?
if ( !mWmsParameters.dpi().isEmpty() )
{
delete c;
return nullptr;
bool ok;
double dpi( mWmsParameters.dpi().toDouble( &ok ) );
if ( ok )
exportSettings.dpi = dpi;
}
c->exportAsPDF( tempFile.fileName() );
ba = tempFile.readAll();
// Export all pages
QgsLayoutExporter exporter( layout.get() );
exporter.exportToPdf( tempOutputFile.fileName(), exportSettings );
}
else //unknown format
{
@ -424,214 +446,180 @@ namespace QgsWms
QStringLiteral( "Output format '%1' is not supported in the GetPrint request" ).arg( formatString ) );
}
delete c;
return ba;
return tempOutputFile.readAll();
}
bool QgsRenderer::configureComposition( QgsComposition *c, const QgsMapSettings &mapSettings )
bool QgsRenderer::configurePrintLayout( QgsPrintLayout *c, const QgsMapSettings &mapSettings )
{
if ( !mWmsParameters.dpi().isEmpty() )
c->setPrintResolution( mWmsParameters.dpiAsInt() );
// Composer items
QList<QgsComposerItem * > itemList;
c->composerItems( itemList );
// Composer legends have to be updated after the other items
QList<QgsComposerLegend *> composerLegends;
// Update composer items
QList<QgsComposerItem *>::iterator itemIt = itemList.begin();
for ( ; itemIt != itemList.end(); ++itemIt )
// Maps are configured first
QList<QgsLayoutItemMap *> maps;
c->layoutItems<QgsLayoutItemMap>( maps );
// Layout maps now use a string UUID as "id", let's assume that the first map
// has id 0 and so on ...
int mapId = 0;
for ( auto &map : maps )
{
// firstly the composer maps
if ( ( *itemIt )->type() == QgsComposerItem::ComposerMap )
QgsWmsParametersComposerMap cMapParams = mWmsParameters.composerMapParameters( mapId );
mapId++;
//map extent is mandatory
if ( !cMapParams.mHasExtent )
{
QgsComposerMap *map = qobject_cast< QgsComposerMap *>( *itemIt );
if ( !map )
continue;
//remove map from composition if not referenced by the request
c->removeItem( map );
delete map;
continue;
}
// Change CRS of map set to "project CRS" to match requested CRS
// (if map has a valid preset crs then we keep this crs and don't use the
// requested crs for this map item)
if ( mapSettings.destinationCrs().isValid() && !map->presetCrs().isValid() )
map->setCrs( mapSettings.destinationCrs() );
QgsWmsParametersComposerMap cMapParams = mWmsParameters.composerMapParameters( map->id() );
QgsRectangle r( cMapParams.mExtent );
if ( mWmsParameters.versionAsNumber() >= QgsProjectVersion( 1, 3, 0 ) &&
mapSettings.destinationCrs().hasAxisInverted() )
{
r.invert();
}
map->setExtent( r );
//map extent is mandatory
if ( !cMapParams.mHasExtent )
// scale
if ( cMapParams.mScale > 0 )
{
map->setScale( cMapParams.mScale );
}
// rotation
if ( cMapParams.mRotation )
{
map->setMapRotation( cMapParams.mRotation );
}
if ( !map->keepLayerSet() )
{
if ( cMapParams.mLayers.isEmpty() )
{
//remove map from composition if not referenced by the request
c->removeItem( map );
delete map;
continue;
map->setLayers( mapSettings.layers() );
}
// Change CRS of map set to "project CRS" to match requested CRS
// (if map has a valid preset crs then we keep this crs and don't use the
// requested crs for this map item)
if ( mapSettings.destinationCrs().isValid() && !map->presetCrs().isValid() )
map->setCrs( mapSettings.destinationCrs() );
QgsRectangle r( cMapParams.mExtent );
if ( mWmsParameters.versionAsNumber() >= QgsProjectVersion( 1, 3, 0 ) && mapSettings.destinationCrs().hasAxisInverted() )
else
{
r.invert();
}
map->setNewExtent( r );
// scale
if ( cMapParams.mScale > 0 )
{
map->setNewScale( cMapParams.mScale );
}
// rotation
if ( cMapParams.mRotation )
{
map->setMapRotation( cMapParams.mRotation );
}
if ( !map->keepLayerSet() )
{
if ( cMapParams.mLayers.isEmpty() )
QList<QgsMapLayer *> layerSet;
const QList<QgsWmsParametersLayer> layers = cMapParams.mLayers;
for ( const auto &layer : layers )
{
map->setLayers( mapSettings.layers() );
}
else
{
QList<QgsMapLayer *> layerSet;
QList<QgsWmsParametersLayer> layers = cMapParams.mLayers;
Q_FOREACH ( QgsWmsParametersLayer layer, layers )
QString nickname = layer.mNickname;
QString style = layer.mStyle;
if ( mNicknameLayers.contains( nickname ) && !mRestrictedLayers.contains( nickname ) )
{
QString nickname = layer.mNickname;
QString style = layer.mStyle;
if ( mNicknameLayers.contains( nickname ) && !mRestrictedLayers.contains( nickname ) )
if ( !style.isEmpty() )
{
if ( !style.isEmpty() )
bool rc = mNicknameLayers[nickname]->styleManager()->setCurrentStyle( style );
if ( ! rc )
{
bool rc = mNicknameLayers[nickname]->styleManager()->setCurrentStyle( style );
if ( ! rc )
{
throw QgsMapServiceException( QStringLiteral( "StyleNotDefined" ), QStringLiteral( "Style \"%1\" does not exist for layer \"%2\"" ).arg( style, nickname ) );
}
throw QgsMapServiceException( QStringLiteral( "StyleNotDefined" ), QStringLiteral( "Style \"%1\" does not exist for layer \"%2\"" ).arg( style, nickname ) );
}
layerSet << mNicknameLayers[nickname];
}
else
{
throw QgsBadRequestException( QStringLiteral( "LayerNotDefined" ),
QStringLiteral( "Layer \"%1\" does not exist" ).arg( nickname ) );
}
layerSet << mNicknameLayers[nickname];
}
else
{
throw QgsBadRequestException( QStringLiteral( "LayerNotDefined" ),
QStringLiteral( "Layer \"%1\" does not exist" ).arg( nickname ) );
}
layerSet << highlightLayers( cMapParams.mHighlightLayers );
std::reverse( layerSet.begin(), layerSet.end() );
map->setLayers( layerSet );
}
map->setKeepLayerSet( true );
layerSet << highlightLayers( cMapParams.mHighlightLayers );
std::reverse( layerSet.begin(), layerSet.end() );
map->setLayers( layerSet );
}
//grid space x / y
if ( cMapParams.mGridX > 0 && cMapParams.mGridY > 0 )
{
map->grid()->setIntervalX( cMapParams.mGridX );
map->grid()->setIntervalY( cMapParams.mGridY );
}
continue;
map->setKeepLayerSet( true );
}
// secondly the composer legends
else if ( ( *itemIt )->type() == QgsComposerItem::ComposerLegend )
//grid space x / y
if ( cMapParams.mGridX > 0 && cMapParams.mGridY > 0 )
{
QgsComposerLegend *legend = qobject_cast< QgsComposerLegend *>( *itemIt );
if ( !legend )
continue;
composerLegends.push_back( legend );
continue;
}
// thirdly the composer labels
else if ( ( *itemIt )->type() == QgsComposerItem::ComposerLabel )
{
QgsComposerLabel *label = qobject_cast< QgsComposerLabel *>( *itemIt );
if ( !label )
continue;
QString labelId = label->id().toUpper();
if ( !mParameters.contains( labelId ) )
continue;
QString labelParam = mParameters[ labelId ];
if ( labelParam.isEmpty() )
{
//remove exported labels referenced in the request
//but with empty string
c->removeItem( label );
delete label;
continue;
}
label->setText( labelParam );
continue;
}
// forthly the composer HTMLs
// an html item will be a composer frame and if it is we can try to get
// its multiframe parent and then try to cast that to a composer html
else if ( ( *itemIt )->type() == QgsComposerItem::ComposerFrame )
{
QgsComposerFrame *frame = qobject_cast< QgsComposerFrame *>( *itemIt );
if ( !frame )
continue;
const QgsComposerMultiFrame *multiFrame = frame->multiFrame();
const QgsComposerHtml *composerHtml = qobject_cast<const QgsComposerHtml *>( multiFrame );
if ( !composerHtml )
continue;
QgsComposerHtml *html = const_cast<QgsComposerHtml *>( composerHtml );
QgsComposerFrame *htmlFrame = html->frame( 0 );
QString htmlId = htmlFrame->id().toUpper();
if ( !mParameters.contains( htmlId ) )
{
html->update();
continue;
}
QString url = mParameters[ htmlId ];
//remove exported Htmls referenced in the request
//but with empty string
if ( url.isEmpty() )
{
c->removeMultiFrame( html );
delete composerHtml;
continue;
}
QUrl newUrl( url );
html->setUrl( newUrl );
html->update();
continue;
map->grid()->setIntervalX( cMapParams.mGridX );
map->grid()->setIntervalY( cMapParams.mGridY );
}
}
//update legend
// if it has an auto-update model
Q_FOREACH ( QgsComposerLegend *currentLegend, composerLegends )
// Labels
QList<QgsLayoutItemLabel *> labels;
c->layoutItems<QgsLayoutItemLabel>( labels );
for ( auto &label : labels )
{
if ( !currentLegend )
QString labelId = label->id().toUpper();
if ( !mParameters.contains( labelId ) )
continue;
QString labelParam = mParameters[ labelId ];
if ( labelParam.isEmpty() )
{
//remove exported labels referenced in the request
//but with empty string
c->removeItem( label );
delete label;
continue;
}
if ( currentLegend->autoUpdateModel() )
label->setText( labelParam );
}
// HTMLs
QList<QgsLayoutItemHtml *> htmls;
c->layoutObjects<QgsLayoutItemHtml>( htmls );
for ( auto &html : htmls )
{
if ( html->frameCount() == 0 )
continue;
QgsLayoutFrame *htmlFrame = html->frame( 0 );
QString htmlId = htmlFrame->id().toUpper();
if ( !mParameters.contains( htmlId ) )
{
html->update();
continue;
}
QString url = mParameters[ htmlId ];
//remove exported Htmls referenced in the request
//but with empty string
if ( url.isEmpty() )
{
c->removeMultiFrame( html );
delete html;
continue;
}
QUrl newUrl( url );
html->setUrl( newUrl );
html->update();
}
// legends
QList<QgsLayoutItemLegend *> legends;
c->layoutItems<QgsLayoutItemLegend>( legends );
for ( auto &legend : legends )
{
if ( legend->autoUpdateModel() )
{
// the legend has an auto-update model
// we will update it with map's layers
const QgsComposerMap *map = currentLegend->composerMap();
const QgsLayoutItemMap *map = legend->linkedMap();
if ( !map )
{
continue;
}
currentLegend->setAutoUpdateModel( false );
legend->setAutoUpdateModel( false );
// get model and layer tree root of the legend
QgsLegendModel *model = currentLegend->model();
QgsLegendModel *model = legend->model();
QStringList layerSet;
Q_FOREACH ( QgsMapLayer *layer, map->layers() )
for ( const auto &layer : map->layers() )
layerSet << layer->id();
//setLayerIdsToLegendModel( model, layerSet, map->scale() );
// get model and layer tree root of the legend
@ -640,9 +628,9 @@ namespace QgsWms
// get layerIds find in the layer tree root
QStringList layerIds = root->findLayerIds();
// Q_FOREACH layer find in the layer tree
// find the layer in the layer tree
// remove it if the layer id is not in map layerIds
Q_FOREACH ( const QString &layerId, layerIds )
for ( const auto &layerId : layerIds )
{
QgsLayerTreeLayer *nodeLayer = root->findLayer( layerId );
if ( !nodeLayer )
@ -668,24 +656,6 @@ namespace QgsWms
return true;
}
#if 0
QImage *QgsWMSServer::printCompositionToImage( QgsComposition *c ) const
{
int width = ( int )( c->paperWidth() * c->printResolution() / 25.4 ); //width in pixel
int height = ( int )( c->paperHeight() * c->printResolution() / 25.4 ); //height in pixel
QImage *image = new QImage( QSize( width, height ), QImage::Format_ARGB32 );
image->setDotsPerMeterX( c->printResolution() / 25.4 * 1000 );
image->setDotsPerMeterY( c->printResolution() / 25.4 * 1000 );
image->fill( 0 );
QPainter p( image );
QRectF sourceArea( 0, 0, c->paperWidth(), c->paperHeight() );
QRectF targetArea( 0, 0, width, height );
c->render( &p, targetArea, sourceArea );
p.end();
return image;
}
#endif
QImage *QgsRenderer::getMap( HitTest *hitTest )
{
QgsMapSettings mapSettings;

View File

@ -31,7 +31,7 @@
class QgsCapabilitiesCache;
class QgsCoordinateReferenceSystem;
class QgsComposition;
class QgsPrintLayout;
class QgsConfigParser;
class QgsFeature;
class QgsFeatureRenderer;
@ -274,8 +274,8 @@ namespace QgsWms
//! Gets layer search rectangle (depending on request parameter, layer type, map and layer crs)
QgsRectangle featureInfoSearchRect( QgsVectorLayer *ml, const QgsMapSettings &ms, const QgsRenderContext &rct, const QgsPointXY &infoPoint ) const;
//! configure the composition for the GetPrint request
bool configureComposition( QgsComposition *c, const QgsMapSettings &mapSettings );
//! configure the print layout for the GetPrint request
bool configurePrintLayout( QgsPrintLayout *c, const QgsMapSettings &mapSettings );
//! Creates external WMS layer. Caller takes ownership
QgsMapLayer *createExternalWMSLayer( const QString &externalLayerId ) const;

View File

@ -10,6 +10,11 @@ A XYZ map service is also available for multithreading testing:
?MAP=/path/to/projects.qgs&SERVICE=XYZ&X=1&Y=0&Z=1&LAYERS=world
Note that multi threading in QGIS server is not officially supported and
it is not supposed to work in any case
Set MULTITHREADING environment varialbe to 1 to activate.
For testing purposes, HTTP Basic can be enabled by setting the following
environment variables:
@ -188,7 +193,10 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
if __name__ == '__main__':
server = ThreadedHTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler)
if os.environ.get('MULTITHREADING') == '1':
server = ThreadedHTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler)
else:
server = HTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler)
if https:
server.socket = ssl.wrap_socket(server.socket,
certfile=QGIS_SERVER_PKI_CERTIFICATE,

View File

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsServer.
Set the env var NO_ENCODED_OUTPUT to disable printing the base64 encoded image diff
FIXME: keep here only generic server tests and move specific services
tests to test_qgsserver_<service>.py
@ -189,17 +191,23 @@ class QgsServerTestBase(unittest.TestCase):
with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file:
encoded_rendered_file = base64.b64encode(rendered_file.read())
message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % (
report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image
)
if os.environ.get('NO_ENCODED_OUTPUT'):
message = "Image is wrong\: rendered file %s/%s_result.png" % (tempfile.gettempdir(), image)
else:
message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.png" % (
report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image
)
# If the failure is in image sizes the diff file will not exists.
if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")):
with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file:
encoded_diff_file = base64.b64encode(diff_file.read())
message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % (
encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image
)
if os.environ.get('NO_ENCODED_OUTPUT'):
message = "Image is wrong\: diff file %s/%s_result_diff.png" % (tempfile.gettempdir(), image)
else:
encoded_diff_file = base64.b64encode(diff_file.read())
message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.png" % (
encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image
)
self.assertTrue(test, message)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsServer WMS.
"""QGIS Unit tests for QgsServer WMS GetPrint.
From build dir, run: ctest -R PyQgsServerWMS -V
From build dir, run: ctest -R PyQgsServerWMSGetPrint -V
.. note:: This program is free software; you can redistribute it and/or modify

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 KiB

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 252 KiB