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