QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgscomposerhtml.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgscomposerhtml.cpp
3  ------------------------------------------------------------
4  begin : July 2012
5  copyright : (C) 2012 by Marco Hugentobler
6  email : marco dot hugentobler at sourcepole dot ch
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgscomposerhtml.h"
17 #include "qgscomposerframe.h"
18 #include "qgscomposition.h"
21 #include "qgsmessagelog.h"
22 #include "qgsexpression.h"
23 #include "qgslogger.h"
25 #include "qgsvectorlayer.h"
26 #include "qgsproject.h"
27 
28 #include <QCoreApplication>
29 #include <QPainter>
30 #include <QWebFrame>
31 #include <QWebPage>
32 #include <QImage>
33 #include <QNetworkReply>
34 
35 QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands )
36  : QgsComposerMultiFrame( c, createUndoCommands )
37  , mContentMode( QgsComposerHtml::Url )
38  , mWebPage( 0 )
39  , mLoaded( false )
40  , mHtmlUnitsToMM( 1.0 )
41  , mRenderedPage( 0 )
42  , mEvaluateExpressions( true )
43  , mUseSmartBreaks( true )
44  , mMaxBreakDistance( 10 )
45  , mExpressionFeature( 0 )
46  , mExpressionLayer( 0 )
47  , mDistanceArea( 0 )
48  , mEnableUserStylesheet( false )
49  , mFetcher( 0 )
50 {
51  mDistanceArea = new QgsDistanceArea();
52  mHtmlUnitsToMM = htmlUnitsToMM();
53  mWebPage = new QWebPage();
54  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
55  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Vertical, Qt::ScrollBarAlwaysOff );
56 
57  //This makes the background transparent. Found on http://blog.qt.digia.com/blog/2009/06/30/transparent-qwebview-or-qwebpage/
58  QPalette palette = mWebPage->palette();
59  palette.setBrush( QPalette::Base, Qt::transparent );
60  mWebPage->setPalette( palette );
61 
62  mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
63  QObject::connect( mWebPage, SIGNAL( loadFinished( bool ) ), this, SLOT( frameLoaded( bool ) ) );
64  if ( mComposition )
65  {
66  QObject::connect( mComposition, SIGNAL( itemRemoved( QgsComposerItem* ) ), this, SLOT( handleFrameRemoval( QgsComposerItem* ) ) );
67  }
68 
69  // data defined strings
70  mDataDefinedNames.insert( QgsComposerObject::SourceUrl, QString( "dataDefinedSourceUrl" ) );
71 
73  {
74  //a html item added while atlas preview is enabled needs to have the expression context set,
75  //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457)
77  }
78 
79  //connect to atlas feature changes
80  //to update the expression context
81  connect( &mComposition->atlasComposition(), SIGNAL( featureChanged( QgsFeature* ) ), this, SLOT( refreshExpressionContext() ) );
82 
83  mFetcher = new QgsNetworkContentFetcher();
84  connect( mFetcher, SIGNAL( finished() ), this, SLOT( frameLoaded() ) );
85 
86 }
87 
89  : QgsComposerMultiFrame( 0, false )
90  , mContentMode( QgsComposerHtml::Url )
91  , mWebPage( 0 )
92  , mLoaded( false )
93  , mHtmlUnitsToMM( 1.0 )
94  , mRenderedPage( 0 )
95  , mEvaluateExpressions( true )
96  , mUseSmartBreaks( true )
97  , mMaxBreakDistance( 10 )
98  , mExpressionFeature( 0 )
99  , mExpressionLayer( 0 )
100  , mDistanceArea( 0 )
101  , mEnableUserStylesheet( false )
102  , mFetcher( 0 )
103 {
104  mDistanceArea = new QgsDistanceArea();
105  mFetcher = new QgsNetworkContentFetcher();
106  connect( mFetcher, SIGNAL( finished() ), this, SLOT( frameLoaded() ) );
107 }
108 
110 {
111  delete mDistanceArea;
112  delete mWebPage;
113  delete mRenderedPage;
114  mFetcher->deleteLater();
115 }
116 
117 void QgsComposerHtml::setUrl( const QUrl& url )
118 {
119  if ( !mWebPage )
120  {
121  return;
122  }
123 
124  mUrl = url;
125  loadHtml( true );
126  emit changed();
127 }
128 
129 void QgsComposerHtml::setHtml( const QString html )
130 {
131  mHtml = html;
132  //TODO - this signal should be emitted, but without changing the signal which sets the html
133  //to an equivalent of editingFinished it causes a lot of problems. Need to investigate
134  //ways of doing this using QScintilla widgets.
135  //emit changed();
136 }
137 
138 void QgsComposerHtml::setEvaluateExpressions( bool evaluateExpressions )
139 {
140  mEvaluateExpressions = evaluateExpressions;
141  loadHtml( true );
142  emit changed();
143 }
144 
145 void QgsComposerHtml::loadHtml( const bool useCache )
146 {
147  if ( !mWebPage )
148  {
149  return;
150  }
151 
152  QString loadedHtml;
153  switch ( mContentMode )
154  {
156  {
157 
158  QString currentUrl = mUrl.toString();
159 
160  //data defined url set?
161  QVariant exprVal;
163  {
164  currentUrl = exprVal.toString().trimmed();;
165  QgsDebugMsg( QString( "exprVal Source Url:%1" ).arg( currentUrl ) );
166  }
167  if ( currentUrl.isEmpty() )
168  {
169  return;
170  }
171  if ( !( useCache && currentUrl == mLastFetchedUrl ) )
172  {
173  loadedHtml = fetchHtml( QUrl( currentUrl ) );
174  mLastFetchedUrl = currentUrl;
175  }
176  else
177  {
178  loadedHtml = mFetchedHtml;
179  }
180 
181  break;
182  }
184  loadedHtml = mHtml;
185  break;
186  }
187 
188  //evaluate expressions
189  if ( mEvaluateExpressions )
190  {
191  loadedHtml = QgsExpression::replaceExpressionText( loadedHtml, mExpressionFeature, mExpressionLayer, 0, mDistanceArea );
192  }
193 
194  mLoaded = false;
195 
196  //reset page size. otherwise viewport size increases but never decreases again
197  mWebPage->setViewportSize( QSize( maxFrameWidth() * mHtmlUnitsToMM, 0 ) );
198 
199  //set html, using the specified url as base if in Url mode
200  mWebPage->mainFrame()->setHtml( loadedHtml, mContentMode == QgsComposerHtml::Url ? QUrl( mActualFetchedUrl ) : QUrl() );
201 
202  //set user stylesheet
203  QWebSettings* settings = mWebPage->settings();
204  if ( mEnableUserStylesheet && ! mUserStylesheet.isEmpty() )
205  {
206  QByteArray ba;
207  ba.append( mUserStylesheet.toUtf8() );
208  QUrl cssFileURL = QUrl( "data:text/css;charset=utf-8;base64," + ba.toBase64() );
209  settings->setUserStyleSheetUrl( cssFileURL );
210  }
211  else
212  {
213  settings->setUserStyleSheetUrl( QUrl() );
214  }
215 
216  while ( !mLoaded )
217  {
218  qApp->processEvents();
219  }
220 
222  //trigger a repaint
223  emit contentsChanged();
224 }
225 
226 void QgsComposerHtml::frameLoaded( bool ok )
227 {
228  Q_UNUSED( ok );
229  mLoaded = true;
230 }
231 
232 double QgsComposerHtml::maxFrameWidth() const
233 {
234  double maxWidth = 0;
235  QList<QgsComposerFrame*>::const_iterator frameIt = mFrameItems.constBegin();
236  for ( ; frameIt != mFrameItems.constEnd(); ++frameIt )
237  {
238  maxWidth = qMax( maxWidth, ( double )(( *frameIt )->boundingRect().width() ) );
239  }
240 
241  return maxWidth;
242 }
243 
245 {
246  if ( frameCount() < 1 ) return;
247 
248  QSize contentsSize = mWebPage->mainFrame()->contentsSize();
249 
250  //find maximum frame width
251  double maxWidth = maxFrameWidth();
252  //set content width to match maximum frame width
253  contentsSize.setWidth( maxWidth * mHtmlUnitsToMM );
254 
255  mWebPage->setViewportSize( contentsSize );
256  mSize.setWidth( contentsSize.width() / mHtmlUnitsToMM );
257  mSize.setHeight( contentsSize.height() / mHtmlUnitsToMM );
258  if ( contentsSize.isValid() )
259  {
260  renderCachedImage();
261  }
263  emit changed();
264 }
265 
266 void QgsComposerHtml::renderCachedImage()
267 {
268  //render page to cache image
269  if ( mRenderedPage )
270  {
271  delete mRenderedPage;
272  }
273  mRenderedPage = new QImage( mWebPage->viewportSize(), QImage::Format_ARGB32 );
274  if ( mRenderedPage->isNull() )
275  {
276  return;
277  }
278  mRenderedPage->fill( Qt::transparent );
279  QPainter painter;
280  painter.begin( mRenderedPage );
281  mWebPage->mainFrame()->render( &painter );
282  painter.end();
283 }
284 
285 QString QgsComposerHtml::fetchHtml( QUrl url )
286 {
287  //pause until HTML fetch
288  mLoaded = false;
289  mFetcher->fetchContent( url );
290 
291  while ( !mLoaded )
292  {
293  qApp->processEvents();
294  }
295 
296  mFetchedHtml = mFetcher->contentAsString();
297  mActualFetchedUrl = mFetcher->reply()->url().toString();
298  return mFetchedHtml;
299 }
300 
302 {
303  return mSize;
304 }
305 
306 void QgsComposerHtml::render( QPainter* p, const QRectF& renderExtent, const int frameIndex )
307 {
308  Q_UNUSED( frameIndex );
309 
310  if ( !mWebPage )
311  {
312  return;
313  }
314 
315  p->save();
316  p->setRenderHint( QPainter::Antialiasing );
317  p->scale( 1.0 / mHtmlUnitsToMM, 1.0 / mHtmlUnitsToMM );
318  p->translate( 0.0, -renderExtent.top() * mHtmlUnitsToMM );
319  mWebPage->mainFrame()->render( p, QRegion( renderExtent.left(), renderExtent.top() * mHtmlUnitsToMM, renderExtent.width() * mHtmlUnitsToMM, renderExtent.height() * mHtmlUnitsToMM ) );
320  p->restore();
321 }
322 
323 double QgsComposerHtml::htmlUnitsToMM()
324 {
325  if ( !mComposition )
326  {
327  return 1.0;
328  }
329 
330  return ( mComposition->printResolution() / 72.0 ); //webkit seems to assume a standard dpi of 96
331 }
332 
333 void QgsComposerHtml::addFrame( QgsComposerFrame* frame, bool recalcFrameSizes )
334 {
335  mFrameItems.push_back( frame );
336  QObject::connect( frame, SIGNAL( sizeChanged() ), this, SLOT( recalculateFrameSizes() ) );
337  if ( mComposition )
338  {
339  mComposition->addComposerHtmlFrame( this, frame );
340  }
341 
342  if ( recalcFrameSizes )
343  {
345  }
346 }
347 
348 bool candidateSort( const QPair<int, int> &c1, const QPair<int, int> &c2 )
349 {
350  if ( c1.second < c2.second )
351  return true;
352  else if ( c1.second > c2.second )
353  return false;
354  else if ( c1.first > c2.first )
355  return true;
356  else
357  return false;
358 }
359 
361 {
362  if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
363  {
364  return yPos;
365  }
366 
367  //convert yPos to pixels
368  int idealPos = yPos * htmlUnitsToMM();
369 
370  //if ideal break pos is past end of page, there's nothing we need to do
371  if ( idealPos >= mRenderedPage->height() )
372  {
373  return yPos;
374  }
375 
376  int maxSearchDistance = mMaxBreakDistance * htmlUnitsToMM();
377 
378  //loop through all lines just before ideal break location, up to max distance
379  //of maxSearchDistance
380  int changes = 0;
381  QRgb currentColor;
382  bool currentPixelTransparent = false;
383  bool previousPixelTransparent = false;
384  QRgb pixelColor;
385  QList< QPair<int, int> > candidates;
386  int minRow = qMax( idealPos - maxSearchDistance, 0 );
387  for ( int candidateRow = idealPos; candidateRow >= minRow; --candidateRow )
388  {
389  changes = 0;
390  currentColor = qRgba( 0, 0, 0, 0 );
391  //check all pixels in this line
392  for ( int col = 0; col < mRenderedPage->width(); ++col )
393  {
394  //count how many times the pixels change color in this row
395  //eventually, we select a row to break at with the minimum number of color changes
396  //since this is likely a line break, or gap between table cells, etc
397  //but very unlikely to be midway through a text line or picture
398  pixelColor = mRenderedPage->pixel( col, candidateRow );
399  currentPixelTransparent = qAlpha( pixelColor ) == 0;
400  if ( pixelColor != currentColor && !( currentPixelTransparent && previousPixelTransparent ) )
401  {
402  //color has changed
403  currentColor = pixelColor;
404  changes++;
405  }
406  previousPixelTransparent = currentPixelTransparent;
407  }
408  candidates.append( qMakePair( candidateRow, changes ) );
409  }
410 
411  //sort candidate rows by number of changes ascending, row number descending
412  qSort( candidates.begin(), candidates.end(), candidateSort );
413  //first candidate is now the largest row with smallest number of changes
414 
415  //ok, now take the mid point of the best candidate position
416  //we do this so that the spacing between text lines is likely to be split in half
417  //otherwise the html will be broken immediately above a line of text, which
418  //looks a little messy
419  int maxCandidateRow = candidates[0].first;
420  int minCandidateRow = maxCandidateRow + 1;
421  int minCandidateChanges = candidates[0].second;
422 
423  QList< QPair<int, int> >::iterator it;
424  for ( it = candidates.begin(); it != candidates.end(); ++it )
425  {
426  if (( *it ).second != minCandidateChanges || ( *it ).first != minCandidateRow - 1 )
427  {
428  //no longer in a consecutive block of rows of minimum pixel color changes
429  //so return the row mid-way through the block
430  //first converting back to mm
431  return ( minCandidateRow + ( maxCandidateRow - minCandidateRow ) / 2 ) / htmlUnitsToMM();
432  }
433  minCandidateRow = ( *it ).first;
434  }
435 
436  //above loop didn't work for some reason
437  //return first candidate converted to mm
438  return candidates[0].first / htmlUnitsToMM();
439 }
440 
441 void QgsComposerHtml::setUseSmartBreaks( bool useSmartBreaks )
442 {
443  mUseSmartBreaks = useSmartBreaks;
445  emit changed();
446 }
447 
448 void QgsComposerHtml::setMaxBreakDistance( double maxBreakDistance )
449 {
450  mMaxBreakDistance = maxBreakDistance;
452  emit changed();
453 }
454 
455 void QgsComposerHtml::setUserStylesheet( const QString stylesheet )
456 {
457  mUserStylesheet = stylesheet;
458  //TODO - this signal should be emitted, but without changing the signal which sets the css
459  //to an equivalent of editingFinished it causes a lot of problems. Need to investigate
460  //ways of doing this using QScintilla widgets.
461  //emit changed();
462 }
463 
464 void QgsComposerHtml::setUserStylesheetEnabled( const bool stylesheetEnabled )
465 {
466  if ( mEnableUserStylesheet != stylesheetEnabled )
467  {
468  mEnableUserStylesheet = stylesheetEnabled;
469  loadHtml( true );
470  emit changed();
471  }
472 }
473 
475 {
476  return tr( "<html frame>" );
477 }
478 
479 bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool ignoreFrames ) const
480 {
481  QDomElement htmlElem = doc.createElement( "ComposerHtml" );
482  htmlElem.setAttribute( "contentMode", QString::number(( int ) mContentMode ) );
483  htmlElem.setAttribute( "url", mUrl.toString() );
484  htmlElem.setAttribute( "html", mHtml );
485  htmlElem.setAttribute( "evaluateExpressions", mEvaluateExpressions ? "true" : "false" );
486  htmlElem.setAttribute( "useSmartBreaks", mUseSmartBreaks ? "true" : "false" );
487  htmlElem.setAttribute( "maxBreakDistance", QString::number( mMaxBreakDistance ) );
488  htmlElem.setAttribute( "stylesheet", mUserStylesheet );
489  htmlElem.setAttribute( "stylesheetEnabled", mEnableUserStylesheet ? "true" : "false" );
490 
491  bool state = _writeXML( htmlElem, doc, ignoreFrames );
492  elem.appendChild( htmlElem );
493  return state;
494 }
495 
496 bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument& doc, bool ignoreFrames )
497 {
498  if ( !ignoreFrames )
499  {
500  deleteFrames();
501  }
502 
503  //first create the frames
504  if ( !_readXML( itemElem, doc, ignoreFrames ) )
505  {
506  return false;
507  }
508 
509  bool contentModeOK;
510  mContentMode = ( QgsComposerHtml::ContentMode )itemElem.attribute( "contentMode" ).toInt( &contentModeOK );
511  if ( !contentModeOK )
512  {
513  mContentMode = QgsComposerHtml::Url;
514  }
515  mEvaluateExpressions = itemElem.attribute( "evaluateExpressions", "true" ) == "true" ? true : false;
516  mUseSmartBreaks = itemElem.attribute( "useSmartBreaks", "true" ) == "true" ? true : false;
517  mMaxBreakDistance = itemElem.attribute( "maxBreakDistance", "10" ).toDouble();
518  mHtml = itemElem.attribute( "html" );
519  mUserStylesheet = itemElem.attribute( "stylesheet" );
520  mEnableUserStylesheet = itemElem.attribute( "stylesheetEnabled", "false" ) == "true" ? true : false;
521 
522  //finally load the set url
523  QString urlString = itemElem.attribute( "url" );
524  if ( !urlString.isEmpty() )
525  {
526  mUrl = urlString;
527  }
528  loadHtml( true );
529 
530  //since frames had to be created before, we need to emit a changed signal to refresh the widget
531  emit changed();
532  return true;
533 }
534 
535 void QgsComposerHtml::setExpressionContext( QgsFeature* feature, QgsVectorLayer* layer )
536 {
537  mExpressionFeature = feature;
538  mExpressionLayer = layer;
539 
540  //setup distance area conversion
541  if ( layer )
542  {
543  mDistanceArea->setSourceCrs( layer->crs().srsid() );
544  }
545  else if ( mComposition )
546  {
547  //set to composition's mapsettings' crs
548  mDistanceArea->setSourceCrs( mComposition->mapSettings().destinationCrs().srsid() );
549  }
550  if ( mComposition )
551  {
553  }
554  mDistanceArea->setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) );
555 }
556 
558 {
559  QgsVectorLayer * vl = 0;
560  QgsFeature* feature = 0;
561 
563  {
565  }
567  {
569  }
570 
571  setExpressionContext( feature, vl );
572  loadHtml( true );
573 }
574 
576 {
577  //updates data defined properties and redraws item to match
578  if ( property == QgsComposerObject::SourceUrl || property == QgsComposerObject::AllProperties )
579  {
580  loadHtml( true );
581  }
583 }