25 #include <QCryptographicHash> 
   32   : mBasePath( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( 
"/control_images/" ) ) 
 
   38   return mBasePath + ( mBasePath.endsWith( 
'/' ) ? QString() : QStringLiteral( 
"/" ) ) + mControlPathPrefix;
 
   54   if ( !name.isEmpty() )
 
   55     mControlPathSuffix = name + 
'/';
 
   57     mControlPathSuffix.clear();
 
   63   myImage.load( imageFile );
 
   64   QByteArray myByteArray;
 
   65   QBuffer myBuffer( &myByteArray );
 
   66   myImage.save( &myBuffer, 
"PNG" );
 
   67   QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
 
   68   QCryptographicHash myHash( QCryptographicHash::Md5 );
 
   69   myHash.addData( myImageString.toUtf8() );
 
   70   return myHash.result().toHex().constData();
 
   75   mMapSettings = mapSettings;
 
   81   uchar pixDataRGB[] = { 255, 255, 255, 255,
 
   87   QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
 
   88   QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
 
   92   brush.setTexture( pix );
 
   94   p.setRenderHint( QPainter::Antialiasing, 
false );
 
   95   p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
 
  102   QDir myDirectory = QDir( myControlImageDir );
 
  104   QString myFilename = QStringLiteral( 
"*" );
 
  105   myList = myDirectory.entryList( QStringList( myFilename ),
 
  106                                   QDir::Files | QDir::NoSymLinks );
 
  111   QString myImageHash = 
imageToHash( diffImageFile );
 
  114   for ( 
int i = 0; i < myList.size(); ++i )
 
  116     QString myFile = myList.at( i );
 
  117     mReport += 
"<tr><td colspan=3>" 
  118                "Checking if " + myFile + 
" is a known anomaly.";
 
  119     mReport += QLatin1String( 
"</td></tr>" );
 
  121     QString myHashMessage = QStringLiteral(
 
  122                               "Checking if anomaly %1 (hash %2)<br>" )
 
  125     myHashMessage += QStringLiteral( 
"  matches %1 (hash %2)" )
 
  131     mReport += 
"<tr><td colspan=3>" + myHashMessage + 
"</td></tr>";
 
  132     if ( myImageHash == myAnomalyHash )
 
  134       mReport += 
"<tr><td colspan=3>" 
  135                  "Anomaly found! " + myFile;
 
  136       mReport += QLatin1String( 
"</td></tr>" );
 
  140   mReport += 
"<tr><td colspan=3>" 
  141              "No anomaly found! ";
 
  142   mReport += QLatin1String( 
"</td></tr>" );
 
  148   if ( mBufferDashMessages )
 
  149     mDashMessages << dashMessage;
 
  160                                 unsigned int mismatchCount )
 
  164     qDebug( 
"QgsRenderChecker::runTest failed - Expected Image File not set." );
 
  166               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  167               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 
  168               "Image File not set.</td></tr></table>\n";
 
  175   if ( myExpectedImage.isNull() )
 
  177     qDebug() << 
"QgsRenderChecker::runTest failed - Could not load expected image from " << 
mExpectedImageFile;
 
  179               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  180               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 
  181               "Image File could not be loaded.</td></tr></table>\n";
 
  184   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
 
  192   QElapsedTimer myTime;
 
  210   myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
 
  211   myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
 
  214     qDebug() << 
