Branch data Line data Source code
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"
31 : : #include "qgsnetworkcontentfetcher.h"
32 : : #include "qgssymbollayerutils.h"
33 : : #include "qgssvgcache.h"
34 : : #include "qgslogger.h"
35 : : #include "qgsbearingutils.h"
36 : : #include "qgsmapsettings.h"
37 : : #include "qgsreadwritecontext.h"
38 : : #include "qgsimagecache.h"
39 : : #include "qgslayoutnortharrowhandler.h"
40 : :
41 : : #include <QDomDocument>
42 : : #include <QDomElement>
43 : : #include <QFileInfo>
44 : : #include <QImageReader>
45 : : #include <QPainter>
46 : : #include <QSvgRenderer>
47 : : #include <QNetworkRequest>
48 : : #include <QNetworkReply>
49 : : #include <QEventLoop>
50 : : #include <QCoreApplication>
51 : : #include <QUrl>
52 : :
53 : 0 : QgsLayoutItemPicture::QgsLayoutItemPicture( QgsLayout *layout )
54 : 0 : : QgsLayoutItem( layout )
55 : 0 : , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) )
56 : 0 : {
57 : : //default to no background
58 : 0 : setBackgroundEnabled( false );
59 : :
60 : : //connect some signals
61 : :
62 : : //connect to atlas feature changing
63 : : //to update the picture source expression
64 : 0 : connect( &layout->reportContext(), &QgsLayoutReportContext::changed, this, [ = ] { refreshPicture(); } );
65 : :
66 : : //connect to layout print resolution changing
67 : 0 : connect( &layout->renderContext(), &QgsLayoutRenderContext::dpiChanged, this, &QgsLayoutItemPicture::recalculateSize );
68 : :
69 : 0 : connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged );
70 : 0 : connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemPicture::updateNorthArrowRotation );
71 : 0 : }
72 : :
73 : 0 : int QgsLayoutItemPicture::type() const
74 : : {
75 : 0 : return QgsLayoutItemRegistry::LayoutPicture;
76 : : }
77 : :
78 : 0 : QIcon QgsLayoutItemPicture::icon() const
79 : : {
80 : 0 : return QgsApplication::getThemeIcon( QStringLiteral( "/mLayoutItemPicture.svg" ) );
81 : 0 : }
82 : :
83 : 0 : QgsLayoutItemPicture *QgsLayoutItemPicture::create( QgsLayout *layout )
84 : : {
85 : 0 : return new QgsLayoutItemPicture( layout );
86 : 0 : }
87 : :
88 : 0 : void QgsLayoutItemPicture::draw( QgsLayoutItemRenderContext &context )
89 : : {
90 : 0 : QPainter *painter = context.renderContext().painter();
91 : 0 : QgsScopedQPainterState painterState( painter );
92 : : // painter is scaled to dots, so scale back to layout units
93 : 0 : painter->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
94 : :
95 : : //picture resizing
96 : 0 : if ( mMode != FormatUnknown )
97 : : {
98 : : double boundRectWidthMM;
99 : : double boundRectHeightMM;
100 : 0 : QRect imageRect;
101 : 0 : if ( mResizeMode == QgsLayoutItemPicture::Zoom || mResizeMode == QgsLayoutItemPicture::ZoomResizeFrame )
102 : : {
103 : 0 : boundRectWidthMM = mPictureWidth;
104 : 0 : boundRectHeightMM = mPictureHeight;
105 : 0 : imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
106 : 0 : }
107 : 0 : else if ( mResizeMode == QgsLayoutItemPicture::Stretch )
108 : : {
109 : 0 : boundRectWidthMM = rect().width();
110 : 0 : boundRectHeightMM = rect().height();
111 : 0 : imageRect = QRect( 0, 0, mImage.width(), mImage.height() );
112 : 0 : }
113 : 0 : else if ( mResizeMode == QgsLayoutItemPicture::Clip )
114 : : {
115 : 0 : boundRectWidthMM = rect().width();
116 : 0 : boundRectHeightMM = rect().height();
117 : 0 : int imageRectWidthPixels = mImage.width();
118 : 0 : int imageRectHeightPixels = mImage.height();
119 : 0 : imageRect = clippedImageRect( boundRectWidthMM, boundRectHeightMM,
120 : 0 : QSize( imageRectWidthPixels, imageRectHeightPixels ) );
121 : 0 : }
122 : : else
123 : : {
124 : 0 : boundRectWidthMM = rect().width();
125 : 0 : boundRectHeightMM = rect().height();
126 : 0 : imageRect = QRect( 0, 0, mLayout->convertFromLayoutUnits( rect().width(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4,
127 : 0 : mLayout->convertFromLayoutUnits( rect().height(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4 );
128 : : }
129 : :
130 : : //zoom mode - calculate anchor point and rotation
131 : 0 : if ( mResizeMode == Zoom )
132 : : {
133 : : //TODO - allow placement modes with rotation set. for now, setting a rotation
134 : : //always places picture in center of frame
135 : 0 : if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
136 : : {
137 : 0 : painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
138 : 0 : painter->rotate( mPictureRotation );
139 : 0 : painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
140 : 0 : }
141 : : else
142 : : {
143 : : //shift painter to edge/middle of frame depending on placement
144 : 0 : double diffX = rect().width() - boundRectWidthMM;
145 : 0 : double diffY = rect().height() - boundRectHeightMM;
146 : :
147 : 0 : double dX = 0;
148 : 0 : double dY = 0;
149 : 0 : switch ( mPictureAnchor )
150 : : {
151 : : case UpperLeft:
152 : : case MiddleLeft:
153 : : case LowerLeft:
154 : : //nothing to do
155 : 0 : break;
156 : : case UpperMiddle:
157 : : case Middle:
158 : : case LowerMiddle:
159 : 0 : dX = diffX / 2.0;
160 : 0 : break;
161 : : case UpperRight:
162 : : case MiddleRight:
163 : : case LowerRight:
164 : 0 : dX = diffX;
165 : 0 : break;
166 : : }
167 : 0 : switch ( mPictureAnchor )
168 : : {
169 : : case UpperLeft:
170 : : case UpperMiddle:
171 : : case UpperRight:
172 : : //nothing to do
173 : 0 : break;
174 : : case MiddleLeft:
175 : : case Middle:
176 : : case MiddleRight:
177 : 0 : dY = diffY / 2.0;
178 : 0 : break;
179 : : case LowerLeft:
180 : : case LowerMiddle:
181 : : case LowerRight:
182 : 0 : dY = diffY;
183 : 0 : break;
184 : : }
185 : 0 : painter->translate( dX, dY );
186 : : }
187 : 0 : }
188 : 0 : else if ( mResizeMode == ZoomResizeFrame )
189 : : {
190 : 0 : if ( !qgsDoubleNear( mPictureRotation, 0.0 ) )
191 : : {
192 : 0 : painter->translate( rect().width() / 2.0, rect().height() / 2.0 );
193 : 0 : painter->rotate( mPictureRotation );
194 : 0 : painter->translate( -boundRectWidthMM / 2.0, -boundRectHeightMM / 2.0 );
195 : 0 : }
196 : 0 : }
197 : :
198 : 0 : if ( mMode == FormatSVG )
199 : : {
200 : 0 : mSVG.render( painter, QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ) );
201 : 0 : }
202 : 0 : else if ( mMode == FormatRaster )
203 : : {
204 : 0 : painter->drawImage( QRectF( 0, 0, boundRectWidthMM, boundRectHeightMM ), mImage, imageRect );
205 : 0 : }
206 : :
207 : 0 : }
208 : 0 : }
209 : :
210 : 0 : QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF targetSize )
211 : : {
212 : 0 : QSizeF currentPictureSize = pictureSize();
213 : 0 : QSizeF newSize = targetSize;
214 : 0 : if ( mResizeMode == QgsLayoutItemPicture::Clip )
215 : : {
216 : 0 : mPictureWidth = targetSize.width();
217 : 0 : mPictureHeight = targetSize.height();
218 : 0 : }
219 : : else
220 : : {
221 : 0 : if ( mResizeMode == ZoomResizeFrame && !rect().isEmpty() && !( currentPictureSize.isEmpty() ) )
222 : : {
223 : 0 : QSizeF targetImageSize;
224 : 0 : if ( qgsDoubleNear( mPictureRotation, 0.0 ) )
225 : : {
226 : 0 : targetImageSize = currentPictureSize;
227 : 0 : }
228 : : else
229 : : {
230 : : //calculate aspect ratio of bounds of rotated image
231 : 0 : QTransform tr;
232 : 0 : tr.rotate( mPictureRotation );
233 : 0 : QRectF rotatedBounds = tr.mapRect( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ) );
234 : 0 : targetImageSize = QSizeF( rotatedBounds.width(), rotatedBounds.height() );
235 : : }
236 : :
237 : : //if height has changed more than width, then fix width and set height correspondingly
238 : : //else, do the opposite
239 : 0 : if ( std::fabs( rect().width() - targetSize.width() ) <
240 : 0 : std::fabs( rect().height() - targetSize.height() ) )
241 : : {
242 : 0 : newSize.setHeight( targetImageSize.height() * newSize.width() / targetImageSize.width() );
243 : 0 : }
244 : : else
245 : : {
246 : 0 : newSize.setWidth( targetImageSize.width() * newSize.height() / targetImageSize.height() );
247 : : }
248 : 0 : }
249 : 0 : else if ( mResizeMode == FrameToImageSize )
250 : : {
251 : 0 : if ( !( currentPictureSize.isEmpty() ) )
252 : : {
253 : 0 : QgsLayoutSize sizeMM = mLayout->convertFromLayoutUnits( currentPictureSize, QgsUnitTypes::LayoutMillimeters );
254 : 0 : newSize.setWidth( sizeMM.width() * 25.4 / mLayout->renderContext().dpi() );
255 : 0 : newSize.setHeight( sizeMM.height() * 25.4 / mLayout->renderContext().dpi() );
256 : 0 : }
257 : 0 : }
258 : :
259 : : //find largest scaling of picture with this rotation which fits in item
260 : 0 : if ( mResizeMode == Zoom || mResizeMode == ZoomResizeFrame )
261 : : {
262 : 0 : QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ),
263 : 0 : QRectF( 0, 0, newSize.width(), newSize.height() ), mPictureRotation );
264 : 0 : mPictureWidth = rotatedImageRect.width();
265 : 0 : mPictureHeight = rotatedImageRect.height();
266 : 0 : }
267 : : else
268 : : {
269 : 0 : mPictureWidth = newSize.width();
270 : 0 : mPictureHeight = newSize.height();
271 : : }
272 : :
273 : 0 : if ( newSize != targetSize )
274 : : {
275 : 0 : emit changed();
276 : 0 : }
277 : : }
278 : :
279 : 0 : return newSize;
280 : : }
281 : :
282 : 0 : QRect QgsLayoutItemPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels )
283 : : {
284 : 0 : int boundRectWidthPixels = boundRectWidthMM * mLayout->renderContext().dpi() / 25.4;
285 : 0 : int boundRectHeightPixels = boundRectHeightMM * mLayout->renderContext().dpi() / 25.4;
286 : :
287 : : //update boundRectWidth/Height so that they exactly match pixel bounds
288 : 0 : boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->renderContext().dpi();
289 : 0 : boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->renderContext().dpi();
290 : :
291 : : //calculate part of image which fits in bounds
292 : 0 : int leftClip = 0;
293 : 0 : int topClip = 0;
294 : :
295 : : //calculate left crop
296 : 0 : switch ( mPictureAnchor )
297 : : {
298 : : case UpperLeft:
299 : : case MiddleLeft:
300 : : case LowerLeft:
301 : 0 : leftClip = 0;
302 : 0 : break;
303 : : case UpperMiddle:
304 : : case Middle:
305 : : case LowerMiddle:
306 : 0 : leftClip = ( imageRectPixels.width() - boundRectWidthPixels ) / 2;
307 : 0 : break;
308 : : case UpperRight:
309 : : case MiddleRight:
310 : : case LowerRight:
311 : 0 : leftClip = imageRectPixels.width() - boundRectWidthPixels;
312 : 0 : break;
313 : : }
314 : :
315 : : //calculate top crop
316 : 0 : switch ( mPictureAnchor )
317 : : {
318 : : case UpperLeft:
319 : : case UpperMiddle:
320 : 0 : case UpperRight:
321 : 0 : topClip = 0;
322 : 0 : break;
323 : : case MiddleLeft:
324 : : case Middle:
325 : 0 : case MiddleRight:
326 : 0 : topClip = ( imageRectPixels.height() - boundRectHeightPixels ) / 2;
327 : 0 : break;
328 : : case LowerLeft:
329 : : case LowerMiddle:
330 : 0 : case LowerRight:
331 : 0 : topClip = imageRectPixels.height() - boundRectHeightPixels;
332 : 0 : break;
333 : : }
334 : 0 :
335 : 0 : return QRect( leftClip, topClip, boundRectWidthPixels, boundRectHeightPixels );
336 : : }
337 : 0 :
338 : 0 : void QgsLayoutItemPicture::refreshPicture( const QgsExpressionContext *context )
339 : 0 : {
340 : 0 : QgsExpressionContext scopedContext = createExpressionContext();
341 : 0 : const QgsExpressionContext *evalContext = context ? context : &scopedContext;
342 : 0 :
343 : 0 : mDataDefinedProperties.prepare( *evalContext );
344 : 0 :
345 : 0 : QVariant source( mSourcePath );
346 : :
347 : : //data defined source set?
348 : 0 : mHasExpressionError = false;
349 : 0 : if ( mDataDefinedProperties.isActive( QgsLayoutObject::PictureSource ) )
350 : : {
351 : 0 : mMode = FormatUnknown;
352 : 0 : bool ok = false;
353 : 0 : const QgsProperty &sourceProperty = mDataDefinedProperties.property( QgsLayoutObject::PictureSource );
354 : 0 : source = sourceProperty.value( *evalContext, source, &ok );
355 : 0 : if ( !ok || !source.canConvert( QMetaType::QString ) )
356 : : {
357 : 0 : mHasExpressionError = true;
358 : 0 : source = QString();
359 : 0 : QgsMessageLog::logMessage( tr( "Picture expression eval error" ) );
360 : 0 : }
361 : 0 : else if ( source.type() != QVariant::ByteArray )
362 : : {
363 : 0 : source = source.toString().trimmed();
364 : 0 : QgsDebugMsgLevel( QStringLiteral( "exprVal PictureSource:%1" ).arg( source.toString() ), 2 );
365 : 0 : }
366 : 0 : }
367 : :
368 : 0 : loadPicture( source );
369 : 0 : }
370 : :
371 : 0 : void QgsLayoutItemPicture::loadRemotePicture( const QString &url )
372 : : {
373 : : //remote location
374 : :
375 : 0 : QgsNetworkContentFetcher fetcher;
376 : 0 : QEventLoop loop;
377 : 0 : connect( &fetcher, &QgsNetworkContentFetcher::finished, &loop, &QEventLoop::quit );
378 : 0 : fetcher.fetchContent( QUrl( url ) );
379 : :
380 : : //wait until picture fetched
381 : 0 : loop.exec( QEventLoop::ExcludeUserInputEvents );
382 : :
383 : 0 : QNetworkReply *reply = fetcher.reply();
384 : 0 : if ( reply )
385 : : {
386 : 0 : QImageReader imageReader( reply );
387 : 0 : mImage = imageReader.read();
388 : 0 : mMode = FormatRaster;
389 : 0 : }
390 : : else
391 : : {
392 : 0 : mMode = FormatUnknown;
393 : : }
394 : 0 : }
395 : :
396 : 0 : void QgsLayoutItemPicture::loadLocalPicture( const QString &path )
397 : : {
398 : 0 : QFile pic;
399 : 0 : pic.setFileName( path );
400 : :
401 : 0 : if ( !pic.exists() )
402 : : {
403 : 0 : mMode = FormatUnknown;
404 : 0 : }
405 : : else
406 : : {
407 : 0 : QFileInfo sourceFileInfo( pic );
408 : 0 : QString sourceFileSuffix = sourceFileInfo.suffix();
409 : 0 : if ( sourceFileSuffix.compare( QLatin1String( "svg" ), Qt::CaseInsensitive ) == 0 )
410 : : {
411 : : //try to open svg
412 : 0 : QgsExpressionContext context = createExpressionContext();
413 : 0 : QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
414 : 0 : QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
415 : 0 : double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
416 : 0 : const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
417 : : 1.0 );
418 : 0 : mSVG.load( svgContent );
419 : 0 : if ( mSVG.isValid() )
420 : : {
421 : 0 : mMode = FormatSVG;
422 : 0 : QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
423 : 0 : mDefaultSvgSize.setWidth( viewBox.width() );
424 : 0 : mDefaultSvgSize.setHeight( viewBox.height() );
425 : 0 : }
426 : : else
427 : : {
428 : 0 : mMode = FormatUnknown;
429 : : }
430 : 0 : }
431 : : else
432 : : {
433 : : //try to open raster with QImageReader
434 : 0 : QImageReader imageReader( pic.fileName() );
435 : 0 : if ( imageReader.read( &mImage ) )
436 : : {
437 : 0 : mMode = FormatRaster;
438 : 0 : }
439 : : else
440 : : {
441 : 0 : mMode = FormatUnknown;
442 : : }
443 : 0 : }
444 : 0 : }
445 : 0 : }
446 : :
447 : 0 : void QgsLayoutItemPicture::loadPictureUsingCache( const QString &path )
448 : : {
449 : 0 : if ( path.isEmpty() )
450 : : {
451 : 0 : mImage = QImage();
452 : 0 : return;
453 : : }
454 : :
455 : 0 : switch ( mMode )
456 : : {
457 : : case FormatUnknown:
458 : 0 : break;
459 : :
460 : : case FormatRaster:
461 : : {
462 : 0 : bool fitsInCache = false;
463 : 0 : bool isMissing = false;
464 : 0 : mImage = QgsApplication::imageCache()->pathAsImage( path, QSize(), true, 1, fitsInCache, true, &isMissing );
465 : 0 : if ( mImage.isNull() || isMissing )
466 : 0 : mMode = FormatUnknown;
467 : 0 : break;
468 : : }
469 : :
470 : : case FormatSVG:
471 : : {
472 : 0 : QgsExpressionContext context = createExpressionContext();
473 : 0 : QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor );
474 : 0 : QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor );
475 : 0 : double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth );
476 : : // TODO parameters (handle this in the gui part)
477 : 0 : QMap<QString, QString> parameters;
478 : 0 : bool isMissingImage = false;
479 : 0 : const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth,
480 : : 1.0, 0, false, parameters, &isMissingImage );
481 : 0 : mSVG.load( svgContent );
482 : 0 : if ( mSVG.isValid() && !isMissingImage )
483 : : {
484 : 0 : mMode = FormatSVG;
485 : 0 : QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
486 : 0 : mDefaultSvgSize.setWidth( viewBox.width() );
487 : 0 : mDefaultSvgSize.setHeight( viewBox.height() );
488 : 0 : }
489 : : else
490 : : {
491 : 0 : mMode = FormatUnknown;
492 : : }
493 : : break;
494 : 0 : }
495 : : }
496 : 0 : }
497 : :
498 : 0 : void QgsLayoutItemPicture::updateNorthArrowRotation( double rotation )
499 : : {
500 : 0 : setPictureRotation( rotation );
501 : 0 : emit pictureRotationChanged( rotation );
502 : 0 : }
503 : :
504 : 0 : void QgsLayoutItemPicture::loadPicture( const QVariant &data )
505 : : {
506 : 0 : mIsMissingImage = false;
507 : 0 : QVariant imageData( data );
508 : 0 : mEvaluatedPath = data.toString();
509 : :
510 : 0 : if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) && mMode == FormatUnknown )
511 : : {
512 : 0 : QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
513 : 0 : imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
514 : 0 : }
515 : :
516 : 0 : if ( imageData.type() == QVariant::ByteArray )
517 : : {
518 : 0 : if ( mImage.loadFromData( imageData.toByteArray() ) )
519 : : {
520 : 0 : mMode = FormatRaster;
521 : 0 : }
522 : 0 : }
523 : 0 : else if ( mMode == FormatUnknown && mEvaluatedPath.startsWith( QLatin1String( "http" ) ) )
524 : : {
525 : : //remote location (unsafe way, uses QEventLoop) - for old API/project compatibility only!!
526 : 0 : loadRemotePicture( mEvaluatedPath );
527 : 0 : }
528 : 0 : else if ( mMode == FormatUnknown )
529 : : {
530 : : //local location - for old API/project compatibility only!!
531 : 0 : loadLocalPicture( mEvaluatedPath );
532 : 0 : }
533 : : else
534 : : {
535 : 0 : loadPictureUsingCache( mEvaluatedPath );
536 : : }
537 : :
538 : 0 : if ( mMode != FormatUnknown ) //make sure we start with a new QImage
539 : : {
540 : 0 : recalculateSize();
541 : 0 : }
542 : 0 : else if ( mHasExpressionError || !mEvaluatedPath.isEmpty() )
543 : : {
544 : : //trying to load an invalid file or bad expression, show cross picture
545 : 0 : mMode = FormatSVG;
546 : 0 : mIsMissingImage = true;
547 : 0 : QString badFile( QStringLiteral( ":/images/composer/missing_image.svg" ) );
548 : 0 : mSVG.load( badFile );
549 : 0 : if ( mSVG.isValid() )
550 : : {
551 : 0 : mMode = FormatSVG;
552 : 0 : QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size
553 : 0 : mDefaultSvgSize.setWidth( viewBox.width() );
554 : 0 : mDefaultSvgSize.setHeight( viewBox.height() );
555 : 0 : recalculateSize();
556 : 0 : }
557 : 0 : }
558 : :
559 : 0 : update();
560 : 0 : emit changed();
561 : 0 : }
562 : :
563 : 0 : QRectF QgsLayoutItemPicture::boundedImageRect( double deviceWidth, double deviceHeight )
564 : : {
565 : : double imageToDeviceRatio;
566 : 0 : if ( mImage.width() / deviceWidth > mImage.height() / deviceHeight )
567 : : {
568 : 0 : imageToDeviceRatio = deviceWidth / mImage.width();
569 : 0 : double height = imageToDeviceRatio * mImage.height();
570 : 0 : return QRectF( 0, 0, deviceWidth, height );
571 : : }
572 : : else
573 : : {
574 : 0 : imageToDeviceRatio = deviceHeight / mImage.height();
575 : 0 : double width = imageToDeviceRatio * mImage.width();
576 : 0 : return QRectF( 0, 0, width, deviceHeight );
577 : : }
578 : 0 : }
579 : :
580 : 0 : QRectF QgsLayoutItemPicture::boundedSVGRect( double deviceWidth, double deviceHeight )
581 : : {
582 : : double imageToSvgRatio;
583 : 0 : if ( deviceWidth / mDefaultSvgSize.width() > deviceHeight / mDefaultSvgSize.height() )
584 : : {
585 : 0 : imageToSvgRatio = deviceHeight / mDefaultSvgSize.height();
586 : 0 : double width = mDefaultSvgSize.width() * imageToSvgRatio;
587 : 0 : return QRectF( 0, 0, width, deviceHeight );
588 : : }
589 : : else
590 : : {
591 : 0 : imageToSvgRatio = deviceWidth / mDefaultSvgSize.width();
592 : 0 : double height = mDefaultSvgSize.height() * imageToSvgRatio;
593 : 0 : return QRectF( 0, 0, deviceWidth, height );
594 : : }
595 : 0 : }
596 : :
597 : 0 : QSizeF QgsLayoutItemPicture::pictureSize()
598 : : {
599 : 0 : if ( mMode == FormatSVG )
600 : : {
601 : 0 : return mDefaultSvgSize;
602 : : }
603 : 0 : else if ( mMode == FormatRaster )
604 : : {
605 : 0 : return QSizeF( mImage.width(), mImage.height() );
606 : : }
607 : : else
608 : : {
609 : 0 : return QSizeF( 0, 0 );
610 : : }
611 : 0 : }
612 : :
613 : 0 : bool QgsLayoutItemPicture::isMissingImage() const
614 : : {
615 : 0 : return mIsMissingImage;
616 : : }
617 : :
618 : 0 : QString QgsLayoutItemPicture::evaluatedPath() const
619 : : {
620 : 0 : return mEvaluatedPath;
621 : : }
622 : :
623 : 0 : void QgsLayoutItemPicture::shapeChanged()
624 : : {
625 : 0 : if ( mMode == FormatSVG && !mLoadingSvg )
626 : : {
627 : 0 : mLoadingSvg = true;
628 : 0 : refreshPicture();
629 : 0 : mLoadingSvg = false;
630 : 0 : }
631 : 0 : }
632 : :
633 : 0 : void QgsLayoutItemPicture::setPictureRotation( double rotation )
634 : : {
635 : 0 : double oldRotation = mPictureRotation;
636 : 0 : mPictureRotation = rotation;
637 : :
638 : 0 : if ( mResizeMode == Zoom )
639 : : {
640 : : //find largest scaling of picture with this rotation which fits in item
641 : 0 : QSizeF currentPictureSize = pictureSize();
642 : 0 : QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), mPictureRotation );
643 : 0 : mPictureWidth = rotatedImageRect.width();
644 : 0 : mPictureHeight = rotatedImageRect.height();
645 : 0 : update();
646 : 0 : }
647 : 0 : else if ( mResizeMode == ZoomResizeFrame )
648 : : {
649 : 0 : QSizeF currentPictureSize = pictureSize();
650 : 0 : QRectF oldRect = QRectF( pos().x(), pos().y(), rect().width(), rect().height() );
651 : :
652 : : //calculate actual size of image inside frame
653 : 0 : QRectF rotatedImageRect = QgsLayoutUtils::largestRotatedRectWithinBounds( QRectF( 0, 0, currentPictureSize.width(), currentPictureSize.height() ), rect(), oldRotation );
654 : :
655 : : //rotate image rect by new rotation and get bounding box
656 : 0 : QTransform tr;
657 : 0 : tr.rotate( mPictureRotation );
658 : 0 : QRectF newRect = tr.mapRect( QRectF( 0, 0, rotatedImageRect.width(), rotatedImageRect.height() ) );
659 : :
660 : : //keep the center in the same location
661 : 0 : newRect.moveCenter( oldRect.center() );
662 : 0 : attemptSetSceneRect( newRect );
663 : 0 : emit changed();
664 : 0 : }
665 : :
666 : 0 : emit pictureRotationChanged( mPictureRotation );
667 : 0 : }
668 : :
669 : 0 : void QgsLayoutItemPicture::setLinkedMap( QgsLayoutItemMap *map )
670 : : {
671 : 0 : mNorthArrowHandler->setLinkedMap( map );
672 : 0 : }
673 : :
674 : 0 : void QgsLayoutItemPicture::setResizeMode( QgsLayoutItemPicture::ResizeMode mode )
675 : : {
676 : 0 : mResizeMode = mode;
677 : 0 : if ( mode == QgsLayoutItemPicture::ZoomResizeFrame || mode == QgsLayoutItemPicture::FrameToImageSize
678 : 0 : || ( mode == QgsLayoutItemPicture::Zoom && !qgsDoubleNear( mPictureRotation, 0.0 ) ) )
679 : : {
680 : : //call set scene rect to force item to resize to fit picture
681 : 0 : recalculateSize();
682 : 0 : }
683 : 0 : update();
684 : 0 : }
685 : :
686 : 0 : void QgsLayoutItemPicture::recalculateSize()
687 : : {
688 : : //call set scene rect with current position/size, as this will trigger the
689 : : //picture item to recalculate its frame and image size
690 : 0 : attemptSetSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
691 : 0 : }
692 : :
693 : 0 : void QgsLayoutItemPicture::refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property )
694 : : {
695 : 0 : if ( property == QgsLayoutObject::PictureSource || property == QgsLayoutObject::PictureSvgBackgroundColor
696 : 0 : || property == QgsLayoutObject::PictureSvgStrokeColor || property == QgsLayoutObject::PictureSvgStrokeWidth
697 : 0 : || property == QgsLayoutObject::AllProperties )
698 : : {
699 : 0 : QgsExpressionContext context = createExpressionContext();
700 : 0 : refreshPicture( &context );
701 : 0 : }
702 : :
703 : 0 : QgsLayoutItem::refreshDataDefinedProperty( property );
704 : 0 : }
705 : :
706 : 0 : void QgsLayoutItemPicture::setPicturePath( const QString &path, Format format )
707 : : {
708 : 0 : mMode = format;
709 : 0 : mSourcePath = path;
710 : 0 : refreshPicture();
711 : 0 : }
712 : :
713 : 0 : QString QgsLayoutItemPicture::picturePath() const
714 : : {
715 : 0 : return mSourcePath;
716 : : }
717 : :
718 : 0 : bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocument &, const QgsReadWriteContext &context ) const
719 : : {
720 : 0 : QString imagePath = mSourcePath;
721 : :
722 : : // convert from absolute path to relative. For SVG we also need to consider system SVG paths
723 : 0 : QgsPathResolver pathResolver = context.pathResolver();
724 : 0 : if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
725 : 0 : imagePath = QgsSymbolLayerUtils::svgSymbolPathToName( imagePath, pathResolver );
726 : : else
727 : 0 : imagePath = pathResolver.writePath( imagePath );
728 : :
729 : 0 : elem.setAttribute( QStringLiteral( "file" ), imagePath );
730 : 0 : elem.setAttribute( QStringLiteral( "pictureWidth" ), QString::number( mPictureWidth ) );
731 : 0 : elem.setAttribute( QStringLiteral( "pictureHeight" ), QString::number( mPictureHeight ) );
732 : 0 : elem.setAttribute( QStringLiteral( "resizeMode" ), QString::number( static_cast< int >( mResizeMode ) ) );
733 : 0 : elem.setAttribute( QStringLiteral( "anchorPoint" ), QString::number( static_cast< int >( mPictureAnchor ) ) );
734 : 0 : elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) );
735 : 0 : elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) );
736 : 0 : elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) );
737 : 0 : elem.setAttribute( QStringLiteral( "mode" ), mMode );
738 : :
739 : : //rotation
740 : 0 : elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) );
741 : 0 : if ( !mNorthArrowHandler->linkedMap() )
742 : : {
743 : 0 : elem.setAttribute( QStringLiteral( "mapUuid" ), QString() );
744 : 0 : }
745 : : else
746 : : {
747 : 0 : elem.setAttribute( QStringLiteral( "mapUuid" ), mNorthArrowHandler->linkedMap()->uuid() );
748 : : }
749 : 0 : elem.setAttribute( QStringLiteral( "northMode" ), mNorthArrowHandler->northMode() );
750 : 0 : elem.setAttribute( QStringLiteral( "northOffset" ), mNorthArrowHandler->northOffset() );
751 : : return true;
752 : 0 : }
753 : :
754 : 0 : bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
755 : : {
756 : 0 : mPictureWidth = itemElem.attribute( QStringLiteral( "pictureWidth" ), QStringLiteral( "10" ) ).toDouble();
757 : 0 : mPictureHeight = itemElem.attribute( QStringLiteral( "pictureHeight" ), QStringLiteral( "10" ) ).toDouble();
758 : 0 : mResizeMode = QgsLayoutItemPicture::ResizeMode( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
759 : : //when loading from xml, default to anchor point of middle to match pre 2.4 behavior
760 : 0 : mPictureAnchor = static_cast< QgsLayoutItem::ReferencePoint >( itemElem.attribute( QStringLiteral( "anchorPoint" ), QString::number( QgsLayoutItem::Middle ) ).toInt() );
761 : :
762 : 0 : mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) );
763 : 0 : mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) );
764 : 0 : mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble();
765 : 0 : mMode = static_cast< Format >( itemElem.attribute( QStringLiteral( "mode" ), QString::number( FormatUnknown ) ).toInt() );
766 : :
767 : 0 : QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) );
768 : 0 : if ( !composerItemList.isEmpty() )
769 : : {
770 : 0 : QDomElement composerItemElem = composerItemList.at( 0 ).toElement();
771 : :
772 : 0 : if ( !qgsDoubleNear( composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
773 : : {
774 : : //in versions prior to 2.1 picture rotation was stored in the rotation attribute
775 : 0 : mPictureRotation = composerItemElem.attribute( QStringLiteral( "rotation" ), QStringLiteral( "0" ) ).toDouble();
776 : 0 : }
777 : 0 : }
778 : :
779 : 0 : mDefaultSvgSize = QSize( 0, 0 );
780 : :
781 : 0 : if ( itemElem.hasAttribute( QStringLiteral( "sourceExpression" ) ) )
782 : : {
783 : : //update pre 2.5 picture expression to use data defined expression
784 : 0 : QString sourceExpression = itemElem.attribute( QStringLiteral( "sourceExpression" ), QString() );
785 : 0 : QString useExpression = itemElem.attribute( QStringLiteral( "useExpression" ) );
786 : : bool expressionActive;
787 : 0 : expressionActive = ( useExpression.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 );
788 : :
789 : 0 : mDataDefinedProperties.setProperty( QgsLayoutObject::PictureSource, QgsProperty::fromExpression( sourceExpression, expressionActive ) );
790 : 0 : }
791 : :
792 : 0 : QString imagePath = itemElem.attribute( QStringLiteral( "file" ) );
793 : :
794 : : // convert from relative path to absolute. For SVG we also need to consider system SVG paths
795 : 0 : QgsPathResolver pathResolver = context.pathResolver();
796 : 0 : if ( imagePath.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) )
797 : 0 : imagePath = QgsSymbolLayerUtils::svgSymbolNameToPath( imagePath, pathResolver );
798 : : else
799 : 0 : imagePath = pathResolver.readPath( imagePath );
800 : :
801 : 0 : mSourcePath = imagePath;
802 : :
803 : : //picture rotation
804 : 0 : if ( !qgsDoubleNear( itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) )
805 : : {
806 : 0 : mPictureRotation = itemElem.attribute( QStringLiteral( "pictureRotation" ), QStringLiteral( "0" ) ).toDouble();
807 : 0 : }
808 : :
809 : : //rotation map
810 : 0 : mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ) );
811 : 0 : mNorthArrowHandler->setNorthOffset( itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble() );
812 : :
813 : 0 : mNorthArrowHandler->setLinkedMap( nullptr );
814 : 0 : mRotationMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) );
815 : :
816 : : return true;
817 : 0 : }
818 : :
819 : 0 : QgsLayoutItemMap *QgsLayoutItemPicture::linkedMap() const
820 : : {
821 : 0 : return mNorthArrowHandler->linkedMap();
822 : : }
823 : :
824 : 0 : QgsLayoutItemPicture::NorthMode QgsLayoutItemPicture::northMode() const
825 : : {
826 : 0 : return static_cast< QgsLayoutItemPicture::NorthMode >( mNorthArrowHandler->northMode() );
827 : : }
828 : :
829 : 0 : void QgsLayoutItemPicture::setNorthMode( QgsLayoutItemPicture::NorthMode mode )
830 : : {
831 : 0 : mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( mode ) );
832 : 0 : }
833 : :
834 : 0 : double QgsLayoutItemPicture::northOffset() const
835 : : {
836 : 0 : return mNorthArrowHandler->northOffset();
837 : : }
838 : :
839 : 0 : void QgsLayoutItemPicture::setNorthOffset( double offset )
840 : : {
841 : 0 : mNorthArrowHandler->setNorthOffset( offset );
842 : 0 : }
843 : :
844 : 0 : void QgsLayoutItemPicture::setPictureAnchor( ReferencePoint anchor )
845 : : {
846 : 0 : mPictureAnchor = anchor;
847 : 0 : update();
848 : 0 : }
849 : :
850 : 0 : void QgsLayoutItemPicture::setSvgFillColor( const QColor &color )
851 : : {
852 : 0 : mSvgFillColor = color;
853 : 0 : refreshPicture();
854 : 0 : }
855 : :
856 : 0 : void QgsLayoutItemPicture::setSvgStrokeColor( const QColor &color )
857 : : {
858 : 0 : mSvgStrokeColor = color;
859 : 0 : refreshPicture();
860 : 0 : }
861 : :
862 : 0 : void QgsLayoutItemPicture::setSvgStrokeWidth( double width )
863 : : {
864 : 0 : mSvgStrokeWidth = width;
865 : 0 : refreshPicture();
866 : 0 : }
867 : :
868 : 0 : void QgsLayoutItemPicture::setMode( QgsLayoutItemPicture::Format mode )
869 : : {
870 : 0 : if ( mMode == mode )
871 : 0 : return;
872 : :
873 : 0 : mMode = mode;
874 : 0 : refreshPicture();
875 : 0 : }
876 : :
877 : 0 : void QgsLayoutItemPicture::finalizeRestoreFromXml()
878 : : {
879 : 0 : if ( !mLayout || mRotationMapUuid.isEmpty() )
880 : : {
881 : 0 : mNorthArrowHandler->setLinkedMap( nullptr );
882 : 0 : }
883 : : else
884 : : {
885 : 0 : mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) );
886 : : }
887 : :
888 : 0 : refreshPicture();
889 : 0 : }
|