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.