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"
25 
26 #include <QCoreApplication>
27 #include <QPainter>
28 #include <QWebFrame>
29 #include <QWebPage>
30 #include <QImage>
31 #include <QNetworkReply>
32 
33 QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands ): QgsComposerMultiFrame( c, createUndoCommands ),
34  mContentMode( QgsComposerHtml::Url ),
35  mWebPage( 0 ),
36  mLoaded( false ),
37  mHtmlUnitsToMM( 1.0 ),
38  mRenderedPage( 0 ),
39  mEvaluateExpressions( true ),
40  mUseSmartBreaks( true ),
41  mMaxBreakDistance( 10 ),
42  mExpressionFeature( 0 ),
43  mExpressionLayer( 0 ),
44  mEnableUserStylesheet( false )
45 {
47  mWebPage = new QWebPage();
48  mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
49  QObject::connect( mWebPage, SIGNAL( loadFinished( bool ) ), this, SLOT( frameLoaded( bool ) ) );
50  if ( mComposition )
51  {
52  QObject::connect( mComposition, SIGNAL( itemRemoved( QgsComposerItem* ) ), this, SLOT( handleFrameRemoval( QgsComposerItem* ) ) );
53  }
54 
55  // data defined strings
56  mDataDefinedNames.insert( QgsComposerObject::SourceUrl, QString( "dataDefinedSourceUrl" ) );
57 
59  {
60  //a html item added while atlas preview is enabled needs to have the expression context set,
61  //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457)
63  }
64 
65  //connect to atlas feature changes
66  //to update the expression context
67  connect( &mComposition->atlasComposition(), SIGNAL( featureChanged( QgsFeature* ) ), this, SLOT( refreshExpressionContext() ) );
68 
69 }
70 
72  mContentMode( QgsComposerHtml::Url ),
73  mWebPage( 0 ),
74  mLoaded( false ),
75  mHtmlUnitsToMM( 1.0 ),
76  mRenderedPage( 0 ),
77  mUseSmartBreaks( true ),
78  mMaxBreakDistance( 10 ),
79  mExpressionFeature( 0 ),
80  mExpressionLayer( 0 )
81 {
82 }
83 
85 {
86  delete mWebPage;
87  delete mRenderedPage;
88 }
89 
90 void QgsComposerHtml::setUrl( const QUrl& url )
91 {
92  if ( !mWebPage )
93  {
94  return;
95  }
96 
97  mUrl = url;
98  loadHtml();
99 }
100 
101 void QgsComposerHtml::setHtml( const QString html )
102 {
103  mHtml = html;
104 }
105 
106 void QgsComposerHtml::setEvaluateExpressions( bool evaluateExpressions )
107 {
109  loadHtml();
110 }
111 
113 {
114  if ( !mWebPage )
115  {
116  return;
117  }
118 
119  QString loadedHtml;
120  switch ( mContentMode )
121  {
123  {
124 
125  QString currentUrl = mUrl.toString();
126 
127  //data defined url set?
128  QVariant exprVal;
130  {
131  currentUrl = exprVal.toString().trimmed();;
132  QgsDebugMsg( QString( "exprVal Source Url:%1" ).arg( currentUrl ) );
133  }
134  if ( currentUrl.isEmpty() )
135  {
136  return;
137  }
138  if ( currentUrl != mLastFetchedUrl )
139  {
140  loadedHtml = fetchHtml( QUrl( currentUrl ) );
141  mLastFetchedUrl = currentUrl;
142  }
143  else
144  {
145  loadedHtml = mFetchedHtml;
146  }
147 
148  break;
149  }
151  loadedHtml = mHtml;
152  break;
153  }
154 
155  //evaluate expressions
156  if ( mEvaluateExpressions )
157  {
159  }
160 
161  mLoaded = false;
162  //set html, using the specified url as base if in Url mode
163  mWebPage->mainFrame()->setHtml( loadedHtml, mContentMode == QgsComposerHtml::Url ? QUrl( mActualFetchedUrl ) : QUrl() );
164 
165  //set user stylesheet
166  QWebSettings* settings = mWebPage->settings();
167  if ( mEnableUserStylesheet && ! mUserStylesheet.isEmpty() )
168  {
169  QByteArray ba;
170  ba.append( mUserStylesheet.toUtf8() );
171  QUrl cssFileURL = QUrl( "data:text/css;charset=utf-8;base64," + ba.toBase64() );
172  settings->setUserStyleSheetUrl( cssFileURL );
173  }
174  else
175  {
176  settings->setUserStyleSheetUrl( QUrl() );
177  }
178 
179  while ( !mLoaded )
180  {
181  qApp->processEvents();
182  }
183 
184  if ( frameCount() < 1 ) return;
185 
186  QSize contentsSize = mWebPage->mainFrame()->contentsSize();
187 
188  //find maximum frame width
189  double maxFrameWidth = 0;
190  QList<QgsComposerFrame*>::const_iterator frameIt = mFrameItems.constBegin();
191  for ( ; frameIt != mFrameItems.constEnd(); ++frameIt )
192  {
193  maxFrameWidth = qMax( maxFrameWidth, ( *frameIt )->boundingRect().width() );
194  }
195  //set content width to match maximum frame width
196  contentsSize.setWidth( maxFrameWidth * mHtmlUnitsToMM );
197 
198  mWebPage->setViewportSize( contentsSize );
199  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
200  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Vertical, Qt::ScrollBarAlwaysOff );
201  mSize.setWidth( contentsSize.width() / mHtmlUnitsToMM );
202  mSize.setHeight( contentsSize.height() / mHtmlUnitsToMM );
203 
205 
207  emit changed();
208  //trigger a repaint
209  emit contentsChanged();
210 }
211 
213 {
214  Q_UNUSED( ok );
215  mLoaded = true;
216 }
217 
219 {
220  //render page to cache image
221  if ( mRenderedPage )
222  {
223  delete mRenderedPage;
224  }
225  mRenderedPage = new QImage( mWebPage->viewportSize(), QImage::Format_ARGB32 );
226  QPainter painter;
227  painter.begin( mRenderedPage );
228  mWebPage->mainFrame()->render( &painter );
229  painter.end();
230 }
231 
232 QString QgsComposerHtml::fetchHtml( QUrl url )
233 {
234  QgsNetworkContentFetcher fetcher;
235  //pause until HTML fetch
236  mLoaded = false;
237  fetcher.fetchContent( url );
238  connect( &fetcher, SIGNAL( finished() ), this, SLOT( frameLoaded() ) );
239 
240  while ( !mLoaded )
241  {
242  qApp->processEvents();
243  }
244 
245  mFetchedHtml = fetcher.contentAsString();
246  mActualFetchedUrl = fetcher.reply()->url().toString();
247  return mFetchedHtml;
248 }
249 
251 {
252  return mSize;
253 }
254 
255 void QgsComposerHtml::render( QPainter* p, const QRectF& renderExtent )
256 {
257  if ( !mWebPage )
258  {
259  return;
260  }
261 
262  p->save();
263  p->setRenderHint( QPainter::Antialiasing );
264  p->scale( 1.0 / mHtmlUnitsToMM, 1.0 / mHtmlUnitsToMM );
265  p->translate( 0.0, -renderExtent.top() * mHtmlUnitsToMM );
266  mWebPage->mainFrame()->render( p, QRegion( renderExtent.left(), renderExtent.top() * mHtmlUnitsToMM, renderExtent.width() * mHtmlUnitsToMM, renderExtent.height() * mHtmlUnitsToMM ) );
267  p->restore();
268 }
269 
271 {
272  if ( !mComposition )
273  {
274  return 1.0;
275  }
276 
277  return ( mComposition->printResolution() / 72.0 ); //webkit seems to assume a standard dpi of 96
278 }
279 
280 void QgsComposerHtml::addFrame( QgsComposerFrame* frame, bool recalcFrameSizes )
281 {
282  mFrameItems.push_back( frame );
283  QObject::connect( frame, SIGNAL( sizeChanged() ), this, SLOT( recalculateFrameSizes() ) );
284  if ( mComposition )
285  {
286  mComposition->addComposerHtmlFrame( this, frame );
287  }
288 
289  if ( recalcFrameSizes )
290  {
292  }
293 }
294 
295 bool candidateSort( const QPair<int, int> &c1, const QPair<int, int> &c2 )
296 {
297  if ( c1.second < c2.second )
298  return true;
299  else if ( c1.second > c2.second )
300  return false;
301  else if ( c1.first > c2.first )
302  return true;
303  else
304  return false;
305 }
306 
308 {
309  if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
310  {
311  return yPos;
312  }
313 
314  //convert yPos to pixels
315  int idealPos = yPos * htmlUnitsToMM();
316 
317  //if ideal break pos is past end of page, there's nothing we need to do
318  if ( idealPos >= mRenderedPage->height() )
319  {
320  return yPos;
321  }
322 
323  int maxSearchDistance = mMaxBreakDistance * htmlUnitsToMM();
324 
325  //loop through all lines just before ideal break location, up to max distance
326  //of maxSearchDistance
327  int changes = 0;
328  QRgb currentColor;
329  QRgb pixelColor;
330  QList< QPair<int, int> > candidates;
331  int minRow = qMax( idealPos - maxSearchDistance, 0 );
332  for ( int candidateRow = idealPos; candidateRow >= minRow; --candidateRow )
333  {
334  changes = 0;
335  currentColor = qRgba( 0, 0, 0, 0 );
336  //check all pixels in this line
337  for ( int col = 0; col < mRenderedPage->width(); ++col )
338  {
339  //count how many times the pixels change color in this row
340  //eventually, we select a row to break at with the minimum number of color changes
341  //since this is likely a line break, or gap between table cells, etc
342  //but very unlikely to be midway through a text line or picture
343  pixelColor = mRenderedPage->pixel( col, candidateRow );
344  if ( pixelColor != currentColor )
345  {
346  //color has changed
347  currentColor = pixelColor;
348  changes++;
349  }
350  }
351  candidates.append( qMakePair( candidateRow, changes ) );
352  }
353 
354  //sort candidate rows by number of changes ascending, row number descending
355  qSort( candidates.begin(), candidates.end(), candidateSort );
356  //first candidate is now the largest row with smallest number of changes
357 
358  //ok, now take the mid point of the best candidate position
359  //we do this so that the spacing between text lines is likely to be split in half
360  //otherwise the html will be broken immediately above a line of text, which
361  //looks a little messy
362  int maxCandidateRow = candidates[0].first;
363  int minCandidateRow = maxCandidateRow + 1;
364  int minCandidateChanges = candidates[0].second;
365 
366  QList< QPair<int, int> >::iterator it;
367  for ( it = candidates.begin(); it != candidates.end(); ++it )
368  {
369  if (( *it ).second != minCandidateChanges || ( *it ).first != minCandidateRow - 1 )
370  {
371  //no longer in a consecutive block of rows of minimum pixel color changes
372  //so return the row mid-way through the block
373  //first converting back to mm
374  return ( minCandidateRow + ( maxCandidateRow - minCandidateRow ) / 2 ) / htmlUnitsToMM();
375  }
376  minCandidateRow = ( *it ).first;
377  }
378 
379  //above loop didn't work for some reason
380  //return first candidate converted to mm
381  return candidates[0].first / htmlUnitsToMM();
382 }
383 
384 void QgsComposerHtml::setUseSmartBreaks( bool useSmartBreaks )
385 {
388  emit changed();
389 }
390 
391 void QgsComposerHtml::setMaxBreakDistance( double maxBreakDistance )
392 {
395  emit changed();
396 }
397 
398 void QgsComposerHtml::setUserStylesheet( const QString stylesheet )
399 {
400  mUserStylesheet = stylesheet;
401 }
402 
403 void QgsComposerHtml::setUserStylesheetEnabled( const bool stylesheetEnabled )
404 {
405  if ( mEnableUserStylesheet != stylesheetEnabled )
406  {
407  mEnableUserStylesheet = stylesheetEnabled;
408  loadHtml();
409  }
410 }
411 
413 {
414  return tr( "<html frame>" );
415 }
416 
417 bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool ignoreFrames ) const
418 {
419  QDomElement htmlElem = doc.createElement( "ComposerHtml" );
420  htmlElem.setAttribute( "contentMode", QString::number(( int ) mContentMode ) );
421  htmlElem.setAttribute( "url", mUrl.toString() );
422  htmlElem.setAttribute( "html", mHtml );
423  htmlElem.setAttribute( "evaluateExpressions", mEvaluateExpressions ? "true" : "false" );
424  htmlElem.setAttribute( "useSmartBreaks", mUseSmartBreaks ? "true" : "false" );
425  htmlElem.setAttribute( "maxBreakDistance", QString::number( mMaxBreakDistance ) );
426  htmlElem.setAttribute( "stylesheet", mUserStylesheet );
427  htmlElem.setAttribute( "stylesheetEnabled", mEnableUserStylesheet ? "true" : "false" );
428 
429  bool state = _writeXML( htmlElem, doc, ignoreFrames );
430  elem.appendChild( htmlElem );
431  return state;
432 }
433 
434 bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument& doc, bool ignoreFrames )
435 {
436  deleteFrames();
437 
438  //first create the frames
439  if ( !_readXML( itemElem, doc, ignoreFrames ) )
440  {
441  return false;
442  }
443 
444  bool contentModeOK;
445  mContentMode = ( QgsComposerHtml::ContentMode )itemElem.attribute( "contentMode" ).toInt( &contentModeOK );
446  if ( !contentModeOK )
447  {
449  }
450  mEvaluateExpressions = itemElem.attribute( "evaluateExpressions", "true" ) == "true" ? true : false;
451  mUseSmartBreaks = itemElem.attribute( "useSmartBreaks", "true" ) == "true" ? true : false;
452  mMaxBreakDistance = itemElem.attribute( "maxBreakDistance", "10" ).toDouble();
453  mHtml = itemElem.attribute( "html" );
454  mUserStylesheet = itemElem.attribute( "stylesheet" );
455  mEnableUserStylesheet = itemElem.attribute( "stylesheetEnabled", "false" ) == "true" ? true : false;
456 
457  //finally load the set url
458  QString urlString = itemElem.attribute( "url" );
459  if ( !urlString.isEmpty() )
460  {
461  mUrl = urlString;
462  }
463  loadHtml();
464 
465  //since frames had to be created before, we need to emit a changed signal to refresh the widget
466  emit changed();
467  return true;
468 }
469 
471 {
472  mExpressionFeature = feature;
473  mExpressionLayer = layer;
474 }
475 
477 {
478  QgsVectorLayer * vl = 0;
479  QgsFeature* feature = 0;
480 
482  {
484  }
486  {
488  }
489 
490  setExpressionContext( feature, vl );
491  loadHtml();
492 }
493 
495 {
496  //updates data defined properties and redraws item to match
497  if ( property == QgsComposerObject::SourceUrl || property == QgsComposerObject::AllProperties )
498  {
499  loadHtml();
500  }
502 }
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.
QString contentAsString() const
Returns the fetched content as a string.
#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 fetchContent(const QUrl url)
Fetches content from a remote URL and handles redirects.
void setExpressionContext(QgsFeature *feature, QgsVectorLayer *layer)
Sets the current feature, the current layer and a list of local variable substitutions for evaluating...
QNetworkReply * reply()
Returns a reference to the network reply.
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.
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)
HTTP network content fetcher.
int printResolution() const
QImage * mRenderedPage
virtual QString displayName() const
Get multiframe display name.
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.
QString mActualFetchedUrl
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)