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