"QgsRenderChecker::runTest failed - Could not save rendered image to " << 
mRenderedImageFile;
 
  216               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  217               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 
  218               "Image File could not be saved.</td></tr></table>\n";
 
  224   QFile wldFile( QDir::tempPath() + 
'/' + testName + 
"_result.wld" );
 
  225   if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
 
  229     QTextStream stream( &wldFile );
 
  230     stream << QStringLiteral( 
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
 
  242                                       unsigned int mismatchCount,
 
  243                                       const QString &renderedImageFile )
 
  247     qDebug( 
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
 
  249               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  250               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 
  251               "Image File not set.</td></tr></table>\n";
 
  260   if ( ! renderedImageFile.isEmpty() )
 
  270     qDebug( 
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
 
  272               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  273               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 
  274               "Image File not set.</td></tr></table>\n";
 
  281   QImage myExpectedImage( referenceImageFile );
 
  283   if ( myResultImage.isNull() )
 
  285     qDebug() << 
"QgsRenderChecker::runTest failed - Could not load rendered image from " << 
mRenderedImageFile;
 
  287               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 
  288               "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 
  289               "Image File could not be loaded.</td></tr></table>\n";
 
  292   QImage myDifferenceImage( myExpectedImage.width(),
 
  293                             myExpectedImage.height(),
 
  294                             QImage::Format_RGB32 );
 
  295   QString myDiffImageFile = QDir::tempPath() + 
'/' + testName + 
"_result_diff.png";
 
  296   myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
 
  299   QString maskImagePath = referenceImageFile;
 
  300   maskImagePath.chop( 4 ); 
 
  301   maskImagePath += QLatin1String( 
"_mask.png" );
 
  302   const QImage maskImage( maskImagePath );
 
  303   const bool hasMask = !maskImage.isNull();
 
  308   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
 
  309   unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
 
  313   mReport = QStringLiteral( 
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
 
  314   mReport += QLatin1String( 
"<table>" );
 
  315   mReport += QLatin1String( 
"<tr><td colspan=2>" );
 
  316   mReport += QString( 
"<tr><td colspan=2>" 
  317                       "Test image and result image for %1<br>" 
  318                       "Expected size: %2 w x %3 h (%4 pixels)<br>" 
  319                       "Actual   size: %5 w x %6 h (%7 pixels)" 
  322              .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg( 
mMatchTarget )
 
  323              .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
 
  324   mReport += QString( 
"<tr><td colspan=2>\n" 
  325                       "Expected Duration : <= %1 (0 indicates not specified)<br>" 
  326                       "Actual Duration : %2 ms<br></td></tr>" )
 
  327              .arg( mElapsedTimeTarget )
 
  333   if ( ! myExpectedImage.isNull() )
 
  335     imgWidth = std::min( myExpectedImage.width(), imgWidth );
 
  336     imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
 
  339   QString myImagesString = QString(
 
  341                              "<td colspan=2>Compare actual and expected result</td>" 
  342                              "<td>Difference (all blue is good, any red is bad)</td>" 
  344                              "<td colspan=2 id=\"td-%1-%7\"></td>\n" 
  345                              "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n" 
  348                              "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
 
  353                            .arg( imgWidth ).arg( imgHeight )
 
  354                            .arg( QUuid::createUuid().toString().mid( 1, 6 ) );
 
  357   if ( !mControlPathPrefix.isNull() )
 
  359     prefix = QStringLiteral( 
" (prefix %1)" ).arg( mControlPathPrefix );
 
  371   if ( myExpectedImage.width() != myResultImage.width() || myExpectedImage.height() != myResultImage.height() )
 
  373     qDebug( 
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
 
  374     qDebug( 
"Actual   size: %dw x %dh", myResultImage.width(), myResultImage.height() );
 
  376       qDebug( 
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
 
  381     qDebug( 
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
 
  383     if ( std::abs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
 
  384          std::abs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
 
  386       mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  387       mReport += 
"<font color=red>Expected image and result image for " + testName + 
" are different dimensions - FAILING!</font>";
 
  388       mReport += QLatin1String( 
"</td></tr>" );
 
  394       mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  395       mReport += 
"Expected image and result image for " + testName + 
" are different dimensions, but within tolerance";
 
  396       mReport += QLatin1String( 
"</td></tr>" );
 
  400   if ( myExpectedImage.format() == QImage::Format_Indexed8 )
 
  402     if ( myResultImage.format() != QImage::Format_Indexed8 )
 
  404       qDebug() << 
"Expected image and result image for " << testName << 
" have different formats (8bit format is expected) - FAILING!";
 
  406       mReport += QLatin1String( 
"<tr><td colspan=3>" );
 
  407       mReport += 
"<font color=red>Expected image and result image for " + testName + 
" have different formats (8bit format is expected) - FAILING!</font>";
 
  408       mReport += QLatin1String( 
"</td></tr>" );
 
  416     myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
 
  417     myExpectedImage = myExpectedImage.convertToFormat( QImage::Format_ARGB32 );
 
  426   int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
 
  427   int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
 
  430   int colorTolerance = 
static_cast< int >( mColorTolerance );
 
  431   for ( 
int y = 0; y < maxHeight; ++y )
 
  433     const QRgb *expectedScanline = 
reinterpret_cast< const QRgb * 
>( myExpectedImage.constScanLine( y ) );
 
  434     const QRgb *resultScanline = 
reinterpret_cast< const QRgb * 
>( myResultImage.constScanLine( y ) );
 
  435     const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ? 
reinterpret_cast< const QRgb * 
>( maskImage.constScanLine( y ) ) : 
nullptr;
 
  436     QRgb *diffScanline = 
reinterpret_cast< QRgb * 
>( myDifferenceImage.scanLine( y ) );
 
  438     for ( 
int x = 0; x < maxWidth; ++x )
 
  440       int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
 
  441       int pixelTolerance = std::max( colorTolerance, maskTolerance );
 
  442       if ( pixelTolerance == 255 )
 
  448       QRgb myExpectedPixel = expectedScanline[x];
 
  449       QRgb myActualPixel = resultScanline[x];
 
  450       if ( pixelTolerance == 0 )
 
  452         if ( myExpectedPixel != myActualPixel )
 
  455           diffScanline[ x ] = qRgb( 255, 0, 0 );
 
  460         if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
 
  461              std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
 
  462              std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
 
  463              std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
 
  466           diffScanline[ x ] = qRgb( 255, 0, 0 );
 
  474   myDifferenceImage.save( myDiffImageFile );
 
  488   mReport += QStringLiteral( 
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
 
  498     mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  499     mReport += 
"Test image and result image for " + testName + 
" are matched<br>";
 
  500     mReport += QLatin1String( 
"</td></tr>" );
 
  501     if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < 
mElapsedTime )
 
  504       qDebug( 
"Test failed because render step took too long" );
 
  505       mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  506       mReport += QLatin1String( 
"<font color=red>Test failed because render step took too long</font>" );
 
  507       mReport += QLatin1String( 
"</td></tr>" );
 
  519   if ( myAnomalyMatchFlag )
 
  521     mReport += 
"<tr><td colspan=3>" 
  522                "Difference image matched a known anomaly - passing test! " 
  527   mReport += QLatin1String( 
"<tr><td colspan=3></td></tr>" );
 
  528   emitDashMessage( QStringLiteral( 
"Image mismatch" ), 
QgsDartMeasurement::Text, 
"Difference image did not match any known anomaly or mask." 
  529                    " If you feel the difference image should be considered an anomaly " 
  530                    "you can do something like this\n" 
  532                    "/\nIf it should be included in the mask run\n" 
  533                    "scripts/generate_test_mask_image.py '" + referenceImageFile + 
"' '" + 
mRenderedImageFile + 
"'\n" );
 
  535   mReport += QLatin1String( 
"<tr><td colspan = 3>\n" );
 
  536   mReport += 
"<font color=red>Test image and result image for " + testName + 
" are mismatched</font><br>";
 
  537   mReport += QLatin1String( 
"</td></tr>" );
 
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.
@ Antialiasing
Enable anti-aliasing for map rendering.
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 setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
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.
A rectangle specified with double values.
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
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).
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)
QString imageToHash(const QString &imageFile)
Gets an md5 hash that uniquely identifies an image.
QString mRenderedImageFile
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that opacity is visible without requiring a tran...
QgsRenderChecker()
Constructor for QgsRenderChecker.
QString mExpectedImageFile
bool runTest(const QString &testName, unsigned int mismatchCount=0)
Test using renderer to generate the image to be compared.
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.
bool compareImages(const QString &testName, unsigned int mismatchCount=0, const QString &renderedImageFile=QString())
Test using two arbitrary images (map renderer will not be used)
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.