387  if ( ! renderedImageFile.isEmpty() )
 
  397    qDebug( 
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
 
  399              "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  400              "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 
  401              "Image File not set.</td></tr></table>\n";
 
  402    mMarkdownReport = QStringLiteral( 
"Failed because rendered image file was not set\n" );
 
  403    performPostTestActions( flags );
 
  410  QImage expectedImage( referenceImageFile );
 
  411  if ( expectedImage.isNull() )
 
  413    qDebug() << 
"QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
 
  415              "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  416              "<tr><td>Nothing rendered</td>\n<td>Failed because control " 
  417              "image file could not be loaded.</td></tr></table>\n";
 
  418    mMarkdownReport = QStringLiteral( 
"Failed because expected image file (%1) could not be loaded\n" ).arg( referenceImageFile );
 
  419    performPostTestActions( flags );
 
  423  const QString expectedImageString = QStringLiteral( 
"<a href=\"%1\" style=\"color: inherit\" target=\"_blank\">expected</a> image" ).arg( QUrl::fromLocalFile( referenceImageFile ).toString() );
 
  424  const QString renderedImageString = QStringLiteral( 
"<a href=\"%2\" style=\"color: inherit\" target=\"_blank\">rendered</a> image" ).arg( QUrl::fromLocalFile( renderedImageFile ).toString() );
 
  425  auto upperFirst = []( 
const QString & string ) -> QString
 
  427    const int firstNonTagIndex = 
string.indexOf( 
'>' ) + 1;
 
  428    return string.left( firstNonTagIndex ) + 
string.at( firstNonTagIndex ).toUpper() + 
string.mid( firstNonTagIndex + 1 );
 
  432  if ( myResultImage.isNull() )
 
  434    qDebug() << 
"QgsRenderChecker::runTest failed - Could not load rendered image from " << 
mRenderedImageFile;
 
  435    mReport = QStringLiteral( 
"<table>" 
  436                              "<tr><td>Test Result:</td><td>%1:</td></tr>\n" 
  437                              "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 
  438                              "Image File could not be loaded.</td></tr></table>\n" ).arg( upperFirst( expectedImageString ) );
 
  440    performPostTestActions( flags );
 
  443  QImage myDifferenceImage( expectedImage.width(),
 
  444                            expectedImage.height(),
 
  445                            QImage::Format_RGB32 );
 
  446  mDiffImageFile = QDir::tempPath() + 
'/' + testName + 
"_result_diff.png";
 
  447  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
 
  450  QString maskImagePath = referenceImageFile;
 
  451  maskImagePath.chop( 4 ); 
 
  452  maskImagePath += QLatin1String( 
"_mask.png" );
 
  453  const QImage maskImage( maskImagePath );
 
  454  const bool hasMask = !maskImage.isNull();
 
  459  mMatchTarget = expectedImage.width() * expectedImage.height();
 
  460  const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
 
  464  mReport += QLatin1String( 
"<table>" );
 
  465  mReport += QLatin1String( 
"<tr><td colspan=2>" );
 
  466  mReport += QStringLiteral( 
"<tr><td colspan=2>" 
  467                             "%8 and %9 for %1<br>" 
  468                             "Expected size: %2 w x %3 h (%4 pixels)<br>" 
  469                             "Rendered size: %5 w x %6 h (%7 pixels)" 
  472             .arg( expectedImage.width() ).arg( expectedImage.height() ).arg( 
mMatchTarget )
 
  473             .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount )
 
  474             .arg( upperFirst( expectedImageString ), renderedImageString );
 
  475  mReport += QString( 
"<tr><td colspan=2>\n" 
  476                      "Expected Duration : <= %1 (0 indicates not specified)<br>" 
  477                      "Actual Duration : %2 ms<br></td></tr>" )
 
  478             .arg( mElapsedTimeTarget )
 
  484  if ( ! expectedImage.isNull() )
 
  486    imgWidth = std::min( expectedImage.width(), imgWidth );
 
  487    imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
 
  491  const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
 
  492  const QString myImagesString = QString(
 
  494                                   "<td colspan=2>Compare %10 and %11</td>" 
  495                                   "<td>Difference (all blue is good, any red is bad)</td>" 
  497                                   "<td colspan=2 id=\"td-%1-%7\"></td>\n" 
  498                                   "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n" 
  501                                   "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n" 
  502                                   "<p>If the new image looks good, create or update a test mask with<br>" 
  503                                   "<code onclick=\"copyToClipboard(this)\" class=\"copy-code\" data-tooltip=\"Click to copy\">scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>" 
  507                                       renderedImageFileName,
 
  509                                 .arg( imgWidth ).arg( imgHeight )
 
  510                                 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
 
  518  if ( !mControlPathPrefix.isNull() )
 
  520    prefix = QStringLiteral( 
" (prefix %1)" ).arg( mControlPathPrefix );
 
  528       && ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() )
 
  531    qDebug( 
"Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
 
  532    qDebug( 
"Actual   size: %dw x %dh", myResultImage.width(), myResultImage.height() );
 
  534      qDebug( 
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
 
  541      qDebug( 
"Expected image and rendered image for %s are different dimensions", testName.toLocal8Bit().constData() );
 
  544    if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
 
  545         std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
 
  550      mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  551      mReport += QStringLiteral( 
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName + 
" are different dimensions - FAILING!</font>";
 
  552      mReport += QLatin1String( 
"</td></tr>" );
 
  553      mMarkdownReport += QStringLiteral( 
"Failed because rendered image and expected image are different dimensions (%1x%2 v2 %3x%4)\n" )
 
  554                         .arg( myResultImage.width() )
 
  555                         .arg( myResultImage.height() )
 
  556                         .arg( expectedImage.width() )
 
  557                         .arg( expectedImage.height() );
 
  559      const QString diffSizeImagesString = QString(
 
  561                                             "<td colspan=3>Compare %5 and %6</td>" 
  563                                             "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n" 
  564                                             "<td align=center><img src=\"%1\"></td>\n" 
  568                                             renderedImageFileName,
 
  570                                           .arg( imgWidth ).arg( imgHeight )
 
  571                                           .arg( expectedImageString, renderedImageString );
 
  573      mReport += diffSizeImagesString;
 
  574      performPostTestActions( flags );
 
  579      mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  580      mReport += QStringLiteral( 
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName + 
" are different dimensions, but within tolerance";
 
  581      mReport += QLatin1String( 
"</td></tr>" );
 
  585  if ( expectedImage.format() == QImage::Format_Indexed8 )
 
  587    if ( myResultImage.format() != QImage::Format_Indexed8 )
 
  592      qDebug() << 
"Expected image and rendered image for " << testName << 
" have different formats (8bit format is expected) - FAILING!";
 
  594      mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  595      mReport += 
"<font color=red>Expected image and rendered image for " + testName + 
" have different formats (8bit format is expected) - FAILING!</font>";
 
  596      mReport += QLatin1String( 
"</td></tr>" );
 
  599      mMarkdownReport += QLatin1String( 
"Failed because rendered image and expected image have different formats (8bit format is expected)\n" );
 
  600      performPostTestActions( flags );
 
  607    myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
 
  608    expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
 
  610  if ( expectedImage.format() != QImage::Format_RGB32
 
  611       && expectedImage.format() != QImage::Format_ARGB32
 
  612       && expectedImage.format() != QImage::Format_ARGB32_Premultiplied )
 
  614    mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  615    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() ) );
 
  616    mReport += QLatin1String( 
"</td></tr>" );
 
  619    mMarkdownReport += QStringLiteral( 
"Failed because expected image has an incompatible format - %1 (32 bit format is expected)\n" ).arg( 
qgsEnumValueToKey( expectedImage.format() ) );
 
  620    performPostTestActions( flags );
 
  623  if ( myResultImage.format() != QImage::Format_RGB32
 
  624       && myResultImage.format() != QImage::Format_ARGB32
 
  625       && myResultImage.format() != QImage::Format_ARGB32_Premultiplied )
 
  627    mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  628    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() ) );
 
  629    mReport += QLatin1String( 
"</td></tr>" );
 
  632    mMarkdownReport += QStringLiteral( 
"Failed because rendered image has an incompatible format - %1 (32 bit format is expected)\n" ).arg( 
qgsEnumValueToKey( myResultImage.format() ) );
 
  633    performPostTestActions( flags );
 
  642  const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
 
  643  const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
 
  645  const int maskWidth = maskImage.width();
 
  648  const int colorTolerance = 
static_cast< int >( mColorTolerance );
 
  649  for ( 
int y = 0; y < maxHeight; ++y )
 
  651    const QRgb *expectedScanline = 
reinterpret_cast< const QRgb * 
>( expectedImage.constScanLine( y ) );
 
  652    const QRgb *resultScanline = 
reinterpret_cast< const QRgb * 
>( myResultImage.constScanLine( y ) );
 
  653    const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ? 
reinterpret_cast< const QRgb * 
>( maskImage.constScanLine( y ) ) : 
nullptr;
 
  654    QRgb *diffScanline = 
reinterpret_cast< QRgb * 
>( myDifferenceImage.scanLine( y ) );
 
  656    for ( 
int x = 0; x < maxWidth; ++x )
 
  658      const int pixelTolerance = maskScanline
 
  659                                 ? std::max( colorTolerance, ( maskWidth > x ) ? qRed( maskScanline[ x ] ) : 0 )
 
  661      if ( pixelTolerance == 255 )
 
  667      const QRgb myExpectedPixel = expectedScanline[x];
 
  668      const QRgb myActualPixel = resultScanline[x];
 
  669      if ( pixelTolerance == 0 )
 
  671        if ( myExpectedPixel != myActualPixel )
 
  674          diffScanline[ x ] = qRgb( 255, 0, 0 );
 
  679        if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
 
  680             std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
 
  681             std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
 
  682             std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
 
  685          diffScanline[ x ] = qRgb( 255, 0, 0 );
 
  707    myDifferenceImage.save( mDiffImageFile );
 
  714  mReport += QStringLiteral( 
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
 
  720  if ( mMismatchCount > 0 )
 
  727    mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  728    mReport += QStringLiteral( 
"%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName + 
" are matched<br>";
 
  729    mReport += QLatin1String( 
"</td></tr>" );
 
  730    if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < 
mElapsedTime )
 
  733      qDebug( 
"Test failed because render step took too long" );
 
  734      mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  735      mReport += QLatin1String( 
"<font color=red>Test failed because render step took too long</font>" );
 
  736      mReport += QLatin1String( 
"</td></tr>" );
 
  739      mMarkdownReport += QLatin1String( 
"Test failed because render step took too long\n" );
 
  741      performPostTestActions( flags );
 
  748      performPostTestActions( flags );
 
  753  mReport += QLatin1String( 
"<tr><td colspan=3></td></tr>" );
 
  754  emitDashMessage( QStringLiteral( 
"Image mismatch" ), 
QgsDartMeasurement::Text, 
"Difference image did not match any known anomaly or mask." 
  755                   " If you feel the difference image should be considered an anomaly " 
  756                   "you can do something like this\n" 
  758                   "/\nIf it should be included in the mask run\n" 
  759                   "scripts/generate_test_mask_image.py '" + referenceImageFile + 
"' '" + 
mRenderedImageFile + 
"'\n" );
 
  761  mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  762  mReport += QStringLiteral( 
"<font color=red>%1 and %2 for " ).arg( upperFirst( expectedImageString ), renderedImageString ) + testName + 
" are mismatched</font><br>";
 
  763  mReport += QLatin1String( 
"</td></tr>" );
 
  766  const QString githubSha = qgetenv( 
"GITHUB_SHA" );
 
  767  if ( !githubSha.isEmpty() )
 
  769    const QString githubBlobUrl = QStringLiteral( 
"https://github.com/qgis/QGIS/blob/%1/%2" ).arg(
 
  770                                    githubSha, QDir( 
sourcePath() ).relativeFilePath( referenceImageFile ) );
 
  771    mMarkdownReport += QStringLiteral( 
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
 
  772                         QDir( 
sourcePath() ).relativeFilePath( referenceImageFile ), githubBlobUrl ).arg( mMismatchCount );
 
  776    mMarkdownReport += QStringLiteral( 
"Rendered image did not match [%1](%2) (found %3 pixels different)\n" ).arg(
 
  777                         QDir( 
sourcePath() ).relativeFilePath( referenceImageFile ),
 
  778                         QUrl::fromLocalFile( referenceImageFile ).toString() ).arg( mMismatchCount );
 
  781  performPostTestActions( flags );