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