QGIS API Documentation 3.27.0-Master (9c08adf5ef)
qgslayoutitempicture.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslayoutitempicture.cpp
3 ------------------------
4 begin : October 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
20#include "qgslayout.h"
23#include "qgslayoutitemmap.h"
24#include "qgslayoututils.h"
25#include "qgsproject.h"
26#include "qgsexpression.h"
27#include "qgsvectorlayer.h"
28#include "qgsmessagelog.h"
29#include "qgspathresolver.h"
30#include "qgsproperty.h"
32#include "qgssymbollayerutils.h"
33#include "qgssvgcache.h"
34#include "qgslogger.h"
35#include "qgsbearingutils.h"
36#include "qgsmapsettings.h"
37#include "qgsreadwritecontext.h"
38#include "qgsimagecache.h"
40
41#include <QDomDocument>
42#include <QDomElement>
43#include <QFileInfo>
44#include <QImageReader>
45#include <QPainter>
46#include <QSvgRenderer>
47#include <QNetworkRequest>
48#include <QNetworkReply>
49#include <QEventLoop>
50#include <QCoreApplication>
51#include <QUrl>
52
54 : QgsLayoutItem( layout )
55 , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) )
56{
57 //default to no background
58 setBackgroundEnabled( false );
59
60 //connect some signals
61
62 //connect to atlas feature changing
63 //to update the picture source expression
64 connect( &layout->reportContext(), &QgsLayoutReportContext::changed, this, [ = ] { refreshPicture(); } );
65
66 //connect to layout print resolution changing
68
69 connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged );
70 connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemPicture::updateNorthArrowRotation );
71}
72
74{
76}
77
79{
80 return QgsApplication::getThemeIcon( QStringLiteral( "/mLayoutItemPicture.svg" ) );
81}
82
84{
85 return new QgsLayoutItemPicture( layout );
86}
87
89{
90 QPainter *painter = context.renderContext().painter();
91 const QgsScopedQPainterState painterState( painter );
92 // painter is scaled to dots, so scale back to layout units
93 painter->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
94
95 const bool prevSmoothTransform = painter->testRenderHint( QPainter::RenderHint::SmoothPixmapTransform );
96 if ( mLayout->renderContext().testFlag( QgsLayoutRenderContext::FlagAntialiasing ) )
97 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, true );
98
99 //picture resizing
100 if ( mMode != FormatUnknown )
101 {
102 double boundRectWidthMM;
103 double boundRectHeightMM;
104 QRect imageRect;
105 if ( mResizeMode == QgsLayoutItemPicture::Zoom || mResizeMode == QgsLayoutItemPicture::ZoomResizeFrame )
106 {
107 boundRectWidthMM = mPictureWidth;
108 boundRectHeightMM = mPictureHeight;
109 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
110 }
111 else if ( mResizeMode == QgsLayoutItemPicture::Stretch )
112 {
113 boundRectWidthMM = rect().width();
114 boundRectHeightMM = rect().height();
115 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
116 }
117 else if ( mResizeMode == QgsLayoutItemPicture::Clip )
118 {
119 boundRectWidthMM = rect().width();
120 boundRectHeightMM = rect().height();
121 const int imageRectWidthPixels = mImage.width();
122 const int imageRectHeightPixels = mImage.height();
123 imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM,
124 QSize( imageRectWidthPixels, imageRectHeightPixels ) );
125 }
126 else
127 {
128 boundRectWidthMM = rect().width();
129 boundRectHeightMM = rect().height();
130 imageRect = QRect( 0, 0, mLayout->convertFromLayoutUnits( rect().width(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4,
131 mLayout->convertFromLayoutUnits( rect().height(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4 );
132 }
133
134 //zoom mode - calculate anchor point and rotation
135 if ( mResizeMode == Zoom )
136 {
137 //TODO - allow placement modes with rotation set. for now, setting a rotation
138 //always places picture in center of frame
139 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
140 {
141 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
142 painter->rotate( mPictureRotation );
143 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
144 }
145 else
146 {
147 //shift painter to edge/middle of frame depending on placement
148 const double diffX = rect().width() - boundRectWidthMM;
149 const double diffY = rect().height() - boundRectHeightMM;
150
151 double dX = 0;
152 double dY = 0;
153 switch ( mPictureAnchor )
154 {
155 case UpperLeft:
156 case MiddleLeft:
157 case LowerLeft:
158 //nothing to do
159 break;
160 case UpperMiddle:
161 case Middle:
162 case LowerMiddle:
163 dX = diffX / 2.0;
164 break;
165 case UpperRight:
166 case MiddleRight:
167 case LowerRight:
168 dX = diffX;
169 break;
170 }
171 switch ( mPictureAnchor )
172 {
173 case UpperLeft:
174 case UpperMiddle:
175 case UpperRight:
176 //nothing to do
177 break;
178 case MiddleLeft:
179 case Middle:
180 case MiddleRight:
181 dY = diffY / 2.0;
182 break;
183 case LowerLeft:
184 case LowerMiddle:
185 case LowerRight:
186 dY = diffY;
187 break;
188 }
189 painter->translate( dX, dY );
190 }
191 }
192 else if ( mResizeMode == ZoomResizeFrame )
193 {
194 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
195 {
196 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
197 painter->rotate( mPictureRotation );
198 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
199 }
200 }
201
202 if ( mMode == FormatSVG )
203 {
204 mSVG.render( painter, QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ) );
205 }
206 else if ( mMode == FormatRaster )
207 {
208 painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect );
209 }
210 }
211 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, prevSmoothTransform );
212}
213
214QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF targetSize )
215{
216 const QSizeF currentPictureSize = pictureSize();
217 QSizeF newSize = targetSize;
218 if ( mResizeMode == QgsLayoutItemPicture::Clip )
219 {
220 mPictureWidth = targetSize.width();
221 mPictureHeight = targetSize.height();
222 }
223 else
224 {
225 if ( mResizeMode == ZoomResizeFrame && !rect().isEmpty() && !( currentPictureSize.isEmpty() ) )
226 {
227 QSizeF targetImageSize;
228 if ( qgsDoubleNear( mPictureRotation, 0.0 ) )
229 {
230 targetImageSize = currentPictureSize;
231 }
232 else
233 {
234 //calculate aspect ratio of bounds of rotated image
235 QTransform tr;
236 tr.rotate( mPictureRotation );
237 const QRectF rotatedBounds = tr.mapRect( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ) );
238 targetImageSize = QSizeF( rotatedBounds.width(), rotatedBounds.height() );
239 }
240
241 //if height has changed more than width, then fix width and set height correspondingly
242 //else, do the opposite
243 if ( std::fabs( rect().width() - targetSize.width() ) <
244 std::fabs( rect().height() - targetSize.height() ) )
245 {
246 newSize.setHeight( targetImageSize.height() * newSize.width() / targetImageSize.width() );
247 }
248 else
249 {
250 newSize.setWidth( targetImageSize.width() * newSize.height() / targetImageSize.height() );
251 }
252 }
253 else if ( mResizeMode == FrameToImageSize )
254 {
255 if ( !( currentPictureSize.isEmpty() ) )
256 {
257 const QgsLayoutSize sizeMM = mLayout->convertFromLayoutUnits( currentPictureSize, QgsUnitTypes::LayoutMillimeters );
258 newSize.setWidth( sizeMM.width() * 25.4 / mLayout->renderContext().dpi() );
259 newSize.setHeight( sizeMM.height() * 25.4 / mLayout->renderContext().dpi() );
260 }
261 }
262
263 //find largest scaling of picture with this rotation which fits in item
264 if ( mResizeMode == Zoom || mResizeMode == ZoomResizeFrame )
265 {
266 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ),
267 QRectF( 0, 0, newSize.width(), newSize.height() ), mPictureRotation );
268 mPictureWidth = rotatedImageRect.width();
269 mPictureHeight = rotatedImageRect.height();
270 }
271 else
272 {
273 mPictureWidth = newSize.width();
274 mPictureHeight = newSize.height();
275 }
276
277 if ( newSize != targetSize )
278 {
279 emit changed();
280 }
281 }
282
283 return newSize;
284}
285
286QRect QgsLayoutItemPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels )
287{
288 const int boundRectWidthPixels = boundRectWidthMM * mLayout->renderContext().dpi() / 25.4;
289 const int boundRectHeightPixels = boundRectHeightMM * mLayout->renderContext().dpi() / 25.4;
290
291 //update boundRectWidth/Height so that they exactly match pixel bounds
292 boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->renderContext().dpi();
293 boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->renderContext().dpi();
294
295 //calculate part of image which fits in bounds
296 int leftClip = 0;
297 int topClip = 0;
298
299 //calculate left crop
300 switch ( mPictureAnchor )
301 {
302 case UpperLeft:
303 case MiddleLeft:
304 case LowerLeft:
305 leftClip = 0;
306 break;
307 case UpperMiddle:
308 case Middle:
309 case LowerMiddle:
310 leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2;
311 break;
312 case UpperRight:
313 case MiddleRight:
314 case LowerRight:
315 leftClip = imageRectPixels.width() - boundRectWidthPixels;
316 break;
317 }
318
319 //calculate top crop
320 switch ( mPictureAnchor )
321 {
322 case UpperLeft:
323 case UpperMiddle:
324 case UpperRight:
325 topClip = 0;
326 break;
327 case MiddleLeft:
328 case Middle:
329 case MiddleRight:
330 topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2;
331 break;
332 case LowerLeft:
333 case LowerMiddle:
334 case LowerRight:
335 topClip = imageRectPixels.height() - boundRectHeightPixels;
336 break;
337 }
338
339 return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels );
340}
341
343{
344 const QgsExpressionContext scopedContext = createExpressionContext();
345 const QgsExpressionContext *evalContext = context ? context : &scopedContext;
346
347 mDataDefinedProperties.prepare( *evalContext );
348
349 QVariant source( mSourcePath );
350
351 //data defined source set?
352 mHasExpressionError = false;
354 {
355 mMode = FormatUnknown;
356 bool ok = false;
358 source = sourceProperty.value( *evalContext, source, &ok );
359 if ( !ok || !source.canConvert( QMetaType::QString ) )
360 {
361 mHasExpressionError = true;
362 source = QString();
363 QgsMessageLog::logMessage( tr( "Picture expression eval error" ) );
364 }
365 else if ( source.type() != QVariant::ByteArray )
366 {
367 source = source.toString().trimmed();
368 QgsDebugMsgLevel( QStringLiteral( "exprVal PictureSource:%1" ).arg( source.toString() ), 2 );
369 }
370 }
371
372 loadPicture( source );
373}
374
375void QgsLayoutItemPicture::loadRemotePicture( const QString &url )
376{
377 //remote location
378
380 QEventLoop loop;
381 connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
382 fetcher.fetchContent( QUrl( url ) );
383
384 //wait until picture fetched
385 loop.exec( QEventLoop::ExcludeUserInputEvents );
386
387 QNetworkReply *reply = fetcher.reply();
388 if ( reply )
389 {
390 QImageReader imageReader( reply );
391 imageReader.setAutoTransform( true );
392
393 if ( imageReader.format() == "pdf" )
394 {
395 // special handling for this format -- we need to pass the desired target size onto the image reader
396 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
397 // a very low resolution image (the driver assumes points == pixels!)
398 // For other image formats, we read the original image size only and defer resampling to later in this
399 // function. That gives us more control over the resampling method used.
400
401 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
402 const QSize sizeAt72Dpi = imageReader.size();
403 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
404 imageReader.setScaledSize( sizeAtTargetDpi );
405 }
406
407 mImage = imageReader.read();
408 mMode = FormatRaster;
409 }
410 else
411 {
412 mMode = FormatUnknown;
413 }
414}
415
416void QgsLayoutItemPicture::loadLocalPicture( const QString &path )
417{
418 QFile pic;
419 pic.setFileName( path );
420
421 if ( !pic.exists() )
422 {
423 mMode = FormatUnknown;
424 }
425 else
426 {
427 const QFileInfo sourceFileInfo( pic );
428 const QString sourceFileSuffix = sourceFileInfo.suffix();
429 if ( sourceFileSuffix.compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 )
430 {
431 //try to open svg
433 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
434 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
435 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
436 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
437
438 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
439 1.0, 0, false, evaluatedParameters );
440 mSVG.load( svgContent );
441 if ( mSVG.isValid() )
442 {
443 mMode = FormatSVG;
444 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
445 mDefaultSvgSize.setWidth( viewBox.width() );
446 mDefaultSvgSize.setHeight( viewBox.height() );
447 }
448 else
449 {
450 mMode = FormatUnknown;
451 }
452 }
453 else
454 {
455 //try to open raster with QImageReader
456 QImageReader imageReader( pic.fileName() );
457 imageReader.setAutoTransform( true );
458
459 if ( imageReader.format() == "pdf" )
460 {
461 // special handling for this format -- we need to pass the desired target size onto the image reader
462 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
463 // a very low resolution image (the driver assumes points == pixels!)
464 // For other image formats, we read the original image size only and defer resampling to later in this
465 // function. That gives us more control over the resampling method used.
466
467 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
468 const QSize sizeAt72Dpi = imageReader.size();
469 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
470 imageReader.setScaledSize( sizeAtTargetDpi );
471 }
472
473 if ( imageReader.read( &mImage ) )
474 {
475 mMode = FormatRaster;
476 }
477 else
478 {
479 mMode = FormatUnknown;
480 }
481 }
482 }
483}
484
485void QgsLayoutItemPicture::loadPictureUsingCache( const QString &path )
486{
487 if ( path.isEmpty() )
488 {
489 mImage = QImage();
490 return;
491 }
492
493 switch ( mMode )
494 {
495 case FormatUnknown:
496 break;
497
498 case FormatRaster:
499 {
500 bool fitsInCache = false;
501 bool isMissing = false;
502 mImage = QgsApplication::imageCache()->pathAsImage( path, QSize(), true, 1, fitsInCache, true, mLayout->renderContext().dpi(), -1, &isMissing );
503 if ( mImage.isNull() || isMissing )
504 mMode = FormatUnknown;
505 break;
506 }
507
508 case FormatSVG:
509 {
511 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
512 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
513 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
514
515 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
516
517 bool isMissingImage = false;
518 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
519 1.0, 0, false, evaluatedParameters, &isMissingImage );
520 mSVG.load( svgContent );
521 if ( mSVG.isValid() && !isMissingImage )
522 {
523 mMode = FormatSVG;
524 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
525 mDefaultSvgSize.setWidth( viewBox.width() );
526 mDefaultSvgSize.setHeight( viewBox.height() );
527 }
528 else
529 {
530 mMode = FormatUnknown;
531 }
532 break;
533 }
534 }
535}
536
537void QgsLayoutItemPicture::updateNorthArrowRotation( double rotation )
538{
539 setPictureRotation( rotation );
540 emit pictureRotationChanged( rotation );
541}
542
543void QgsLayoutItemPicture::loadPicture( const QVariant &data )
544{
545 const Format origFormat = mMode;
546 mIsMissingImage = false;
547 QVariant imageData( data );
548 mEvaluatedPath = data.toString();
549
550 if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) && mMode == FormatUnknown )
551 {
552 const QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
553 imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
554 }
555
556 if ( imageData.type() == QVariant::ByteArray )
557 {
558 if ( mImage.loadFromData( imageData.toByteArray() ) )
559 {
560 mMode = FormatRaster;
561 }
562 }
563 else if ( mMode == FormatUnknown && mEvaluatedPath.startsWith( QLatin1String( "http" ) ) )
564 {
565 //remote location (unsafe way, uses QEventLoop) - for old API/project compatibility only!!
566 loadRemotePicture( mEvaluatedPath );
567 }
568 else if ( mMode == FormatUnknown )
569 {
570 //local location - for old API/project compatibility only!!
571 loadLocalPicture( mEvaluatedPath );
572 }
573 else
574 {
575 loadPictureUsingCache( mEvaluatedPath );
576 }
577
578 if ( mMode != FormatUnknown ) //make sure we start with a new QImage
579 {
581 }
582 else if ( mHasExpressionError || !mEvaluatedPath.isEmpty() )
583 {
584 //trying to load an invalid file or bad expression, show cross picture
585 mIsMissingImage = true;
586 if ( origFormat == FormatRaster )
587 {
588 const QString badFile( QStringLiteral( ":/images/composer/missing_image.png" ) );
589 QImageReader imageReader( badFile );
590 if ( imageReader.read( &mImage ) )
591 mMode = FormatRaster;
592 }
593 else
594 {
595 const QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
596 mSVG.load( badFile );
597 if ( mSVG.isValid() )
598 {
599 mMode = FormatSVG;
600 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
601 mDefaultSvgSize.setWidth( viewBox.width() );
602 mDefaultSvgSize.setHeight( viewBox.height() );
603 }
604 }
606 }
607
608 update();
609 emit changed();
610}
611
612QRectF QgsLayoutItemPicture::boundedImageRect( double deviceWidth, double deviceHeight )
613{
614 double imageToDeviceRatio;
615 if ( mImage.width() / deviceWidth > mImage.height() / deviceHeight )
616 {
617 imageToDeviceRatio = deviceWidth / mImage.width();
618 const double height = imageToDeviceRatio * mImage.height();
619 return QRectF( 0, 0, deviceWidth, height );
620 }
621 else
622 {
623 imageToDeviceRatio = deviceHeight / mImage.height();
624 const double width = imageToDeviceRatio * mImage.width();
625 return QRectF( 0, 0, width, deviceHeight );
626 }
627}
628
629QRectF QgsLayoutItemPicture::boundedSVGRect( double deviceWidth, double deviceHeight )
630{
631 double imageToSvgRatio;
632 if ( deviceWidth / mDefaultSvgSize.width() > deviceHeight / mDefaultSvgSize.height() )
633 {
634 imageToSvgRatio = deviceHeight / mDefaultSvgSize.height();
635 const double width = mDefaultSvgSize.width() * imageToSvgRatio;
636 return QRectF( 0, 0, width, deviceHeight );
637 }
638 else
639 {
640 imageToSvgRatio = deviceWidth / mDefaultSvgSize.width();
641 const double height = mDefaultSvgSize.height() * imageToSvgRatio;
642 return QRectF( 0, 0, deviceWidth, height );
643 }
644}
645
646QSizeF QgsLayoutItemPicture::pictureSize()
647{
648 if ( mMode == FormatSVG )
649 {
650 return mDefaultSvgSize;
651 }
652 else if ( mMode == FormatRaster )
653 {
654 return QSizeF( mImage.width(), mImage.height() );
655 }
656 else
657 {
658 return QSizeF( 0, 0 );
659 }
660}
661
663{
664 return mIsMissingImage;
665}
666
668{
669 return mEvaluatedPath;
670}
671
672QMap<QString, QgsProperty> QgsLayoutItemPicture::svgDynamicParameters() const
673{
674 const QVariantMap parameters = mCustomProperties.value( QStringLiteral( "svg-dynamic-parameters" ), QVariantMap() ).toMap();
675 return QgsProperty::variantMapToPropertyMap( parameters );
676}
677
678void QgsLayoutItemPicture::setSvgDynamicParameters( const QMap<QString, QgsProperty> &parameters )
679{
680 const QVariantMap variantMap = QgsProperty::propertyMapToVariantMap( parameters );
681 mCustomProperties.setValue( QStringLiteral( "svg-dynamic-parameters" ), variantMap );
683}
684
685void QgsLayoutItemPicture::shapeChanged()
686{
687 if ( mMode == FormatSVG && !mLoadingSvg )
688 {
689 mLoadingSvg = true;
691 mLoadingSvg = false;
692 }
693}
694
696{
697 const double oldRotation = mPictureRotation;
698 mPictureRotation = rotation;
699
700 if ( mResizeMode == Zoom )
701 {
702 //find largest scaling of picture with this rotation which fits in item
703 const QSizeF currentPictureSize = pictureSize();
704 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
705 mPictureWidth = rotatedImageRect.width();
706 mPictureHeight = rotatedImageRect.height();
707 update();
708 }
709 else if ( mResizeMode == ZoomResizeFrame )
710 {
711 const QSizeF currentPictureSize = pictureSize();
712 const QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
713
714 //calculate actual size of image inside frame
715 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
716
717 //rotate image rect by new rotation and get bounding box
718 QTransform tr;
719 tr.rotate( mPictureRotation );
720 QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
721
722 //keep the center in the same location
723 newRect.moveCenter( oldRect.center() );
724 attemptSetSceneRect( newRect );
725 emit changed();
726 }
727
728 emit pictureRotationChanged( mPictureRotation );
729}
730
732{
733 mNorthArrowHandler->setLinkedMap( map );
734}
735
737{
738 mResizeMode = mode;
740 || ( mode == QgsLayoutItemPicture::Zoom && !qgsDoubleNear( mPictureRotation, 0.0 ) ) )
741 {
742 //call set scene rect to force item to resize to fit picture
744 }
745 update();
746}
747
749{
750 //call set scene rect with current position/size, as this will trigger the
751 //picture item to recalculate its frame and image size
752 attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
753}
754
756{
759 || property == QgsLayoutObject::AllProperties )
760 {
762 refreshPicture( &context );
763 }
764
766}
767
768void QgsLayoutItemPicture::setPicturePath( const QString &path, Format format )
769{
770 mMode = format;
771 mSourcePath = path;
773}
774
776{
777 return mSourcePath;
778}
779
780bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
781{
782 QString imagePath = mSourcePath;
783
784 // convert from absolute path to relative. For SVG we also need to consider system SVG paths
785 const QgsPathResolver pathResolver = context.pathResolver();
786 if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
787 imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
788 else
789 imagePath = pathResolver.writePath( imagePath );
790
791 elem.setAttribute( QStringLiteral( "file" ), imagePath );
792 elem.setAttribute( QStringLiteral( "pictureWidth" ), QString::number( mPictureWidth ) );
793 elem.setAttribute( QStringLiteral( "pictureHeight" ), QString::number( mPictureHeight ) );
794 elem.setAttribute( QStringLiteral( "resizeMode" ), QString::number( static_cast< int >( mResizeMode ) ) );
795 elem.setAttribute( QStringLiteral( "anchorPoint" ), QString::number( static_cast< int >( mPictureAnchor ) ) );
796 elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) );
797 elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) );
798 elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) );
799 elem.setAttribute( QStringLiteral( "mode" ), mMode );
800
801 //rotation
802 elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) );
803 if ( !mNorthArrowHandler->linkedMap() )
804 {
805 elem.setAttribute( QStringLiteral( "mapUuid" ), QString() );
806 }
807 else
808 {
809 elem.setAttribute( QStringLiteral( "mapUuid" ), mNorthArrowHandler->linkedMap()->uuid() );
810 }
811 elem.setAttribute( QStringLiteral( "northMode" ), mNorthArrowHandler->northMode() );
812 elem.setAttribute( QStringLiteral( "northOffset" ), mNorthArrowHandler->northOffset() );
813 return true;
814}
815
816bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
817{
818 mPictureWidth = itemElem.attribute( QStringLiteral( "pictureWidth" ), QStringLiteral( "10" ) ).toDouble();
819 mPictureHeight = itemElem.attribute( QStringLiteral( "pictureHeight" ), QStringLiteral( "10" ) ).toDouble();
820 mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
821 //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
822 mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( QStringLiteral( "anchorPoint" ), QString::number( QgsLayoutItem::Middle ) ).toInt() );
823
824 mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) );
825 mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) );
826 mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble();
827 mMode = static_cast< Format >( itemElem.attribute( QStringLiteral( "mode" ), QString::number( FormatUnknown ) ).toInt() );
828
829 const QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) );
830 if ( !composerItemList.isEmpty() )
831 {
832 const QDomElement composerItemElem = composerItemList.at( 0 ).toElement();
833
834 if ( !qgsDoubleNear( composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
835 {
836 //in versions prior to 2.1 picture rotation was stored in the rotation attribute
837 mPictureRotation = composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble();
838 }
839 }
840
841 mDefaultSvgSize = QSize( 0, 0 );
842
843 if ( itemElem.hasAttribute( QStringLiteral( "sourceExpression" ) ) )
844 {
845 //update pre 2.5 picture expression to use data defined expression
846 const QString sourceExpression = itemElem.attribute( QStringLiteral( "sourceExpression" ), QString() );
847 const QString useExpression = itemElem.attribute( QStringLiteral( "useExpression" ) );
848 bool expressionActive;
849 expressionActive = ( useExpression.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 );
850
852 }
853
854 QString imagePath = itemElem.attribute( QStringLiteral( "file" ) );
855
856 // convert from relative path to absolute. For SVG we also need to consider system SVG paths
857 const QgsPathResolver pathResolver = context.pathResolver();
858 if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
859 imagePath = QgsSymbolLayerUtils::svgSymbolNameToPath( imagePath, pathResolver );
860 else
861 imagePath = pathResolver.readPath( imagePath );
862
863 mSourcePath = imagePath;
864
865 //picture rotation
866 if ( !qgsDoubleNear( itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
867 {
868 mPictureRotation = itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble();
869 }
870
871 //rotation map
872 mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ) );
873 mNorthArrowHandler->setNorthOffset( itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble() );
874
875 mNorthArrowHandler->setLinkedMap( nullptr );
876 mRotationMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) );
877
878 return true;
879}
880
882{
883 return mNorthArrowHandler->linkedMap();
884}
885
887{
888 return static_cast< QgsLayoutItemPicture::NorthMode >( mNorthArrowHandler->northMode() );
889}
890
892{
893 mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( mode ) );
894}
895
897{
898 return mNorthArrowHandler->northOffset();
899}
900
902{
903 mNorthArrowHandler->setNorthOffset( offset );
904}
905
907{
908 mPictureAnchor = anchor;
909 update();
910}
911
912void QgsLayoutItemPicture::setSvgFillColor( const QColor &color )
913{
914 mSvgFillColor = color;
916}
917
919{
920 mSvgStrokeColor = color;
922}
923
925{
926 mSvgStrokeWidth = width;
928}
929
931{
932 if ( mMode == mode )
933 return;
934
935 mMode = mode;
937}
938
940{
941 if ( !mLayout || mRotationMapUuid.isEmpty() )
942 {
943 mNorthArrowHandler->setLinkedMap( nullptr );
944 }
945 else
946 {
947 mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) );
948 }
949
951}
QColor valueAsColor(int key, const QgsExpressionContext &context, const QColor &defaultColor=QColor(), bool *ok=nullptr) const
Calculates the current value of the property with the specified key and interprets it as a color.
double valueAsDouble(int key, const QgsExpressionContext &context, double defaultValue=0.0, bool *ok=nullptr) const
Calculates the current value of the property with the specified key and interprets it as a double.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
static QgsSvgCache * svgCache()
Returns the application's SVG cache, used for caching SVG images and handling parameter replacement w...
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
Layout graphical items for displaying a map.
A layout item subclass that displays SVG files or raster format images (jpg, png, ....
void setResizeMode(QgsLayoutItemPicture::ResizeMode mode)
Sets the resize mode used for drawing the picture within the item bounds.
void setSvgDynamicParameters(const QMap< QString, QgsProperty > &parameters)
Sets the SVG dynamic parameters.
QMap< QString, QgsProperty > svgDynamicParameters() const
Returns the SVG dynamic parameters.
QString picturePath() const
Returns the path of the source image.
void setPictureAnchor(QgsLayoutItem::ReferencePoint anchor)
Sets the picture's anchor point, which controls how it is placed within the picture item's frame.
void setNorthMode(NorthMode mode)
Sets the mode used to align the picture to a map's North.
double northOffset() const
Returns the offset added to the picture's rotation from a map's North.
QgsLayoutItemPicture(QgsLayout *layout)
Constructor for QgsLayoutItemPicture, with the specified parent layout.
void setLinkedMap(QgsLayoutItemMap *map)
Sets the map object for rotation.
void setPictureRotation(double rotation)
Sets the picture rotation within the item bounds, in degrees clockwise.
bool isMissingImage() const
Returns true if the source image is missing and the picture cannot be rendered.
bool writePropertiesToElement(QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context) const override
Stores item state within an XML DOM element.
bool readPropertiesFromElement(const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context) override
Sets item state from a DOM element.
void setPicturePath(const QString &path, Format format=FormatUnknown)
Sets the source path of the image (may be svg or a raster format).
void setSvgStrokeWidth(double width)
Sets the stroke width (in layout units) used for parametrized SVG files.
QgsLayoutItemMap * linkedMap() const
Returns the linked rotation map, if set.
void pictureRotationChanged(double newRotation)
Emitted on picture rotation change.
void draw(QgsLayoutItemRenderContext &context) override
Draws the item's contents using the specified item render context.
static QgsLayoutItemPicture * create(QgsLayout *layout)
Returns a new picture item for the specified layout.
NorthMode
Method for syncing rotation to a map's North direction.
void finalizeRestoreFromXml() override
Called after all pending items have been restored from XML.
Format mode() const
Returns the current picture mode (image format).
QString evaluatedPath() const
Returns the current evaluated picture path, which includes the result of data defined path overrides.
QSizeF applyItemSizeConstraint(QSizeF targetSize) override
Applies any item-specific size constraint handling to a given targetSize in layout units.
void setSvgStrokeColor(const QColor &color)
Sets the stroke color used for parametrized SVG files.
int type() const override
void refreshDataDefinedProperty(QgsLayoutObject::DataDefinedProperty property=QgsLayoutObject::AllProperties) override
QIcon icon() const override
Returns the item's icon.
void refreshPicture(const QgsExpressionContext *context=nullptr)
Recalculates the source image (if using an expression for picture's source) and reloads and redraws t...
void setMode(Format mode)
Sets the current picture mode (image format).
NorthMode northMode() const
Returns the mode used to align the picture to a map's North.
void setSvgFillColor(const QColor &color)
Sets the fill color used for parametrized SVG files.
void recalculateSize()
Forces a recalculation of the picture's frame size.
ResizeMode
Controls how pictures are scaled within the item's frame.
@ FrameToImageSize
Sets size of frame to match original size of image without scaling.
@ ZoomResizeFrame
Enlarges image to fit frame, then resizes frame to fit resultant image.
@ Clip
Draws image at original size and clips any portion which falls outside frame.
@ Stretch
Stretches image to fit frame, ignores aspect ratio.
@ Zoom
Enlarges image to fit frame while maintaining aspect ratio of picture.
Format
Format of source image.
@ FormatUnknown
Invalid or unknown image type.
void setNorthOffset(double offset)
Sets the offset added to the picture's rotation from a map's North.
Contains settings and helpers relating to a render of a QgsLayoutItem.
Definition: qgslayoutitem.h:45
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
Definition: qgslayoutitem.h:72
Base class for graphical items within a QgsLayout.
ReferencePoint
Fixed position reference point.
@ LowerMiddle
Lower center of item.
@ MiddleLeft
Middle left of item.
@ Middle
Center of item.
@ UpperRight
Upper right corner of item.
@ LowerLeft
Lower left corner of item.
@ UpperLeft
Upper left corner of item.
@ UpperMiddle
Upper center of item.
@ MiddleRight
Middle right of item.
@ LowerRight
Lower right corner of item.
QgsExpressionContext createExpressionContext() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
virtual void refreshDataDefinedProperty(QgsLayoutObject::DataDefinedProperty property=QgsLayoutObject::AllProperties)
Refreshes a data defined property for the item by reevaluating the property's value and redrawing the...
void sizePositionChanged()
Emitted when the item's size or position changes.
virtual QString uuid() const
Returns the item identification string.
void attemptSetSceneRect(const QRectF &rect, bool includesFrame=false)
Attempts to update the item's position and size to match the passed rect in layout coordinates.
void setBackgroundEnabled(bool drawBackground)
Sets whether this item has a background drawn under it or not.
An object which handles north-arrow type behavior for layout items.
void setNorthOffset(double offset)
Sets the offset added to the arrows's rotation from a map's North.
NorthMode northMode() const
Returns the mode used to calculate the arrow rotation.
QgsLayoutItemMap * linkedMap() const
Returns the linked rotation map, if set.
void arrowRotationChanged(double newRotation)
Emitted on arrow rotation change.
NorthMode
Method for syncing rotation to a map's North direction.
void setNorthMode(NorthMode mode)
Sets the mode used to calculate the arrow rotation.
void setLinkedMap(QgsLayoutItemMap *map)
Sets the linked map item.
double northOffset() const
Returns the offset added to the arrows's rotation from a map's North.
QgsObjectCustomProperties mCustomProperties
Custom properties for object.
QgsPropertyCollection mDataDefinedProperties
const QgsLayout * layout() const
Returns the layout the object is attached to.
void changed()
Emitted when the object's properties change.
QPointer< QgsLayout > mLayout
DataDefinedProperty
Data defined properties for different item types.
@ PictureSvgStrokeColor
SVG stroke color.
@ AllProperties
All properties for item.
@ PictureSvgStrokeWidth
SVG stroke width.
@ PictureSvgBackgroundColor
SVG background color.
@ PictureSource
Picture source url.
void dpiChanged()
Emitted when the context's DPI is changed.
@ FlagAntialiasing
Use antialiasing when drawing items.
void changed()
Emitted certain settings in the context is changed, e.g.
This class provides a method of storing sizes, consisting of a width and height, for use in QGIS layo...
Definition: qgslayoutsize.h:41
double height() const
Returns the height of the size.
Definition: qgslayoutsize.h:90
double width() const
Returns the width of the size.
Definition: qgslayoutsize.h:76
static QRectF largestRotatedRectWithinBounds(const QRectF &originalRect, const QRectF &boundsRect, double rotation)
Calculates the largest scaled version of originalRect which fits within boundsRect,...
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
QgsLayoutRenderContext & renderContext()
Returns a reference to the layout's render context, which stores information relating to the current ...
Definition: qgslayout.cpp:359
QgsLayoutReportContext & reportContext()
Returns a reference to the layout's report context, which stores information relating to the current ...
Definition: qgslayout.cpp:369
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
HTTP network content fetcher.
void finished()
Emitted when content has loaded.
QNetworkReply * reply()
Returns a reference to the network reply.
void fetchContent(const QUrl &url, const QString &authcfg=QString())
Fetches content from a remote URL and handles redirects.
void setValue(const QString &key, const QVariant &value)
Add an entry to the store with the specified key.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant()) const
Returns the value for the given key.
Resolves relative paths into absolute paths and vice versa.
QString writePath(const QString &filename) const
Prepare a filename to save it to the project file.
QString readPath(const QString &filename) const
Turn filename read from the project file to an absolute path.
QgsProperty property(int key) const override
Returns a matching property from the collection, if one exists.
void setProperty(int key, const QgsProperty &property)
Adds a property to the collection and takes ownership of it.
bool isActive(int key) const override
Returns true if the collection contains an active property with the specified key.
bool prepare(const QgsExpressionContext &context=QgsExpressionContext()) const override
Prepares the collection against a specified expression context.
A store for object properties.
Definition: qgsproperty.h:231
static QVariantMap propertyMapToVariantMap(const QMap< QString, QgsProperty > &propertyMap)
Convert a map of QgsProperty to a map of QVariant This is useful to save a map of properties.
QVariant value(const QgsExpressionContext &context, const QVariant &defaultValue=QVariant(), bool *ok=nullptr) const
Calculates the current value of the property, including any transforms which are set for the property...
static QgsProperty fromExpression(const QString &expression, bool isActive=true)
Returns a new ExpressionBasedProperty created from the specified expression.
static QMap< QString, QgsProperty > variantMapToPropertyMap(const QVariantMap &variantMap)
Convert a map of QVariant to a map of QgsProperty This is useful to restore a map of properties.
The class is used as a container of context for various read/write operations on other objects.
const QgsPathResolver & pathResolver() const
Returns path resolver for conversion between relative and absolute paths.
double scaleFactor() const
Returns the scaling factor for the render to convert painter units to physical sizes.
QPainter * painter()
Returns the destination QPainter for the render operation.
Scoped object for saving and restoring a QPainter object's state.
QByteArray svgContent(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >(), bool *isMissingImage=nullptr)
Gets the SVG content corresponding to the given path.
static QgsStringMap evaluatePropertiesMap(const QMap< QString, QgsProperty > &propertiesMap, const QgsExpressionContext &context)
Evaluates a map of properties using the given context and returns a variant map with evaluated expres...
static QColor decodeColor(const QString &str)
static QString svgSymbolPathToName(const QString &path, const QgsPathResolver &pathResolver)
Determines an SVG symbol's name from its path.
static QString svgSymbolNameToPath(const QString &name, const QgsPathResolver &pathResolver)
Determines an SVG symbol's path from its name.
static QString encodeColor(const QColor &color)
@ LayoutMillimeters
Millimeters.
Definition: qgsunittypes.h:183
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2404
QMap< QString, QString > QgsStringMap
Definition: qgis.h:2920
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39