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