QGIS API Documentation  2.5.0-Master
 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"
24 
25 #include <QCoreApplication>
26 #include <QPainter>
27 #include <QWebFrame>
28 #include <QWebPage>
29 #include <QImage>
30 #include <QNetworkReply>
31 
32 QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands ): QgsComposerMultiFrame( c, createUndoCommands ),
33  mContentMode( QgsComposerHtml::Url ),
34  mWebPage( 0 ),
35  mLoaded( false ),
36  mHtmlUnitsToMM( 1.0 ),
37  mRenderedPage( 0 ),
38  mEvaluateExpressions( true ),
39  mUseSmartBreaks( true ),
40  mMaxBreakDistance( 10 ),
41  mExpressionFeature( 0 ),
42  mExpressionLayer( 0 ),
43  mEnableUserStylesheet( false )
44 {
46  mWebPage = new QWebPage();
47  mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
48  QObject::connect( mWebPage, SIGNAL( loadFinished( bool ) ), this, SLOT( frameLoaded( bool ) ) );
49  if ( mComposition )
50  {
51  QObject::connect( mComposition, SIGNAL( itemRemoved( QgsComposerItem* ) ), this, SLOT( handleFrameRemoval( QgsComposerItem* ) ) );
52  }
53 
54  // data defined strings
55  mDataDefinedNames.insert( QgsComposerObject::SourceUrl, QString( "dataDefinedSourceUrl" ) );
56 
58  {
59  //a html item added while atlas preview is enabled needs to have the expression context set,
60  //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457)
62  }
63 
64  //connect to atlas feature changes
65  //to update the expression context
66  connect( &mComposition->atlasComposition(), SIGNAL( featureChanged( QgsFeature* ) ), this, SLOT( refreshExpressionContext() ) );
67 
68 }
69 
71  mContentMode( QgsComposerHtml::Url ),
72  mWebPage( 0 ),
73  mLoaded( false ),
74  mHtmlUnitsToMM( 1.0 ),
75  mRenderedPage( 0 ),
76  mUseSmartBreaks( true ),
77  mMaxBreakDistance( 10 ),
78  mExpressionFeature( 0 ),
79  mExpressionLayer( 0 )
80 {
81 }
82 
84 {
85  delete mWebPage;
86  delete mRenderedPage;
87 }
88 
89 void QgsComposerHtml::setUrl( const QUrl& url )
90 {
91  if ( !mWebPage )
92  {
93  return;
94  }
95 
96  mUrl = url;
97  loadHtml();
98 }
99 
100 void QgsComposerHtml::setHtml( const QString html )
101 {
102  mHtml = html;
103 }
104 
105 void QgsComposerHtml::setEvaluateExpressions( bool evaluateExpressions )
106 {
108  loadHtml();
109 }
110 
111 QString QgsComposerHtml::fetchHtml( QUrl url )
112 {
113  QUrl nextUrlToFetch = url;
114  QNetworkReply* reply = 0;
115 
116  //loop until fetched valid html
117  while ( 1 )
118  {
119  //set contents
120  QNetworkRequest request( nextUrlToFetch );
121  reply = QgsNetworkAccessManager::instance()->get( request );
122  connect( reply, SIGNAL( finished() ), this, SLOT( frameLoaded() ) );
123  //pause until HTML fetch
124  mLoaded = false;
125  while ( !mLoaded )
126  {
127  qApp->processEvents();
128  }
129 
130  if ( reply->error() != QNetworkReply::NoError )
131  {
132  QgsMessageLog::logMessage( tr( "HTML fetch %1 failed with error %2" ).arg( reply->url().toString() ).arg( reply->errorString() ) );
133  reply->deleteLater();
134  return QString();
135  }
136 
137  QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
138  if ( redirect.isNull() )
139  {
140  //no error or redirect, got target
141  break;
142  }
143 
144  //redirect, so fetch redirect target
145  nextUrlToFetch = redirect.toUrl();
146  reply->deleteLater();
147  }
148 
149  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
150  if ( !status.isNull() && status.toInt() >= 400 )
151  {
152  QgsMessageLog::logMessage( tr( "HTML fetch %1 failed with error %2" ).arg( reply->url().toString() ).arg( status.toString() ) );
153  reply->deleteLater();
154  return QString();
155  }
156 
157  QByteArray array = reply->readAll();
158  reply->deleteLater();
159  mFetchedHtml = QString( array );
160  return mFetchedHtml;
161 }
162 
164 {
165  if ( !mWebPage )
166  {
167  return;
168  }
169 
170  QString loadedHtml;
171  switch ( mContentMode )
172  {
174  {
175 
176  QString currentUrl = mUrl.toString();
177 
178  //data defined url set?
179  QVariant exprVal;
181  {
182  currentUrl = exprVal.toString().trimmed();;
183  QgsDebugMsg( QString( "exprVal Source Url:%1" ).arg( currentUrl ) );
184  }
185  if ( currentUrl.isEmpty() )
186  {
187  return;
188  }
189  if ( currentUrl != mLastFetchedUrl )
190  {
191  loadedHtml = fetchHtml( QUrl( currentUrl ) );
192  mLastFetchedUrl = currentUrl;
193  }
194  else
195  {
196  loadedHtml = mFetchedHtml;
197  }
198  break;
199  }
201  loadedHtml = mHtml;
202  break;
203  }
204 
205  //evaluate expressions
206  if ( mEvaluateExpressions )
207  {
209  }
210 
211  mLoaded = false;
212  //set html, using the specified url as base if in Url mode
213  mWebPage->mainFrame()->setHtml( loadedHtml, mContentMode == QgsComposerHtml::Url ? QUrl( mLastFetchedUrl ) : QUrl() );
214 
215  //set user stylesheet
216  QWebSettings* settings = mWebPage->settings();
217  if ( mEnableUserStylesheet && ! mUserStylesheet.isEmpty() )
218  {
219  QByteArray ba;
220  ba.append( mUserStylesheet.toUtf8() );
221  QUrl cssFileURL = QUrl( "data:text/css;charset=utf-8;base64," + ba.toBase64() );
222  settings->setUserStyleSheetUrl( cssFileURL );
223  }
224  else
225  {
226  settings->setUserStyleSheetUrl( QUrl() );
227  }
228 
229  while ( !mLoaded )
230  {
231  qApp->processEvents();
232  }
233 
234  if ( frameCount() < 1 ) return;
235 
236  QSize contentsSize = mWebPage->mainFrame()->contentsSize();
237 
238  //find maximum frame width
239  double maxFrameWidth = 0;
240  QList<QgsComposerFrame*>::const_iterator frameIt = mFrameItems.constBegin();
241  for ( ; frameIt != mFrameItems.constEnd(); ++frameIt )
242  {
243  maxFrameWidth = qMax( maxFrameWidth, ( *frameIt )->boundingRect().width() );
244  }
245  //set content width to match maximum frame width
246  contentsSize.setWidth( maxFrameWidth * mHtmlUnitsToMM );
247 
248  mWebPage->setViewportSize( contentsSize );
249  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
250  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Vertical, Qt::ScrollBarAlwaysOff );
251  mSize.setWidth( contentsSize.width() / mHtmlUnitsToMM );
252  mSize.setHeight( contentsSize.height() / mHtmlUnitsToMM );
253 
255 
257  emit changed();
258  //trigger a repaint
259  emit contentsChanged();
260 }
261 
263 {
264  Q_UNUSED( ok );
265  mLoaded = true;
266 }
267 
269 {
270  //render page to cache image
271  if ( mRenderedPage )
272  {
273  delete mRenderedPage;
274  }
275  mRenderedPage = new QImage( mWebPage->viewportSize(), QImage::Format_ARGB32 );
276  QPainter painter;
277  painter.begin( mRenderedPage );
278  mWebPage->mainFrame()->render( &painter );
279  painter.end();
280 }
281 
283 {
284  return mSize;
285 }
286 
287 void QgsComposerHtml::render( QPainter* p, const QRectF& renderExtent )
288 {
289  if ( !mWebPage )
290  {
291  return;
292  }
293 
294  p->save();
295  p->setRenderHint( QPainter::Antialiasing );
296  p->scale( 1.0 / mHtmlUnitsToMM, 1.0 / mHtmlUnitsToMM );
297  p->translate( 0.0, -renderExtent.top() * mHtmlUnitsToMM );
298  mWebPage->mainFrame()->render( p, QRegion( renderExtent.left(), renderExtent.top() * mHtmlUnitsToMM, renderExtent.width() * mHtmlUnitsToMM, renderExtent.height() * mHtmlUnitsToMM ) );
299  p->restore();
300 }
301 
303 {
304  if ( !mComposition )
305  {
306  return 1.0;
307  }
308 
309  return ( mComposition->printResolution() / 72.0 ); //webkit seems to assume a standard dpi of 96
310 }
311 
312 void QgsComposerHtml::addFrame( QgsComposerFrame* frame, bool recalcFrameSizes )
313 {
314  mFrameItems.push_back( frame );
315  QObject::connect( frame, SIGNAL( sizeChanged() ), this, SLOT( recalculateFrameSizes() ) );
316  if ( mComposition )
317  {
318  mComposition->addComposerHtmlFrame( this, frame );
319  }
320 
321  if ( recalcFrameSizes )
322  {
324  }
325 }
326 
327 bool candidateSort( const QPair<int, int> &c1, const QPair<int, int> &c2 )
328 {
329  if ( c1.second < c2.second )
330  return true;
331  else if ( c1.second > c2.second )
332  return false;
333  else if ( c1.first > c2.first )
334  return true;
335  else
336  return false;
337 }
338 
340 {
341  if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
342  {
343  return yPos;
344  }
345 
346  //convert yPos to pixels
347  int idealPos = yPos * htmlUnitsToMM();
348 
349  //if ideal break pos is past end of page, there's nothing we need to do
350  if ( idealPos >= mRenderedPage->height() )
351  {
352  return yPos;
353  }
354 
355  int maxSearchDistance = mMaxBreakDistance * htmlUnitsToMM();
356 
357  //loop through all lines just before ideal break location, up to max distance
358  //of maxSearchDistance
359  int changes = 0;
360  QRgb currentColor;
361  QRgb pixelColor;
362  QList< QPair<int, int> > candidates;
363  int minRow = qMax( idealPos - maxSearchDistance, 0 );
364  for ( int candidateRow = idealPos; candidateRow >= minRow; --candidateRow )
365  {
366  changes = 0;
367  currentColor = qRgba( 0, 0, 0, 0 );
368  //check all pixels in this line
369  for ( int col = 0; col < mRenderedPage->width(); ++col )
370  {
371  //count how many times the pixels change color in this row
372  //eventually, we select a row to break at with the minimum number of color changes
373  //since this is likely a line break, or gap between table cells, etc
374  //but very unlikely to be midway through a text line or picture
375  pixelColor = mRenderedPage->pixel( col, candidateRow );
376  if ( pixelColor != currentColor )
377  {
378  //color has changed
379  currentColor = pixelColor;
380  changes++;
381  }
382  }
383  candidates.append( qMakePair( candidateRow, changes ) );
384  }
385 
386  //sort candidate rows by number of changes ascending, row number descending
387  qSort( candidates.begin(), candidates.end(), candidateSort );
388  //first candidate is now the largest row with smallest number of changes
389 
390  //ok, now take the mid point of the best candidate position
391  //we do this so that the spacing between text lines is likely to be split in half
392  //otherwise the html will be broken immediately above a line of text, which
393  //looks a little messy
394  int maxCandidateRow = candidates[0].first;
395  int minCandidateRow = maxCandidateRow + 1;
396  int minCandidateChanges = candidates[0].second;
397 
398  QList< QPair<int, int> >::iterator it;
399  for ( it = candidates.begin(); it != candidates.end(); ++it )
400  {
401  if (( *it ).second != minCandidateChanges || ( *it ).first != minCandidateRow - 1 )
402  {
403  //no longer in a consecutive block of rows of minimum pixel color changes
404  //so return the row mid-way through the block
405  //first converting back to mm
406  return ( minCandidateRow + ( maxCandidateRow - minCandidateRow ) / 2 ) / htmlUnitsToMM();
407  }
408  minCandidateRow = ( *it ).first;
409  }
410 
411  //above loop didn't work for some reason
412  //return first candidate converted to mm
413  return candidates[0].first / htmlUnitsToMM();
414 }
415 
416 void QgsComposerHtml::setUseSmartBreaks( bool useSmartBreaks )
417 {
420  emit changed();
421 }
422 
423 void QgsComposerHtml::setMaxBreakDistance( double maxBreakDistance )
424 {
427  emit changed();
428 }
429 
430 void QgsComposerHtml::setUserStylesheet( const QString stylesheet )
431 {
432  mUserStylesheet = stylesheet;
433 }
434 
435 void QgsComposerHtml::setUserStylesheetEnabled( const bool stylesheetEnabled )
436 {
437  if ( mEnableUserStylesheet != stylesheetEnabled )
438  {
439  mEnableUserStylesheet = stylesheetEnabled;
440  loadHtml();
441  }
442 }
443 
444 bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool ignoreFrames ) const
445 {
446  QDomElement htmlElem = doc.createElement( "ComposerHtml" );
447  htmlElem.setAttribute( "contentMode", QString::number(( int ) mContentMode ) );
448  htmlElem.setAttribute( "url", mUrl.toString() );
449  htmlElem.setAttribute( "html", mHtml );
450  htmlElem.setAttribute( "evaluateExpressions", mEvaluateExpressions ? "true" : "false" );
451  htmlElem.setAttribute( "useSmartBreaks", mUseSmartBreaks ? "true" : "false" );
452  htmlElem.setAttribute( "maxBreakDistance", QString::number( mMaxBreakDistance ) );
453  htmlElem.setAttribute( "stylesheet", mUserStylesheet );
454  htmlElem.setAttribute( "stylesheetEnabled", mEnableUserStylesheet ? "true" : "false" );
455 
456  bool state = _writeXML( htmlElem, doc, ignoreFrames );
457  elem.appendChild( htmlElem );
458  return state;
459 }
460 
461 bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument& doc, bool ignoreFrames )
462 {
463  deleteFrames();
464 
465  //first create the frames
466  if ( !_readXML( itemElem, doc, ignoreFrames ) )
467  {
468  return false;
469  }
470 
471  bool contentModeOK;
472  mContentMode = ( QgsComposerHtml::ContentMode )itemElem.attribute( "contentMode" ).toInt( &contentModeOK );
473  if ( !contentModeOK )
474  {
476  }
477  mEvaluateExpressions = itemElem.attribute( "evaluateExpressions", "true" ) == "true" ? true : false;
478  mUseSmartBreaks = itemElem.attribute( "useSmartBreaks", "true" ) == "true" ? true : false;
479  mMaxBreakDistance = itemElem.attribute( "maxBreakDistance", "10" ).toDouble();
480  mHtml = itemElem.attribute( "html" );
481  mUserStylesheet = itemElem.attribute( "stylesheet" );
482  mEnableUserStylesheet = itemElem.attribute( "stylesheetEnabled", "false" ) == "true" ? true : false;
483 
484  //finally load the set url
485  QString urlString = itemElem.attribute( "url" );
486  if ( !urlString.isEmpty() )
487  {
488  mUrl = urlString;
489  }
490  loadHtml();
491 
492  //since frames had to be created before, we need to emit a changed signal to refresh the widget
493  emit changed();
494  return true;
495 }
496 
498 {
499  mExpressionFeature = feature;
500  mExpressionLayer = layer;
501 }
502 
504 {
505  QgsVectorLayer * vl = 0;
506  QgsFeature* feature = 0;
507 
509  {
511  }
513  {
515  }
516 
517  setExpressionContext( feature, vl );
518  loadHtml();
519 }
520 
522 {
523  //updates data defined properties and redraws item to match
524  if ( property == QgsComposerObject::SourceUrl || property == QgsComposerObject::AllProperties )
525  {
526  loadHtml();
527  }
529 }
QWebPage * mWebPage
void recalculateFrameSizes()
Recalculates the portion of the multiframe item which is shown in each of it's component frames...
double findNearbyPageBreak(double yPos)
Finds the optimal position to break a frame at.
QgsComposition::AtlasMode atlasMode() const
Returns the current atlas mode of the composition.
#define QgsDebugMsg(str)
Definition: qgslogger.h:36
void setHtml(const QString html)
Sets the HTML to display in the item when the item is using the QgsComposerHtml::ManualHtml mode...
void setUserStylesheet(const QString stylesheet)
Sets the user stylesheet CSS rules to use while rendering the HTML content.
QMap< QgsComposerObject::DataDefinedProperty, QString > mDataDefinedNames
Map of data defined properties for the item to string name to use when exporting item to xml...
A item that forms part of a map composition.
bool enabled() const
Returns whether the atlas generation is enabled.
void setExpressionContext(QgsFeature *feature, QgsVectorLayer *layer)
Sets the current feature, the current layer and a list of local variable substitutions for evaluating...
void setEvaluateExpressions(bool evaluateExpressions)
Sets whether the html item will evaluate QGIS expressions prior to rendering the HTML content...
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:113
void frameLoaded(bool ok=true)
DataDefinedProperty
Data defined properties for different item types.
bool dataDefinedEvaluate(const QgsComposerObject::DataDefinedProperty property, QVariant &expressionValue)
Evaluate a data defined property and return the calculated value.
bool useSmartBreaks() const
Returns whether html item is using smart breaks.
static void logMessage(QString message, QString tag=QString::null, MessageLevel level=WARNING)
add a message to the instance (and create it if necessary)
QString html() const
Returns the HTML source displayed in the item if the item is using the QgsComposerHtml::ManualHtml mo...
bool _readXML(const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames=false)
int printResolution() const
QImage * mRenderedPage
Abstract base class for composer entries with the ability to distribute the content to several frames...
QString fetchHtml(QUrl url)
QList< QgsComposerFrame * > mFrameItems
bool _writeXML(QDomElement &elem, QDomDocument &doc, bool ignoreFrames=false) const
void setMaxBreakDistance(double maxBreakDistance)
Sets the maximum distance allowed when calculating where to place page breaks in the html...
bool evaluateExpressions() const
Returns whether html item will evaluate QGIS expressions prior to rendering the HTML content...
void setUseSmartBreaks(bool useSmartBreaks)
Sets whether the html item should use smart breaks.
int frameCount() const
Return the number of frames associated with this multiframeset.
Graphics scene for map printing.
QgsFeature * currentFeature()
Returns the current atlas feature.
Frame for html, table, text which can be divided onto several frames.
virtual void refreshDataDefinedProperty(const QgsComposerObject::DataDefinedProperty property=QgsComposerObject::AllProperties)
const QUrl & url() const
Returns the URL of the content displayed in the item if the item is using the QgsComposerHtml::Url mo...
void loadHtml()
Reloads the html source from the url and redraws the item.
QgsComposition * mComposition
void deleteFrames()
Removes and deletes all frames from mComposition.
QgsVectorLayer * mExpressionLayer
ContentMode mContentMode
void refreshExpressionContext()
void contentsChanged()
Emitted when the contents of the multi frame have changed and the frames must be redrawn.
static QgsNetworkAccessManager * instance()
returns a pointer to the single instance
bool writeXML(QDomElement &elem, QDomDocument &doc, bool ignoreFrames=false) const
double maxBreakDistance() const
Returns the maximum distance allowed when calculating where to place page breaks in the html...
void addFrame(QgsComposerFrame *frame, bool recalcFrameSizes=true)
void addComposerHtmlFrame(QgsComposerHtml *html, QgsComposerFrame *frame)
Adds composer html frame and advices composer to create a widget for it (through signal) ...
void setUrl(const QUrl &url)
Sets the URL for content to display in the item when the item is using the QgsComposerHtml::Url mode...
QSizeF totalSize() const
QgsFeature * mExpressionFeature
QgsAtlasComposition & atlasComposition()
void render(QPainter *p, const QRectF &renderExtent)
QgsVectorLayer * coverageLayer() const
Returns the coverage layer used for the atlas features.
void setUserStylesheetEnabled(const bool stylesheetEnabled)
Sets whether user stylesheets are enabled for the HTML content.
Represents a vector layer which manages a vector based data sets.
ContentMode
Source modes for the HTML content to render in the item.
void handleFrameRemoval(QgsComposerItem *item)
Called before a frame is going to be removed (update frame list)
bool candidateSort(const QPair< int, int > &c1, const QPair< int, int > &c2)
virtual void refreshDataDefinedProperty(const DataDefinedProperty property=AllProperties)
Refreshes a data defined property for the item by reevaluating the property's value and redrawing the...
static QString replaceExpressionText(const QString &action, const QgsFeature *feat, QgsVectorLayer *layer, const QMap< QString, QVariant > *substitutionMap=0)
This function currently replaces each expression between [% and %] in the string with the result of i...
bool readXML(const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames=false)
#define tr(sourceText)