QGIS API Documentation  2.11.0-Master
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
qgsrenderchecker.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsrenderchecker.cpp
3  --------------------------------------
4  Date : 18 Jan 2008
5  Copyright : (C) 2008 by Tim Sutton
6  Email : tim @ linfiniti.com
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 "qgsrenderchecker.h"
17 
18 #include "qgis.h"
20 
21 #include <QColor>
22 #include <QPainter>
23 #include <QImage>
24 #include <QTime>
25 #include <QCryptographicHash>
26 #include <QByteArray>
27 #include <QDebug>
28 #include <QBuffer>
29 
30 static int renderCounter = 0;
31 
33  : mReport( "" )
34  , mMatchTarget( 0 )
35  , mElapsedTime( 0 )
36  , mRenderedImageFile( "" )
37  , mExpectedImageFile( "" )
38  , mMismatchCount( 0 )
39  , mColorTolerance( 0 )
40  , mElapsedTimeTarget( 0 )
41  , mBufferDashMessages( false )
42 {
43 }
44 
46 {
47  QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
48  QString myControlImageDir = myDataDir + "/control_images/" + mControlPathPrefix;
49  return myControlImageDir;
50 }
51 
53 {
54  mControlName = theName;
55  mExpectedImageFile = controlImagePath() + theName + "/" + mControlPathSuffix + theName + ".png";
56 }
57 
59 {
60  QImage myImage;
61  myImage.load( theImageFile );
62  QByteArray myByteArray;
63  QBuffer myBuffer( &myByteArray );
64  myImage.save( &myBuffer, "PNG" );
65  QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
66  QCryptographicHash myHash( QCryptographicHash::Md5 );
67  myHash.addData( myImageString.toUtf8() );
68  return myHash.result().toHex().constData();
69 }
70 
72 {
73  mMapSettings = thepMapRenderer->mapSettings();
74 }
75 
77 {
78  mMapSettings = mapSettings;
79 }
80 
82 {
83  // create a 2x2 checker-board image
84  uchar pixDataRGB[] = { 255, 255, 255, 255,
85  127, 127, 127, 255,
86  127, 127, 127, 255,
87  255, 255, 255, 255
88  };
89 
90  QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
91  QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
92 
93  // fill image with texture
94  QBrush brush;
95  brush.setTexture( pix );
96  QPainter p( image );
97  p.setRenderHint( QPainter::Antialiasing, false );
98  p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
99  p.end();
100 }
101 
103 {
104  QString myControlImageDir = controlImagePath() + mControlName + "/";
105  QDir myDirectory = QDir( myControlImageDir );
106  QStringList myList;
107  QString myFilename = "*";
108  myList = myDirectory.entryList( QStringList( myFilename ),
109  QDir::Files | QDir::NoSymLinks );
110  //remove the control file from the list as the anomalies are
111  //all files except the control file
112  myList.removeAt( myList.indexOf( QFileInfo( mExpectedImageFile ).fileName() ) );
113 
114  QString myImageHash = imageToHash( theDiffImageFile );
115 
116 
117  for ( int i = 0; i < myList.size(); ++i )
118  {
119  QString myFile = myList.at( i );
120  mReport += "<tr><td colspan=3>"
121  "Checking if " + myFile + " is a known anomaly.";
122  mReport += "</td></tr>";
123  QString myAnomalyHash = imageToHash( controlImagePath() + mControlName + "/" + myFile );
124  QString myHashMessage = QString(
125  "Checking if anomaly %1 (hash %2)<br>" )
126  .arg( myFile )
127  .arg( myAnomalyHash );
128  myHashMessage += QString( "&nbsp; matches %1 (hash %2)" )
129  .arg( theDiffImageFile )
130  .arg( myImageHash );
131  //foo CDash
132  emitDashMessage( "Anomaly check", QgsDartMeasurement::Text, myHashMessage );
133 
134  mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
135  if ( myImageHash == myAnomalyHash )
136  {
137  mReport += "<tr><td colspan=3>"
138  "Anomaly found! " + myFile;
139  mReport += "</td></tr>";
140  return true;
141  }
142  }
143  mReport += "<tr><td colspan=3>"
144  "No anomaly found! ";
145  mReport += "</td></tr>";
146  return false;
147 }
148 
149 void QgsRenderChecker::emitDashMessage( const QgsDartMeasurement& dashMessage )
150 {
151  if ( mBufferDashMessages )
152  mDashMessages << dashMessage;
153  else
154  dashMessage.send();
155 }
156 
157 void QgsRenderChecker::emitDashMessage( const QString& name, QgsDartMeasurement::Type type, const QString& value )
158 {
159  emitDashMessage( QgsDartMeasurement( name, type, value ) );
160 }
161 
163  unsigned int theMismatchCount )
164 {
165  if ( mExpectedImageFile.isEmpty() )
166  {
167  qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
168  mReport = "<table>"
169  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
170  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
171  "Image File not set.</td></tr></table>\n";
172  return false;
173  }
174  //
175  // Load the expected result pixmap
176  //
177  QImage myExpectedImage( mExpectedImageFile );
178  if ( myExpectedImage.isNull() )
179  {
180  qDebug() << "QgsRenderChecker::runTest failed - Could not load expected image from " << mExpectedImageFile;
181  mReport = "<table>"
182  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
183  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
184  "Image File could not be loaded.</td></tr></table>\n";
185  return false;
186  }
187  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
188  //
189  // Now render our layers onto a pixmap
190  //
191  mMapSettings.setBackgroundColor( qRgb( 152, 219, 249 ) );
192  mMapSettings.setFlag( QgsMapSettings::Antialiasing );
193  mMapSettings.setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) );
194 
195  QTime myTime;
196  myTime.start();
197 
198  QgsMapRendererSequentialJob job( mMapSettings );
199  job.start();
200  job.waitForFinished();
201 
202  mElapsedTime = myTime.elapsed();
203 
204  QImage myImage = job.renderedImage();
205 
206  //
207  // Save the pixmap to disk so the user can make a
208  // visual assessment if needed
209  //
210  mRenderedImageFile = QDir::tempPath() + "/" + theTestName + "_result.png";
211 
212  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
213  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
214  if ( ! myImage.save( mRenderedImageFile, "PNG", 100 ) )
215  {
216  qDebug() << "QgsRenderChecker::runTest failed - Could not save rendered image to " << mRenderedImageFile;
217  mReport = "<table>"
218  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
219  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
220  "Image File could not be saved.</td></tr></table>\n";
221  return false;
222  }
223 
224  //create a world file to go with the image...
225 
226  QFile wldFile( QDir::tempPath() + "/" + theTestName + "_result.wld" );
227  if ( wldFile.open( QIODevice::WriteOnly ) )
228  {
229  QgsRectangle r = mMapSettings.extent();
230 
231  QTextStream stream( &wldFile );
232  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
233  .arg( qgsDoubleToString( mMapSettings.mapUnitsPerPixel() ) )
234  .arg( qgsDoubleToString( -mMapSettings.mapUnitsPerPixel() ) )
235  .arg( qgsDoubleToString( r.xMinimum() + mMapSettings.mapUnitsPerPixel() / 2.0 ) )
236  .arg( qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
237  }
238 
239  return compareImages( theTestName, theMismatchCount );
240 }
241 
242 
244  unsigned int theMismatchCount,
245  QString theRenderedImageFile )
246 {
247  if ( mExpectedImageFile.isEmpty() )
248  {
249  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
250  mReport = "<table>"
251  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
252  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
253  "Image File not set.</td></tr></table>\n";
254  return false;
255  }
256  if ( ! theRenderedImageFile.isEmpty() )
257  {
258  mRenderedImageFile = theRenderedImageFile;
259  }
260  else if ( mRenderedImageFile.isEmpty() )
261  {
262  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
263  mReport = "<table>"
264  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
265  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
266  "Image File not set.</td></tr></table>\n";
267  return false;
268  }
269  //
270  // Load /create the images
271  //
272  QImage myExpectedImage( mExpectedImageFile );
273  QImage myResultImage( mRenderedImageFile );
274  if ( myResultImage.isNull() )
275  {
276  qDebug() << "QgsRenderChecker::runTest failed - Could not load rendered image from " << mRenderedImageFile;
277  mReport = "<table>"
278  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
279  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
280  "Image File could not be loaded.</td></tr></table>\n";
281  return false;
282  }
283  QImage myDifferenceImage( myExpectedImage.width(),
284  myExpectedImage.height(),
285  QImage::Format_RGB32 );
286  QString myDiffImageFile = QDir::tempPath() + "/" + theTestName + "_result_diff.png";
287  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
288 
289  //check for mask
290  QString maskImagePath = mExpectedImageFile;
291  maskImagePath.chop( 4 ); //remove .png extension
292  maskImagePath += "_mask.png";
293  QImage* maskImage = new QImage( maskImagePath );
294  bool hasMask = !maskImage->isNull();
295  if ( hasMask )
296  {
297  qDebug( "QgsRenderChecker using mask image" );
298  }
299 
300  //
301  // Set pixel count score and target
302  //
303  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
304  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
305  //
306  // Set the report with the result
307  //
308  mReport = QString( "<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
309  mReport += "<table>";
310  mReport += "<tr><td colspan=2>";
311  mReport += QString( "Test image and result image for %1<br>"
312  "Expected size: %2 w x %3 h (%4 pixels)<br>"
313  "Actual size: %5 w x %6 h (%7 pixels)" )
314  .arg( theTestName )
315  .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg( mMatchTarget )
316  .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
317 
318  mReport += "</td></tr>";
319  mReport += "<tr><td colspan=2>\n";
320  mReport += QString( "Expected Duration : <= %1 (0 indicates not specified)<br>"
321  "Actual Duration : %2 ms<br></td></tr>" )
322  .arg( mElapsedTimeTarget )
323  .arg( mElapsedTime );
324 
325  // limit image size in page to something reasonable
326  int imgWidth = 420;
327  int imgHeight = 280;
328  if ( ! myExpectedImage.isNull() )
329  {
330  imgWidth = qMin( myExpectedImage.width(), imgWidth );
331  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
332  }
333 
334  QString myImagesString = QString(
335  "<tr>"
336  "<td colspan=2>Compare actual and expected result</td>"
337  "<td>Difference (all blue is good, any red is bad)</td>"
338  "</tr>\n<tr>"
339  "<td colspan=2 id=\"td-%1-%7\"></td>\n"
340  "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n"
341  "</tr>"
342  "</table>\n"
343  "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
344  .arg( theTestName )
345  .arg( myDiffImageFile )
348  .arg( imgWidth ).arg( imgHeight )
349  .arg( renderCounter++ );
350 
351  QString prefix;
352  if ( !mControlPathPrefix.isNull() )
353  {
354  prefix = QString( " (prefix %1)" ).arg( mControlPathPrefix );
355  }
356  //
357  // To get the images into CDash
358  //
359  emitDashMessage( "Rendered Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
360  emitDashMessage( "Expected Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mExpectedImageFile );
361 
362  //
363  // Put the same info to debug too
364  //
365 
366  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
367  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
368 
369  if ( mMatchTarget != myPixelCount )
370  {
371  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
372  mReport += "<tr><td colspan=3>";
373  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
374  mReport += "</td></tr>";
375  mReport += myImagesString;
376  delete maskImage;
377  return false;
378  }
379 
380  //
381  // Now iterate through them counting how many
382  // dissimilar pixel values there are
383  //
384 
385  mMismatchCount = 0;
386  int colorTolerance = ( int ) mColorTolerance;
387  for ( int y = 0; y < myExpectedImage.height(); ++y )
388  {
389  const QRgb* expectedScanline = ( const QRgb* )myExpectedImage.constScanLine( y );
390  const QRgb* resultScanline = ( const QRgb* )myResultImage.constScanLine( y );
391  const QRgb* maskScanline = hasMask ? ( const QRgb* )maskImage->constScanLine( y ) : 0;
392  QRgb* diffScanline = ( QRgb* )myDifferenceImage.scanLine( y );
393 
394  for ( int x = 0; x < myExpectedImage.width(); ++x )
395  {
396  int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
397  int pixelTolerance = qMax( colorTolerance, maskTolerance );
398  if ( pixelTolerance == 255 )
399  {
400  //skip pixel
401  continue;
402  }
403 
404  QRgb myExpectedPixel = expectedScanline[x];
405  QRgb myActualPixel = resultScanline[x];
406  if ( pixelTolerance == 0 )
407  {
408  if ( myExpectedPixel != myActualPixel )
409  {
410  ++mMismatchCount;
411  diffScanline[ x ] = qRgb( 255, 0, 0 );
412  }
413  }
414  else
415  {
416  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
417  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
418  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
419  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
420  {
421  ++mMismatchCount;
422  diffScanline[ x ] = qRgb( 255, 0, 0 );
423  }
424  }
425  }
426  }
427  //
428  //save the diff image to disk
429  //
430  myDifferenceImage.save( myDiffImageFile );
431  emitDashMessage( "Difference Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, myDiffImageFile );
432  delete maskImage;
433 
434  //
435  // Send match result to debug
436  //
437  qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, theMismatchCount );
438 
439  //
440  // Send match result to report
441  //
442  mReport += "<tr><td colspan=3>" +
443  QString::number( mMismatchCount ) + "/" +
445  " pixels mismatched (allowed threshold: " +
446  QString::number( theMismatchCount ) +
447  ", allowed color component tolerance: " +
448  QString::number( mColorTolerance ) + ")";
449  mReport += "</td></tr>";
450 
451  //
452  // And send it to CDash
453  //
454  emitDashMessage( "Mismatch Count", QgsDartMeasurement::Integer, QString( "%1/%2" ).arg( mMismatchCount ).arg( mMatchTarget ) );
455 
456  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
457 
458  if ( myAnomalyMatchFlag )
459  {
460  mReport += "<tr><td colspan=3>"
461  "Difference image matched a known anomaly - passing test! "
462  "</td></tr>";
463  return true;
464  }
465  else
466  {
467  mReport += "<tr><td colspan=3>"
468  "</td></tr>";
469  emitDashMessage( "No Anomalies Match", QgsDartMeasurement::Text, "Difference image did not match any known anomaly."
470  " If you feel the difference image should be considered an anomaly "
471  "you can do something like this\n"
472  "cp " + myDiffImageFile + " ../tests/testdata/control_images/" + theTestName +
473  "/<imagename>.{wld,png}" );
474  }
475 
476  if ( mMismatchCount <= theMismatchCount )
477  {
478  mReport += "<tr><td colspan = 3>\n";
479  mReport += "Test image and result image for " + theTestName + " are matched<br>";
480  mReport += "</td></tr>";
481  if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
482  {
483  //test failed because it took too long...
484  qDebug( "Test failed because render step took too long" );
485  mReport += "<tr><td colspan = 3>\n";
486  mReport += "<font color=red>Test failed because render step took too long</font>";
487  mReport += "</td></tr>";
488  mReport += myImagesString;
489  return false;
490  }
491  else
492  {
493  mReport += myImagesString;
494  return true;
495  }
496  }
497  else
498  {
499  mReport += "<tr><td colspan = 3>\n";
500  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
501  mReport += "</td></tr>";
502  mReport += myImagesString;
503  return false;
504  }
505 }
const QgsMapSettings & mapSettings()
bridge to QgsMapSettings
A rectangle specified with double values.
Definition: qgsrectangle.h:35
void setDotsPerMeterX(int x)
void setDotsPerMeterY(int y)
bool load(QIODevice *device, const char *format)
virtual void start() override
Start the rendering job and immediately return.
bool end()
QString & fill(QChar ch, int size)
void fillRect(const QRectF &rectangle, const QBrush &brush)
const uchar * constScanLine(int i) const
void setRenderHint(RenderHint hint, bool on)
void setControlName(const QString &theName)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:192
bool save(const QString &fileName, const char *format, int quality) const
const T & at(int i) const
void removeAt(int i)
QPixmap fromImage(const QImage &image, QFlags< Qt::ImageConversionFlag > flags)
int dotsPerMeterX() const
int dotsPerMeterY() const
Q_DECL_DEPRECATED void setMapRenderer(QgsMapRenderer *thepMapRenderer)
bool runTest(QString theTestName, unsigned int theMismatchCount=0)
Test using renderer to generate the image to be compared.
bool isNull() const
A non GUI class for rendering a map layer set onto a QPainter.
void chop(int n)
void setMapSettings(const QgsMapSettings &mapSettings)
int size() const
bool isNull() const
void setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
The QgsMapSettings class contains configuration for rendering of the map.
int elapsed() const
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
QString tempPath()
QString controlImagePath() const
int width() const
QString imageToHash(QString theImageFile)
Get an md5 hash that uniquely identifies an image.
bool isEmpty() const
Enable anti-aliasin for map rendering.
const char * constData() const
double mapUnitsPerPixel() const
Return the distance in geographical coordinates that equals to one pixel in the map.
unsigned int mMatchTarget
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
static int renderCounter
QByteArray toLocal8Bit() const
QString qgsDoubleToString(const double &a, const int &precision=17)
Definition: qgis.h:339
void setTexture(const QPixmap &pixmap)
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that transparency is visible without requiring a...
Job implementation that renders everything sequentially in one thread.
void setBackgroundColor(const QColor &color)
Set the background color of the map.
bool isKnownAnomaly(QString theDiffImageFile)
Get a list of all the anomalies.
void setOutputSize(const QSize &size)
Set the size of the resulting map image.
virtual QImage renderedImage() override
Get a preview/resulting image.
QStringList entryList(QFlags< QDir::Filter > filters, QFlags< QDir::SortFlag > sort) const
QgsRectangle extent() const
Return geographical coordinates of the rectangle that should be rendered.
char * data()
void start()
int height() const
int indexOf(const QRegExp &rx, int from) const
bool compareImages(QString theTestName, unsigned int theMismatchCount=0, QString theRenderedImageFile="")
Test using two arbitary images (map renderer will not be used)
QByteArray toBase64() const
virtual void waitForFinished() override
Block until the job has finished.
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:187
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const
QByteArray toUtf8() const