24#include <QCryptographicHash>
30#ifndef CMAKE_SOURCE_DIR
31#error CMAKE_SOURCE_DIR undefined
36 static QString sSourcePathPrefix;
37 static std::once_flag initialized;
38 std::call_once( initialized, []
40 sSourcePathPrefix = QString( CMAKE_SOURCE_DIR );
41 if ( sSourcePathPrefix.endsWith(
'/' ) )
42 sSourcePathPrefix.chop( 1 );
44 return sSourcePathPrefix;
48 : mBasePath( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral(
"/control_images/" ) )
50 if ( qgetenv(
"QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral(
"true" ) )
56 if ( qgetenv(
"QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral(
"true" ) )
57 return QDir( QDir(
"/root/QGIS" ).filePath( QStringLiteral(
"qgis_test_report" ) ) );
59 return QDir( QDir::temp().filePath( QStringLiteral(
"qgis_test_report" ) ) );
69 return mBasePath + ( mBasePath.endsWith(
'/' ) ? QString() : QStringLiteral(
"/" ) ) + mControlPathPrefix;
79 return ( ( ignoreSuccess && mResult ) || ( mExpectFail && !mResult ) ) ? QString() :
mReport;
84 return ( ( ignoreSuccess && mResult ) || ( mExpectFail && !mResult ) ) ? QString() :
mMarkdownReport;
95 if ( !name.isEmpty() )
96 mControlPathSuffix = name +
'/';
98 mControlPathSuffix.clear();
104 myImage.load( imageFile );
105 QByteArray myByteArray;
106 QBuffer myBuffer( &myByteArray );
107 myImage.save( &myBuffer,
"PNG" );
108 const QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
109 QCryptographicHash myHash( QCryptographicHash::Md5 );
110 myHash.addData( myImageString.toUtf8() );
111 return myHash.result().toHex().constData();
116 mMapSettings = mapSettings;
122 uchar pixDataRGB[] = { 255, 255, 255, 255,
128 const QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
129 const QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
133 brush.setTexture( pix );
135 p.setRenderHint( QPainter::Antialiasing,
false );
136 p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
144 const QDir myDirectory = QDir( myControlImageDir );
146 const QString myFilename = QStringLiteral(
"*" );
147 myList = myDirectory.entryList( QStringList( myFilename ),
148 QDir::Files | QDir::NoSymLinks );
153 const QString myImageHash =
imageToHash( diffImageFile );
156 for (
int i = 0; i < myList.size(); ++i )
158 const QString myFile = myList.at( i );
159 mReport +=
"<tr><td colspan=3>"
160 "Checking if " + myFile +
" is a known anomaly.";
161 mReport += QLatin1String(
"</td></tr>" );
163 QString myHashMessage = QStringLiteral(
164 "Checking if anomaly %1 (hash %2)<br>" )
167 myHashMessage += QStringLiteral(
" matches %1 (hash %2)" )
173 mReport +=
"<tr><td colspan=3>" + myHashMessage +
"</td></tr>";
174 if ( myImageHash == myAnomalyHash )
176 mReport +=
"<tr><td colspan=3>"
177 "Anomaly found! " + myFile;
178 mReport += QLatin1String(
"</td></tr>" );
182 mReport +=
"<tr><td colspan=3>"
183 "No anomaly found! ";
184 mReport += QLatin1String(
"</td></tr>" );
193 if ( mBufferDashMessages )
194 mDashMessages << dashMessage;
204#if DUMP_BASE64_IMAGES
205void QgsRenderChecker::dumpRenderedImageAsBase64()
208 if ( !fileSource.open( QIODevice::ReadOnly ) )
213 const QByteArray blob = fileSource.readAll();
214 const QByteArray encoded = blob.toBase64();
216 qDebug() <<
"################################################################";
218 qDebug() <<
"################################################################";
219 qDebug() <<
"End dump";
223void QgsRenderChecker::performPostTestActions( Flags flags )
225 if ( mResult || mExpectFail )
228#if DUMP_BASE64_IMAGES
230 dumpRenderedImageAsBase64();
236 if ( !reportDir.exists() )
238 if ( !QDir().mkpath( reportDir.path() ) )
240 qDebug() <<
"!!!!! cannot create " << reportDir.path();
247 const QString destPath = reportDir.filePath( fi.fileName() );
248 if ( QFile::exists( destPath ) )
249 QFile::remove( destPath );
257 QFileInfo fi( mDiffImageFile );
258 const QString destPath = reportDir.filePath( fi.fileName() );
259 if ( QFile::exists( destPath ) )
260 QFile::remove( destPath );
261 QFile::copy( mDiffImageFile, destPath );
267 unsigned int mismatchCount,
273 qDebug(
"QgsRenderChecker::runTest failed - Expected Image File not set." );
275 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
276 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
277 "Image File not set.</td></tr></table>\n";
278 mMarkdownReport = QStringLiteral(
"Failed because expected image file not set\n" );
279 performPostTestActions( flags );
286 if ( myExpectedImage.isNull() )
288 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load expected image from " <<
mExpectedImageFile;
290 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
291 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
292 "Image File could not be loaded.</td></tr></table>\n";
294 performPostTestActions( flags );
297 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
305 QElapsedTimer myTime;
323 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
324 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
327 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
329 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
330 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
331 "Image File could not be saved.</td></tr></table>\n";
334 performPostTestActions( flags );
340 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
341 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
345 QTextStream stream( &wldFile );
346 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
358 unsigned int mismatchCount,
359 const QString &renderedImageFile,
365 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
367 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
368 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
369 "Image File not set.</td></tr></table>\n";
370 mMarkdownReport = QStringLiteral(
"Failed because expected image file was not set\n" );
372 performPostTestActions( flags );
382 if ( ! renderedImageFile.isEmpty() )
392 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
394 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
395 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
396 "Image File not set.</td></tr></table>\n";
397 mMarkdownReport = QStringLiteral(
"Failed because rendered image file was not set\n" );
398 performPostTestActions( flags );
405 QImage expectedImage( referenceImageFile );
406 if ( expectedImage.isNull() )
408 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
410 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
411 "<tr><td>Nothing rendered</td>\n<td>Failed because control "
412 "image file could not be loaded.</td></tr></table>\n";
413 mMarkdownReport = QStringLiteral(
"Failed because expected image file (%1) could not be loaded\n" ).arg( referenceImageFile );
414 performPostTestActions( flags );
418 const QString expectedImageString = QStringLiteral(
"<a href=\"%1\" style=\"color: inherit\" target=\"_blank\">expected</a> image" ).arg( QUrl::fromLocalFile( referenceImageFile ).toString() );
419 const QString renderedImageString = QStringLiteral(
"<a href=\"%2\" style=\"color: inherit\" target=\"_blank\">rendered</a> image" ).arg( QUrl::fromLocalFile( renderedImageFile ).toString() );
420 auto upperFirst = [](
const QString & string ) -> QString
422 const int firstNonTagIndex =
string.indexOf(
'>' ) + 1;
423 return string.left( firstNonTagIndex ) +
string.at( firstNonTagIndex ).toUpper() +
string.mid( firstNonTagIndex + 1 );
427 if ( myResultImage.isNull() )
429 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
430 mReport = QStringLiteral(
"<table>"
431 "<tr><td>Test Result:</td><td>%1:</td></tr>\n"
432 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
433 "Image File could not be loaded.</td></tr></table>\n" ).arg( upperFirst( expectedImageString ) );
435 performPostTestActions( flags );
438 QImage myDifferenceImage( expectedImage.width(),
439 expectedImage.height(),
440 QImage::Format_RGB32 );
441 mDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
442 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
445 QString maskImagePath = referenceImageFile;
446 maskImagePath.chop( 4 );
447 maskImagePath += QLatin1String(
"_mask.png" );
448 const QImage maskImage( maskImagePath );
449 const bool hasMask = !maskImage.isNull();
454 mMatchTarget = expectedImage.width() * expectedImage.height();
455 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
459 mReport += QLatin1String(
"<table>" );
460 mReport += QLatin1String(
"<tr><td colspan=2>" );
461 mReport += QStringLiteral(
"<tr><td colspan=2>"
462 "%8 and %9 for %1<br>"
463 "Expected size: %2 w x %3 h (%4 pixels)<br>"
464 "Rendered size: %5 w x %6 h (%7 pixels)"
467 .arg( expectedImage.width() ).arg( expectedImage.height() ).arg(
mMatchTarget )
468 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount )
469 .arg( upperFirst( expectedImageString ), renderedImageString );
470 mReport += QString(
"<tr><td colspan=2>\n"
471 "Expected Duration : <= %1 (0 indicates not specified)<br>"
472 "Actual Duration : %2 ms<br></td></tr>" )
473 .arg( mElapsedTimeTarget )
479 if ( ! expectedImage.isNull() )
481 imgWidth = std::min( expectedImage.width(), imgWidth );
482 imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
486 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
487 const QString myImagesString = QString(
489 "<td colspan=2>Compare %10 and %11</td>"
490 "<td>Difference (all blue is good, any red is bad)</td>"
492 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
493 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
496 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
497 "<p>If the new image looks good, create or update a test mask with<br>"
498 "<code>scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>" )
501 renderedImageFileName,
503 .arg( imgWidth ).arg( imgHeight )
504 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
512 if ( !mControlPathPrefix.isNull() )
514 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
521 if ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() )
523 qDebug(
"Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
524 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
526 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
531 qDebug(
"Expected image and rendered image for %s are different dimensions", testName.toLocal8Bit().constData() );
533 if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
534 std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
539 mReport += QLatin1String(
"<tr><td colspan=3>" );
540 mReport += QStringLiteral(
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions - FAILING!</font>";
541 mReport += QLatin1String(
"</td></tr>" );
542 mMarkdownReport += QStringLiteral(
"Failed because rendered image and expected image are different dimensions (%1x%2 v2 %3x%4)\n" )
543 .arg( myResultImage.width() )
544 .arg( myResultImage.height() )
545 .arg( expectedImage.width() )
546 .arg( expectedImage.height() );
548 const QString diffSizeImagesString = QString(
550 "<td colspan=3>Compare %5 and %6</td>"
552 "<td align=center><img src=\"%1\"></td>\n"
553 "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n"
557 renderedImageFileName,
559 .arg( imgWidth ).arg( imgHeight )
560 .arg( expectedImageString, renderedImageString );
562 mReport += diffSizeImagesString;
563 performPostTestActions( flags );
568 mReport += QLatin1String(
"<tr><td colspan=3>" );
569 mReport += QStringLiteral(
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions, but within tolerance";
570 mReport += QLatin1String(
"</td></tr>" );
574 if ( expectedImage.format() == QImage::Format_Indexed8 )
576 if ( myResultImage.format() != QImage::Format_Indexed8 )
581 qDebug() <<
"Expected image and rendered image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
583 mReport += QLatin1String(
"<tr><td colspan=3>" );
584 mReport +=
"<font color=red>Expected image and rendered image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
585 mReport += QLatin1String(
"</td></tr>" );
588 mMarkdownReport += QLatin1String(
"Failed because rendered image and expected image have different formats (8bit format is expected)\n" );
589 performPostTestActions( flags );
596 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
597 expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
599 if ( expectedImage.format() != QImage::Format_RGB32
600 && expectedImage.format() != QImage::Format_ARGB32
601 && expectedImage.format() != QImage::Format_ARGB32_Premultiplied )
603 mReport += QLatin1String(
"<tr><td colspan=3>" );
604 mReport += QStringLiteral(
"<font color=red>Expected image for %1 is not a compatible format (%2). Must be 32 bit RGB format.</font>" ).arg( testName,
qgsEnumValueToKey( expectedImage.format() ) );
605 mReport += QLatin1String(
"</td></tr>" );
608 mMarkdownReport += QStringLiteral(
"Failed because expected image has an incompatible format - %1 (32 bit format is expected)\n" ).arg(
qgsEnumValueToKey( expectedImage.format() ) );
609 performPostTestActions( flags );
612 if ( myResultImage.format() != QImage::Format_RGB32
613 && myResultImage.format() != QImage::Format_ARGB32
614 && myResultImage.format() != QImage::Format_ARGB32_Premultiplied )
616 mReport += QLatin1String(
"<tr><td colspan=3>" );
617 mReport += QStringLiteral(
"<font color=red>Rendered image for %1 is not a compatible format (%2). Must be 32 bit RGB format.</font>" ).arg( testName,
qgsEnumValueToKey( myResultImage.format() ) );
618 mReport += QLatin1String(
"</td></tr>" );
621 mMarkdownReport += QStringLiteral(
"Failed because rendered image has an incompatible format - %1 (32 bit format is expected)\n" ).arg(
qgsEnumValueToKey( myResultImage.format() ) );
622 performPostTestActions( flags );
631 const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
632 const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
635 const int colorTolerance =
static_cast< int >( mColorTolerance );
636 for (
int y = 0; y < maxHeight; ++y )
638 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( expectedImage.constScanLine( y ) );
639 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
640 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ?
reinterpret_cast< const QRgb *
>( maskImage.constScanLine( y ) ) :
nullptr;
641 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
643 for (
int x = 0; x < maxWidth; ++x )
645 const int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
646 const int pixelTolerance = std::max( colorTolerance, maskTolerance );
647 if ( pixelTolerance == 255 )
653 const QRgb myExpectedPixel = expectedScanline[x];
654 const QRgb myActualPixel = resultScanline[x];
655 if ( pixelTolerance == 0 )
657 if ( myExpectedPixel != myActualPixel )
660 diffScanline[ x ] = qRgb( 255, 0, 0 );
665 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
666 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
667 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
668 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
671 diffScanline[ x ] = qRgb( 255, 0, 0 );
690 myDifferenceImage.save( mDiffImageFile );
697 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
703 if ( mMismatchCount > 0 )
710 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
711 mReport += QStringLiteral(
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are matched<br>";
712 mReport += QLatin1String(
"</td></tr>" );
713 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
716 qDebug(
"Test failed because render step took too long" );
717 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
718 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
719 mReport += QLatin1String(
"</td></tr>" );
722 mMarkdownReport += QLatin1String(
"Test failed because render step took too long\n" );
724 performPostTestActions( flags );
731 performPostTestActions( flags );
736 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
737 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask."
738 " If you feel the difference image should be considered an anomaly "
739 "you can do something like this\n"
741 "/\nIf it should be included in the mask run\n"
742 "scripts/generate_test_mask_image.py '" + referenceImageFile +
"' '" +
mRenderedImageFile +
"'\n" );
744 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
745 mReport += QStringLiteral(
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are mismatched</font><br>";
746 mReport += QLatin1String(
"</td></tr>" );
749 const QString githubSha = qgetenv(
"GITHUB_SHA" );
750 if ( !githubSha.isEmpty() )
752 const QString githubBlobUrl = QStringLiteral(
"https://github.com/qgis/QGIS/blob/%1/%2" ).arg(
753 githubSha, QDir(
sourcePath() ).relativeFilePath( referenceImageFile ) );
754 mMarkdownReport += QStringLiteral(
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
755 QDir(
sourcePath() ).relativeFilePath( referenceImageFile ), githubBlobUrl ).arg( mMismatchCount );
759 mMarkdownReport += QStringLiteral(
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
760 QDir(
sourcePath() ).relativeFilePath( referenceImageFile ),
761 QUrl::fromLocalFile( referenceImageFile ).toString() ).arg( mMismatchCount );
764 performPostTestActions( flags );
@ 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.
double xMinimum() const
Returns the x minimum value (left side of rectangle).
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Q_DECL_DEPRECATED 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).
static QDir testReportDir()
Returns the directory to use for generating a test report.
static QString sourcePath()
Returns the path to the QGIS source code.
QString markdownReport(bool ignoreSuccess=true) const
Returns the markdown report describing the results of the test run.
QString mReport
HTML format report.
static bool shouldGenerateReport()
Returns true if a test report should be generated given the current environment.
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)
bool runTest(const QString &testName, unsigned int mismatchCount=0, QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Test using renderer to generate the image to be compared.
QString imageToHash(const QString &imageFile)
Gets an md5 hash that uniquely identifies an image.
QFlags< Flag > Flags
Render checker flags.
@ AvoidExportingRenderedImage
Avoids exporting rendered images to reports.
QString mRenderedImageFile
QString mMarkdownReport
Markdown report.
QString report(bool ignoreSuccess=true) const
Returns the HTML report describing the results of the test run.
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that opacity is visible without requiring a tran...
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)
QgsRenderChecker()
Constructor for QgsRenderChecker.
QString mExpectedImageFile
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.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.