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();
197 #if QT_VERSION >= 0x050600 207 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
208 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
209 if ( ! myImage.save( mRenderedImageFile,
"PNG", 100 ) )
211 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
213 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 214 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 215 "Image File could not be saved.</td></tr></table>\n";
221 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
222 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
226 QTextStream stream( &wldFile );
227 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
240 const QString &renderedImageFile )
244 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
246 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 247 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 248 "Image File not set.</td></tr></table>\n";
251 if ( ! renderedImageFile.isEmpty() )
261 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
263 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 264 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 265 "Image File not set.</td></tr></table>\n";
274 if ( myResultImage.isNull() )
276 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
278 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 279 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 280 "Image File could not be loaded.</td></tr></table>\n";
283 QImage myDifferenceImage( myExpectedImage.width(),
284 myExpectedImage.height(),
285 QImage::Format_RGB32 );
286 QString myDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
287 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
291 maskImagePath.chop( 4 );
292 maskImagePath += QLatin1String(
"_mask.png" );
293 QImage *maskImage =
new QImage( maskImagePath );
294 bool hasMask = !maskImage->isNull();
297 qDebug(
"QgsRenderChecker using mask image" );
303 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
304 unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
308 mReport = QStringLiteral(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
309 mReport += QLatin1String(
"<table>" );
310 mReport += QLatin1String(
"<tr><td colspan=2>" );
311 mReport += QString(
"<tr><td colspan=2>" 312 "Test image and result image for %1<br>" 313 "Expected size: %2 w x %3 h (%4 pixels)<br>" 314 "Actual size: %5 w x %6 h (%7 pixels)" 317 .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg(
mMatchTarget )
318 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
319 mReport += QString(
"<tr><td colspan=2>\n" 320 "Expected Duration : <= %1 (0 indicates not specified)<br>" 321 "Actual Duration : %2 ms<br></td></tr>" )
322 .arg( mElapsedTimeTarget )
328 if ( ! myExpectedImage.isNull() )
330 imgWidth = std::min( myExpectedImage.width(), imgWidth );
331 imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
334 QString myImagesString = QString(
336 "<td colspan=2>Compare actual and expected result</td>" 337 "<td>Difference (all blue is good, any red is bad)</td>" 339 "<td colspan=2 id=\"td-%1-%7\"></td>\n" 340 "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n" 343 "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
348 .arg( imgWidth ).arg( imgHeight )
349 .arg( sRenderCounter++ );
352 if ( !mControlPathPrefix.isNull() )
354 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
366 qDebug(
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
367 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
371 qDebug(
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
373 if ( std::abs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
374 std::abs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
376 mReport += QLatin1String(
"<tr><td colspan=3>" );
377 mReport +=
"<font color=red>Expected image and result image for " + testName +
" are different dimensions - FAILING!</font>";
378 mReport += QLatin1String(
"</td></tr>" );
385 mReport += QLatin1String(
"<tr><td colspan=3>" );
386 mReport +=
"Expected image and result image for " + testName +
" are different dimensions, but within tolerance";
387 mReport += QLatin1String(
"</td></tr>" );
391 if ( myExpectedImage.format() == QImage::Format_Indexed8 )
393 if ( myResultImage.format() != QImage::Format_Indexed8 )
395 qDebug() <<
"Expected image and result image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
397 mReport += QLatin1String(
"<tr><td colspan=3>" );
398 mReport +=
"<font color=red>Expected image and result image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
399 mReport += QLatin1String(
"</td></tr>" );
408 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
409 myExpectedImage = myExpectedImage.convertToFormat( QImage::Format_ARGB32 );
418 int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
419 int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
422 int colorTolerance =
static_cast< int >( mColorTolerance );
423 for (
int y = 0; y < maxHeight; ++y )
425 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
426 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
427 const QRgb *maskScanline = hasMask ?
reinterpret_cast< const QRgb *
>( maskImage->constScanLine( y ) ) :
nullptr;
428 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
430 for (
int x = 0; x < maxWidth; ++x )
432 int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
433 int pixelTolerance = std::max( colorTolerance, maskTolerance );
434 if ( pixelTolerance == 255 )
440 QRgb myExpectedPixel = expectedScanline[x];
441 QRgb myActualPixel = resultScanline[x];
442 if ( pixelTolerance == 0 )
444 if ( myExpectedPixel != myActualPixel )
447 diffScanline[ x ] = qRgb( 255, 0, 0 );
452 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
453 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
454 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
455 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
458 diffScanline[ x ] = qRgb( 255, 0, 0 );
466 myDifferenceImage.save( myDiffImageFile );
473 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, mismatchCount );
478 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
479 .arg( mMismatchCount ).arg(
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
486 if ( mMismatchCount <= mismatchCount )
488 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
489 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
490 mReport += QLatin1String(
"</td></tr>" );
491 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
494 qDebug(
"Test failed because render step took too long" );
495 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
496 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
497 mReport += QLatin1String(
"</td></tr>" );
509 if ( myAnomalyMatchFlag )
511 mReport +=
"<tr><td colspan=3>" 512 "Difference image matched a known anomaly - passing test! " 517 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
518 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 519 " If you feel the difference image should be considered an anomaly " 520 "you can do something like this\n" 522 "/\nIf it should be included in the mask run\n" 525 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
526 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
527 mReport += QLatin1String(
"</td></tr>" );
A rectangle specified with double values.
void start() override
Start the rendering job and immediately return.
double yMaximum() const
Returns the y maximum value (top side of rectangle).
float devicePixelRatio() const
Returns device pixel ratio Common values are 1 for normal-dpi displays and 2 for high-dpi "retina" di...
void setMapSettings(const QgsMapSettings &mapSettings)
void setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
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...
void setOutputSize(QSize size)
Sets the size of the resulting map image.
QString controlImagePath() const
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Enable anti-aliasing for map rendering.
double mapUnitsPerPixel() const
Returns the distance in geographical coordinates that equals to one pixel in the map.
bool isKnownAnomaly(const QString &diffImageFile)
Gets a list of all the anomalies.
unsigned int mMatchTarget
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.
QgsRectangle extent() const
Returns geographical coordinates of the rectangle that should be rendered.
unsigned int mismatchCount()
void waitForFinished() override
Block until the job has finished.
void setControlPathSuffix(const QString &name)
double xMinimum() const
Returns the x minimum value (left side of rectangle).