388 if ( ! renderedImageFile.isEmpty() )
398 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
400 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
401 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
402 "Image File not set.</td></tr></table>\n";
403 mMarkdownReport = QStringLiteral(
"Failed because rendered image file was not set\n" );
404 performPostTestActions( flags );
411 QImage expectedImage( referenceImageFile );
412 if ( expectedImage.isNull() )
414 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
416 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
417 "<tr><td>Nothing rendered</td>\n<td>Failed because control "
418 "image file could not be loaded.</td></tr></table>\n";
419 mMarkdownReport = QStringLiteral(
"Failed because expected image file (%1) could not be loaded\n" ).arg( referenceImageFile );
420 performPostTestActions( flags );
424 const QString expectedImageString = QStringLiteral(
"<a href=\"%1\" style=\"color: inherit\" target=\"_blank\">expected</a> image" ).arg( QUrl::fromLocalFile( referenceImageFile ).toString() );
425 const QString renderedImageString = QStringLiteral(
"<a href=\"%2\" style=\"color: inherit\" target=\"_blank\">rendered</a> image" ).arg( QUrl::fromLocalFile( renderedImageFile ).toString() );
426 auto upperFirst = [](
const QString & string ) -> QString
428 const int firstNonTagIndex =
string.indexOf(
'>' ) + 1;
429 return string.left( firstNonTagIndex ) +
string.at( firstNonTagIndex ).toUpper() +
string.mid( firstNonTagIndex + 1 );
433 if ( myResultImage.isNull() )
435 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
436 mReport = QStringLiteral(
"<table>"
437 "<tr><td>Test Result:</td><td>%1:</td></tr>\n"
438 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
439 "Image File could not be loaded.</td></tr></table>\n" ).arg( upperFirst( expectedImageString ) );
441 performPostTestActions( flags );
444 QImage myDifferenceImage( expectedImage.width(),
445 expectedImage.height(),
446 QImage::Format_RGB32 );
447 mDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
448 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
451 QString maskImagePath = referenceImageFile;
452 maskImagePath.chop( 4 );
453 maskImagePath += QLatin1String(
"_mask.png" );
454 const QImage maskImage( maskImagePath );
455 const bool hasMask = !maskImage.isNull();
460 mMatchTarget = expectedImage.width() * expectedImage.height();
461 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
465 mReport += QLatin1String(
"<table>" );
466 mReport += QLatin1String(
"<tr><td colspan=2>" );
467 mReport += QStringLiteral(
"<tr><td colspan=2>"
468 "%8 and %9 for %1<br>"
469 "Expected size: %2 w x %3 h (%4 pixels)<br>"
470 "Rendered size: %5 w x %6 h (%7 pixels)"
473 .arg( expectedImage.width() ).arg( expectedImage.height() ).arg(
mMatchTarget )
474 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount )
475 .arg( upperFirst( expectedImageString ), renderedImageString );
476 mReport += QString(
"<tr><td colspan=2>\n"
477 "Expected Duration : <= %1 (0 indicates not specified)<br>"
478 "Actual Duration : %2 ms<br></td></tr>" )
479 .arg( mElapsedTimeTarget )
485 if ( ! expectedImage.isNull() )
487 imgWidth = std::min( expectedImage.width(), imgWidth );
488 imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
492 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
493 const QString myImagesString = QString(
495 "<td colspan=2>Compare %10 and %11</td>"
496 "<td>Difference (all blue is good, any red is bad)</td>"
498 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
499 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
502 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
503 "<p>If the new image looks good, create or update a test mask with<br>"
504 "<code onclick=\"copyToClipboard(this)\" class=\"copy-code\" data-tooltip=\"Click to copy\">scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>"
508 renderedImageFileName,
510 .arg( imgWidth ).arg( imgHeight )
511 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
519 if ( !mControlPathPrefix.isNull() )
521 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
529 && ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() )
532 qDebug(
"Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
533 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
535 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
542 qDebug(
"Expected image and rendered image for %s are different dimensions", testName.toLocal8Bit().constData() );
545 if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
546 std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
551 mReport += QLatin1String(
"<tr><td colspan=3>" );
552 mReport += QStringLiteral(
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions - FAILING!</font>";
553 mReport += QLatin1String(
"</td></tr>" );
554 mMarkdownReport += QStringLiteral(
"Failed because rendered image and expected image are different dimensions (%1x%2 v2 %3x%4)\n" )
555 .arg( myResultImage.width() )
556 .arg( myResultImage.height() )
557 .arg( expectedImage.width() )
558 .arg( expectedImage.height() );
560 const QString diffSizeImagesString = QString(
562 "<td colspan=3>Compare %5 and %6</td>"
564 "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n"
565 "<td align=center><img src=\"%1\"></td>\n"
569 renderedImageFileName,
571 .arg( imgWidth ).arg( imgHeight )
572 .arg( expectedImageString, renderedImageString );
574 mReport += diffSizeImagesString;
575 performPostTestActions( flags );
580 mReport += QLatin1String(
"<tr><td colspan=3>" );
581 mReport += QStringLiteral(
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are different dimensions, but within tolerance";
582 mReport += QLatin1String(
"</td></tr>" );
586 if ( expectedImage.format() == QImage::Format_Indexed8 )
588 if ( myResultImage.format() != QImage::Format_Indexed8 )
593 qDebug() <<
"Expected image and rendered image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
595 mReport += QLatin1String(
"<tr><td colspan=3>" );
596 mReport +=
"<font color=red>Expected image and rendered image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
597 mReport += QLatin1String(
"</td></tr>" );
600 mMarkdownReport += QLatin1String(
"Failed because rendered image and expected image have different formats (8bit format is expected)\n" );
601 performPostTestActions( flags );
608 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
609 expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
611 if ( expectedImage.format() != QImage::Format_RGB32
612 && expectedImage.format() != QImage::Format_ARGB32
613 && expectedImage.format() != QImage::Format_ARGB32_Premultiplied )
615 mReport += QLatin1String(
"<tr><td colspan=3>" );
616 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() ) );
617 mReport += QLatin1String(
"</td></tr>" );
620 mMarkdownReport += QStringLiteral(
"Failed because expected image has an incompatible format - %1 (32 bit format is expected)\n" ).arg(
qgsEnumValueToKey( expectedImage.format() ) );
621 performPostTestActions( flags );
624 if ( myResultImage.format() != QImage::Format_RGB32
625 && myResultImage.format() != QImage::Format_ARGB32
626 && myResultImage.format() != QImage::Format_ARGB32_Premultiplied )
628 mReport += QLatin1String(
"<tr><td colspan=3>" );
629 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() ) );
630 mReport += QLatin1String(
"</td></tr>" );
633 mMarkdownReport += QStringLiteral(
"Failed because rendered image has an incompatible format - %1 (32 bit format is expected)\n" ).arg(
qgsEnumValueToKey( myResultImage.format() ) );
634 performPostTestActions( flags );
643 const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
644 const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
646 const int maskWidth = maskImage.width();
649 const int colorTolerance =
static_cast< int >( mColorTolerance );
650 for (
int y = 0; y < maxHeight; ++y )
652 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( expectedImage.constScanLine( y ) );
653 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
654 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ?
reinterpret_cast< const QRgb *
>( maskImage.constScanLine( y ) ) :
nullptr;
655 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
657 for (
int x = 0; x < maxWidth; ++x )
659 const int pixelTolerance = maskScanline
660 ? std::max( colorTolerance, ( maskWidth > x ) ? qRed( maskScanline[ x ] ) : 0 )
662 if ( pixelTolerance == 255 )
668 const QRgb myExpectedPixel = expectedScanline[x];
669 const QRgb myActualPixel = resultScanline[x];
670 if ( pixelTolerance == 0 )
672 if ( myExpectedPixel != myActualPixel )
675 diffScanline[ x ] = qRgb( 255, 0, 0 );
680 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
681 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
682 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
683 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
686 diffScanline[ x ] = qRgb( 255, 0, 0 );
708 myDifferenceImage.save( mDiffImageFile );
715 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
721 if ( mMismatchCount > 0 )
728 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
729 mReport += QStringLiteral(
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are matched<br>";
730 mReport += QLatin1String(
"</td></tr>" );
731 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
734 qDebug(
"Test failed because render step took too long" );
735 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
736 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
737 mReport += QLatin1String(
"</td></tr>" );
740 mMarkdownReport += QLatin1String(
"Test failed because render step took too long\n" );
742 performPostTestActions( flags );
749 performPostTestActions( flags );
754 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
755 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask."
756 " If you feel the difference image should be considered an anomaly "
757 "you can do something like this\n"
759 "/\nIf it should be included in the mask run\n"
760 "scripts/generate_test_mask_image.py '" + referenceImageFile +
"' '" +
mRenderedImageFile +
"'\n" );
762 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
763 mReport += QStringLiteral(
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName +
" are mismatched</font><br>";
764 mReport += QLatin1String(
"</td></tr>" );
767 const QString githubSha = qgetenv(
"GITHUB_SHA" );
768 if ( !githubSha.isEmpty() )
770 const QString githubBlobUrl = QStringLiteral(
"https://github.com/qgis/QGIS/blob/%1/%2" ).arg(
771 githubSha, QDir(
sourcePath() ).relativeFilePath( referenceImageFile ) );
772 mMarkdownReport += QStringLiteral(
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
773 QDir(
sourcePath() ).relativeFilePath( referenceImageFile ), githubBlobUrl ).arg( mMismatchCount );
777 mMarkdownReport += QStringLiteral(
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
778 QDir(
sourcePath() ).relativeFilePath( referenceImageFile ),
779 QUrl::fromLocalFile( referenceImageFile ).toString() ).arg( mMismatchCount );
782 performPostTestActions( flags );