QGIS API Documentation  3.12.1-BucureČ™ti (121cc00ff0)
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  QgsDebugMsgLevel( QStringLiteral( "exprVal PictureSource:%1" ).arg( source.toString() ), 2 );
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::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
448  disconnect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
449  }
450 }
451 
452 void QgsLayoutItemPicture::updateMapRotation()
453 {
454  if ( !mRotationMap )
455  return;
456 
457  // take map rotation
458  double rotation = mRotationMap->mapRotation() + mRotationMap->rotation();
459 
460  // handle true north
461  switch ( mNorthMode )
462  {
463  case GridNorth:
464  break; // nothing to do
465 
466  case TrueNorth:
467  {
468  QgsPointXY center = mRotationMap->extent().center();
469  QgsCoordinateReferenceSystem crs = mRotationMap->crs();
470  QgsCoordinateTransformContext transformContext = mLayout->project()->transformContext();
471 
472  try
473  {
474  double bearing = QgsBearingUtils::bearingTrueNorth( crs, transformContext, center );
475  rotation += bearing;
476  }
477  catch ( QgsException &e )
478  {
479  Q_UNUSED( e )
480  QgsDebugMsg( QStringLiteral( "Caught exception %1" ).arg( e.what() ) );
481  }
482  break;
483  }
484  }
485 
486  rotation += mNorthOffset;
487  setPictureRotation( ( rotation > 360.0 ) ? rotation - 360.0 : rotation );
488 }
489 
490 void QgsLayoutItemPicture::loadPicture( const QVariant &data )
491 {
492  mIsMissingImage = false;
493  QVariant imageData( data );
494  mEvaluatedPath = data.toString();
495 
496  if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) )
497  {
498  QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
499  imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
500  }
501 
502  if ( imageData.type() == QVariant::ByteArray )
503  {
504  if ( mImage.loadFromData( imageData.toByteArray() ) )
505  {
506  mMode = FormatRaster;
507  }
508  }
509  else if ( mEvaluatedPath.startsWith( QLatin1String( "http" ) ) )
510  {
511  //remote location
512  loadRemotePicture( mEvaluatedPath );
513  }
514  else
515  {
516  //local location
517  loadLocalPicture( mEvaluatedPath );
518  }
519  if ( mMode != FormatUnknown ) //make sure we start with a new QImage
520  {
521  recalculateSize();
522  }
523  else if ( mHasExpressionError || !mEvaluatedPath.isEmpty() )
524  {
525  //trying to load an invalid file or bad expression, show cross picture
526  mMode = FormatSVG;
527  mIsMissingImage = true;
528  QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
529  mSVG.load( badFile );
530  if ( mSVG.isValid() )
531  {
532  mMode = FormatSVG;
533  QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
534  mDefaultSvgSize.setWidth( viewBox.width() );
535  mDefaultSvgSize.setHeight( viewBox.height() );
536  recalculateSize();
537  }
538  }
539 
540  update();
541  emit changed();
542 }
543 
544 QRectF QgsLayoutItemPicture::boundedImageRect( double deviceWidth, double deviceHeight )
545 {
546  double imageToDeviceRatio;
547  if ( mImage.width() / deviceWidth > mImage.height() / deviceHeight )
548  {
549  imageToDeviceRatio = deviceWidth / mImage.width();
550  double height = imageToDeviceRatio * mImage.height();
551  return QRectF( 0, 0, deviceWidth, height );
552  }
553  else
554  {
555  imageToDeviceRatio = deviceHeight / mImage.height();
556  double width = imageToDeviceRatio * mImage.width();
557  return QRectF( 0, 0, width, deviceHeight );
558  }
559 }
560 
561 QRectF QgsLayoutItemPicture::boundedSVGRect( double deviceWidth, double deviceHeight )
562 {
563  double imageToSvgRatio;
564  if ( deviceWidth / mDefaultSvgSize.width() > deviceHeight / mDefaultSvgSize.height() )
565  {
566  imageToSvgRatio = deviceHeight / mDefaultSvgSize.height();
567  double width = mDefaultSvgSize.width() * imageToSvgRatio;
568  return QRectF( 0, 0, width, deviceHeight );
569  }
570  else
571  {
572  imageToSvgRatio = deviceWidth / mDefaultSvgSize.width();
573  double height = mDefaultSvgSize.height() * imageToSvgRatio;
574  return QRectF( 0, 0, deviceWidth, height );
575  }
576 }
577 
578 QSizeF QgsLayoutItemPicture::pictureSize()
579 {
580  if ( mMode == FormatSVG )
581  {
582  return mDefaultSvgSize;
583  }
584  else if ( mMode == FormatRaster )
585  {
586  return QSizeF( mImage.width(), mImage.height() );
587  }
588  else
589  {
590  return QSizeF( 0, 0 );
591  }
592 }
593 
595 {
596  return mIsMissingImage;
597 }
598 
600 {
601  return mEvaluatedPath;
602 }
603 
604 void QgsLayoutItemPicture::shapeChanged()
605 {
606  if ( mMode == FormatSVG && !mLoadingSvg )
607  {
608  mLoadingSvg = true;
609  refreshPicture();
610  mLoadingSvg = false;
611  }
612 }
613 
615 {
616  double oldRotation = mPictureRotation;
617  mPictureRotation = rotation;
618 
619  if ( mResizeMode == Zoom )
620  {
621  //find largest scaling of picture with this rotation which fits in item
622  QSizeF currentPictureSize = pictureSize();
623  QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
624  mPictureWidth = rotatedImageRect.width();
625  mPictureHeight = rotatedImageRect.height();
626  update();
627  }
628  else if ( mResizeMode == ZoomResizeFrame )
629  {
630  QSizeF currentPictureSize = pictureSize();
631  QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
632 
633  //calculate actual size of image inside frame
634  QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
635 
636  //rotate image rect by new rotation and get bounding box
637  QTransform tr;
638  tr.rotate( mPictureRotation );
639  QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
640 
641  //keep the center in the same location
642  newRect.moveCenter( oldRect.center() );
643  attemptSetSceneRect( newRect );
644  emit changed();
645  }
646 
647  emit pictureRotationChanged( mPictureRotation );
648 }
649 
651 {
652  if ( mRotationMap )
653  {
654  disconnectMap( mRotationMap );
655  }
656 
657  if ( !map ) //disable rotation from map
658  {
659  mRotationMap = nullptr;
660  }
661  else
662  {
663  mPictureRotation = map->mapRotation();
664  connect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
665  connect( map, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
666  connect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
667  mRotationMap = map;
668  updateMapRotation();
669  emit pictureRotationChanged( mPictureRotation );
670  }
671 }
672 
674 {
675  mResizeMode = mode;
677  || ( mode == QgsLayoutItemPicture::Zoom && !qgsDoubleNear( mPictureRotation, 0.0 ) ) )
678  {
679  //call set scene rect to force item to resize to fit picture
680  recalculateSize();
681  }
682  update();
683 }
684 
686 {
687  //call set scene rect with current position/size, as this will trigger the
688  //picture item to recalculate its frame and image size
689  attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
690 }
691 
693 {
696  || property == QgsLayoutObject::AllProperties )
697  {
699  refreshPicture( &context );
700  }
701 
703 }
704 
705 void QgsLayoutItemPicture::setPicturePath( const QString &path )
706 {
707  mSourcePath = path;
708  refreshPicture();
709 }
710 
712 {
713  return mSourcePath;
714 }
715 
716 bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
717 {
718  QString imagePath = mSourcePath;
719 
720  // convert from absolute path to relative. For SVG we also need to consider system SVG paths
721  QgsPathResolver pathResolver = context.pathResolver();
722  if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
723  imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
724  else
725  imagePath = pathResolver.writePath( imagePath );
726 
727  elem.setAttribute( QStringLiteral( "file" ), imagePath );
728  elem.setAttribute( QStringLiteral( "pictureWidth" ), QString::number( mPictureWidth ) );
729  elem.setAttribute( QStringLiteral( "pictureHeight" ), QString::number( mPictureHeight ) );
730  elem.setAttribute( QStringLiteral( "resizeMode" ), QString::number( static_cast< int >( mResizeMode ) ) );
731  elem.setAttribute( QStringLiteral( "anchorPoint" ), QString::number( static_cast< int >( mPictureAnchor ) ) );
732  elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) );
733  elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) );
734  elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) );
735 
736  //rotation
737  elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) );
738  if ( !mRotationMap )
739  {
740  elem.setAttribute( QStringLiteral( "mapUuid" ), QString() );
741  }
742  else
743  {
744  elem.setAttribute( QStringLiteral( "mapUuid" ), mRotationMap->uuid() );
745  }
746  elem.setAttribute( QStringLiteral( "northMode" ), mNorthMode );
747  elem.setAttribute( QStringLiteral( "northOffset" ), mNorthOffset );
748  return true;
749 }
750 
751 bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
752 {
753  mPictureWidth = itemElem.attribute( QStringLiteral( "pictureWidth" ), QStringLiteral( "10" ) ).toDouble();
754  mPictureHeight = itemElem.attribute( QStringLiteral( "pictureHeight" ), QStringLiteral( "10" ) ).toDouble();
755  mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
756  //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
757  mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( QStringLiteral( "anchorPoint" ), QString::number( QgsLayoutItem::Middle ) ).toInt() );
758 
759  mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) );
760  mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) );
761  mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble();
762 
763  QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) );
764  if ( !composerItemList.isEmpty() )
765  {
766  QDomElement composerItemElem = composerItemList.at( 0 ).toElement();
767 
768  if ( !qgsDoubleNear( composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
769  {
770  //in versions prior to 2.1 picture rotation was stored in the rotation attribute
771  mPictureRotation = composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble();
772  }
773  }
774 
775  mDefaultSvgSize = QSize( 0, 0 );
776 
777  if ( itemElem.hasAttribute( QStringLiteral( "sourceExpression" ) ) )
778  {
779  //update pre 2.5 picture expression to use data defined expression
780  QString sourceExpression = itemElem.attribute( QStringLiteral( "sourceExpression" ), QString() );
781  QString useExpression = itemElem.attribute( QStringLiteral( "useExpression" ) );
782  bool expressionActive;
783  expressionActive = ( useExpression.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 );
784 
786  }
787 
788  QString imagePath = itemElem.attribute( QStringLiteral( "file" ) );
789 
790  // convert from relative path to absolute. For SVG we also need to consider system SVG paths
791  QgsPathResolver pathResolver = context.pathResolver();
792  if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
793  imagePath = QgsSymbolLayerUtils::svgSymbolNameToPath( imagePath, pathResolver );
794  else
795  imagePath = pathResolver.readPath( imagePath );
796 
797  mSourcePath = imagePath;
798 
799  //picture rotation
800  if ( !qgsDoubleNear( itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
801  {
802  mPictureRotation = itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble();
803  }
804 
805  //rotation map
806  mNorthMode = static_cast< NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() );
807  mNorthOffset = itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble();
808 
809  disconnectMap( mRotationMap );
810  mRotationMap = nullptr;
811  mRotationMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) );
812 
813  return true;
814 }
815 
817 {
818  return mRotationMap;
819 }
820 
822 {
823  mNorthMode = mode;
824  updateMapRotation();
825 }
826 
828 {
829  mNorthOffset = offset;
830  updateMapRotation();
831 }
832 
834 {
835  mPictureAnchor = anchor;
836  update();
837 }
838 
839 void QgsLayoutItemPicture::setSvgFillColor( const QColor &color )
840 {
841  mSvgFillColor = color;
842  refreshPicture();
843 }
844 
845 void QgsLayoutItemPicture::setSvgStrokeColor( const QColor &color )
846 {
847  mSvgStrokeColor = color;
848  refreshPicture();
849 }
850 
852 {
853  mSvgStrokeWidth = width;
854  refreshPicture();
855 }
856 
858 {
859  if ( !mLayout || mRotationMapUuid.isEmpty() )
860  {
861  mRotationMap = nullptr;
862  }
863  else
864  {
865  if ( mRotationMap )
866  {
867  disconnectMap( mRotationMap );
868  }
869  if ( ( mRotationMap = qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) ) )
870  {
871  connect( mRotationMap, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
872  connect( mRotationMap, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation );
873  connect( mRotationMap, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation );
874  }
875  }
876 
877  refreshPicture();
878 }
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 Q_INVOKABLE 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:315
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.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
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 rotationChanged(double newRotation)
Emitted on item rotation change.
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