QGIS API Documentation 4.3.0-Master (b46232e4f9a)
Loading...
Searching...
No Matches
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
19
20#include "qgscolorutils.h"
21#include "qgsimagecache.h"
22#include "qgslayout.h"
23#include "qgslayoutitemmap.h"
28#include "qgslayoututils.h"
29#include "qgslogger.h"
30#include "qgsmessagelog.h"
32#include "qgspainting.h"
33#include "qgspathresolver.h"
34#include "qgsproperty.h"
35#include "qgsreadwritecontext.h"
36#include "qgssvgcache.h"
37#include "qgssymbollayerutils.h"
38
39#include <QCoreApplication>
40#include <QDomDocument>
41#include <QDomElement>
42#include <QEventLoop>
43#include <QFileInfo>
44#include <QImageReader>
45#include <QNetworkReply>
46#include <QNetworkRequest>
47#include <QPainter>
48#include <QString>
49#include <QSvgRenderer>
50#include <QUrl>
51
52#include "moc_qgslayoutitempicture.cpp"
53
54using namespace Qt::StringLiterals;
55
58 , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) )
59{
60 //default to no background
61 setBackgroundEnabled( false );
62
63 //connect some signals
64
65 //connect to atlas feature changing
66 //to update the picture source expression
67 connect( &layout->reportContext(), &QgsLayoutReportContext::changed, this, [this] { refreshPicture(); } );
68
69 //connect to layout print resolution changing
71
72 connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged );
73 connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemPicture::updateNorthArrowRotation );
74}
75
80
82{
83 return QgsApplication::getThemeIcon( u"/mLayoutItemPicture.svg"_s );
84}
85
90
91QPainterPath QgsLayoutItemPicture::customFramePath() const
92{
93 QPainterPath painterPath;
94 if ( mClipToItem && mClippingItem )
95 {
96 QgsGeometry g( mClippingItem->clipPath() );
97 g.transform( sceneTransform().inverted() );
98 if ( !g.isNull() )
99 {
100 painterPath = g.constGet()->asQPainterPath();
101 }
102 }
103 return painterPath;
104}
105
107{
108 QPainterPath custumPainterPath = customFramePath();
109 return !custumPainterPath.isEmpty() ? custumPainterPath : QgsLayoutItem::framePath();
110}
111
113{
114 QPainter *painter = context.renderContext().painter();
115
116 const QgsScopedQPainterState painterState( painter );
117 // painter is scaled to dots, so scale back to layout units
118 painter->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
119
120 const QPainterPath custumPainterPath = customFramePath();
121 if ( !custumPainterPath.isEmpty() )
122 {
123 painter->setClipPath( custumPainterPath );
124 }
125
126 const bool prevSmoothTransform = painter->testRenderHint( QPainter::RenderHint::SmoothPixmapTransform );
127 if ( mLayout->renderContext().testFlag( Qgis::LayoutRenderFlag::Antialiasing ) )
128 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, true );
129
130 //picture resizing
131 if ( mMode != Qgis::PictureFormat::Unknown )
132 {
133 double boundRectWidthMM;
134 double boundRectHeightMM;
135 QRect imageRect;
136 if ( mResizeMode == QgsLayoutItemPicture::Zoom || mResizeMode == QgsLayoutItemPicture::ZoomResizeFrame )
137 {
138 boundRectWidthMM = mPictureWidth;
139 boundRectHeightMM = mPictureHeight;
140 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
141 }
142 else if ( mResizeMode == QgsLayoutItemPicture::Stretch )
143 {
144 boundRectWidthMM = rect().width();
145 boundRectHeightMM = rect().height();
146 imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
147 }
148 else if ( mResizeMode == QgsLayoutItemPicture::Clip )
149 {
150 boundRectWidthMM = rect().width();
151 boundRectHeightMM = rect().height();
152 const int imageRectWidthPixels = mImage.width();
153 const int imageRectHeightPixels = mImage.height();
154 imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM, QSize( imageRectWidthPixels, imageRectHeightPixels ) );
155 }
156 else
157 {
158 boundRectWidthMM = rect().width();
159 boundRectHeightMM = rect().height();
160 imageRect = QRect(
161 0,
162 0,
163 mLayout->convertFromLayoutUnits( rect().width(), Qgis::LayoutUnit::Millimeters ).length() * mLayout->renderContext().dpi() / 25.4,
164 mLayout->convertFromLayoutUnits( rect().height(), Qgis::LayoutUnit::Millimeters ).length() * mLayout->renderContext().dpi() / 25.4
165 );
166 }
167
168 //zoom mode - calculate anchor point and rotation
169 if ( mResizeMode == Zoom )
170 {
171 //TODO - allow placement modes with rotation set. for now, setting a rotation
172 //always places picture in center of frame
173 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
174 {
175 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
176 painter->rotate( mPictureRotation );
177 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
178 }
179 else
180 {
181 //shift painter to edge/middle of frame depending on placement
182 const double diffX = rect().width() - boundRectWidthMM;
183 const double diffY = rect().height() - boundRectHeightMM;
184
185 double dX = 0;
186 double dY = 0;
187 switch ( mPictureAnchor )
188 {
189 case UpperLeft:
190 case MiddleLeft:
191 case LowerLeft:
192 //nothing to do
193 break;
194 case UpperMiddle:
195 case Middle:
196 case LowerMiddle:
197 dX = diffX / 2.0;
198 break;
199 case UpperRight:
200 case MiddleRight:
201 case LowerRight:
202 dX = diffX;
203 break;
204 }
205 switch ( mPictureAnchor )
206 {
207 case UpperLeft:
208 case UpperMiddle:
209 case UpperRight:
210 //nothing to do
211 break;
212 case MiddleLeft:
213 case Middle:
214 case MiddleRight:
215 dY = diffY / 2.0;
216 break;
217 case LowerLeft:
218 case LowerMiddle:
219 case LowerRight:
220 dY = diffY;
221 break;
222 }
223 painter->translate( dX, dY );
224 }
225 }
226 else if ( mResizeMode == ZoomResizeFrame )
227 {
228 if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
229 {
230 painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
231 painter->rotate( mPictureRotation );
232 painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
233 }
234 }
235
236 if ( mMode == Qgis::PictureFormat::SVG )
237 {
238 mSVG.render( painter, QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ) );
239 }
240 else if ( mMode == Qgis::PictureFormat::Raster )
241 {
242 painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect );
243 }
244 }
245 painter->setRenderHint( QPainter::RenderHint::SmoothPixmapTransform, prevSmoothTransform );
246}
247
248QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF targetSize )
249{
250 const QSizeF currentPictureSize = pictureSize();
251 QSizeF newSize = targetSize;
252 if ( mResizeMode == QgsLayoutItemPicture::Clip )
253 {
254 mPictureWidth = targetSize.width();
255 mPictureHeight = targetSize.height();
256 }
257 else
258 {
259 if ( mResizeMode == ZoomResizeFrame && !rect().isEmpty() && !( currentPictureSize.isEmpty() ) )
260 {
261 QSizeF targetImageSize;
262 if ( qgsDoubleNear( mPictureRotation, 0.0 ) )
263 {
264 targetImageSize = currentPictureSize;
265 }
266 else
267 {
268 //calculate aspect ratio of bounds of rotated image
269 QTransform tr;
270 tr.rotate( mPictureRotation );
271 const QRectF rotatedBounds = tr.mapRect( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ) );
272 targetImageSize = QSizeF( rotatedBounds.width(), rotatedBounds.height() );
273 }
274
275 //if height has changed more than width, then fix width and set height correspondingly
276 //else, do the opposite
277 if ( std::fabs( rect().width() - targetSize.width() ) < std::fabs( rect().height() - targetSize.height() ) )
278 {
279 newSize.setHeight( targetImageSize.height() * newSize.width() / targetImageSize.width() );
280 }
281 else
282 {
283 newSize.setWidth( targetImageSize.width() * newSize.height() / targetImageSize.height() );
284 }
285 }
286 else if ( mResizeMode == FrameToImageSize )
287 {
288 if ( !( currentPictureSize.isEmpty() ) )
289 {
290 const QgsLayoutSize sizeMM = mLayout->convertFromLayoutUnits( currentPictureSize, Qgis::LayoutUnit::Millimeters );
291 newSize.setWidth( sizeMM.width() * 25.4 / mLayout->renderContext().dpi() );
292 newSize.setHeight( sizeMM.height() * 25.4 / mLayout->renderContext().dpi() );
293 }
294 }
295
296 //find largest scaling of picture with this rotation which fits in item
297 if ( mResizeMode == Zoom || mResizeMode == ZoomResizeFrame )
298 {
299 const QRectF rotatedImageRect
300 = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), QRectF( 0, 0, newSize.width(), newSize.height() ), mPictureRotation );
301 mPictureWidth = rotatedImageRect.width();
302 mPictureHeight = rotatedImageRect.height();
303 }
304 else
305 {
306 mPictureWidth = newSize.width();
307 mPictureHeight = newSize.height();
308 }
309
310 if ( newSize != targetSize )
311 {
312 emit changed();
313 }
314 }
315
316 return newSize;
317}
318
319QRect QgsLayoutItemPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels )
320{
321 const int boundRectWidthPixels = boundRectWidthMM * mLayout->renderContext().dpi() / 25.4;
322 const int boundRectHeightPixels = boundRectHeightMM * mLayout->renderContext().dpi() / 25.4;
323
324 //update boundRectWidth/Height so that they exactly match pixel bounds
325 boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->renderContext().dpi();
326 boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->renderContext().dpi();
327
328 //calculate part of image which fits in bounds
329 int leftClip = 0;
330 int topClip = 0;
331
332 //calculate left crop
333 switch ( mPictureAnchor )
334 {
335 case UpperLeft:
336 case MiddleLeft:
337 case LowerLeft:
338 leftClip = 0;
339 break;
340 case UpperMiddle:
341 case Middle:
342 case LowerMiddle:
343 leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2;
344 break;
345 case UpperRight:
346 case MiddleRight:
347 case LowerRight:
348 leftClip = imageRectPixels.width() - boundRectWidthPixels;
349 break;
350 }
351
352 //calculate top crop
353 switch ( mPictureAnchor )
354 {
355 case UpperLeft:
356 case UpperMiddle:
357 case UpperRight:
358 topClip = 0;
359 break;
360 case MiddleLeft:
361 case Middle:
362 case MiddleRight:
363 topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2;
364 break;
365 case LowerLeft:
366 case LowerMiddle:
367 case LowerRight:
368 topClip = imageRectPixels.height() - boundRectHeightPixels;
369 break;
370 }
371
372 return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels );
373}
374
376{
377 const QgsExpressionContext scopedContext = createExpressionContext();
378 const QgsExpressionContext *evalContext = context ? context : &scopedContext;
379
380 mDataDefinedProperties.prepare( *evalContext );
381
382 QVariant source( mSourcePath );
383
384 //data defined source set?
385 mHasExpressionError = false;
387 {
389 bool ok = false;
391 source = sourceProperty.value( *evalContext, source, &ok );
392 if ( !ok || !source.canConvert( QMetaType::QString ) )
393 {
394 mHasExpressionError = true;
395 source = QString();
396 if ( scopedContext.feature().isValid() )
397 {
398 QgsMessageLog::logMessage( u"%1: %2"_s.arg( tr( "Picture expression eval error" ), sourceProperty.asExpression() ) );
399 }
400 }
401 else if ( source.userType() != QMetaType::Type::QByteArray )
402 {
403 source = source.toString().trimmed();
404 QgsDebugMsgLevel( u"exprVal PictureSource:%1"_s.arg( source.toString() ), 2 );
405 }
406 }
407
408 loadPicture( source );
409}
410
411void QgsLayoutItemPicture::loadRemotePicture( const QString &url )
412{
413 //remote location
414
416 QEventLoop loop;
417 connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
418 fetcher.fetchContent( QUrl( url ) );
419
420 //wait until picture fetched
421 loop.exec( QEventLoop::ExcludeUserInputEvents );
422
423 QNetworkReply *reply = fetcher.reply();
424 if ( reply )
425 {
426 QImageReader imageReader( reply );
427 imageReader.setAutoTransform( true );
428
429 if ( imageReader.format() == "pdf" )
430 {
431 // special handling for this format -- we need to pass the desired target size onto the image reader
432 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
433 // a very low resolution image (the driver assumes points == pixels!)
434 // For other image formats, we read the original image size only and defer resampling to later in this
435 // function. That gives us more control over the resampling method used.
436
437 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
438 const QSize sizeAt72Dpi = imageReader.size();
439 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
440 imageReader.setScaledSize( sizeAtTargetDpi );
441 }
442
443 mImage = imageReader.read();
445 }
446 else
447 {
449 }
450}
451
452void QgsLayoutItemPicture::loadLocalPicture( const QString &path )
453{
454 QFile pic;
455 pic.setFileName( path );
456
457 if ( !pic.exists() )
458 {
460 }
461 else
462 {
463 const QFileInfo sourceFileInfo( pic );
464 const QString sourceFileSuffix = sourceFileInfo.suffix();
465 if ( sourceFileSuffix.compare( "svg"_L1, Qt::CaseInsensitive ) == 0 )
466 {
467 //try to open svg
468 const QgsExpressionContext context = createExpressionContext();
469 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::DataDefinedProperty::PictureSvgBackgroundColor, context, mSvgFillColor );
470 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::DataDefinedProperty::PictureSvgStrokeColor, context, mSvgStrokeColor );
471 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
472 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
473
474 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth, 1.0, 0, false, evaluatedParameters );
475 mSVG.load( svgContent );
476 if ( mSVG.isValid() )
477 {
479 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
480 mDefaultSvgSize.setWidth( viewBox.width() );
481 mDefaultSvgSize.setHeight( viewBox.height() );
482 }
483 else
484 {
486 }
487 }
488 else
489 {
490 //try to open raster with QImageReader
491 QImageReader imageReader( pic.fileName() );
492 imageReader.setAutoTransform( true );
493
494 if ( imageReader.format() == "pdf" )
495 {
496 // special handling for this format -- we need to pass the desired target size onto the image reader
497 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
498 // a very low resolution image (the driver assumes points == pixels!)
499 // For other image formats, we read the original image size only and defer resampling to later in this
500 // function. That gives us more control over the resampling method used.
501
502 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
503 const QSize sizeAt72Dpi = imageReader.size();
504 const QSize sizeAtTargetDpi = sizeAt72Dpi * mLayout->renderContext().dpi() / 72;
505 imageReader.setScaledSize( sizeAtTargetDpi );
506 }
507
508 if ( imageReader.read( &mImage ) )
509 {
511 }
512 else
513 {
515 }
516 }
517 }
518}
519
520void QgsLayoutItemPicture::loadPictureUsingCache( const QString &path )
521{
522 if ( path.isEmpty() )
523 {
524 mImage = QImage();
525 mSVG.load( QByteArray() );
526 return;
527 }
528
529 switch ( mMode )
530 {
532 break;
533
535 {
536 bool fitsInCache = false;
537 bool isMissing = false;
538 mImage = QgsApplication::imageCache()->pathAsImage( path, QSize(), true, 1, fitsInCache, true, mLayout->renderContext().dpi(), -1, &isMissing );
539 if ( mImage.isNull() || isMissing )
541 break;
542 }
543
545 {
546 const QgsExpressionContext context = createExpressionContext();
547 const QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::DataDefinedProperty::PictureSvgBackgroundColor, context, mSvgFillColor );
548 const QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::DataDefinedProperty::PictureSvgStrokeColor, context, mSvgStrokeColor );
549 const double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
550
551 const QgsStringMap evaluatedParameters = QgsSymbolLayerUtils::evaluatePropertiesMap( svgDynamicParameters(), context );
552
553 bool isMissingImage = false;
554 const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth, 1.0, 0, false, evaluatedParameters, &isMissingImage );
555 mSVG.load( svgContent );
556 if ( mSVG.isValid() && !isMissingImage )
557 {
559 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
560 mDefaultSvgSize.setWidth( viewBox.width() );
561 mDefaultSvgSize.setHeight( viewBox.height() );
562 }
563 else
564 {
566 }
567 break;
568 }
569 }
570}
571
572void QgsLayoutItemPicture::updateNorthArrowRotation( double rotation )
573{
574 setPictureRotation( rotation );
575 emit pictureRotationChanged( rotation );
576}
577
578void QgsLayoutItemPicture::loadPicture( const QVariant &data )
579{
580 mIsMissingImage = false;
581 QVariant imageData( data );
582 mEvaluatedPath = data.toString();
583
584 if ( mEvaluatedPath.startsWith( "base64:"_L1, Qt::CaseInsensitive ) && mMode == Qgis::PictureFormat::Unknown )
585 {
586 const QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
587 imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
588 }
589
590 if ( imageData.userType() == QMetaType::Type::QByteArray )
591 {
592 if ( mImage.loadFromData( imageData.toByteArray() ) )
593 {
595 }
596 }
597 else if ( mMode == Qgis::PictureFormat::Unknown && mEvaluatedPath.startsWith( "http"_L1 ) )
598 {
599 //remote location (unsafe way, uses QEventLoop) - for old API/project compatibility only!!
600 loadRemotePicture( mEvaluatedPath );
601 }
602 else if ( mMode == Qgis::PictureFormat::Unknown )
603 {
604 //local location - for old API/project compatibility only!!
605 loadLocalPicture( mEvaluatedPath );
606 }
607 else
608 {
609 loadPictureUsingCache( mEvaluatedPath );
610 }
611
612 mLoaded = false;
613 if ( mMode != Qgis::PictureFormat::Unknown ) //make sure we start with a new QImage
614 {
616 mLoaded = true;
617 }
618 else if ( mHasExpressionError || !mEvaluatedPath.isEmpty() )
619 {
620 //trying to load an invalid file or bad expression, show cross picture
621 mIsMissingImage = true;
622 if ( mOriginalMode == Qgis::PictureFormat::Raster )
623 {
624 const QString badFile( u":/images/composer/missing_image.png"_s );
625 QImageReader imageReader( badFile );
626 if ( imageReader.read( &mImage ) )
628 }
629 else if ( mOriginalMode == Qgis::PictureFormat::SVG )
630 {
631 const QString badFile( u":/images/composer/missing_image.svg"_s );
632 mSVG.load( badFile );
633 if ( mSVG.isValid() )
634 {
636 const QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
637 mDefaultSvgSize.setWidth( viewBox.width() );
638 mDefaultSvgSize.setHeight( viewBox.height() );
639 }
640 }
642 }
643
644 update();
645 emit changed();
646}
647
648QSizeF QgsLayoutItemPicture::pictureSize()
649{
650 if ( mMode == Qgis::PictureFormat::SVG )
651 {
652 return mDefaultSvgSize;
653 }
654 else if ( mMode == Qgis::PictureFormat::Raster )
655 {
656 return QSizeF( mImage.width(), mImage.height() );
657 }
658 else
659 {
660 return QSizeF( 0, 0 );
661 }
662}
663
665{
666 return mIsMissingImage;
667}
668
670{
671 return mEvaluatedPath;
672}
673
674QMap<QString, QgsProperty> QgsLayoutItemPicture::svgDynamicParameters() const
675{
676 const QVariantMap parameters = mCustomProperties.value( u"svg-dynamic-parameters"_s, QVariantMap() ).toMap();
677 return QgsProperty::variantMapToPropertyMap( parameters );
678}
679
680void QgsLayoutItemPicture::setSvgDynamicParameters( const QMap<QString, QgsProperty> &parameters )
681{
682 const QVariantMap variantMap = QgsProperty::propertyMapToVariantMap( parameters );
683 mCustomProperties.setValue( u"svg-dynamic-parameters"_s, variantMap );
685}
686
687void QgsLayoutItemPicture::shapeChanged()
688{
689 if ( mMode == Qgis::PictureFormat::SVG && !mLoadingSvg )
690 {
691 mLoadingSvg = true;
693 mLoadingSvg = false;
694 }
695}
696
698{
699 const double oldRotation = mPictureRotation;
700 mPictureRotation = rotation;
701 const QSizeF currentPictureSize = pictureSize();
702
703 // If the picture is not loaded yet, do not compute its rotated size
704 if ( !mLoaded || currentPictureSize == QSizeF( 0, 0 ) )
705 return;
706
707 if ( mResizeMode == Zoom )
708 {
709 //find largest scaling of picture with this rotation which fits in item
710 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
711 mPictureWidth = rotatedImageRect.width();
712 mPictureHeight = rotatedImageRect.height();
713 update();
714 }
715 else if ( mResizeMode == ZoomResizeFrame )
716 {
717 const QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
718
719 //calculate actual size of image inside frame
720 const QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
721
722 //rotate image rect by new rotation and get bounding box
723 QTransform tr;
724 tr.rotate( mPictureRotation );
725 QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
726
727 //keep the center in the same location
728 newRect.moveCenter( oldRect.center() );
729 attemptSetSceneRect( newRect );
730 emit changed();
731 }
732
733 emit pictureRotationChanged( mPictureRotation );
734}
735
737{
738 mNorthArrowHandler->setLinkedMap( map );
739}
740
742{
743 mResizeMode = mode;
745 {
746 //call set scene rect to force item to resize to fit picture
748 }
749 update();
750}
751
753{
754 //call set scene rect with current position/size, as this will trigger the
755 //picture item to recalculate its frame and image size
756 attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
757}
758
773
775{
776 mOriginalMode = format;
777 mMode = format;
778 mSourcePath = path;
780}
781
783{
784 return mSourcePath;
785}
786
787bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
788{
789 QString imagePath = mSourcePath;
790
791 // convert from absolute path to relative. For SVG we also need to consider system SVG paths
792 const QgsPathResolver pathResolver = context.pathResolver();
793 if ( imagePath.endsWith( ".svg"_L1, Qt::CaseInsensitive ) )
794 imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
795 else
796 imagePath = pathResolver.writePath( imagePath );
797
798 elem.setAttribute( u"file"_s, imagePath );
799 elem.setAttribute( u"pictureWidth"_s, QString::number( mPictureWidth ) );
800 elem.setAttribute( u"pictureHeight"_s, QString::number( mPictureHeight ) );
801 elem.setAttribute( u"resizeMode"_s, QString::number( static_cast< int >( mResizeMode ) ) );
802 elem.setAttribute( u"anchorPoint"_s, QString::number( static_cast< int >( mPictureAnchor ) ) );
803 elem.setAttribute( u"svgFillColor"_s, QgsColorUtils::colorToString( mSvgFillColor ) );
804 elem.setAttribute( u"svgBorderColor"_s, QgsColorUtils::colorToString( mSvgStrokeColor ) );
805 elem.setAttribute( u"svgBorderWidth"_s, QString::number( mSvgStrokeWidth ) );
806 elem.setAttribute( u"mode"_s, static_cast< int >( mOriginalMode ) );
807
808 //rotation
809 elem.setAttribute( u"pictureRotation"_s, QString::number( mPictureRotation ) );
810 if ( !mNorthArrowHandler->linkedMap() )
811 {
812 elem.setAttribute( u"mapUuid"_s, QString() );
813 }
814 else
815 {
816 elem.setAttribute( u"mapUuid"_s, mNorthArrowHandler->linkedMap()->uuid() );
817 }
818 elem.setAttribute( u"northMode"_s, mNorthArrowHandler->northMode() );
819 elem.setAttribute( u"northOffset"_s, mNorthArrowHandler->northOffset() );
820
821 //clipping
822 if ( mClipToItem )
823 {
824 elem.setAttribute( u"clipToItem"_s, u"1"_s );
825 }
826 if ( mClippingItem )
827 {
828 elem.setAttribute( u"clippingItem"_s, mClippingItem->uuid() );
829 }
830
831 return true;
832}
833
834bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
835{
836 mPictureWidth = itemElem.attribute( u"pictureWidth"_s, u"10"_s ).toDouble();
837 mPictureHeight = itemElem.attribute( u"pictureHeight"_s, u"10"_s ).toDouble();
838 mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( u"resizeMode"_s, u"0"_s ).toInt() );
839 //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
840 mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( u"anchorPoint"_s, QString::number( QgsLayoutItem::Middle ) ).toInt() );
841
842 mSvgFillColor = QgsColorUtils::colorFromString( itemElem.attribute( u"svgFillColor"_s, QgsColorUtils::colorToString( QColor( 255, 255, 255 ) ) ) );
843 mSvgStrokeColor = QgsColorUtils::colorFromString( itemElem.attribute( u"svgBorderColor"_s, QgsColorUtils::colorToString( QColor( 0, 0, 0 ) ) ) );
844 mSvgStrokeWidth = itemElem.attribute( u"svgBorderWidth"_s, u"0.2"_s ).toDouble();
845 mOriginalMode = static_cast< Qgis::PictureFormat >( itemElem.attribute( u"mode"_s, QString::number( static_cast< int >( Qgis::PictureFormat::Unknown ) ) ).toInt() );
846 mMode = mOriginalMode;
847
848 const QDomNodeList composerItemList = itemElem.elementsByTagName( u"ComposerItem"_s );
849 if ( !composerItemList.isEmpty() )
850 {
851 const QDomElement composerItemElem = composerItemList.at( 0 ).toElement();
852
853 if ( !qgsDoubleNear( composerItemElem.attribute( u"rotation"_s, u"0"_s ).toDouble(), 0.0 ) )
854 {
855 //in versions prior to 2.1 picture rotation was stored in the rotation attribute
856 mPictureRotation = composerItemElem.attribute( u"rotation"_s, u"0"_s ).toDouble();
857 }
858 }
859
860 mDefaultSvgSize = QSize( 0, 0 );
861
862 if ( itemElem.hasAttribute( u"sourceExpression"_s ) )
863 {
864 //update pre 2.5 picture expression to use data defined expression
865 const QString sourceExpression = itemElem.attribute( u"sourceExpression"_s, QString() );
866 const QString useExpression = itemElem.attribute( u"useExpression"_s );
867 bool expressionActive;
868 expressionActive = ( useExpression.compare( "true"_L1, Qt::CaseInsensitive ) == 0 );
869
871 }
872
873 QString imagePath = itemElem.attribute( u"file"_s );
874
875 // convert from relative path to absolute. For SVG we also need to consider system SVG paths
876 const QgsPathResolver pathResolver = context.pathResolver();
877 if ( imagePath.endsWith( ".svg"_L1, Qt::CaseInsensitive ) )
878 imagePath = QgsSymbolLayerUtils::svgSymbolNameToPath( imagePath, pathResolver );
879 else
880 imagePath = pathResolver.readPath( imagePath );
881
882 mSourcePath = imagePath;
883
884 //picture rotation
885 if ( !qgsDoubleNear( itemElem.attribute( u"pictureRotation"_s, u"0"_s ).toDouble(), 0.0 ) )
886 {
887 mPictureRotation = itemElem.attribute( u"pictureRotation"_s, u"0"_s ).toDouble();
888 }
889
890 //rotation map
891 mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( itemElem.attribute( u"northMode"_s, u"0"_s ).toInt() ) );
892 mNorthArrowHandler->setNorthOffset( itemElem.attribute( u"northOffset"_s, u"0"_s ).toDouble() );
893
894 mNorthArrowHandler->setLinkedMap( nullptr );
895 mRotationMapUuid = itemElem.attribute( u"mapUuid"_s );
896
897 mClipToItem = itemElem.attribute( u"clipToItem"_s, u"0"_s ).toInt();
898 mClippingItemUuid = itemElem.attribute( u"clippingItem"_s );
899
900 return true;
901}
902
904{
905 return mNorthArrowHandler->linkedMap();
906}
907
909{
910 return static_cast< QgsLayoutItemPicture::NorthMode >( mNorthArrowHandler->northMode() );
911}
912
914{
915 mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( mode ) );
916}
917
919{
920 return mNorthArrowHandler->northOffset();
921}
922
924{
925 mNorthArrowHandler->setNorthOffset( offset );
926}
927
929{
930 mPictureAnchor = anchor;
931 update();
932}
933
934void QgsLayoutItemPicture::setSvgFillColor( const QColor &color )
935{
936 mSvgFillColor = color;
938}
939
941{
942 mSvgStrokeColor = color;
944}
945
947{
948 mSvgStrokeWidth = width;
950}
951
953{
954 if ( mOriginalMode == mode )
955 return;
956
957 mOriginalMode = mode;
958 mMode = mode;
960}
961
963{
964 if ( !mLayout || mRotationMapUuid.isEmpty() )
965 {
966 mNorthArrowHandler->setLinkedMap( nullptr );
967 }
968 else
969 {
970 mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) );
971 }
972
973 if ( !mClippingItemUuid.isEmpty() )
974 {
975 if ( QgsLayoutItem *item = mLayout->itemByUuid( mClippingItemUuid, true ) )
976 {
977 setClippingItem( item );
978 }
979 }
980
982}
983
985{
986 return mClipToItem;
987}
988
990{
991 if ( mClipToItem == clipToItem )
992 return;
993
994 mClipToItem = clipToItem;
995
997}
998
1000{
1001 return mClippingItem;
1002}
1003
1005{
1006 if ( mClippingItem == item )
1007 return;
1008
1009 if ( mClippingItem )
1010 {
1011 disconnect( mClippingItem, &QgsLayoutItem::clipPathChanged, this, &QgsLayoutItemPicture::refresh );
1012 disconnect( mClippingItem, &QgsLayoutItem::rotationChanged, this, &QgsLayoutItemPicture::refresh );
1013 }
1014
1015 QgsLayoutItem *oldItem = mClippingItem;
1016 mClippingItem = item;
1017
1018 if ( mClippingItem )
1019 {
1020 connect( mClippingItem, &QgsLayoutItem::clipPathChanged, this, &QgsLayoutItemPicture::refresh );
1021 connect( mClippingItem, &QgsLayoutItem::rotationChanged, this, &QgsLayoutItemPicture::refresh );
1022 // trigger a redraw of the clipping item, so that it becomes invisible
1023 mClippingItem->refresh();
1024 }
1025
1026 if ( oldItem )
1027 {
1028 // may need to refresh the previous item in order to get it to render
1029 oldItem->refresh();
1030 }
1031
1032 refresh();
1033}
@ Millimeters
Millimeters.
Definition qgis.h:5779
PictureFormat
Picture formats.
Definition qgis.h:5852
@ Raster
Raster image.
Definition qgis.h:5854
@ Unknown
Invalid or unknown image type.
Definition qgis.h:5855
@ SVG
SVG image.
Definition qgis.h:5853
@ Antialiasing
Use antialiasing when drawing items.
Definition qgis.h:5815
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...
static QColor colorFromString(const QString &string)
Decodes a string into a color value.
static QString colorToString(const QColor &color)
Encodes a color into a string value.
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.
A geometry is the spatial representation of a feature.
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.
void setResizeMode(QgsLayoutItemPicture::ResizeMode mode)
Sets the resize mode used for drawing the picture within the item bounds.
void setMode(Qgis::PictureFormat mode)
Sets the current picture mode (image format).
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 setPicturePath(const QString &path, Qgis::PictureFormat format=Qgis::PictureFormat::Unknown)
Sets the source path of the image (may be svg or a raster format).
QgsLayoutItem * clippingItem() const
Returns the item that will will provide the clipping path for the picture, or nullptr if no item is s...
void setPictureRotation(double rotation)
Sets the picture rotation within the item bounds, in degrees clockwise.
bool clipToItem() const
Returns true if the picture item should by clipped to the associated item.
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.
QPainterPath framePath() const override
Returns the path to use when drawing the item's frame or background.
bool readPropertiesFromElement(const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context) override
Sets item state from a DOM element.
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.
Qgis::PictureFormat mode() const
Returns the current picture mode (image format), FormatUnknown if given picture format is unknown.
NorthMode
Method for syncing rotation to a map's North direction.
void finalizeRestoreFromXml() override
Called after all pending items have been restored from XML.
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 setClipToItem(bool clipToItem)
Sets whether the picture item should by clipped to the associated item.
void setSvgStrokeColor(const QColor &color)
Sets the stroke color used for parametrized SVG files.
int type() const override
void refreshDataDefinedProperty(QgsLayoutObject::DataDefinedProperty property=QgsLayoutObject::DataDefinedProperty::AllProperties) override
void setClippingItem(QgsLayoutItem *item)
Sets the item that will will provide the clipping path for the picture.
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...
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.
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.
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
friend class QgsLayout
virtual QPainterPath framePath() const
Returns the path to use when drawing the item's frame or background.
virtual void refreshDataDefinedProperty(QgsLayoutObject::DataDefinedProperty property=QgsLayoutObject::DataDefinedProperty::AllProperties)
Refreshes a data defined property for the item by reevaluating the property's value and redrawing the...
QgsLayoutItem(QgsLayout *layout, bool manageZValue=true)
Constructor for QgsLayoutItem, with the specified parent layout.
void rotationChanged(double newRotation)
Emitted on item rotation change.
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.
friend class QgsLayoutItemMap
QgsExpressionContext createExpressionContext() const override
This method needs to be reimplemented in all classes which implement this interface and return an exp...
void sizePositionChanged()
Emitted when the item's size or position changes.
void clipPathChanged()
Emitted when the item's clipping path has changed.
void refresh() override
Refreshes the item, causing a recalculation of any property overrides and recalculation of its positi...
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 arrowRotationChanged(double newRotation)
Emitted on arrow rotation change.
NorthMode
Method for syncing rotation to a map's North direction.
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.
@ PictureSvgBackgroundColor
SVG background color.
@ AllProperties
All properties for item.
void dpiChanged()
Emitted when the context's DPI is changed.
void changed()
Emitted certain settings in the context is changed, e.g.
Provides a method of storing sizes, consisting of a width and height, for use in QGIS layouts.
double height() const
Returns the height of the size.
double width() const
Returns the width of the size.
static QRectF largestRotatedRectWithinBounds(const QRectF &originalRect, const QRectF &boundsRect, double rotation)
Calculates the largest scaled version of originalRect which fits within boundsRect,...
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
HTTP network content fetcher.
void fetchContent(const QUrl &url, const QString &authcfg=QString(), const QgsHttpHeaders &headers=QgsHttpHeaders())
Fetches content from a remote URL and handles redirects.
void finished()
Emitted when content has loaded.
QNetworkReply * reply()
Returns a reference to the network reply.
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.
A store for object properties.
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.
A container for the context for various read/write operations on 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 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.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7725
QMap< QString, QString > QgsStringMap
Definition qgis.h:8332
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80