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>" );
389 if ( myExpectedImage.format() == QImage::Format_Indexed8 )
391 if ( myResultImage.format() != QImage::Format_Indexed8 )
393 qDebug() <<
"Expected image and result image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
395 mReport += QLatin1String(
"<tr><td colspan=3>" );
396 mReport +=
"<font color=red>Expected image and result image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
397 mReport += QLatin1String(
"</td></tr>" );
406 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
407 myExpectedImage = myExpectedImage.convertToFormat( QImage::Format_ARGB32 );
416 int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
417 int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
420 int colorTolerance =
static_cast< int >( mColorTolerance );
421 for (
int y = 0; y < maxHeight; ++y )
423 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
424 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
425 const QRgb *maskScanline = hasMask ?
reinterpret_cast< const QRgb *
>( maskImage->constScanLine( y ) ) :
nullptr;
426 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
428 for (
int x = 0; x < maxWidth; ++x )
430 int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
431 int pixelTolerance = std::max( colorTolerance, maskTolerance );
432 if ( pixelTolerance == 255 )
438 QRgb myExpectedPixel = expectedScanline[x];
439 QRgb myActualPixel = resultScanline[x];
440 if ( pixelTolerance == 0 )
442 if ( myExpectedPixel != myActualPixel )
445 diffScanline[ x ] = qRgb( 255, 0, 0 );
450 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
451 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
452 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
453 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
456 diffScanline[ x ] = qRgb( 255, 0, 0 );
464 myDifferenceImage.save( myDiffImageFile );
471 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, mismatchCount );
476 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
477 .arg( mMismatchCount ).arg(
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
484 if ( mMismatchCount <= mismatchCount )
486 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
487 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
488 mReport += QLatin1String(
"</td></tr>" );
489 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
492 qDebug(
"Test failed because render step took too long" );
493 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
494 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
495 mReport += QLatin1String(
"</td></tr>" );
507 if ( myAnomalyMatchFlag )
509 mReport +=
"<tr><td colspan=3>" 510 "Difference image matched a known anomaly - passing test! " 515 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
516 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 517 " If you feel the difference image should be considered an anomaly " 518 "you can do something like this\n" 520 "/\nIf it should be included in the mask run\n" 523 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
524 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
525 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)