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();
185 mMapSettings.
setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) );
204 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
205 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
208 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
210 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 211 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 212 "Image File could not be saved.</td></tr></table>\n";
218 QFile wldFile( QDir::tempPath() +
'/' + testName +
"_result.wld" );
219 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
223 QTextStream stream( &wldFile );
224 stream << QStringLiteral(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
237 const QString &renderedImageFile )
241 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
243 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 244 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 245 "Image File not set.</td></tr></table>\n";
248 if ( ! renderedImageFile.isEmpty() )
258 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
260 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 261 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 262 "Image File not set.</td></tr></table>\n";
271 if ( myResultImage.isNull() )
273 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
275 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 276 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 277 "Image File could not be loaded.</td></tr></table>\n";
280 QImage myDifferenceImage( myExpectedImage.width(),
281 myExpectedImage.height(),
282 QImage::Format_RGB32 );
283 QString myDiffImageFile = QDir::tempPath() +
'/' + testName +
"_result_diff.png";
284 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
288 maskImagePath.chop( 4 );
289 maskImagePath += QLatin1String(
"_mask.png" );
290 QImage *maskImage =
new QImage( maskImagePath );
291 bool hasMask = !maskImage->isNull();
294 qDebug(
"QgsRenderChecker using mask image" );
300 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
301 unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
305 mReport = QStringLiteral(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
306 mReport += QLatin1String(
"<table>" );
307 mReport += QLatin1String(
"<tr><td colspan=2>" );
308 mReport += QString(
"<tr><td colspan=2>" 309 "Test image and result image for %1<br>" 310 "Expected size: %2 w x %3 h (%4 pixels)<br>" 311 "Actual size: %5 w x %6 h (%7 pixels)" 314 .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg(
mMatchTarget )
315 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
316 mReport += QString(
"<tr><td colspan=2>\n" 317 "Expected Duration : <= %1 (0 indicates not specified)<br>" 318 "Actual Duration : %2 ms<br></td></tr>" )
319 .arg( mElapsedTimeTarget )
325 if ( ! myExpectedImage.isNull() )
327 imgWidth = std::min( myExpectedImage.width(), imgWidth );
328 imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
331 QString myImagesString = QString(
333 "<td colspan=2>Compare actual and expected result</td>" 334 "<td>Difference (all blue is good, any red is bad)</td>" 336 "<td colspan=2 id=\"td-%1-%7\"></td>\n" 337 "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n" 340 "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
345 .arg( imgWidth ).arg( imgHeight )
346 .arg( sRenderCounter++ );
349 if ( !mControlPathPrefix.isNull() )
351 prefix = QStringLiteral(
" (prefix %1)" ).arg( mControlPathPrefix );
363 qDebug(
"Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
364 qDebug(
"Actual size: %dw x %dh", myResultImage.width(), myResultImage.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>" );
382 mReport += QLatin1String(
"<tr><td colspan=3>" );
383 mReport +=
"Expected image and result image for " + testName +
" are different dimensions, but within tolerance";
384 mReport += QLatin1String(
"</td></tr>" );
393 int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
394 int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
397 int colorTolerance =
static_cast< int >( mColorTolerance );
398 for (
int y = 0; y < maxHeight; ++y )
400 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
401 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
402 const QRgb *maskScanline = hasMask ?
reinterpret_cast< const QRgb *
>( maskImage->constScanLine( y ) ) :
nullptr;
403 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
405 for (
int x = 0; x < maxWidth; ++x )
407 int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
408 int pixelTolerance = std::max( colorTolerance, maskTolerance );
409 if ( pixelTolerance == 255 )
415 QRgb myExpectedPixel = expectedScanline[x];
416 QRgb myActualPixel = resultScanline[x];
417 if ( pixelTolerance == 0 )
419 if ( myExpectedPixel != myActualPixel )
422 diffScanline[ x ] = qRgb( 255, 0, 0 );
427 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
428 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
429 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
430 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
433 diffScanline[ x ] = qRgb( 255, 0, 0 );
441 myDifferenceImage.save( myDiffImageFile );
448 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, mismatchCount );
453 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
454 .arg( mMismatchCount ).arg(
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
461 if ( mMismatchCount <= mismatchCount )
463 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
464 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
465 mReport += QLatin1String(
"</td></tr>" );
466 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
469 qDebug(
"Test failed because render step took too long" );
470 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
471 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
472 mReport += QLatin1String(
"</td></tr>" );
484 if ( myAnomalyMatchFlag )
486 mReport +=
"<tr><td colspan=3>" 487 "Difference image matched a known anomaly - passing test! " 492 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
493 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 494 " If you feel the difference image should be considered an anomaly " 495 "you can do something like this\n" 497 "/\nIf it should be included in the mask run\n" 500 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
501 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
502 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
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)