25 #include <QCryptographicHash> 33 QString myDataDir( TEST_DATA_DIR );
34 QString myControlImageDir = myDataDir +
"/control_images/" + mControlPathPrefix;
35 return myControlImageDir;
46 if ( !name.isEmpty() )
47 mControlPathSuffix = name +
'/';
49 mControlPathSuffix.clear();
55 myImage.load( imageFile );
56 QByteArray myByteArray;
57 QBuffer myBuffer( &myByteArray );
58 myImage.save( &myBuffer,
"PNG" );
59 QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
60 QCryptographicHash myHash( QCryptographicHash::Md5 );
61 myHash.addData( myImageString.toUtf8() );
62 return myHash.result().toHex().constData();
67 mMapSettings = mapSettings;
73 uchar pixDataRGB[] = { 255, 255, 255, 255,
79 QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
80 QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
84 brush.setTexture( pix );
86 p.setRenderHint( QPainter::Antialiasing,
false );
87 p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
94 QDir myDirectory = QDir( myControlImageDir );
96 QString myFilename = QStringLiteral(
"*" );
97 myList = myDirectory.entryList( QStringList( myFilename ),
98 QDir::Files | QDir::NoSymLinks );
103 QString myImageHash =
imageToHash( diffImageFile );
106 for (
int i = 0; i < myList.size(); ++i )
108 QString myFile = myList.at( i );
109 mReport +=
"<tr><td colspan=3>" 110 "Checking if " + myFile +
" is a known anomaly.";
111 mReport += QLatin1String(
"</td></tr>" );
113 QString myHashMessage = QStringLiteral(
114 "Checking if anomaly %1 (hash %2)<br>" )
117 myHashMessage += QStringLiteral(
" matches %1 (hash %2)" )
123 mReport +=
"<tr><td colspan=3>" + myHashMessage +
"</td></tr>";
124 if ( myImageHash == myAnomalyHash )
126 mReport +=
"<tr><td colspan=3>" 127 "Anomaly found! " + myFile;
128 mReport += QLatin1String(
"</td></tr>" );
132 mReport +=
"<tr><td colspan=3>" 133 "No anomaly found! ";
134 mReport += QLatin1String(
"</td></tr>" );
140 if ( mBufferDashMessages )
141 mDashMessages << dashMessage;
156 qDebug(
"QgsRenderChecker::runTest failed - Expected Image File not set." );
158 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 159 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 160 "Image File not set.</td></tr></table>\n";
167 if ( myExpectedImage.isNull() )
169 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load expected image from " <<
mExpectedImageFile;
171 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 172 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 173 "Image File could not be loaded.</td></tr></table>\n";
176 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
184 QElapsedTimer myTime;
202 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
203 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
204 if ( ! myImage.save( mRenderedImageFile,
"PNG", 100 ) )
206 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
208 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 209 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 210 "Image File could not be saved.</td></tr></table>\n";
216 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
217 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
221 QTextStream stream( &wldFile );
222 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
235 const QString &renderedImageFile )
239 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
241 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 242 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 243 "Image File not set.</td></tr></table>\n";
246 if ( ! renderedImageFile.isEmpty() )
256 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
258 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 259 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 260 "Image File not set.</td></tr></table>\n";
269 if ( myResultImage.isNull() )
271 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
273 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 274 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 275 "Image File could not be loaded.</td></tr></table>\n";
278 QImage myDifferenceImage( myExpectedImage.width(),
279 myExpectedImage.height(),
280 QImage::Format_RGB32 );
281 QString myDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
282 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
286 maskImagePath.chop( 4 );
287 maskImagePath += QLatin1String(
"_mask.png" );
288 const QImage maskImage( maskImagePath );
289 const bool hasMask = !maskImage.isNull();
292 qDebug(
"QgsRenderChecker using mask image" );
298 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
299 unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
303 mReport = QStringLiteral(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
304 mReport += QLatin1String(
"<table>" );
305 mReport += QLatin1String(
"<tr><td colspan=2>" );
306 mReport += QString(
"<tr><td colspan=2>" 307 "Test image and result image for %1<br>" 308 "Expected size: %2 w x %3 h (%4 pixels)<br>" 309 "Actual size: %5 w x %6 h (%7 pixels)" 312 .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg(
mMatchTarget )
313 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
314 mReport += QString(
"<tr><td colspan=2>\n" 315 "Expected Duration : <= %1 (0 indicates not specified)<br>" 316 "Actual Duration : %2 ms<br></td></tr>" )
317 .arg( mElapsedTimeTarget )
323 if ( ! myExpectedImage.isNull() )
325 imgWidth = std::min( myExpectedImage.width(), imgWidth );
326 imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
329 QString myImagesString = QString(
331 "<td colspan=2>Compare actual and expected result</td>" 332 "<td>Difference (all blue is good, any red is bad)</td>" 334 "<td colspan=2 id=\"td-%1-%7\"></td>\n" 335 "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n" 338 "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
343 .arg( imgWidth ).arg( imgHeight )
344 .arg( QUuid::createUuid().toString().mid( 1, 6 ) );
347 if ( !mControlPathPrefix.isNull() )
349 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
361 qDebug(
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
362 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
364 qDebug(
"Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
368 qDebug(
"Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
370 if ( std::abs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
371 std::abs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
373 mReport += QLatin1String(
"<tr><td colspan=3>" );
374 mReport +=
"<font color=red>Expected image and result image for " + testName +
" are different dimensions - FAILING!</font>";
375 mReport += QLatin1String(
"</td></tr>" );
381 mReport += QLatin1String(
"<tr><td colspan=3>" );
382 mReport +=
"Expected image and result image for " + testName +
" are different dimensions, but within tolerance";
383 mReport += QLatin1String(
"</td></tr>" );
387 if ( myExpectedImage.format() == QImage::Format_Indexed8 )
389 if ( myResultImage.format() != QImage::Format_Indexed8 )
391 qDebug() <<
"Expected image and result image for " << testName <<
" have different formats (8bit format is expected) - FAILING!";
393 mReport += QLatin1String(
"<tr><td colspan=3>" );
394 mReport +=
"<font color=red>Expected image and result image for " + testName +
" have different formats (8bit format is expected) - FAILING!</font>";
395 mReport += QLatin1String(
"</td></tr>" );
403 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
404 myExpectedImage = myExpectedImage.convertToFormat( QImage::Format_ARGB32 );
413 int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
414 int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
417 int colorTolerance =
static_cast< int >( mColorTolerance );
418 for (
int y = 0; y < maxHeight; ++y )
420 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
421 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
422 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ? reinterpret_cast< const QRgb * >( maskImage.constScanLine( y ) ) :
nullptr;
423 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
425 for (
int x = 0; x < maxWidth; ++x )
427 int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
428 int pixelTolerance = std::max( colorTolerance, maskTolerance );
429 if ( pixelTolerance == 255 )
435 QRgb myExpectedPixel = expectedScanline[x];
436 QRgb myActualPixel = resultScanline[x];
437 if ( pixelTolerance == 0 )
439 if ( myExpectedPixel != myActualPixel )
442 diffScanline[ x ] = qRgb( 255, 0, 0 );
447 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
448 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
449 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
450 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
453 diffScanline[ x ] = qRgb( 255, 0, 0 );
461 myDifferenceImage.save( myDiffImageFile );
467 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, mismatchCount );
472 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
473 .arg( mMismatchCount ).arg(
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
480 if ( mMismatchCount <= mismatchCount )
482 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
483 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
484 mReport += QLatin1String(
"</td></tr>" );
485 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
488 qDebug(
"Test failed because render step took too long" );
489 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
490 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
491 mReport += QLatin1String(
"</td></tr>" );
503 if ( myAnomalyMatchFlag )
505 mReport +=
"<tr><td colspan=3>" 506 "Difference image matched a known anomaly - passing test! " 511 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
512 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 513 " If you feel the difference image should be considered an anomaly " 514 "you can do something like this\n" 516 "/\nIf it should be included in the mask run\n" 519 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
520 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
521 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)