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>" );
394 int maxHeight = std::min( myExpectedImage.height(), myResultImage.height() );
395 int maxWidth = std::min( myExpectedImage.width(), myResultImage.width() );
398 int colorTolerance =
static_cast< int >( mColorTolerance );
399 for (
int y = 0; y < maxHeight; ++y )
401 const QRgb *expectedScanline =
reinterpret_cast< const QRgb *
>( myExpectedImage.constScanLine( y ) );
402 const QRgb *resultScanline =
reinterpret_cast< const QRgb *
>( myResultImage.constScanLine( y ) );
403 const QRgb *maskScanline = hasMask ?
reinterpret_cast< const QRgb *
>( maskImage->constScanLine( y ) ) :
nullptr;
404 QRgb *diffScanline =
reinterpret_cast< QRgb *
>( myDifferenceImage.scanLine( y ) );
406 for (
int x = 0; x < maxWidth; ++x )
408 int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
409 int pixelTolerance = std::max( colorTolerance, maskTolerance );
410 if ( pixelTolerance == 255 )
416 QRgb myExpectedPixel = expectedScanline[x];
417 QRgb myActualPixel = resultScanline[x];
418 if ( pixelTolerance == 0 )
420 if ( myExpectedPixel != myActualPixel )
423 diffScanline[ x ] = qRgb( 255, 0, 0 );
428 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
429 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
430 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
431 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
434 diffScanline[ x ] = qRgb( 255, 0, 0 );
442 myDifferenceImage.save( myDiffImageFile );
449 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, mismatchCount );
454 mReport += QStringLiteral(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
455 .arg( mMismatchCount ).arg(
mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
462 if ( mMismatchCount <= mismatchCount )
464 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
465 mReport +=
"Test image and result image for " + testName +
" are matched<br>";
466 mReport += QLatin1String(
"</td></tr>" );
467 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
470 qDebug(
"Test failed because render step took too long" );
471 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
472 mReport += QLatin1String(
"<font color=red>Test failed because render step took too long</font>" );
473 mReport += QLatin1String(
"</td></tr>" );
485 if ( myAnomalyMatchFlag )
487 mReport +=
"<tr><td colspan=3>" 488 "Difference image matched a known anomaly - passing test! " 493 mReport += QLatin1String(
"<tr><td colspan=3></td></tr>" );
494 emitDashMessage( QStringLiteral(
"Image mismatch" ),
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 495 " If you feel the difference image should be considered an anomaly " 496 "you can do something like this\n" 498 "/\nIf it should be included in the mask run\n" 501 mReport += QLatin1String(
"<tr><td colspan = 3>\n" );
502 mReport +=
"<font color=red>Test image and result image for " + testName +
" are mismatched</font><br>";
503 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)