381 if ( !renderedImageFile.isEmpty() )
391 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
393 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
394 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
395 "Image File not set.</td></tr></table>\n";
396 mMarkdownReport = u
"Failed because rendered image file was not set\n"_s;
397 performPostTestActions( flags );
404 QImage expectedImage( referenceImageFile );
405 if ( expectedImage.isNull() )
407 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
409 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
410 "<tr><td>Nothing rendered</td>\n<td>Failed because control "
411 "image file could not be loaded.</td></tr></table>\n";
412 mMarkdownReport = u
"Failed because expected image file (%1) could not be loaded\n"_s.arg( referenceImageFile );
413 performPostTestActions( flags );
417 const QString expectedImageString = u
"<a href=\"%1\" style=\"color: inherit\" target=\"_blank\">expected</a> image"_s.arg( QUrl::fromLocalFile( referenceImageFile ).toString() );
418 const QString renderedImageString = u
"<a href=\"%2\" style=\"color: inherit\" target=\"_blank\">rendered</a> image"_s.arg( QUrl::fromLocalFile( renderedImageFile ).toString() );
419 auto upperFirst = [](
const QString &string ) -> QString {
420 const int firstNonTagIndex =
string.indexOf(
'>' ) + 1;
421 return string.left( firstNonTagIndex ) +
string.at( firstNonTagIndex ).toUpper() +
string.mid( firstNonTagIndex + 1 );
425 if ( myResultImage.isNull() )
427 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
430 "<tr><td>Test Result:</td><td>%1:</td></tr>\n"
431 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
432 "Image File could not be loaded.</td></tr></table>\n"
434 .arg( upperFirst( expectedImageString ) );
436 performPostTestActions( flags );
439 QImage myDifferenceImage( expectedImage.width(), expectedImage.height(), QImage::Format_RGB32 );
440 mDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
441 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
444 QString maskImagePath = referenceImageFile;
445 maskImagePath.chop( 4 );
446 maskImagePath +=
"_mask.png"_L1;
447 const QImage maskImage( maskImagePath );
448 const bool hasMask = !maskImage.isNull();
453 mMatchTarget = expectedImage.width() * expectedImage.height();
454 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
459 mReport +=
"<tr><td colspan=2>"_L1;
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)"
468 .arg( expectedImage.width() )
469 .arg( expectedImage.height() )
471 .arg( myResultImage.width() )
472 .arg( myResultImage.height() )
474 .arg( upperFirst( expectedImageString ), renderedImageString );
476 "<tr><td colspan=2>\n"
477 "Expected Duration : <= %1 (0 indicates not specified)<br>"
478 "Actual Duration : %2 ms<br></td></tr>"
480 .arg( mElapsedTimeTarget )
486 if ( !expectedImage.isNull() )
488 imgWidth = std::min( expectedImage.width(), imgWidth );
489 imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
493 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
494 const QString myImagesString = QString(
496 "<td colspan=2>Compare %10 and %11</td>"
497 "<td>Difference (all blue is good, any red is bad)</td>"
499 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
500 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
503 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
504 "<p>If the new image looks good, create or update a test mask with<br>"
505 "<code onclick=\"copyToClipboard(this)\" class=\"copy-code\" data-tooltip=\"Click to copy\">scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>"
507 .arg( testName, diffImageFileName, renderedImageFileName, referenceImageFile )
510 .arg( QUuid::createUuid().toString().mid( 1, 6 ), referenceImageFile,
mRenderedImageFile, expectedImageString, renderedImageString );
513 if ( !mControlPathPrefix.isNull() )
515 prefix = u
" (prefix %1)"_s.arg( mControlPathPrefix );
522 if ( !flags.testFlag(
Flag::Silent ) && ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() ) )
524 qDebug(
"Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
525 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
527 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
534 qDebug(
"Expected image and rendered image for %s are different dimensions", testName.toLocal8Bit().constData() );
537 if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX || std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
542 mReport +=
"<tr><td colspan=3>"_L1;
543 mReport += u
"<font color=red>%1 and %2 for "_s.arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions - FAILING!</font>";
545 mMarkdownReport += u
"Failed because rendered image and expected image are different dimensions (%1x%2 v2 %3x%4)\n"_s.arg( myResultImage.width() )
546 .arg( myResultImage.height() )
547 .arg( expectedImage.width() )
548 .arg( expectedImage.height() );
550 const QString diffSizeImagesString = QString(
552 "<td colspan=3>Compare %5 and %6</td>"
554 "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n"
555 "<td align=center><img src=\"%1\"></td>\n"
559 .arg( renderedImageFileName, referenceImageFile )
562 .arg( expectedImageString, renderedImageString );
564 mReport += diffSizeImagesString;
565 performPostTestActions( flags );
570 mReport +=
"<tr><td colspan=3>"_L1;
571 mReport += u
"%1 and %2 for "_s.arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions, but within tolerance";
576 if ( expectedImage.format() == QImage::Format_Indexed8 )
578 if ( myResultImage.format() != QImage::Format_Indexed8 )
583 qDebug() <<
"Expected image and rendered image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
585 mReport +=
"<tr><td colspan=3>"_L1;
586 mReport +=
"<font color=red>Expected image and rendered image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
590 mMarkdownReport +=
"Failed because rendered image and expected image have different formats (8bit format is expected)\n"_L1;
591 performPostTestActions( flags );
598 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
599 expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
601 if ( expectedImage.format() != QImage::Format_RGB32 && expectedImage.format() != QImage::Format_ARGB32 && expectedImage.format() != QImage::Format_ARGB32_Premultiplied )
603 mReport +=
"<tr><td colspan=3>"_L1;
604 mReport += u
"<font color=red>Expected image for %1 is not a compatible format (%2). Must be 32 bit RGB format.</font>"_s.arg( testName,
qgsEnumValueToKey( expectedImage.format() ) );
608 mMarkdownReport += u
"Failed because expected image has an incompatible format - %1 (32 bit format is expected)\n"_s.arg(
qgsEnumValueToKey( expectedImage.format() ) );
609 performPostTestActions( flags );
612 if ( myResultImage.format() != QImage::Format_RGB32 && myResultImage.format() != QImage::Format_ARGB32 && myResultImage.format() != QImage::Format_ARGB32_Premultiplied )
614 mReport +=
"<tr><td colspan=3>"_L1;
615 mReport += u
"<font color=red>Rendered image for %1 is not a compatible format (%2). Must be 32 bit RGB format.</font>"_s.arg( testName,
qgsEnumValueToKey( myResultImage.format() ) );
619 mMarkdownReport += u
"Failed because rendered image has an incompatible format - %1 (32 bit format is expected)\n"_s.arg(
qgsEnumValueToKey( myResultImage.format() ) );
620 performPostTestActions( flags );
629 const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
630 const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
632 const int maskWidth = maskImage.width();
634 const int colorTolerance =
static_cast< int >( mColorTolerance );
642 const int threadCount = std::min( QThread::idealThreadCount(), 32 );
643 QList<RowBlock> blocks;
644 blocks.reserve( threadCount );
645 const int rowsPerBlock = maxHeight / threadCount;
647 for (
int i = 0; i < threadCount; ++i )
649 const int endY = startY + rowsPerBlock;
650 blocks.append( { startY, std::min( endY, maxHeight ) } );
654 blocks.last().endY = maxHeight;
656 auto processBlock = [&](
const RowBlock &block ) ->
int {
657 int blockMismatches = 0;
659 for (
int y = block.startY; y < block.endY; ++y )
661 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( expectedImage.constScanLine( y ) );
662 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
663 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ?
reinterpret_cast< const QRgb *
>( maskImage.constScanLine( y ) ) :
nullptr;
664 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
666 for (
int x = 0; x < maxWidth; ++x )
668 const int pixelTolerance = maskScanline ? std::max( colorTolerance, ( maskWidth > x ) ? qRed( maskScanline[x] ) : 0 ) : colorTolerance;
669 if ( pixelTolerance == 255 )
675 const QRgb myExpectedPixel = expectedScanline[x];
676 const QRgb myActualPixel = resultScanline[x];
677 if ( myExpectedPixel == myActualPixel )
680 if ( pixelTolerance == 0 )
683 diffScanline[x] = qRgb( 255, 0, 0 );
687 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance
688 || std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance
689 || std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance
690 || std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
693 diffScanline[x] = qRgb( 255, 0, 0 );
699 return blockMismatches;
702 auto sumMismatches = [](
int &totalResult,
int blockResult ) { totalResult += blockResult; };
704 mMismatchCount = QtConcurrent::blockingMappedReduced( blocks, processBlock, sumMismatches );
722 myDifferenceImage.save( mDiffImageFile );
729 mReport += u
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>"_s.arg( mMismatchCount ).arg(
mMatchTarget ).arg(
mismatchCount ).arg( mColorTolerance );
734 if ( mMismatchCount > 0 )
741 mReport +=
"<tr><td colspan = 3>\n"_L1;
742 mReport += u
"%1 and %2 for "_s.arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are matched<br>";
744 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
747 qDebug(
"Test failed because render step took too long" );
748 mReport +=
"<tr><td colspan = 3>\n"_L1;
749 mReport +=
"<font color=red>Test failed because render step took too long</font>"_L1;
753 mMarkdownReport +=
"Test failed because render step took too long\n"_L1;
755 performPostTestActions( flags );
762 performPostTestActions( flags );
767 mReport +=
"<tr><td colspan=3></td></tr>"_L1;
771 "Difference image did not match any known anomaly or mask."
772 " If you feel the difference image should be considered an anomaly "
773 "you can do something like this\n"
779 +
"/\nIf it should be included in the mask run\n"
780 "scripts/generate_test_mask_image.py '"
787 mReport +=
"<tr><td colspan = 3>\n"_L1;
788 mReport += u
"<font color=red>%1 and %2 for "_s.arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are mismatched</font><br>";
792 const QString githubSha = qgetenv(
"GITHUB_SHA" );
793 if ( !githubSha.isEmpty() )
795 const QString githubBlobUrl = u
"https://github.com/qgis/QGIS/blob/%1/%2"_s.arg( githubSha, QDir(
sourcePath() ).relativeFilePath( referenceImageFile ) );
796 mMarkdownReport += u
"Rendered image did not match [%1](%2) (found %3 pixels different)\n"_s.arg( QDir(
sourcePath() ).relativeFilePath( referenceImageFile ), githubBlobUrl ).arg( mMismatchCount );
800 mMarkdownReport += u
"Rendered image did not match [%1](%2) (found %3 pixels different)\n"_s
801 .arg( QDir(
sourcePath() ).relativeFilePath( referenceImageFile ), QUrl::fromLocalFile( referenceImageFile ).toString() )
802 .arg( mMismatchCount );
805 performPostTestActions( flags );