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