26 #include <QCryptographicHash>    31 static int sRenderCounter = 0;
    36   QString myDataDir( TEST_DATA_DIR ); 
    37   QString myControlImageDir = myDataDir + 
"/control_images/" + mControlPathPrefix;
    38   return myControlImageDir;
    49   if ( !name.isEmpty() )
    50     mControlPathSuffix = name + 
'/';
    52     mControlPathSuffix.clear();
    58   myImage.load( imageFile );
    59   QByteArray myByteArray;
    60   QBuffer myBuffer( &myByteArray );
    61   myImage.save( &myBuffer, 
"PNG" );
    62   QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
    63   QCryptographicHash myHash( QCryptographicHash::Md5 );
    64   myHash.addData( myImageString.toUtf8() );
    65   return myHash.result().toHex().constData();
    70   mMapSettings = mapSettings;
    76   uchar pixDataRGB[] = { 255, 255, 255, 255,
    82   QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
    83   QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
    87   brush.setTexture( pix );
    89   p.setRenderHint( QPainter::Antialiasing, 
false );
    90   p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
    97   QDir myDirectory = QDir( myControlImageDir );
    99   QString myFilename = QStringLiteral( 
"*" );
   100   myList = myDirectory.entryList( QStringList( myFilename ),
   101                                   QDir::Files | QDir::NoSymLinks );
   106   QString myImageHash = 
imageToHash( diffImageFile );
   109   for ( 
int i = 0; i < myList.size(); ++i )
   111     QString myFile = myList.at( i );
   112     mReport += 
"<tr><td colspan=3>"   113                "Checking if " + myFile + 
" is a known anomaly.";
   114     mReport += QLatin1String( 
"</td></tr>" );
   116     QString myHashMessage = QStringLiteral(
   117                               "Checking if anomaly %1 (hash %2)<br>" )
   120     myHashMessage += QStringLiteral( 
"  matches %1 (hash %2)" )
   126     mReport += 
"<tr><td colspan=3>" + myHashMessage + 
"</td></tr>";
   127     if ( myImageHash == myAnomalyHash )
   129       mReport += 
"<tr><td colspan=3>"   130                  "Anomaly found! " + myFile;
   131       mReport += QLatin1String( 
"</td></tr>" );
   135   mReport += 
"<tr><td colspan=3>"   136              "No anomaly found! ";
   137   mReport += QLatin1String( 
"</td></tr>" );
   143   if ( mBufferDashMessages )
   144     mDashMessages << dashMessage;
   159     qDebug( 
"QgsRenderChecker::runTest failed - Expected Image File not set." );
   161               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   162               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "   163               "Image File not set.</td></tr></table>\n";
   170   if ( myExpectedImage.isNull() )
   172     qDebug() << 
"QgsRenderChecker::runTest failed - Could not load expected image from " << 
mExpectedImageFile;
   174               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   175               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "   176               "Image File could not be loaded.</td></tr></table>\n";
   179   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
   205   myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
   206   myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
   207   if ( ! myImage.save( mRenderedImageFile, 
"PNG", 100 ) )
   209     qDebug() << 
"QgsRenderChecker::runTest failed - Could not save rendered image to " << 
mRenderedImageFile;
   211               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   212               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "   213               "Image File could not be saved.</td></tr></table>\n";
   219   QFile wldFile( QDir::tempPath() + 
'/' + testName + 
"_result.wld" );
   220   if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
   224     QTextStream stream( &wldFile );
   225     stream << QStringLiteral( 
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
   238                                       const QString &renderedImageFile )
   242     qDebug( 
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
   244               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   245               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "   246               "Image File not set.</td></tr></table>\n";
   249   if ( ! renderedImageFile.isEmpty() )
   259     qDebug( 
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
   261               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   262               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "   263               "Image File not set.</td></tr></table>\n";
   272   if ( myResultImage.isNull() )
   274     qDebug() << 
"QgsRenderChecker::runTest failed - Could not load rendered image from " << 
mRenderedImageFile;
   276               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"   277               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "   278               "Image File could not be loaded.</td></tr></table>\n";
   281   QImage myDifferenceImage( myExpectedImage.width(),
   282                             myExpectedImage.height(),
   283                             QImage::Format_RGB32 );
   284   QString myDiffImageFile = QDir::tempPath() + 
'/' + testName + 
"_result_diff.png";
   285   myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
   289   maskImagePath.chop( 4 ); 
   290   maskImagePath += QLatin1String( 
"_mask.png" );
   291   QImage *maskImage = 
new QImage( maskImagePath );
   292   bool hasMask = !maskImage->isNull();
   295     qDebug( 
"QgsRenderChecker using mask image" );
   301   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
   302   unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
   306   mReport = QStringLiteral( 
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
   307   mReport += QLatin1String( 
"<table>" );
   308   mReport += QLatin1String( 
"<tr><td colspan=2>" );
   309   mReport += QString( 
"<tr><td colspan=2>"   310                       "Test image and result image for %1<br>"   311                       "Expected size: %2 w x %3 h (%4 pixels)<br>"   312                       "Actual   size: %5 w x %6 h (%7 pixels)"   315              .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg( 
mMatchTarget )
   316              .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
   317   mReport += QString( 
"<tr><td colspan=2>\n"   318                       "Expected Duration : <= %1 (0 indicates not specified)<br>"   319                       "Actual Duration : %2 ms<br></td></tr>" )
   320              .arg( mElapsedTimeTarget )
   326   if ( ! myExpectedImage.isNull() )
   328     imgWidth = std::min( myExpectedImage.width(), imgWidth );
   329     imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
   332   QString myImagesString = QString(
   334                              "<td colspan=2>Compare actual and expected result</td>"   335                              "<td>Difference (all blue is good, any red is bad)</td>"   337                              "<td colspan=2 id=\"td-%1-%7\"></td>\n"   338                              "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n"   341                              "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
   346                            .arg( imgWidth ).arg( imgHeight )
   347                            .arg( sRenderCounter++ );
   350   if ( !mControlPathPrefix.isNull() )
   352     prefix = QStringLiteral( 
" (prefix %1)" ).arg( mControlPathPrefix );
   364   qDebug( 
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
   365   qDebug( 
"Actual   size: %dw x %dh", myResultImage.width(), myResultImage.height() );
   369     qDebug( 
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
   371     if ( std::abs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
   372          std::abs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
   374       mReport += QLatin1String( 
"<tr><td colspan=3>" );
   375       mReport += 
"<font color=red>Expected image and result image for " + testName + 
" are different dimensions - FAILING!</font>";
   376       mReport += QLatin1String( 
"</td></tr>" );
   383       mReport += QLatin1String( 
"<tr><td colspan=3>" );
   384       mReport += 
"Expected image and result image for " + testName + 
" are different dimensions, but within tolerance";
   385       mReport += QLatin1String( 
"</td></tr>" );
   394   int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
   395   int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
   398   int colorTolerance = 
static_cast< int >( mColorTolerance );
   399   for ( 
int y = 0; y < maxHeight; ++y )
   401     const QRgb *expectedScanline = 
reinterpret_cast< const QRgb * 
>( myExpectedImage.constScanLine( y ) );
   402     const QRgb *resultScanline = 
reinterpret_cast< const QRgb * 
>( myResultImage.constScanLine( y ) );
   403     const QRgb *maskScanline = hasMask ? 
reinterpret_cast< const QRgb * 
>( maskImage->constScanLine( y ) ) : 
nullptr;
   404     QRgb *diffScanline = 
reinterpret_cast< QRgb * 
>( myDifferenceImage.scanLine( y ) );
   406     for ( 
int x = 0; x < maxWidth; ++x )
   408       int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
   409       int pixelTolerance = std::max( colorTolerance, maskTolerance );
   410       if ( pixelTolerance == 255 )
   416       QRgb myExpectedPixel = expectedScanline[x];
   417       QRgb myActualPixel = resultScanline[x];
   418       if ( pixelTolerance == 0 )
   420         if ( myExpectedPixel != myActualPixel )
   423           diffScanline[ x ] = qRgb( 255, 0, 0 );
   428         if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
   429              std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
   430              std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
   431              std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
   434           diffScanline[ x ] = qRgb( 255, 0, 0 );
   442   myDifferenceImage.save( myDiffImageFile );
   449   qDebug( 
"%d/%d pixels mismatched (%d allowed)", mMismatchCount, 
mMatchTarget, mismatchCount );
   454   mReport += QStringLiteral( 
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
   455              .arg( mMismatchCount ).arg( 
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
   462   if ( mMismatchCount <= mismatchCount )
   464     mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
   465     mReport += 
"Test image and result image for " + testName + 
" are matched<br>";
   466     mReport += QLatin1String( 
"</td></tr>" );
   467     if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < 
mElapsedTime )
   470       qDebug( 
"Test failed because render step took too long" );
   471       mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
   472       mReport += QLatin1String( 
"<font color=red>Test failed because render step took too long</font>" );
   473       mReport += QLatin1String( 
"</td></tr>" );
   485   if ( myAnomalyMatchFlag )
   487     mReport += 
"<tr><td colspan=3>"   488                "Difference image matched a known anomaly - passing test! "   493   mReport += QLatin1String( 
"<tr><td colspan=3></td></tr>" );
   494   emitDashMessage( QStringLiteral( 
"Image mismatch" ), 
QgsDartMeasurement::Text, 
"Difference image did not match any known anomaly or mask."   495                    " If you feel the difference image should be considered an anomaly "   496                    "you can do something like this\n"   498                    "/\nIf it should be included in the mask run\n"   501   mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
   502   mReport += 
"<font color=red>Test image and result image for " + testName + 
" are mismatched</font><br>";
   503   mReport += QLatin1String( 
"</td></tr>" );
 A rectangle specified with double values. 
 
void start() override
Start the rendering job and immediately return. 
 
void setMapSettings(const QgsMapSettings &mapSettings)
 
void setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected) 
 
QString controlImagePath() const
 
The QgsMapSettings class contains configuration for rendering of the map. 
 
void setControlName(const QString &name)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
 
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. 
 
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double. 
 
double mapUnitsPerPixel() const
Returns the distance in geographical coordinates that equals to one pixel in the map. 
 
Enable anti-aliasing for map rendering. 
 
bool isKnownAnomaly(const QString &diffImageFile)
Gets a list of all the anomalies. 
 
unsigned int mMatchTarget
 
float devicePixelRatio() const
Returns device pixel ratio Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" di...
 
bool runTest(const QString &testName, unsigned int mismatchCount=0)
Test using renderer to generate the image to be compared. 
 
bool compareImages(const QString &testName, unsigned int mismatchCount=0, const QString &renderedImageFile=QString())
Test using two arbitrary images (map renderer will not be used) 
 
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that opacity is visible without requiring a tran...
 
Job implementation that renders everything sequentially in one thread. 
 
QString mRenderedImageFile
 
void setBackgroundColor(const QColor &color)
Sets the background color of the map. 
 
QImage renderedImage() override
Gets a preview/resulting image. 
 
QString mExpectedImageFile
 
QString imageToHash(const QString &imageFile)
Gets an md5 hash that uniquely identifies an image. 
 
double xMinimum() const
Returns the x minimum value (left side of rectangle). 
 
double yMaximum() const
Returns the y maximum value (top side of rectangle). 
 
unsigned int mismatchCount()
 
void waitForFinished() override
Block until the job has finished. 
 
void setControlPathSuffix(const QString &name)