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