QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgssvgcache.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgssvgcache.h
3  ------------------------------
4  begin : 2011
5  copyright : (C) 2011 by Marco Hugentobler
6  email : marco dot hugentobler at sourcepole dot ch
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include "qgssvgcache.h"
19 #include "qgis.h"
20 #include "qgslogger.h"
22 #include "qgsmessagelog.h"
23 #include "qgssymbollayerv2utils.h"
24 
25 #include <QApplication>
26 #include <QCoreApplication>
27 #include <QCursor>
28 #include <QDomDocument>
29 #include <QDomElement>
30 #include <QFile>
31 #include <QImage>
32 #include <QPainter>
33 #include <QPicture>
34 #include <QSvgRenderer>
35 #include <QFileInfo>
36 #include <QNetworkReply>
37 #include <QNetworkRequest>
38 
39 QgsSvgCacheEntry::QgsSvgCacheEntry(): file( QString() ), size( 0.0 ), outlineWidth( 0 ), widthScaleFactor( 1.0 ), rasterScaleFactor( 1.0 ), fill( Qt::black ),
40  outline( Qt::black ), image( 0 ), picture( 0 ), nextEntry( 0 ), previousEntry( 0 )
41 {
42 }
43 
44 QgsSvgCacheEntry::QgsSvgCacheEntry( const QString& f, double s, double ow, double wsf, double rsf, const QColor& fi, const QColor& ou ): file( f ), size( s ), outlineWidth( ow ),
45  widthScaleFactor( wsf ), rasterScaleFactor( rsf ), fill( fi ), outline( ou ), image( 0 ), picture( 0 ), nextEntry( 0 ), previousEntry( 0 )
46 {
47 }
48 
49 
51 {
52  delete image;
53  delete picture;
54 }
55 
57 {
58  return ( other.file == file && other.size == size && other.outlineWidth == outlineWidth && other.widthScaleFactor == widthScaleFactor
59  && other.rasterScaleFactor == rasterScaleFactor && other.fill == fill && other.outline == outline );
60 }
61 
63 {
64  int size = svgContent.size();
65  if ( picture )
66  {
67  size += picture->size();
68  }
69  if ( image )
70  {
71  size += ( image->width() * image->height() * 32 );
72  }
73  return size;
74 }
75 
76 QString file;
77 double size;
78 double outlineWidth;
81 QColor fill;
82 QColor outline;
83 
85 {
86  static QgsSvgCache mInstance;
87  return &mInstance;
88 }
89 
90 QgsSvgCache::QgsSvgCache( QObject *parent )
91  : QObject( parent )
92  , mTotalSize( 0 )
93  , mLeastRecentEntry( 0 )
94  , mMostRecentEntry( 0 )
95 {
96  mMissingSvg = QString( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toAscii();
97 }
98 
100 {
101  QMultiHash< QString, QgsSvgCacheEntry* >::iterator it = mEntryLookup.begin();
102  for ( ; it != mEntryLookup.end(); ++it )
103  {
104  delete it.value();
105  }
106 }
107 
108 
109 const QImage& QgsSvgCache::svgAsImage( const QString& file, double size, const QColor& fill, const QColor& outline, double outlineWidth,
110  double widthScaleFactor, double rasterScaleFactor, bool& fitsInCache )
111 {
112  QMutexLocker locker( &mMutex );
113 
114  fitsInCache = true;
115  QgsSvgCacheEntry* currentEntry = cacheEntry( file, size, fill, outline, outlineWidth, widthScaleFactor, rasterScaleFactor );
116 
117  //if current entry image is 0: cache image for entry
118  // checks to see if image will fit into cache
119  //update stats for memory usage
120  if ( !currentEntry->image )
121  {
122  QSvgRenderer r( currentEntry->svgContent );
123  double hwRatio = 1.0;
124  if ( r.viewBoxF().width() > 0 )
125  {
126  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
127  }
128  long cachedDataSize = 0;
129  cachedDataSize += currentEntry->svgContent.size();
130  cachedDataSize += ( int )( currentEntry->size * currentEntry->size * hwRatio * 32 );
131  if ( cachedDataSize > mMaximumSize / 2 )
132  {
133  fitsInCache = false;
134  delete currentEntry->image;
135  currentEntry->image = 0;
136  //currentEntry->image = new QImage( 0, 0 );
137 
138  // instead cache picture
139  if ( !currentEntry->picture )
140  {
141  cachePicture( currentEntry, false );
142  }
143  }
144  else
145  {
146  cacheImage( currentEntry );
147  }
149  }
150 
151  return *( currentEntry->image );
152 }
153 
154 const QPicture& QgsSvgCache::svgAsPicture( const QString& file, double size, const QColor& fill, const QColor& outline, double outlineWidth,
155  double widthScaleFactor, double rasterScaleFactor, bool forceVectorOutput )
156 {
157  QMutexLocker locker( &mMutex );
158 
159  QgsSvgCacheEntry* currentEntry = cacheEntry( file, size, fill, outline, outlineWidth, widthScaleFactor, rasterScaleFactor );
160 
161  //if current entry picture is 0: cache picture for entry
162  //update stats for memory usage
163  if ( !currentEntry->picture )
164  {
165  cachePicture( currentEntry, forceVectorOutput );
167  }
168 
169  return *( currentEntry->picture );
170 }
171 
172 QgsSvgCacheEntry* QgsSvgCache::insertSVG( const QString& file, double size, const QColor& fill, const QColor& outline, double outlineWidth,
173  double widthScaleFactor, double rasterScaleFactor )
174 {
175  // The file may be relative path (e.g. if path is data defined)
176  QString path = QgsSymbolLayerV2Utils::symbolNameToPath( file );
177 
178  QgsSvgCacheEntry* entry = new QgsSvgCacheEntry( path, size, outlineWidth, widthScaleFactor, rasterScaleFactor, fill, outline );
179 
180  replaceParamsAndCacheSvg( entry );
181 
182  mEntryLookup.insert( file, entry );
183 
184  //insert to most recent place in entry list
185  if ( !mMostRecentEntry ) //inserting first entry
186  {
187  mLeastRecentEntry = entry;
188  mMostRecentEntry = entry;
189  entry->previousEntry = 0;
190  entry->nextEntry = 0;
191  }
192  else
193  {
194  entry->previousEntry = mMostRecentEntry;
195  entry->nextEntry = 0;
196  mMostRecentEntry->nextEntry = entry;
197  mMostRecentEntry = entry;
198  }
199 
201  return entry;
202 }
203 
204 void QgsSvgCache::containsParams( const QString& path, bool& hasFillParam, QColor& defaultFillColor, bool& hasOutlineParam, QColor& defaultOutlineColor,
205  bool& hasOutlineWidthParam, double& defaultOutlineWidth ) const
206 {
207  defaultFillColor = QColor( Qt::black );
208  defaultOutlineColor = QColor( Qt::black );
209  defaultOutlineWidth = 1.0;
210 
211  QDomDocument svgDoc;
212  if ( !svgDoc.setContent( getImageData( path ) ) )
213  {
214  return;
215  }
216 
217  QDomElement docElem = svgDoc.documentElement();
218  containsElemParams( docElem, hasFillParam, defaultFillColor, hasOutlineParam, defaultOutlineColor, hasOutlineWidthParam, defaultOutlineWidth );
219 }
220 
222 {
223  if ( !entry )
224  {
225  return;
226  }
227 
228  QDomDocument svgDoc;
229  if ( !svgDoc.setContent( getImageData( entry->file ) ) )
230  {
231  return;
232  }
233 
234  //replace fill color, outline color, outline with in all nodes
235  QDomElement docElem = svgDoc.documentElement();
236  replaceElemParams( docElem, entry->fill, entry->outline, entry->outlineWidth );
237 
238  entry->svgContent = svgDoc.toByteArray();
239  mTotalSize += entry->svgContent.size();
240 }
241 
242 QByteArray QgsSvgCache::getImageData( const QString &path ) const
243 {
244  // is it a path to local file?
245  QFile svgFile( path );
246  if ( svgFile.exists() )
247  {
248  if ( svgFile.open( QIODevice::ReadOnly ) )
249  {
250  return svgFile.readAll();
251  }
252  else
253  {
254  return mMissingSvg;
255  }
256  }
257 
258  // maybe it's a url...
259  if ( !path.contains( "://" ) ) // otherwise short, relative SVG paths might be considered URLs
260  {
261  return mMissingSvg;
262  }
263 
264  QUrl svgUrl( path );
265  if ( !svgUrl.isValid() )
266  {
267  return mMissingSvg;
268  }
269 
270  // check whether it's a url pointing to a local file
271  if ( svgUrl.scheme().compare( "file", Qt::CaseInsensitive ) == 0 )
272  {
273  svgFile.setFileName( svgUrl.toLocalFile() );
274  if ( svgFile.exists() )
275  {
276  if ( svgFile.open( QIODevice::ReadOnly ) )
277  {
278  return svgFile.readAll();
279  }
280  }
281 
282  // not found...
283  return mMissingSvg;
284  }
285 
286  // the url points to a remote resource, download it!
287  QNetworkReply *reply = 0;
288 
289  // The following code blocks until the file is downloaded...
290  // TODO: use signals to get reply finished notification, in this moment
291  // it's executed while rendering.
292  while ( 1 )
293  {
294  QgsDebugMsg( QString( "get svg: %1" ).arg( svgUrl.toString() ) );
295  QNetworkRequest request( svgUrl );
296  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
297  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
298 
299  reply = QgsNetworkAccessManager::instance()->get( request );
300  connect( reply, SIGNAL( downloadProgress( qint64, qint64 ) ), this, SLOT( downloadProgress( qint64, qint64 ) ) );
301 
302  //emit statusChanged( tr( "Downloading svg." ) );
303 
304  // wait until the image download finished
305  // TODO: connect to the reply->finished() signal
306  while ( !reply->isFinished() )
307  {
308  QCoreApplication::processEvents( QEventLoop::ExcludeUserInputEvents, 500 );
309  }
310 
311  if ( reply->error() != QNetworkReply::NoError )
312  {
313  QgsMessageLog::logMessage( tr( "SVG request failed [error: %1 - url: %2]" ).arg( reply->errorString() ).arg( reply->url().toString() ), tr( "SVG" ) );
314 
315  reply->deleteLater();
316  return QByteArray();
317  }
318 
319  QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
320  if ( redirect.isNull() )
321  {
322  // neither network error nor redirection
323  // TODO: cache the image
324  break;
325  }
326 
327  // do a new request to the redirect url
328  svgUrl = redirect.toUrl();
329  reply->deleteLater();
330  }
331 
332  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
333  if ( !status.isNull() && status.toInt() >= 400 )
334  {
335  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
336  QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2]" ).arg( status.toInt() ).arg( phrase.toString() ), tr( "SVG" ) );
337 
338  reply->deleteLater();
339  return mMissingSvg;
340  }
341 
342  QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
343  QgsDebugMsg( "contentType: " + contentType );
344  if ( !contentType.startsWith( "image/svg+xml", Qt::CaseInsensitive ) )
345  {
346  reply->deleteLater();
347  return mMissingSvg;
348  }
349 
350  // read the image data
351  QByteArray ba = reply->readAll();
352  reply->deleteLater();
353 
354  return ba;
355 }
356 
358 {
359  if ( !entry )
360  {
361  return;
362  }
363 
364  delete entry->image;
365  entry->image = 0;
366 
367  QSvgRenderer r( entry->svgContent );
368  double hwRatio = 1.0;
369  if ( r.viewBoxF().width() > 0 )
370  {
371  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
372  }
373  double wSize = entry->size;
374  int wImgSize = ( int )wSize;
375  if ( wImgSize < 1 )
376  {
377  wImgSize = 1;
378  }
379  double hSize = wSize * hwRatio;
380  int hImgSize = ( int )hSize;
381  if ( hImgSize < 1 )
382  {
383  hImgSize = 1;
384  }
385  // cast double image sizes to int for QImage
386  QImage* image = new QImage( wImgSize, hImgSize, QImage::Format_ARGB32_Premultiplied );
387  image->fill( 0 ); // transparent background
388 
389  QPainter p( image );
390  if ( r.viewBoxF().width() == r.viewBoxF().height() )
391  {
392  r.render( &p );
393  }
394  else
395  {
396  QSizeF s( r.viewBoxF().size() );
397  s.scale( wSize, hSize, Qt::KeepAspectRatio );
398  QRectF rect(( wImgSize - s.width() ) / 2, ( hImgSize - s.height() ) / 2, s.width(), s.height() );
399  r.render( &p, rect );
400  }
401 
402  entry->image = image;
403  mTotalSize += ( image->width() * image->height() * 32 );
404 }
405 
406 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
407 {
408  Q_UNUSED( forceVectorOutput );
409  if ( !entry )
410  {
411  return;
412  }
413 
414  delete entry->picture;
415  entry->picture = 0;
416 
417  //correct QPictures dpi correction
418  QPicture* picture = new QPicture();
419  QRectF rect;
420  QSvgRenderer r( entry->svgContent );
421  double hwRatio = 1.0;
422  if ( r.viewBoxF().width() > 0 )
423  {
424  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
425  }
426 
427  double wSize = entry->size;
428  double hSize = wSize * hwRatio;
429  QSizeF s( r.viewBoxF().size() );
430  s.scale( wSize, hSize, Qt::KeepAspectRatio );
431  rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
432 
433  QPainter p( picture );
434  r.render( &p, rect );
435  entry->picture = picture;
436  mTotalSize += entry->picture->size();
437 }
438 
439 QgsSvgCacheEntry* QgsSvgCache::cacheEntry( const QString& file, double size, const QColor& fill, const QColor& outline, double outlineWidth,
440  double widthScaleFactor, double rasterScaleFactor )
441 {
442  //search entries in mEntryLookup
443  QgsSvgCacheEntry* currentEntry = 0;
444  QList<QgsSvgCacheEntry*> entries = mEntryLookup.values( file );
445 
446  QList<QgsSvgCacheEntry*>::iterator entryIt = entries.begin();
447  for ( ; entryIt != entries.end(); ++entryIt )
448  {
449  QgsSvgCacheEntry* cacheEntry = *entryIt;
450  if ( qgsDoubleNear( cacheEntry->size, size ) && cacheEntry->fill == fill && cacheEntry->outline == outline &&
451  cacheEntry->outlineWidth == outlineWidth && cacheEntry->widthScaleFactor == widthScaleFactor && cacheEntry->rasterScaleFactor == rasterScaleFactor )
452  {
453  currentEntry = cacheEntry;
454  break;
455  }
456  }
457 
458  //if not found: create new entry
459  //cache and replace params in svg content
460  if ( !currentEntry )
461  {
462  currentEntry = insertSVG( file, size, fill, outline, outlineWidth, widthScaleFactor, rasterScaleFactor );
463  }
464  else
465  {
466  takeEntryFromList( currentEntry );
467  if ( !mMostRecentEntry ) //list is empty
468  {
469  mMostRecentEntry = currentEntry;
470  mLeastRecentEntry = currentEntry;
471  }
472  else
473  {
474  mMostRecentEntry->nextEntry = currentEntry;
475  currentEntry->previousEntry = mMostRecentEntry;
476  currentEntry->nextEntry = 0;
477  mMostRecentEntry = currentEntry;
478  }
479  }
480 
481  //debugging
482  //printEntryList();
483 
484  return currentEntry;
485 }
486 
487 void QgsSvgCache::replaceElemParams( QDomElement& elem, const QColor& fill, const QColor& outline, double outlineWidth )
488 {
489  if ( elem.isNull() )
490  {
491  return;
492  }
493 
494  //go through attributes
495  QDomNamedNodeMap attributes = elem.attributes();
496  int nAttributes = attributes.count();
497  for ( int i = 0; i < nAttributes; ++i )
498  {
499  QDomAttr attribute = attributes.item( i ).toAttr();
500  //e.g. style="fill:param(fill);param(stroke)"
501  if ( attribute.name().compare( "style", Qt::CaseInsensitive ) == 0 )
502  {
503  //entries separated by ';'
504  QString newAttributeString;
505 
506  QStringList entryList = attribute.value().split( ';' );
507  QStringList::const_iterator entryIt = entryList.constBegin();
508  for ( ; entryIt != entryList.constEnd(); ++entryIt )
509  {
510  QStringList keyValueSplit = entryIt->split( ':' );
511  if ( keyValueSplit.size() < 2 )
512  {
513  continue;
514  }
515  QString key = keyValueSplit.at( 0 );
516  QString value = keyValueSplit.at( 1 );
517  if ( value.startsWith( "param(fill" ) )
518  {
519  value = fill.name();
520  }
521  else if ( value.startsWith( "param(outline)" ) )
522  {
523  value = outline.name();
524  }
525  else if ( value.startsWith( "param(outline-width)" ) )
526  {
527  value = QString::number( outlineWidth );
528  }
529 
530  if ( entryIt != entryList.constBegin() )
531  {
532  newAttributeString.append( ";" );
533  }
534  newAttributeString.append( key + ":" + value );
535  }
536  elem.setAttribute( attribute.name(), newAttributeString );
537  }
538  else
539  {
540  QString value = attribute.value();
541  if ( value.startsWith( "param(fill)" ) )
542  {
543  elem.setAttribute( attribute.name(), fill.name() );
544  }
545  else if ( value.startsWith( "param(outline)" ) )
546  {
547  elem.setAttribute( attribute.name(), outline.name() );
548  }
549  else if ( value.startsWith( "param(outline-width)" ) )
550  {
551  elem.setAttribute( attribute.name(), QString::number( outlineWidth ) );
552  }
553  }
554  }
555 
556  QDomNodeList childList = elem.childNodes();
557  int nChildren = childList.count();
558  for ( int i = 0; i < nChildren; ++i )
559  {
560  QDomElement childElem = childList.at( i ).toElement();
561  replaceElemParams( childElem, fill, outline, outlineWidth );
562  }
563 }
564 
565 void QgsSvgCache::containsElemParams( const QDomElement& elem, bool& hasFillParam, QColor& defaultFill, bool& hasOutlineParam, QColor& defaultOutline,
566  bool& hasOutlineWidthParam, double& defaultOutlineWidth ) const
567 {
568  if ( elem.isNull() )
569  {
570  return;
571  }
572 
573  //we already have all the information, no need to go deeper
574  if ( hasFillParam && hasOutlineParam && hasOutlineWidthParam )
575  {
576  return;
577  }
578 
579  //check this elements attribute
580  QDomNamedNodeMap attributes = elem.attributes();
581  int nAttributes = attributes.count();
582 
583  QStringList valueSplit;
584  for ( int i = 0; i < nAttributes; ++i )
585  {
586  QDomAttr attribute = attributes.item( i ).toAttr();
587  if ( attribute.name().compare( "style", Qt::CaseInsensitive ) == 0 )
588  {
589  //entries separated by ';'
590  QStringList entryList = attribute.value().split( ';' );
591  QStringList::const_iterator entryIt = entryList.constBegin();
592  for ( ; entryIt != entryList.constEnd(); ++entryIt )
593  {
594  QStringList keyValueSplit = entryIt->split( ':' );
595  if ( keyValueSplit.size() < 2 )
596  {
597  continue;
598  }
599  QString key = keyValueSplit.at( 0 );
600  QString value = keyValueSplit.at( 1 );
601  valueSplit = value.split( " " );
602  if ( !hasFillParam && value.startsWith( "param(fill)" ) )
603  {
604  hasFillParam = true;
605  if ( valueSplit.size() > 1 )
606  {
607  defaultFill = QColor( valueSplit.at( 1 ) );
608  }
609  }
610  else if ( !hasOutlineParam && value.startsWith( "param(outline)" ) )
611  {
612  hasOutlineParam = true;
613  if ( valueSplit.size() > 1 )
614  {
615  defaultOutline = QColor( valueSplit.at( 1 ) );
616  }
617  }
618  else if ( !hasOutlineWidthParam && value.startsWith( "param(outline-width)" ) )
619  {
620  hasOutlineWidthParam = true;
621  if ( valueSplit.size() > 1 )
622  {
623  defaultOutlineWidth = valueSplit.at( 1 ).toDouble();
624  }
625  }
626  }
627  }
628  else
629  {
630  QString value = attribute.value();
631  valueSplit = value.split( " " );
632  if ( !hasFillParam && value.startsWith( "param(fill)" ) )
633  {
634  hasFillParam = true;
635  if ( valueSplit.size() > 1 )
636  {
637  defaultFill = QColor( valueSplit.at( 1 ) );
638  }
639  }
640  else if ( !hasOutlineParam && value.startsWith( "param(outline)" ) )
641  {
642  hasOutlineParam = true;
643  if ( valueSplit.size() > 1 )
644  {
645  defaultOutline = QColor( valueSplit.at( 1 ) );
646  }
647  }
648  else if ( !hasOutlineWidthParam && value.startsWith( "param(outline-width)" ) )
649  {
650  hasOutlineWidthParam = true;
651  if ( valueSplit.size() > 1 )
652  {
653  defaultOutlineWidth = valueSplit.at( 1 ).toDouble();
654  }
655  }
656  }
657  }
658 
659  //pass it further to child items
660  QDomNodeList childList = elem.childNodes();
661  int nChildren = childList.count();
662  for ( int i = 0; i < nChildren; ++i )
663  {
664  QDomElement childElem = childList.at( i ).toElement();
665  containsElemParams( childElem, hasFillParam, defaultFill, hasOutlineParam, defaultOutline, hasOutlineWidthParam, defaultOutlineWidth );
666  }
667 }
668 
669 void QgsSvgCache::removeCacheEntry( QString s, QgsSvgCacheEntry* entry )
670 {
671  delete entry;
672  mEntryLookup.remove( s, entry );
673 }
674 
675 void QgsSvgCache::printEntryList()
676 {
677  QgsDebugMsg( "****************svg cache entry list*************************" );
678  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
679  QgsSvgCacheEntry* entry = mLeastRecentEntry;
680  while ( entry )
681  {
682  QgsDebugMsg( "***Entry:" );
683  QgsDebugMsg( "File:" + entry->file );
684  QgsDebugMsg( "Size:" + QString::number( entry->size ) );
685  QgsDebugMsg( "Width scale factor" + QString::number( entry->widthScaleFactor ) );
686  QgsDebugMsg( "Raster scale factor" + QString::number( entry->rasterScaleFactor ) );
687  entry = entry->nextEntry;
688  }
689 }
690 
692 {
693  //only one entry in cache
694  if ( mLeastRecentEntry == mMostRecentEntry )
695  {
696  return;
697  }
698  QgsSvgCacheEntry* entry = mLeastRecentEntry;
699  while ( entry && ( mTotalSize > mMaximumSize ) )
700  {
701  QgsSvgCacheEntry* bkEntry = entry;
702  entry = entry->nextEntry;
703 
704  takeEntryFromList( bkEntry );
705  mEntryLookup.remove( bkEntry->file, bkEntry );
706  mTotalSize -= bkEntry->dataSize();
707  delete bkEntry;
708  }
709 }
710 
712 {
713  if ( !entry )
714  {
715  return;
716  }
717 
718  if ( entry->previousEntry )
719  {
720  entry->previousEntry->nextEntry = entry->nextEntry;
721  }
722  else
723  {
724  mLeastRecentEntry = entry->nextEntry;
725  }
726  if ( entry->nextEntry )
727  {
728  entry->nextEntry->previousEntry = entry->previousEntry;
729  }
730  else
731  {
732  mMostRecentEntry = entry->previousEntry;
733  }
734 }
735 
736 void QgsSvgCache::downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
737 {
738  QString msg = tr( "%1 of %2 bytes of svg image downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QString( "unknown number of" ) : QString::number( bytesTotal ) );
739  QgsDebugMsg( msg );
740  emit statusChanged( msg );
741 }