QGIS API Documentation  2.4.0-Chugiak
 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 
22 #include <QCoreApplication>
23 #include <QPainter>
24 #include <QWebFrame>
25 #include <QWebPage>
26 #include <QImage>
27 
28 QgsComposerHtml::QgsComposerHtml( QgsComposition* c, bool createUndoCommands ): QgsComposerMultiFrame( c, createUndoCommands ),
29  mWebPage( 0 ),
30  mLoaded( false ),
31  mHtmlUnitsToMM( 1.0 ),
32  mRenderedPage( 0 ),
33  mUseSmartBreaks( true ),
34  mMaxBreakDistance( 10 )
35 {
37  mWebPage = new QWebPage();
38  mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
39  QObject::connect( mWebPage, SIGNAL( loadFinished( bool ) ), this, SLOT( frameLoaded( bool ) ) );
40  if ( mComposition )
41  {
42  QObject::connect( mComposition, SIGNAL( itemRemoved( QgsComposerItem* ) ), this, SLOT( handleFrameRemoval( QgsComposerItem* ) ) );
43  connect( mComposition, SIGNAL( refreshItemsTriggered() ), this, SLOT( loadHtml() ) );
44  }
45 }
46 
48  mWebPage( 0 ),
49  mLoaded( false ),
50  mHtmlUnitsToMM( 1.0 ),
51  mRenderedPage( 0 ),
52  mUseSmartBreaks( true ),
53  mMaxBreakDistance( 10 )
54 {
55 }
56 
58 {
59  delete mWebPage;
60  delete mRenderedPage;
61 }
62 
63 void QgsComposerHtml::setUrl( const QUrl& url )
64 {
65  if ( !mWebPage )
66  {
67  return;
68  }
69 
70  mUrl = url;
71  loadHtml();
72 }
73 
75 {
76  if ( !mWebPage || mUrl.isEmpty() )
77  {
78  return;
79  }
80 
81  mLoaded = false;
82  mWebPage->mainFrame()->load( mUrl );
83  while ( !mLoaded )
84  {
85  qApp->processEvents();
86  }
87 
88  if ( frameCount() < 1 ) return;
89 
90  QSize contentsSize = mWebPage->mainFrame()->contentsSize();
91  contentsSize.setWidth( mFrameItems.at( 0 )->boundingRect().width() * mHtmlUnitsToMM );
92  mWebPage->setViewportSize( contentsSize );
93  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Horizontal, Qt::ScrollBarAlwaysOff );
94  mWebPage->mainFrame()->setScrollBarPolicy( Qt::Vertical, Qt::ScrollBarAlwaysOff );
95  mSize.setWidth( contentsSize.width() / mHtmlUnitsToMM );
96  mSize.setHeight( contentsSize.height() / mHtmlUnitsToMM );
97 
99 
101  emit changed();
102 }
103 
105 {
106  Q_UNUSED( ok );
107  mLoaded = true;
108 }
109 
111 {
112  //render page to cache image
113  if ( mRenderedPage )
114  {
115  delete mRenderedPage;
116  }
117  mRenderedPage = new QImage( mWebPage->viewportSize(), QImage::Format_ARGB32 );
118  QPainter painter;
119  painter.begin( mRenderedPage );
120  mWebPage->mainFrame()->render( &painter );
121  painter.end();
122 }
123 
125 {
126  return mSize;
127 }
128 
129 void QgsComposerHtml::render( QPainter* p, const QRectF& renderExtent )
130 {
131  if ( !mWebPage )
132  {
133  return;
134  }
135 
136  p->save();
137  p->scale( 1.0 / mHtmlUnitsToMM, 1.0 / mHtmlUnitsToMM );
138  p->translate( 0.0, -renderExtent.top() * mHtmlUnitsToMM );
139  mWebPage->mainFrame()->render( p, QRegion( renderExtent.left(), renderExtent.top() * mHtmlUnitsToMM, renderExtent.width() * mHtmlUnitsToMM, renderExtent.height() * mHtmlUnitsToMM ) );
140  p->restore();
141 }
142 
144 {
145  if ( !mComposition )
146  {
147  return 1.0;
148  }
149 
150  return ( mComposition->printResolution() / 72.0 ); //webkit seems to assume a standard dpi of 96
151 }
152 
153 void QgsComposerHtml::addFrame( QgsComposerFrame* frame, bool recalcFrameSizes )
154 {
155  mFrameItems.push_back( frame );
156  QObject::connect( frame, SIGNAL( sizeChanged() ), this, SLOT( recalculateFrameSizes() ) );
157  if ( mComposition )
158  {
159  mComposition->addComposerHtmlFrame( this, frame );
160  }
161 
162  if ( recalcFrameSizes )
163  {
165  }
166 }
167 
168 bool candidateSort( const QPair<int, int> &c1, const QPair<int, int> &c2 )
169 {
170  if ( c1.second < c2.second )
171  return true;
172  else if ( c1.second > c2.second )
173  return false;
174  else if ( c1.first > c2.first )
175  return true;
176  else
177  return false;
178 }
179 
181 {
182  if ( !mWebPage || !mRenderedPage || !mUseSmartBreaks )
183  {
184  return yPos;
185  }
186 
187  //convert yPos to pixels
188  int idealPos = yPos * htmlUnitsToMM();
189 
190  //if ideal break pos is past end of page, there's nothing we need to do
191  if ( idealPos >= mRenderedPage->height() )
192  {
193  return yPos;
194  }
195 
196  int maxSearchDistance = mMaxBreakDistance * htmlUnitsToMM();
197 
198  //loop through all lines just before ideal break location, up to max distance
199  //of maxSearchDistance
200  int changes = 0;
201  QRgb currentColor;
202  QRgb pixelColor;
203  QList< QPair<int, int> > candidates;
204  int minRow = qMax( idealPos - maxSearchDistance, 0 );
205  for ( int candidateRow = idealPos; candidateRow >= minRow; --candidateRow )
206  {
207  changes = 0;
208  currentColor = qRgba( 0, 0, 0, 0 );
209  //check all pixels in this line
210  for ( int col = 0; col < mRenderedPage->width(); ++col )
211  {
212  //count how many times the pixels change color in this row
213  //eventually, we select a row to break at with the minimum number of color changes
214  //since this is likely a line break, or gap between table cells, etc
215  //but very unlikely to be midway through a text line or picture
216  pixelColor = mRenderedPage->pixel( col, candidateRow );
217  if ( pixelColor != currentColor )
218  {
219  //color has changed
220  currentColor = pixelColor;
221  changes++;
222  }
223  }
224  candidates.append( qMakePair( candidateRow, changes ) );
225  }
226 
227  //sort candidate rows by number of changes ascending, row number descending
228  qSort( candidates.begin(), candidates.end(), candidateSort );
229  //first candidate is now the largest row with smallest number of changes
230 
231  //ok, now take the mid point of the best candidate position
232  //we do this so that the spacing between text lines is likely to be split in half
233  //otherwise the html will be broken immediately above a line of text, which
234  //looks a little messy
235  int maxCandidateRow = candidates[0].first;
236  int minCandidateRow = maxCandidateRow + 1;
237  int minCandidateChanges = candidates[0].second;
238 
239  QList< QPair<int, int> >::iterator it;
240  for ( it = candidates.begin(); it != candidates.end(); ++it )
241  {
242  if (( *it ).second != minCandidateChanges || ( *it ).first != minCandidateRow - 1 )
243  {
244  //no longer in a consecutive block of rows of minimum pixel color changes
245  //so return the row mid-way through the block
246  //first converting back to mm
247  return ( minCandidateRow + ( maxCandidateRow - minCandidateRow ) / 2 ) / htmlUnitsToMM();
248  }
249  minCandidateRow = ( *it ).first;
250  }
251 
252  //above loop didn't work for some reason
253  //return first candidate converted to mm
254  return candidates[0].first / htmlUnitsToMM();
255 }
256 
257 void QgsComposerHtml::setUseSmartBreaks( bool useSmartBreaks )
258 {
261  emit changed();
262 }
263 
264 void QgsComposerHtml::setMaxBreakDistance( double maxBreakDistance )
265 {
268  emit changed();
269 }
270 
271 bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool ignoreFrames ) const
272 {
273  QDomElement htmlElem = doc.createElement( "ComposerHtml" );
274  htmlElem.setAttribute( "url", mUrl.toString() );
275  htmlElem.setAttribute( "useSmartBreaks", mUseSmartBreaks ? "true" : "false" );
276  htmlElem.setAttribute( "maxBreakDistance", QString::number( mMaxBreakDistance ) );
277 
278  bool state = _writeXML( htmlElem, doc, ignoreFrames );
279  elem.appendChild( htmlElem );
280  return state;
281 }
282 
283 bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument& doc, bool ignoreFrames )
284 {
285  deleteFrames();
286 
287  //first create the frames
288  if ( !_readXML( itemElem, doc, ignoreFrames ) )
289  {
290  return false;
291  }
292 
293  mUseSmartBreaks = itemElem.attribute( "useSmartBreaks", "true" ) == "true" ? true : false;
294  mMaxBreakDistance = itemElem.attribute( "maxBreakDistance", "10" ).toDouble();
295 
296  //finally load the set url
297  QString urlString = itemElem.attribute( "url" );
298  if ( !urlString.isEmpty() )
299  {
300  setUrl( QUrl( urlString ) );
301  }
302  return true;
303 }
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.
void frameLoaded(bool ok)
A item that forms part of a map composition.
bool useSmartBreaks() const
Returns whether html item is using smart breaks.
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...
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...
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.
Frame for html, table, text which can be divided onto several frames.
const QUrl & url() const
void loadHtml()
Reloads the html source from the url and redraws the item.
void deleteFrames()
Removes and deletes all frames from mComposition.
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)
QSizeF totalSize() const
void render(QPainter *p, const QRectF &renderExtent)
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)
bool readXML(const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames=false)