QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
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 "qgsmessagelog.h"
26#include "qgspathresolver.h"
27#include "qgsproperty.h"
29#include "qgssymbollayerutils.h"
30#include "qgssvgcache.h"
31#include "qgslogger.h"
32#include "qgsreadwritecontext.h"
33#include "qgsimagecache.h"
35
36#include <QDomDocument>
37#include <QDomElement>
38#include <QFileInfo>
39#include <QImageReader>
40#include <QPainter>
41#include <QSvgRenderer>
42#include <QNetworkRequest>
43#include <QNetworkReply>
44#include <QEventLoop>
45#include <QCoreApplication>
46#include <QUrl>
47
49 : QgsLayoutItem( layout )
50 , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) )
51{
52 //default to no background
53 setBackgroundEnabled( false );
54
55 //connect some signals
56
57 //connect to atlas feature changing
58 //to update the picture source expression
59 connect( &layout->reportContext(), &QgsLayoutReportContext::changed, this, [ = ] { refreshPicture(); } );
60
61 //connect to layout print resolution changing
63
64 connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged );
65 connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemPicture::updateNorthArrowRotation );
66}
67
69{
71}
72
74{
75 return QgsApplication::getThemeIcon( QStringLiteral( "/mLayoutItemPicture.svg" ) );
76}
77
79{
80 return new QgsLayoutItemPicture( layout );
81}
82
84{
85 QPainter *painter = context.renderContext().painter();
86 const QgsScopedQPainterState painterState( painter );
87 // painter is scaled to dots, so scale back to layout units
88 painter->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
89
90 const bool prevSmoothTransform = painter->testRenderHint( QPainter::RenderHint::SmoothPixmapTransform );
91 if ( mLayout->renderContext().testFlag( QgsLayoutRenderContext::FlagAntialiasing ) )
92 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, true );
93
94 //picture resizing
95 if ( mMode != FormatUnknown )
96 {
97 double boundRectWidthMM;
98 double boundRectHeightMM;
99 QRect imageRect;
100 if ( mResizeMode == QgsLayoutItemPicture::Zoom || mResizeMode == QgsLayoutItemPicture::ZoomResizeFrame )
101 {
102 boundRectWidthMM = mPictureWidth;
103 boundRectHeightMM = mPictureHeight;
104 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
105 }
106 else if ( mResizeMode == QgsLayoutItemPicture::Stretch )
107 {
108 boundRectWidthMM = rect().width();
109 boundRectHeightMM = rect().height();
110 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
111 }
112 else if ( mResizeMode == QgsLayoutItemPicture::Clip )
113 {
114 boundRectWidthMM = rect().width();
115 boundRectHeightMM = rect().height();
116 const int imageRectWidthPixels = mImage.width();
117 const int imageRectHeightPixels = mImage.height();
118 imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM,
119 QSize( imageRectWidthPixels, imageRectHeightPixels ) );
120 }
121 else
122 {
123 boundRectWidthMM = rect().width();
124 boundRectHeightMM = rect().height();
125 imageRect = QRect( 0, 0, mLayout->convertFromLayoutUnits( rect().width(), Qgis::LayoutUnit::Millimeters ).length() * mLayout->renderContext().dpi() / 25.4,
126 mLayout->convertFromLayoutUnits( rect().height(), Qgis::LayoutUnit::Millimeters ).length() * mLayout->renderContext().dpi() / 25.4 );
127 }
128
129 //zoom mode - calculate anchor point and rotation
130 if ( mResizeMode == Zoom )
131 {
132 //TODO - allow placement modes with rotation set. for now, setting a rotation
133 //always places picture in center of frame
134 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
135 {
136 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
137 painter->rotate( mPictureRotation );
138 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
139 }
140 else
141 {
142 //shift painter to edge/middle of frame depending on placement
143 const double diffX = rect().width() - boundRectWidthMM;
144 const double diffY = rect().height() - boundRectHeightMM;
145
146 double dX = 0;
147 double dY = 0;
148 switch ( mPictureAnchor )
149 {
150 case UpperLeft:
151 case MiddleLeft:
152 case LowerLeft:
153 //nothing to do
154 break;
155 case UpperMiddle:
156 case Middle:
157 case LowerMiddle:
158 dX = diffX / 2.0;
159 break;
160 case UpperRight:
161 case MiddleRight:
162 case LowerRight:
163 dX = diffX;
164 break;
165 }
166 switch ( mPictureAnchor )
167 {
168 case UpperLeft:
169 case UpperMiddle:
170 case UpperRight:
171 //nothing to do
172 break;
173 case MiddleLeft:
174 case Middle:
175 case MiddleRight:
176 dY = diffY / 2.0;
177 break;
178 case LowerLeft:
179 case LowerMiddle:
180 case LowerRight:
181 dY = diffY;
182 break;
183 }
184 painter->translate( dX, dY );
185 }
186 }
187 else if ( mResizeMode == ZoomResizeFrame )
188 {
189 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
190 {
191 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
192 painter->rotate( mPictureRotation );
193 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
194 }
195 }
196
197 if ( mMode == FormatSVG )
198 {
199 mSVG.render( painter, QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ) );
200 }
201 else if ( mMode == FormatRaster )
202 {
203 painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect );
204 }
205 }
206 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, prevSmoothTransform );
207}
208
209QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF targetSize )
210{
211 const QSizeF currentPictureSize = pictureSize();
212 QSizeF newSize = targetSize;
213 if ( mResizeMode == QgsLayoutItemPicture::Clip )
214 {
215 mPictureWidth = targetSize.width();
216 mPictureHeight = targetSize.height();
217 }
218 else
219 {
220 if ( mResizeMode == ZoomResizeFrame && !rect().isEmpty() && !( currentPictureSize.isEmpty() ) )
221 {
222 QSizeF targetImageSize;
223 if ( qgsDoubleNear( mPictureRotation, 0.0 ) )
224 {
225 targetImageSize = currentPictureSize;
226 }
227 else
228 {
229 //calculate aspect ratio of bounds of rotated image
230 QTransform tr;
231 tr.rotate( mPictureRotation );
232 const QRectF rotatedBounds = tr.mapRect( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ) );
233 targetImageSize = QSizeF( rotatedBounds.width(), rotatedBounds.height() );
234 }
235
236 //if height has changed more than width, then fix width and set height correspondingly
237 //else, do the opposite
238 if ( std::fabs( rect().width() - targetSize.width() ) <
239 std::fabs( rect().height() - targetSize.height() ) )
240 {
241 newSize.setHeight( targetImageSize.height() * newSize.width() / targetImageSize.width() );
242 }
243 else
244 {
245 newSize.setWidth( targetImageSize.width() * newSize.height() / targetImageSize.height() );
246 }
247 }
248 else if ( mResizeMode == FrameToImageSize )
249 {
250 if ( !( currentPictureSize.isEmpty() ) )
251 {
252 const QgsLayoutSize sizeMM = mLayout->convertFromLayoutUnits( currentPictureSize, Qgis::LayoutUnit::Millimeters );
253 newSize.setWidth( sizeMM.width() * 25.4 / mLayout->renderContext().dpi() );
254 newSize.setHeight( sizeMM.height() * 25.4 / mLayout->renderContext().dpi() );
255 }
256 }
257
258 //find largest scaling of picture with this rotation which fits in item
259 if ( mResizeMode == Zoom || mResizeMode == ZoomResizeFrame )
260 {
261 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ),
262 QRectF( 0, 0, newSize.width(), newSize.height() ), mPictureRotation );
263 mPictureWidth = rotatedImageRect.width();
264 mPictureHeight = rotatedImageRect.height();
265 }
266 else
267 {
268 mPictureWidth = newSize.width();
269 mPictureHeight = newSize.height();
270 }
271
272 if ( newSize != targetSize )
273 {
274 emit changed();
275 }
276 }
277
278 return newSize;
279}
280
281QRect QgsLayoutItemPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels )
282{
283 const int boundRectWidthPixels = boundRectWidthMM * mLayout->renderContext().dpi() / 25.4;
284 const int boundRectHeightPixels = boundRectHeightMM * mLayout->renderContext().dpi() / 25.4;
285
286 //update boundRectWidth/Height so that they exactly match pixel bounds
287 boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->renderContext().dpi();
288 boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->renderContext().dpi();
289
290 //calculate part of image which fits in bounds
291 int leftClip = 0;
292 int topClip = 0;
293
294 //calculate left crop
295 switch ( mPictureAnchor )
296 {
297 case UpperLeft:
298 case MiddleLeft:
299 case LowerLeft:
300 leftClip = 0;
301 break;
302 case UpperMiddle:
303 case Middle:
304 case LowerMiddle:
305 leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2;
306 break;
307 case UpperRight:
308 case MiddleRight:
309 case LowerRight:
310 leftClip = imageRectPixels.width() - boundRectWidthPixels;
311 break;
312 }
313
314 //calculate top crop
315 switch ( mPictureAnchor )
316 {
317 case UpperLeft:
318 case UpperMiddle:
319 case UpperRight:
320 topClip = 0;
321 break;
322 case MiddleLeft:
323 case Middle:
324 case MiddleRight:
325 topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2;
326 break;
327 case LowerLeft:
328 case LowerMiddle:
329 case LowerRight:
330 topClip = imageRectPixels.height() - boundRectHeightPixels;
331 break;
332 }
333
334 return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels );
335}
336
338{
339 const QgsExpressionContext scopedContext = createExpressionContext();
340 const QgsExpressionContext *evalContext = context ? context : &scopedContext;
341
342 mDataDefinedProperties.prepare( *evalContext );
343
344 QVariant source( mSourcePath );
345
346 //data defined source set?
347 mHasExpressionError = false;
349 {
350 mMode = FormatUnknown;
351 bool ok = false;
353 source = sourceProperty.value( *evalContext, source, &ok );
354 if ( !ok || !source.canConvert( QMetaType::QString ) )
355 {
356 mHasExpressionError = true;
357 source = QString();
358 if ( scopedContext.feature().isValid() )
359 {
360 QgsMessageLog::logMessage( QStringLiteral( "%1: %2" ).arg( tr( "Picture expression eval error" ), sourceProperty.asExpression() ) );
361 }
362 }
363 else if ( source.type() != QVariant::ByteArray )
364 {
365 source = source.toString().trimmed();
366 QgsDebugMsgLevel( QStringLiteral( "exprVal PictureSource:%1" ).arg( source.toString() ), 2 );
367 }
368 }
369
370 loadPicture( source );
371}
372
373void QgsLayoutItemPicture::loadRemotePicture( const QString &url )
374{
375 //remote location
376
378 QEventLoop loop;
379 connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
380 fetcher.fetchContent( QUrl( url ) );
381
382 //wait until picture fetched
383 loop.exec( QEventLoop::ExcludeUserInputEvents );
384
385 QNetworkReply *reply = fetcher.reply();
386 if ( reply )
387 {
388 QImageReader imageReader( reply );
389 imageReader.setAutoTransform( true );
390
391 if ( imageReader.format() == "pdf" )
392 {
393 // special handling for this format -- we need to pass the desired target size onto the image reader
394 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
395 // a very low resolution image (the driver assumes points == pixels!)
396 // For other image formats, we read the original image size only and defer resampling to later in this
397 // function. That gives us more control over the resampling method used.
398
399 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
400 const QSize sizeAt72Dpi = imageReader.size();
401 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
402 imageReader.setScaledSize( sizeAtTargetDpi );
403 }
404
405 mImage = imageReader.read();
406 mMode = FormatRaster;
407 }
408 else
409 {
410 mMode = FormatUnknown;
411 }
412}
413
414void QgsLayoutItemPicture::loadLocalPicture( const QString &path )
415{
416 QFile pic;
417 pic.setFileName( path );
418
419 if ( !pic.exists() )
420 {
421 mMode = FormatUnknown;
422 }
423 else
424 {
425 const QFileInfo sourceFileInfo( pic );
426 const QString sourceFileSuffix = sourceFileInfo.suffix();
427 if ( sourceFileSuffix.compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 )
428 {
429 //try to open svg
431 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
432 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
433 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
434 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
435
436 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
437 1.0, 0, false, evaluatedParameters );
438 mSVG.load( svgContent );
439 if ( mSVG.isValid() )
440 {
441 mMode = FormatSVG;
442 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
443 mDefaultSvgSize.setWidth( viewBox.width() );
444 mDefaultSvgSize.setHeight( viewBox.height() );
445 }
446 else
447 {
448 mMode = FormatUnknown;
449 }
450 }
451 else
452 {
453 //try to open raster with QImageReader
454 QImageReader imageReader( pic.fileName() );
455 imageReader.setAutoTransform( true );
456
457 if ( imageReader.format() == "pdf" )
458 {
459 // special handling for this format -- we need to pass the desired target size onto the image reader
460 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
461 // a very low resolution image (the driver assumes points == pixels!)
462 // For other image formats, we read the original image size only and defer resampling to later in this
463 // function. That gives us more control over the resampling method used.
464
465 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
466 const QSize sizeAt72Dpi = imageReader.size();
467 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
468 imageReader.setScaledSize( sizeAtTargetDpi );
469 }
470
471 if ( imageReader.read( &mImage ) )
472 {
473 mMode = FormatRaster;
474 }
475 else
476 {
477 mMode = FormatUnknown;
478 }
479 }
480 }
481}
482
483void QgsLayoutItemPicture::loadPictureUsingCache( const QString &path )
484{
485 if ( path.isEmpty() )
486 {
487 mImage = QImage();
488 mSVG.load( QByteArray() );
489 return;
490 }
491
492 switch ( mMode )
493 {
494 case FormatUnknown:
495 break;
496
497 case FormatRaster:
498 {
499 bool fitsInCache = false;
500 bool isMissing = false;
501 mImage = QgsApplication::imageCache()->pathAsImage( path, QSize(), true, 1, fitsInCache, true, mLayout->renderContext().dpi(), -1, &isMissing );
502 if ( mImage.isNull() || isMissing )
503 mMode = FormatUnknown;
504 break;
505 }
506
507 case FormatSVG:
508 {
510 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
511 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
512 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
513
514 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
515
516 bool isMissingImage = false;
517 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
518 1.0, 0, false, evaluatedParameters, &isMissingImage );
519 mSVG.load( svgContent );
520 if ( mSVG.isValid() && !isMissingImage )
521 {
522 mMode = FormatSVG;
523 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
524 mDefaultSvgSize.setWidth( viewBox.width() );
525 mDefaultSvgSize.setHeight( viewBox.height() );
526 }
527 else
528 {
529 mMode = FormatUnknown;
530 }
531 break;
532 }
533 }
534}
535
536void QgsLayoutItemPicture::updateNorthArrowRotation( double rotation )
537{
538 setPictureRotation( rotation );
539 emit pictureRotationChanged( rotation );
540}
541
542void QgsLayoutItemPicture::loadPicture( const QVariant &data )
543{
544 mIsMissingImage = false;
545 QVariant imageData( data );
546 mEvaluatedPath = data.toString();
547
548 if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) && mMode == FormatUnknown )
549 {
550 const QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
551 imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
552 }
553
554 if ( imageData.type() == QVariant::ByteArray )
555 {
556 if ( mImage.loadFromData( imageData.toByteArray() ) )
557 {
558 mMode = FormatRaster;
559 }
560 }
561 else if ( mMode == FormatUnknown && mEvaluatedPath.startsWith( QLatin1String( "http" ) ) )
562 {
563 //remote location (unsafe way, uses QEventLoop) - for old API/project compatibility only!!
564 loadRemotePicture( mEvaluatedPath );
565 }
566 else if ( mMode == FormatUnknown )
567 {
568 //local location - for old API/project compatibility only!!
569 loadLocalPicture( mEvaluatedPath );
570 }
571 else
572 {
573 loadPictureUsingCache( mEvaluatedPath );
574 }
575
576 if ( mMode != FormatUnknown ) //make sure we start with a new QImage
577 {
579 }
580 else if ( mHasExpressionError || !mEvaluatedPath.isEmpty() )
581 {
582 //trying to load an invalid file or bad expression, show cross picture
583 mIsMissingImage = true;
584 if ( mOriginalMode == FormatRaster )
585 {
586 const QString badFile( QStringLiteral( ":/images/composer/missing_image.png" ) );
587 QImageReader imageReader( badFile );
588 if ( imageReader.read( &mImage ) )
589 mMode = FormatRaster;
590 }
591 else if ( mOriginalMode == FormatSVG )
592 {
593 const QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
594 mSVG.load( badFile );
595 if ( mSVG.isValid() )
596 {
597 mMode = FormatSVG;
598 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
599 mDefaultSvgSize.setWidth( viewBox.width() );
600 mDefaultSvgSize.setHeight( viewBox.height() );
601 }
602 }
604 }
605
606 update();
607 emit changed();
608}
609
610QRectF QgsLayoutItemPicture::boundedImageRect( double deviceWidth, double deviceHeight )
611{
612 double imageToDeviceRatio;
613 if ( mImage.width() / deviceWidth > mImage.height() / deviceHeight )
614 {
615 imageToDeviceRatio = deviceWidth / mImage.width();
616 const double height = imageToDeviceRatio * mImage.height();
617 return QRectF( 0, 0, deviceWidth, height );
618 }
619 else
620 {
621 imageToDeviceRatio = deviceHeight / mImage.height();
622 const double width = imageToDeviceRatio * mImage.width();
623 return QRectF( 0, 0, width, deviceHeight );
624 }
625}
626
627QRectF QgsLayoutItemPicture::boundedSVGRect( double deviceWidth, double deviceHeight )
628{
629 double imageToSvgRatio;
630 if ( deviceWidth / mDefaultSvgSize.width() > deviceHeight / mDefaultSvgSize.height() )
631 {
632 imageToSvgRatio = deviceHeight / mDefaultSvgSize.height();
633 const double width = mDefaultSvgSize.width() * imageToSvgRatio;
634 return QRectF( 0, 0, width, deviceHeight );
635 }
636 else
637 {
638 imageToSvgRatio = deviceWidth / mDefaultSvgSize.width();
639 const double height = mDefaultSvgSize.height() * imageToSvgRatio;
640 return QRectF( 0, 0, deviceWidth, height );
641 }
642}
643
644QSizeF QgsLayoutItemPicture::pictureSize()
645{
646 if ( mMode == FormatSVG )
647 {
648 return mDefaultSvgSize;
649 }
650 else if ( mMode == FormatRaster )
651 {
652 return QSizeF( mImage.width(), mImage.height() );
653 }
654 else
655 {
656 return QSizeF( 0, 0 );
657 }
658}
659
661{
662 return mIsMissingImage;
663}
664
666{
667 return mEvaluatedPath;
668}
669
670QMap<QString, QgsProperty> QgsLayoutItemPicture::svgDynamicParameters() const
671{
672 const QVariantMap parameters = mCustomProperties.value( QStringLiteral( "svg-dynamic-parameters" ), QVariantMap() ).toMap();
673 return QgsProperty::variantMapToPropertyMap( parameters );
674}
675
676void QgsLayoutItemPicture::setSvgDynamicParameters( const QMap<QString, QgsProperty> &parameters )
677{
678 const QVariantMap variantMap = QgsProperty::propertyMapToVariantMap( parameters );
679 mCustomProperties.setValue( QStringLiteral( "svg-dynamic-parameters" ), variantMap );
681}
682
683void QgsLayoutItemPicture::shapeChanged()
684{
685 if ( mMode == FormatSVG && !mLoadingSvg )
686 {
687 mLoadingSvg = true;
689 mLoadingSvg = false;
690 }
691}
692
694{
695 const double oldRotation = mPictureRotation;
696 mPictureRotation = rotation;
697
698 if ( mResizeMode == Zoom )
699 {
700 //find largest scaling of picture with this rotation which fits in item
701 const QSizeF currentPictureSize = pictureSize();
702 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
703 mPictureWidth = rotatedImageRect.width();
704 mPictureHeight = rotatedImageRect.height();
705 update();
706 }
707 else if ( mResizeMode == ZoomResizeFrame )
708 {
709 const QSizeF currentPictureSize = pictureSize();
710 const QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
711
712 //calculate actual size of image inside frame
713 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
714
715 //rotate image rect by new rotation and get bounding box
716 QTransform tr;
717 tr.rotate( mPictureRotation );
718 QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
719
720 //keep the center in the same location
721 newRect.moveCenter( oldRect.center() );
722 attemptSetSceneRect( newRect );
723 emit changed();
724 }
725
726 emit pictureRotationChanged( mPictureRotation );
727}
728
730{
731 mNorthArrowHandler->setLinkedMap( map );
732}
733
735{
736 mResizeMode = mode;
738 || ( mode == QgsLayoutItemPicture::Zoom && !qgsDoubleNear( mPictureRotation, 0.0 ) ) )
739 {
740 //call set scene rect to force item to resize to fit picture
742 }
743 update();
744}
745
747{
748 //call set scene rect with current position/size, as this will trigger the
749 //picture item to recalculate its frame and image size
750 attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
751}
752
754{
757 || property == QgsLayoutObject::AllProperties )
758 {
760 refreshPicture( &context );
761 }
762
764}
765
766void QgsLayoutItemPicture::setPicturePath( const QString &path, Format format )
767{
768 mOriginalMode = format;
769 mMode = format;
770 mSourcePath = path;
772}
773
775{
776 return mSourcePath;
777}
778
779bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
780{
781 QString imagePath = mSourcePath;
782
783 // convert from absolute path to relative. For SVG we also need to consider system SVG paths
784 const QgsPathResolver pathResolver = context.pathResolver();
785 if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
786 imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
787 else
788 imagePath = pathResolver.writePath( imagePath );
789
790 elem.setAttribute( QStringLiteral( "file" ), imagePath );
791 elem.setAttribute( QStringLiteral( "pictureWidth" ), QString::number( mPictureWidth ) );
792 elem.setAttribute( QStringLiteral( "pictureHeight" ), QString::number( mPictureHeight ) );
793 elem.setAttribute( QStringLiteral( "resizeMode" ), QString::number( static_cast< int >( mResizeMode ) ) );
794 elem.setAttribute( QStringLiteral( "anchorPoint" ), QString::number( static_cast< int >( mPictureAnchor ) ) );
795 elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) );
796 elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) );
797 elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) );
798 elem.setAttribute( QStringLiteral( "mode" ), mOriginalMode );
799
800 //rotation
801 elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) );
802 if ( !mNorthArrowHandler->linkedMap() )
803 {
804 elem.setAttribute( QStringLiteral( "mapUuid" ), QString() );
805 }
806 else
807 {
808 elem.setAttribute( QStringLiteral( "mapUuid" ), mNorthArrowHandler->linkedMap()->uuid() );
809 }
810 elem.setAttribute( QStringLiteral( "northMode" ), mNorthArrowHandler->northMode() );
811 elem.setAttribute( QStringLiteral( "northOffset" ), mNorthArrowHandler->northOffset() );
812 return true;
813}
814
815bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
816{
817 mPictureWidth = itemElem.attribute( QStringLiteral( "pictureWidth" ), QStringLiteral( "10" ) ).toDouble();
818 mPictureHeight = itemElem.attribute( QStringLiteral( "pictureHeight" ), QStringLiteral( "10" ) ).toDouble();
819 mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
820 //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
821 mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( QStringLiteral( "anchorPoint" ), QString::number( QgsLayoutItem::Middle ) ).toInt() );
822
823 mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) );
824 mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) );
825 mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble();
826 mOriginalMode = static_cast< Format >( itemElem.attribute( QStringLiteral( "mode" ), QString::number( FormatUnknown ) ).toInt() );
827 mMode = mOriginalMode;
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 ( mOriginalMode == mode )
933 return;
934
935 mOriginalMode = mode;
936 mMode = mode;
938}
939
941{
942 if ( !mLayout || mRotationMapUuid.isEmpty() )
943 {
944 mNorthArrowHandler->setLinkedMap( nullptr );
945 }
946 else
947 {
948 mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) );
949 }
950
952}
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...
QgsFeature feature() const
Convenience function for retrieving the feature for the context, if set.
bool isValid() const
Returns the validity of this feature.
Definition: qgsfeature.cpp:219
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), FormatUnknown if given picture format is unknown.
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:44
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
Definition: qgslayoutitem.h:71
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:50
QgsLayoutRenderContext & renderContext()
Returns a reference to the layout's render context, which stores information relating to the current ...
Definition: qgslayout.cpp:365
QgsLayoutReportContext & reportContext()
Returns a reference to the layout's report context, which stores information relating to the current ...
Definition: qgslayout.cpp:375
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:230
QString asExpression() const
Returns an expression string representing the state of the property, or an empty string if the proper...
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)
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:3509
QMap< QString, QString > QgsStringMap
Definition: qgis.h:4054
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39