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