QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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  QString source = mSourcePath;
340 
341  //data defined source set?
342  mHasExpressionError = false;
344  {
345  bool ok = false;
346  source = mDataDefinedProperties.valueAsString( QgsLayoutObject::PictureSource, *evalContext, source, &ok );
347  if ( ok )
348  {
349  source = source.trimmed();
350  QgsDebugMsg( QStringLiteral( "exprVal PictureSource:%1" ).arg( source ) );
351  }
352  else
353  {
354  mHasExpressionError = true;
355  source = QString();
356  QgsMessageLog::logMessage( tr( "Picture expression eval error" ) );
357  }
358  }
359 
360  loadPicture( source );
361 }
362 
363 void QgsLayoutItemPicture::loadRemotePicture( const QString &url )
364 {
365  //remote location
366 
367  QgsNetworkContentFetcher fetcher;
368  QEventLoop loop;
369  connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
370  fetcher.fetchContent( QUrl( url ) );
371 
372  //wait until picture fetched
373  loop.exec( QEventLoop::ExcludeUserInputEvents );
374 
375  QNetworkReply *reply = fetcher.reply();
376  if ( reply )
377  {
378  QImageReader imageReader( reply );
379  mImage = imageReader.read();
380  mMode = FormatRaster;
381  }
382  else
383  {
384  mMode = FormatUnknown;
385  }
386 }
387 
388 void QgsLayoutItemPicture::loadLocalPicture( const QString &path )
389 {
390  QFile pic;
391  pic.setFileName( path );
392 
393  if ( !pic.exists() )
394  {
395  mMode = FormatUnknown;
396  }
397  else
398  {
399  QFileInfo sourceFileInfo( pic );
400  QString sourceFileSuffix = sourceFileInfo.suffix();
401  if ( sourceFileSuffix.compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 )
402  {
403  //try to open svg
405  QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
406  QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
407  double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
408  const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
409  1.0 );
410  mSVG.load( svgContent );
411  if ( mSVG.isValid() )
412  {
413  mMode = FormatSVG;
414  QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
415  mDefaultSvgSize.setWidth( viewBox.width() );
416  mDefaultSvgSize.setHeight( viewBox.height() );
417  }
418  else
419  {
420  mMode = FormatUnknown;
421  }
422  }
423  else
424  {
425  //try to open raster with QImageReader
426  QImageReader imageReader( pic.fileName() );
427  if ( imageReader.read( &mImage ) )
428  {
429  mMode = FormatRaster;
430  }
431  else
432  {
433  mMode = FormatUnknown;
434  }
435  }
436  }
437 }
438 
439 void QgsLayoutItemPicture::disconnectMap( QgsLayoutItemMap *map )
440 {
441  if ( map )
442  {
443  disconnect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
444  disconnect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
445  }
446 }
447 
448 void QgsLayoutItemPicture::updateMapRotation()
449 {
450  if ( !mRotationMap )
451  return;
452 
453  // take map rotation
454  double rotation = mRotationMap->mapRotation();
455 
456  // handle true north
457  switch ( mNorthMode )
458  {
459  case GridNorth:
460  break; // nothing to do
461 
462  case TrueNorth:
463  {
464  QgsPointXY center = mRotationMap->extent().center();
465  QgsCoordinateReferenceSystem crs = mRotationMap->crs();
466  QgsCoordinateTransformContext transformContext = mLayout->project()->transformContext();
467 
468  try
469  {
470  double bearing = QgsBearingUtils::bearingTrueNorth( crs, transformContext, center );
471  rotation += bearing;
472  }
473  catch ( QgsException &e )
474  {
475  Q_UNUSED( e )
476  QgsDebugMsg( QStringLiteral( "Caught exception %1" ).arg( e.what() ) );
477  }
478  break;
479  }
480  }
481 
482  rotation += mNorthOffset;
483  setPictureRotation( rotation );
484 }
485 
486 void QgsLayoutItemPicture::loadPicture( const QString &path )
487 {
488  mIsMissingImage = false;
489  mEvaluatedPath = path;
490  if ( path.startsWith( QLatin1String( "http" ) ) )
491  {
492  //remote location
493  loadRemotePicture( path );
494  }
495  else
496  {
497  //local location
498  loadLocalPicture( path );
499  }
500  if ( mMode != FormatUnknown ) //make sure we start with a new QImage
501  {
502  recalculateSize();
503  }
504  else if ( mHasExpressionError || !( path.isEmpty() ) )
505  {
506  //trying to load an invalid file or bad expression, show cross picture
507  mMode = FormatSVG;
508  mIsMissingImage = true;
509  QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
510  mSVG.load( badFile );
511  if ( mSVG.isValid() )
512  {
513  mMode = FormatSVG;
514  QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
515  mDefaultSvgSize.setWidth( viewBox.width() );
516  mDefaultSvgSize.setHeight( viewBox.height() );
517  recalculateSize();
518  }
519  }
520 
521  update();
522  emit changed();
523 }
524 
525 QRectF QgsLayoutItemPicture::boundedImageRect( double deviceWidth, double deviceHeight )
526 {
527  double imageToDeviceRatio;
528  if ( mImage.width() / deviceWidth > mImage.height() / deviceHeight )
529  {
530  imageToDeviceRatio = deviceWidth / mImage.width();
531  double height = imageToDeviceRatio * mImage.height();
532  return QRectF( 0, 0, deviceWidth, height );
533  }
534  else
535  {
536  imageToDeviceRatio = deviceHeight / mImage.height();
537  double width = imageToDeviceRatio * mImage.width();
538  return QRectF( 0, 0, width, deviceHeight );
539  }
540 }
541 
542 QRectF QgsLayoutItemPicture::boundedSVGRect( double deviceWidth, double deviceHeight )
543 {
544  double imageToSvgRatio;
545  if ( deviceWidth / mDefaultSvgSize.width() > deviceHeight / mDefaultSvgSize.height() )
546  {
547  imageToSvgRatio = deviceHeight / mDefaultSvgSize.height();
548  double width = mDefaultSvgSize.width() * imageToSvgRatio;
549  return QRectF( 0, 0, width, deviceHeight );
550  }
551  else
552  {
553  imageToSvgRatio = deviceWidth / mDefaultSvgSize.width();
554  double height = mDefaultSvgSize.height() * imageToSvgRatio;
555  return QRectF( 0, 0, deviceWidth, height );
556  }
557 }
558 
559 QSizeF QgsLayoutItemPicture::pictureSize()
560 {
561  if ( mMode == FormatSVG )
562  {
563  return mDefaultSvgSize;
564  }
565  else if ( mMode == FormatRaster )
566  {
567  return QSizeF( mImage.width(), mImage.height() );
568  }
569  else
570  {
571  return QSizeF( 0, 0 );
572  }
573 }
574 
576 {
577  return mIsMissingImage;
578 }
579 
581 {
582  return mEvaluatedPath;
583 }
584 
585 void QgsLayoutItemPicture::shapeChanged()
586 {
587  if ( mMode == FormatSVG && !mLoadingSvg )
588  {
589  mLoadingSvg = true;
590  refreshPicture();
591  mLoadingSvg = false;
592  }
593 }
594 
596 {
597  double oldRotation = mPictureRotation;
598  mPictureRotation = rotation;
599 
600  if ( mResizeMode == Zoom )
601  {
602  //find largest scaling of picture with this rotation which fits in item
603  QSizeF currentPictureSize = pictureSize();
604  QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
605  mPictureWidth = rotatedImageRect.width();
606  mPictureHeight = rotatedImageRect.height();
607  update();
608  }
609  else if ( mResizeMode == ZoomResizeFrame )
610  {
611  QSizeF currentPictureSize = pictureSize();
612  QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
613 
614  //calculate actual size of image inside frame
615  QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
616 
617  //rotate image rect by new rotation and get bounding box
618  QTransform tr;
619  tr.rotate( mPictureRotation );
620  QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
621 
622  //keep the center in the same location
623  newRect.moveCenter( oldRect.center() );
624  attemptSetSceneRect( newRect );
625  emit changed();
626  }
627 
628  emit pictureRotationChanged( mPictureRotation );
629 }
630 
632 {
633  if ( mRotationMap )
634  {
635  disconnectMap( mRotationMap );
636  }
637 
638  if ( !map ) //disable rotation from map
639  {
640  mRotationMap = nullptr;
641  }
642  else
643  {
644  mPictureRotation = map->mapRotation();
645  connect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
646  connect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
647  mRotationMap = map;
648  updateMapRotation();
649  emit pictureRotationChanged( mPictureRotation );
650  }
651 }
652 
654 {
655  mResizeMode = mode;
657  || ( mode == QgsLayoutItemPicture::Zoom && !qgsDoubleNear( mPictureRotation, 0.0 ) ) )
658  {
659  //call set scene rect to force item to resize to fit picture
660  recalculateSize();
661  }
662  update();
663 }
664 
666 {
667  //call set scene rect with current position/size, as this will trigger the
668  //picture item to recalculate its frame and image size
669  attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
670 }
671 
673 {
676  || property == QgsLayoutObject::AllProperties )
677  {
679  refreshPicture( &context );
680  }
681 
683 }
684 
685 void QgsLayoutItemPicture::setPicturePath( const QString &path )
686 {
687  mSourcePath = path;
688  refreshPicture();
689 }
690 
692 {
693  return mSourcePath;
694 }
695 
696 bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
697 {
698  QString imagePath = mSourcePath;
699 
700  // convert from absolute path to relative. For SVG we also need to consider system SVG paths
701  QgsPathResolver pathResolver = context.pathResolver();
702  if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
703  imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
704  else
705  imagePath = pathResolver.writePath( imagePath );
706 
707  elem.setAttribute( QStringLiteral( "file" ), imagePath );
708  elem.setAttribute( QStringLiteral( "pictureWidth" ), QString::number( mPictureWidth ) );
709  elem.setAttribute( QStringLiteral( "pictureHeight" ), QString::number( mPictureHeight ) );
710  elem.setAttribute( QStringLiteral( "resizeMode" ), QString::number( static_cast< int >( mResizeMode ) ) );
711  elem.setAttribute( QStringLiteral( "anchorPoint" ), QString::number( static_cast< int >( mPictureAnchor ) ) );
712  elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) );
713  elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) );
714  elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) );
715 
716  //rotation
717  elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) );
718  if ( !mRotationMap )
719  {
720  elem.setAttribute( QStringLiteral( "mapUuid" ), QString() );
721  }
722  else
723  {
724  elem.setAttribute( QStringLiteral( "mapUuid" ), mRotationMap->uuid() );
725  }
726  elem.setAttribute( QStringLiteral( "northMode" ), mNorthMode );
727  elem.setAttribute( QStringLiteral( "northOffset" ), mNorthOffset );
728  return true;
729 }
730 
731 bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
732 {
733  mPictureWidth = itemElem.attribute( QStringLiteral( "pictureWidth" ), QStringLiteral( "10" ) ).toDouble();
734  mPictureHeight = itemElem.attribute( QStringLiteral( "pictureHeight" ), QStringLiteral( "10" ) ).toDouble();
735  mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
736  //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
737  mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( QStringLiteral( "anchorPoint" ), QString::number( QgsLayoutItem::Middle ) ).toInt() );
738 
739  mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) );
740  mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) );
741  mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble();
742 
743  QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) );
744  if ( !composerItemList.isEmpty() )
745  {
746  QDomElement composerItemElem = composerItemList.at( 0 ).toElement();
747 
748  if ( !qgsDoubleNear( composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
749  {
750  //in versions prior to 2.1 picture rotation was stored in the rotation attribute
751  mPictureRotation = composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble();
752  }
753  }
754 
755  mDefaultSvgSize = QSize( 0, 0 );
756 
757  if ( itemElem.hasAttribute( QStringLiteral( "sourceExpression" ) ) )
758  {
759  //update pre 2.5 picture expression to use data defined expression
760  QString sourceExpression = itemElem.attribute( QStringLiteral( "sourceExpression" ), QString() );
761  QString useExpression = itemElem.attribute( QStringLiteral( "useExpression" ) );
762  bool expressionActive;
763  expressionActive = ( useExpression.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 );
764 
766  }
767 
768  QString imagePath = itemElem.attribute( QStringLiteral( "file" ) );
769 
770  // convert from relative path to absolute. For SVG we also need to consider system SVG paths
771  QgsPathResolver pathResolver = context.pathResolver();
772  if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
773  imagePath = QgsSymbolLayerUtils::svgSymbolNameToPath( imagePath, pathResolver );
774  else
775  imagePath = pathResolver.readPath( imagePath );
776 
777  mSourcePath = imagePath;
778 
779  //picture rotation
780  if ( !qgsDoubleNear( itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
781  {
782  mPictureRotation = itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble();
783  }
784 
785  //rotation map
786  mNorthMode = static_cast< NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() );
787  mNorthOffset = itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble();
788 
789  disconnectMap( mRotationMap );
790  mRotationMap = nullptr;
791  mRotationMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) );
792 
793  return true;
794 }
795 
797 {
798  return mRotationMap;
799 }
800 
802 {
803  mNorthMode = mode;
804  updateMapRotation();
805 }
806 
808 {
809  mNorthOffset = offset;
810  updateMapRotation();
811 }
812 
814 {
815  mPictureAnchor = anchor;
816  update();
817 }
818 
819 void QgsLayoutItemPicture::setSvgFillColor( const QColor &color )
820 {
821  mSvgFillColor = color;
822  refreshPicture();
823 }
824 
825 void QgsLayoutItemPicture::setSvgStrokeColor( const QColor &color )
826 {
827  mSvgStrokeColor = color;
828  refreshPicture();
829 }
830 
832 {
833  mSvgStrokeWidth = width;
834  refreshPicture();
835 }
836 
838 {
839  if ( !mLayout || mRotationMapUuid.isEmpty() )
840  {
841  mRotationMap = nullptr;
842  }
843  else
844  {
845  if ( mRotationMap )
846  {
847  disconnectMap( mRotationMap );
848  }
849  if ( ( mRotationMap = qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) ) )
850  {
851  connect( mRotationMap, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
852  connect( mRotationMap, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
853  }
854  }
855 
856  refreshPicture();
857 }
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:265
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.
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.
QgsLayoutRenderContext & renderContext()
Returns a reference to the layout&#39;s render context, which stores information relating to the current ...
Definition: qgslayout.cpp:357
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.
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.
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 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:367
QString valueAsString(int key, const QgsExpressionContext &context, const QString &defaultString=QString(), bool *ok=nullptr) const
Calculates the current value of the property with the specified key and interprets it as a string...
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.
QByteArray svgContent(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0)
Gets SVG content.
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 fetchContent(const QUrl &url)
Fetches content from a remote URL and handles redirects.
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
static QColor decodeColor(const QString &str)
All properties for item.
double width() const
Returns the width of the size.
Definition: qgslayoutsize.h:76