25 #include <QCryptographicHash> 36 , mRenderedImageFile(
"" )
37 , mExpectedImageFile(
"" )
39 , mColorTolerance( 0 )
40 , mMaxSizeDifferenceX( 0 )
41 , mMaxSizeDifferenceY( 0 )
42 , mElapsedTimeTarget( 0 )
43 , mBufferDashMessages( false )
49 QString myDataDir( TEST_DATA_DIR );
50 QString myControlImageDir = myDataDir +
"/control_images/" + mControlPathPrefix;
51 return myControlImageDir;
56 mControlName = theName;
63 mControlPathSuffix = theName +
'/';
65 mControlPathSuffix.
clear();
71 myImage.
load( theImageFile );
73 QBuffer myBuffer( &myByteArray );
74 myImage.
save( &myBuffer,
"PNG" );
77 myHash.addData( myImageString.
toUtf8() );
78 return myHash.result().toHex().constData();
88 mMapSettings = mapSettings;
94 uchar pixDataRGB[] = { 255, 255, 255, 255,
100 QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
115 QDir myDirectory =
QDir( myControlImageDir );
119 QDir::Files | QDir::NoSymLinks );
127 for (
int i = 0; i < myList.
size(); ++i )
130 mReport +=
"<tr><td colspan=3>" 131 "Checking if " + myFile +
" is a known anomaly.";
135 "Checking if anomaly %1 (hash %2)<br>" )
138 myHashMessage +=
QString(
" matches %1 (hash %2)" )
139 .
arg( theDiffImageFile,
144 mReport +=
"<tr><td colspan=3>" + myHashMessage +
"</td></tr>";
145 if ( myImageHash == myAnomalyHash )
147 mReport +=
"<tr><td colspan=3>" 148 "Anomaly found! " + myFile;
153 mReport +=
"<tr><td colspan=3>" 154 "No anomaly found! ";
161 if ( mBufferDashMessages )
162 mDashMessages << dashMessage;
173 unsigned int theMismatchCount )
177 qDebug(
"QgsRenderChecker::runTest failed - Expected Image File not set." );
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 not set.</td></tr></table>\n";
188 if ( myExpectedImage.
isNull() )
190 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load expected image from " <<
mExpectedImageFile;
192 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 193 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected " 194 "Image File could not be loaded.</td></tr></table>\n";
226 qDebug() <<
"QgsRenderChecker::runTest failed - Could not save rendered image to " <<
mRenderedImageFile;
228 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 229 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 230 "Image File could not be saved.</td></tr></table>\n";
237 if ( wldFile.
open( QIODevice::WriteOnly ) )
242 stream <<
QString(
"%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
254 unsigned int theMismatchCount,
255 const QString& theRenderedImageFile )
259 qDebug(
"QgsRenderChecker::runTest failed - Expected Image (control) 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 Expected " 263 "Image File not set.</td></tr></table>\n";
266 if ( ! theRenderedImageFile.
isEmpty() )
276 qDebug(
"QgsRenderChecker::runTest failed - Rendered Image File not set." );
278 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 279 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 280 "Image File not set.</td></tr></table>\n";
289 if ( myResultImage.
isNull() )
291 qDebug() <<
"QgsRenderChecker::runTest failed - Could not load rendered image from " <<
mRenderedImageFile;
293 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n" 294 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered " 295 "Image File could not be loaded.</td></tr></table>\n";
300 QImage::Format_RGB32 );
302 myDifferenceImage.
fill( qRgb( 152, 219, 249 ) );
306 maskImagePath.
chop( 4 );
307 maskImagePath +=
"_mask.png";
309 bool hasMask = !maskImage->
isNull();
312 qDebug(
"QgsRenderChecker using mask image" );
319 unsigned int myPixelCount = myResultImage.
width() * myResultImage.
height();
323 mReport =
QString(
"<script src=\"file://%1/../renderchecker.js\"></script>\n" ).
arg( TEST_DATA_DIR );
325 mReport +=
"<tr><td colspan=2>";
327 "Test image and result image for %1<br>" 328 "Expected size: %2 w x %3 h (%4 pixels)<br>" 329 "Actual size: %5 w x %6 h (%7 pixels)" 333 .
arg( myResultImage.
width() ).arg( myResultImage.
height() ).arg( myPixelCount );
335 "Expected Duration : <= %1 (0 indicates not specified)<br>" 336 "Actual Duration : %2 ms<br></td></tr>" )
337 .
arg( mElapsedTimeTarget )
343 if ( ! myExpectedImage.
isNull() )
345 imgWidth = qMin( myExpectedImage.
width(), imgWidth );
346 imgHeight = myExpectedImage.
height() * imgWidth / myExpectedImage.
width();
351 "<td colspan=2>Compare actual and expected result</td>" 352 "<td>Difference (all blue is good, any red is bad)</td>" 354 "<td colspan=2 id=\"td-%1-%7\"></td>\n" 355 "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n" 358 "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
363 .
arg( imgWidth ).
arg( imgHeight )
367 if ( !mControlPathPrefix.
isNull() )
369 prefix =
QString(
" (prefix %1)" ).
arg( mControlPathPrefix );
381 qDebug(
"Expected size: %dw x %dh", myExpectedImage.
width(), myExpectedImage.
height() );
382 qDebug(
"Actual size: %dw x %dh", myResultImage.
width(), myResultImage.
height() );
386 qDebug(
"Test image and result image for %s are different dimensions", theTestName.
toLocal8Bit().
constData() );
388 if ( qAbs( myExpectedImage.
width() - myResultImage.
width() ) > mMaxSizeDifferenceX ||
389 qAbs( myExpectedImage.
height() - myResultImage.
height() ) > mMaxSizeDifferenceY )
391 mReport +=
"<tr><td colspan=3>";
392 mReport +=
"<font color=red>Expected image and result image for " + theTestName +
" are different dimensions - FAILING!</font>";
400 mReport +=
"<tr><td colspan=3>";
401 mReport +=
"Expected image and result image for " + theTestName +
" are different dimensions, but within tolerance";
411 int maxHeight = qMin( myExpectedImage.
height(), myResultImage.
height() );
412 int maxWidth = qMin( myExpectedImage.
width(), myResultImage.
width() );
415 int colorTolerance =
static_cast< int >( mColorTolerance );
416 for (
int y = 0; y < maxHeight; ++y )
418 const QRgb* expectedScanline =
reinterpret_cast< const QRgb*
>( myExpectedImage.
constScanLine( y ) );
419 const QRgb* resultScanline =
reinterpret_cast< const QRgb*
>( myResultImage.
constScanLine( y ) );
420 const QRgb* maskScanline = hasMask ?
reinterpret_cast< const QRgb*
>( maskImage->
constScanLine( y ) ) :
nullptr;
421 QRgb* diffScanline =
reinterpret_cast< QRgb*
>( myDifferenceImage.scanLine( y ) );
423 for (
int x = 0; x < maxWidth; ++x )
425 int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
426 int pixelTolerance = qMax( colorTolerance, maskTolerance );
427 if ( pixelTolerance == 255 )
433 QRgb myExpectedPixel = expectedScanline[x];
434 QRgb myActualPixel = resultScanline[x];
435 if ( pixelTolerance == 0 )
437 if ( myExpectedPixel != myActualPixel )
440 diffScanline[ x ] = qRgb( 255, 0, 0 );
445 if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
446 qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
447 qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
448 qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
451 diffScanline[ x ] = qRgb( 255, 0, 0 );
459 myDifferenceImage.save( myDiffImageFile );
466 qDebug(
"%d/%d pixels mismatched (%d allowed)", mMismatchCount,
mMatchTarget, theMismatchCount );
471 mReport +=
QString(
"<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
479 if ( mMismatchCount <= theMismatchCount )
481 mReport +=
"<tr><td colspan = 3>\n";
482 mReport +=
"Test image and result image for " + theTestName +
" are matched<br>";
484 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget <
mElapsedTime )
487 qDebug(
"Test failed because render step took too long" );
488 mReport +=
"<tr><td colspan = 3>\n";
489 mReport +=
"<font color=red>Test failed because render step took too long</font>";
502 if ( myAnomalyMatchFlag )
504 mReport +=
"<tr><td colspan=3>" 505 "Difference image matched a known anomaly - passing test! " 510 mReport +=
"<tr><td colspan=3></td></tr>";
511 emitDashMessage(
"Image mismatch",
QgsDartMeasurement::Text,
"Difference image did not match any known anomaly or mask." 512 " If you feel the difference image should be considered an anomaly " 513 "you can do something like this\n" 515 "/\nIf it should be included in the mask run\n" 518 mReport +=
"<tr><td colspan = 3>\n";
519 mReport +=
"<font color=red>Test image and result image for " + theTestName +
" are mismatched</font><br>";
const QgsMapSettings & mapSettings()
bridge to QgsMapSettings
void setControlPathSuffix(const QString &theName)
A rectangle specified with double values.
void setDotsPerMeterX(int x)
void setDotsPerMeterY(int y)
bool load(QIODevice *device, const char *format)
virtual void start() override
Start the rendering job and immediately return.
QString imageToHash(const QString &theImageFile)
Get an md5 hash that uniquely identifies an image.
QString & fill(QChar ch, int size)
void fillRect(const QRectF &rectangle, const QBrush &brush)
const uchar * constScanLine(int i) const
void setRenderHint(RenderHint hint, bool on)
void setControlName(const QString &theName)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
double yMaximum() const
Get the y maximum value (top side of rectangle)
bool save(const QString &fileName, const char *format, int quality) const
bool runTest(const QString &theTestName, unsigned int theMismatchCount=0)
Test using renderer to generate the image to be compared.
const T & at(int i) const
QPixmap fromImage(const QImage &image, QFlags< Qt::ImageConversionFlag > flags)
int dotsPerMeterX() const
int dotsPerMeterY() const
Q_DECL_DEPRECATED void setMapRenderer(QgsMapRenderer *thepMapRenderer)
A non GUI class for rendering a map layer set onto a QPainter.
void setMapSettings(const QgsMapSettings &mapSettings)
void setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
The QgsMapSettings class contains configuration for rendering of the map.
bool compareImages(const QString &theTestName, unsigned int theMismatchCount=0, const QString &theRenderedImageFile="")
Test using two arbitary images (map renderer will not be used)
void setOutputSize(QSize size)
Set the size of the resulting map image.
QString fromUtf8(const char *str, int size)
QString controlImagePath() const
QString qgsDoubleToString(double a, int precision=17)
Enable anti-aliasin for map rendering.
const char * constData() const
double mapUnitsPerPixel() const
Return the distance in geographical coordinates that equals to one pixel in the map.
unsigned int mMatchTarget
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
QByteArray toLocal8Bit() const
void setTexture(const QPixmap &pixmap)
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that transparency is visible without requiring a...
Job implementation that renders everything sequentially in one thread.
QString & replace(int position, int n, QChar after)
QString mRenderedImageFile
void setBackgroundColor(const QColor &color)
Set the background color of the map.
bool isKnownAnomaly(const QString &theDiffImageFile)
Get a list of all the anomalies.
virtual QImage renderedImage() override
Get a preview/resulting image.
QStringList entryList(QFlags< QDir::Filter > filters, QFlags< QDir::SortFlag > sort) const
QString mExpectedImageFile
QgsRectangle extent() const
Return geographical coordinates of the rectangle that should be rendered.
int indexOf(const QRegExp &rx, int from) const
QByteArray toBase64() const
virtual void waitForFinished() override
Block until the job has finished.
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
double xMinimum() const
Get the x minimum value (left side of rectangle)
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const
QByteArray toUtf8() const