QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
qgsrenderchecker.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsrenderchecker.cpp
3 --------------------------------------
4 Date : 18 Jan 2008
5 Copyright : (C) 2008 by Tim Sutton
6 Email : tim @ linfiniti.com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsrenderchecker.h"
17
18#include "qgis.h"
20
21#include <QColor>
22#include <QPainter>
23#include <QImage>
24#include <QCryptographicHash>
25#include <QByteArray>
26#include <QDebug>
27#include <QBuffer>
28#include <QUuid>
29
31 : mBasePath( QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/control_images/" ) ) //defined in CmakeLists.txt
32{
33 if ( qgetenv( "QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral( "true" ) )
34 mIsCiRun = true;
35}
36
38{
39 if ( qgetenv( "QGIS_CONTINUOUS_INTEGRATION_RUN" ) == QStringLiteral( "true" ) )
40 return QDir( QDir( "/root/QGIS" ).filePath( QStringLiteral( "qgis_test_report" ) ) );
41 else
42 return QDir( QDir::temp().filePath( QStringLiteral( "qgis_test_report" ) ) );
43}
44
46{
47 return true;
48}
49
51{
52 return mBasePath + ( mBasePath.endsWith( '/' ) ? QString() : QStringLiteral( "/" ) ) + mControlPathPrefix;
53}
54
55void QgsRenderChecker::setControlImagePath( const QString &path )
56{
57 mBasePath = path;
58}
59
60QString QgsRenderChecker::report( bool ignoreSuccess ) const
61{
62 return ( ignoreSuccess && mResult ) ? QString() : mReport;
63}
64
65void QgsRenderChecker::setControlName( const QString &name )
66{
67 mControlName = name;
68 mExpectedImageFile = controlImagePath() + name + '/' + mControlPathSuffix + name + "." + mControlExtension;
69}
70
71void QgsRenderChecker::setControlPathSuffix( const QString &name )
72{
73 if ( !name.isEmpty() )
74 mControlPathSuffix = name + '/';
75 else
76 mControlPathSuffix.clear();
77}
78
79QString QgsRenderChecker::imageToHash( const QString &imageFile )
80{
81 QImage myImage;
82 myImage.load( imageFile );
83 QByteArray myByteArray;
84 QBuffer myBuffer( &myByteArray );
85 myImage.save( &myBuffer, "PNG" );
86 const QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
87 QCryptographicHash myHash( QCryptographicHash::Md5 );
88 myHash.addData( myImageString.toUtf8() );
89 return myHash.result().toHex().constData();
90}
91
93{
94 mMapSettings = mapSettings;
95}
96
98{
99 // create a 2x2 checker-board image
100 uchar pixDataRGB[] = { 255, 255, 255, 255,
101 127, 127, 127, 255,
102 127, 127, 127, 255,
103 255, 255, 255, 255
104 };
105
106 const QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
107 const QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
108
109 // fill image with texture
110 QBrush brush;
111 brush.setTexture( pix );
112 QPainter p( image );
113 p.setRenderHint( QPainter::Antialiasing, false );
114 p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
115 p.end();
116}
117
118bool QgsRenderChecker::isKnownAnomaly( const QString &diffImageFile )
119{
120 const QString myControlImageDir = controlImagePath() + mControlName + '/';
121 const QDir myDirectory = QDir( myControlImageDir );
122 QStringList myList;
123 const QString myFilename = QStringLiteral( "*" );
124 myList = myDirectory.entryList( QStringList( myFilename ),
125 QDir::Files | QDir::NoSymLinks );
126 //remove the control file from the list as the anomalies are
127 //all files except the control file
128 myList.removeAll( QFileInfo( mExpectedImageFile ).fileName() );
129
130 const QString myImageHash = imageToHash( diffImageFile );
131
132
133 for ( int i = 0; i < myList.size(); ++i )
134 {
135 const QString myFile = myList.at( i );
136 mReport += "<tr><td colspan=3>"
137 "Checking if " + myFile + " is a known anomaly.";
138 mReport += QLatin1String( "</td></tr>" );
139 const QString myAnomalyHash = imageToHash( controlImagePath() + mControlName + '/' + myFile );
140 QString myHashMessage = QStringLiteral(
141 "Checking if anomaly %1 (hash %2)<br>" )
142 .arg( myFile,
143 myAnomalyHash );
144 myHashMessage += QStringLiteral( "&nbsp; matches %1 (hash %2)" )
145 .arg( diffImageFile,
146 myImageHash );
147 //foo CDash
148 emitDashMessage( QStringLiteral( "Anomaly check" ), QgsDartMeasurement::Text, myHashMessage );
149
150 mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
151 if ( myImageHash == myAnomalyHash )
152 {
153 mReport += "<tr><td colspan=3>"
154 "Anomaly found! " + myFile;
155 mReport += QLatin1String( "</td></tr>" );
156 return true;
157 }
158 }
159 mReport += "<tr><td colspan=3>"
160 "No anomaly found! ";
161 mReport += QLatin1String( "</td></tr>" );
162 return false;
163}
164
165void QgsRenderChecker::emitDashMessage( const QgsDartMeasurement &dashMessage )
166{
167 if ( !mIsCiRun )
168 return;
169
170 if ( mBufferDashMessages )
171 mDashMessages << dashMessage;
172 else
173 dashMessage.send();
174}
175
176void QgsRenderChecker::emitDashMessage( const QString &name, QgsDartMeasurement::Type type, const QString &value )
177{
178 emitDashMessage( QgsDartMeasurement( name, type, value ) );
179}
180
181#if DUMP_BASE64_IMAGES
182void QgsRenderChecker::dumpRenderedImageAsBase64()
183{
184 QFile fileSource( mRenderedImageFile );
185 if ( !fileSource.open( QIODevice::ReadOnly ) )
186 {
187 return;
188 }
189
190 const QByteArray blob = fileSource.readAll();
191 const QByteArray encoded = blob.toBase64();
192 qDebug() << "Dumping rendered image " << mRenderedImageFile << " as base64\n";
193 qDebug() << "################################################################";
194 qDebug() << encoded;
195 qDebug() << "################################################################";
196 qDebug() << "End dump";
197}
198#endif
199
200void QgsRenderChecker::performPostTestActions( Flags flags )
201{
202 if ( mResult || mExpectFail )
203 return;
204
205#if DUMP_BASE64_IMAGES
206 if ( mIsCiRun && QFile::exists( mRenderedImageFile ) && !( flags & Flag::AvoidExportingRenderedImage ) )
207 dumpRenderedImageAsBase64();
208#endif
209
210 if ( shouldGenerateReport() )
211 {
212 const QDir reportDir = QgsRenderChecker::testReportDir();
213 if ( !reportDir.exists() )
214 {
215 if ( !QDir().mkpath( reportDir.path() ) )
216 {
217 qDebug() << "!!!!! cannot create " << reportDir.path();
218 }
219 }
220
221 if ( QFile::exists( mRenderedImageFile ) && !( flags & Flag::AvoidExportingRenderedImage ) )
222 {
223 QFileInfo fi( mRenderedImageFile );
224 const QString destPath = reportDir.filePath( fi.fileName() );
225 if ( QFile::exists( destPath ) )
226 QFile::remove( destPath );
227 if ( !QFile::copy( mRenderedImageFile, destPath ) )
228 {
229 qDebug() << "!!!!! could not copy " << mRenderedImageFile << " to " << destPath;
230 }
231 }
232 if ( QFile::exists( mDiffImageFile ) && !( flags & Flag::AvoidExportingRenderedImage ) )
233 {
234 QFileInfo fi( mDiffImageFile );
235 const QString destPath = reportDir.filePath( fi.fileName() );
236 if ( QFile::exists( destPath ) )
237 QFile::remove( destPath );
238 QFile::copy( mDiffImageFile, destPath );
239 }
240 }
241}
242
243bool QgsRenderChecker::runTest( const QString &testName,
244 unsigned int mismatchCount,
245 QgsRenderChecker::Flags flags )
246{
247 mResult = false;
248 if ( mExpectedImageFile.isEmpty() )
249 {
250 qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
251 mReport = "<table>"
252 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
253 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
254 "Image File not set.</td></tr></table>\n";
255 performPostTestActions( flags );
256 return mResult;
257 }
258 //
259 // Load the expected result pixmap
260 //
261 const QImage myExpectedImage( mExpectedImageFile );
262 if ( myExpectedImage.isNull() )
263 {
264 qDebug() << "QgsRenderChecker::runTest failed - Could not load expected image from " << mExpectedImageFile;
265 mReport = "<table>"
266 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
267 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
268 "Image File could not be loaded.</td></tr></table>\n";
269 performPostTestActions( flags );
270 return mResult;
271 }
272 mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
273 //
274 // Now render our layers onto a pixmap
275 //
276 mMapSettings.setBackgroundColor( qRgb( 152, 219, 249 ) );
278 mMapSettings.setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) / mMapSettings.devicePixelRatio() );
279
280 QElapsedTimer myTime;
281 myTime.start();
282
283 QgsMapRendererSequentialJob job( mMapSettings );
284 job.start();
285 job.waitForFinished();
286
287 mElapsedTime = myTime.elapsed();
288
289 QImage myImage = job.renderedImage();
290 Q_ASSERT( myImage.devicePixelRatioF() == mMapSettings.devicePixelRatio() );
291
292 //
293 // Save the pixmap to disk so the user can make a
294 // visual assessment if needed
295 //
296 mRenderedImageFile = QDir::tempPath() + '/' + testName + "_result.png";
297
298 myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
299 myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
300 if ( ! myImage.save( mRenderedImageFile, "PNG", 100 ) )
301 {
302 qDebug() << "QgsRenderChecker::runTest failed - Could not save rendered image to " << mRenderedImageFile;
303 mReport = "<table>"
304 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
305 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
306 "Image File could not be saved.</td></tr></table>\n";
307 performPostTestActions( flags );
308 return mResult;
309 }
310
311 //create a world file to go with the image...
312
313 QFile wldFile( QDir::tempPath() + '/' + testName + "_result.wld" );
314 if ( wldFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
315 {
316 const QgsRectangle r = mMapSettings.extent();
317
318 QTextStream stream( &wldFile );
319 stream << QStringLiteral( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
320 .arg( qgsDoubleToString( mMapSettings.mapUnitsPerPixel() ),
321 qgsDoubleToString( -mMapSettings.mapUnitsPerPixel() ),
322 qgsDoubleToString( r.xMinimum() + mMapSettings.mapUnitsPerPixel() / 2.0 ),
323 qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
324 }
325
326 return compareImages( testName, mismatchCount, QString(), flags );
327}
328
329
330bool QgsRenderChecker::compareImages( const QString &testName,
331 unsigned int mismatchCount,
332 const QString &renderedImageFile,
333 QgsRenderChecker::Flags flags )
334{
335 mResult = false;
336 if ( mExpectedImageFile.isEmpty() )
337 {
338 qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
339 mReport = "<table>"
340 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
341 "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
342 "Image File not set.</td></tr></table>\n";
343 performPostTestActions( flags );
344 return mResult;
345 }
346
347 return compareImages( testName, mExpectedImageFile, renderedImageFile, mismatchCount, flags );
348}
349
350bool QgsRenderChecker::compareImages( const QString &testName, const QString &referenceImageFile, const QString &renderedImageFile, unsigned int mismatchCount, QgsRenderChecker::Flags flags )
351{
352 mResult = false;
353 if ( ! renderedImageFile.isEmpty() )
354 {
355 mRenderedImageFile = renderedImageFile;
356#ifdef Q_OS_WIN
357 mRenderedImageFile = mRenderedImageFile.replace( '\\', '/' );
358#endif
359 }
360
361 if ( mRenderedImageFile.isEmpty() )
362 {
363 qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
364 mReport = "<table>"
365 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
366 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
367 "Image File not set.</td></tr></table>\n";
368 performPostTestActions( flags );
369 return mResult;
370 }
371
372 //
373 // Load /create the images
374 //
375 QImage expectedImage( referenceImageFile );
376 if ( expectedImage.isNull() )
377 {
378 qDebug() << "QgsRenderChecker::runTest failed - Could not load control image from " << referenceImageFile;
379 mReport = "<table>"
380 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
381 "<tr><td>Nothing rendered</td>\n<td>Failed because control "
382 "image file could not be loaded.</td></tr></table>\n";
383 performPostTestActions( flags );
384 return mResult;
385 }
386
387 QImage myResultImage( mRenderedImageFile );
388 if ( myResultImage.isNull() )
389 {
390 qDebug() << "QgsRenderChecker::runTest failed - Could not load rendered image from " << mRenderedImageFile;
391 mReport = "<table>"
392 "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
393 "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
394 "Image File could not be loaded.</td></tr></table>\n";
395 performPostTestActions( flags );
396 return mResult;
397 }
398 QImage myDifferenceImage( expectedImage.width(),
399 expectedImage.height(),
400 QImage::Format_RGB32 );
401 mDiffImageFile = QDir::tempPath() + '/' + testName + "_result_diff.png";
402 myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
403
404 //check for mask
405 QString maskImagePath = referenceImageFile;
406 maskImagePath.chop( 4 ); //remove .png extension
407 maskImagePath += QLatin1String( "_mask.png" );
408 const QImage maskImage( maskImagePath );
409 const bool hasMask = !maskImage.isNull();
410
411 //
412 // Set pixel count score and target
413 //
414 mMatchTarget = expectedImage.width() * expectedImage.height();
415 const unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
416 //
417 // Set the report with the result
418 //
419 mReport = QStringLiteral( "<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
420 mReport += QLatin1String( "<table>" );
421 mReport += QLatin1String( "<tr><td colspan=2>" );
422 mReport += QString( "<tr><td colspan=2>"
423 "Test image and result image for %1<br>"
424 "Expected size: %2 w x %3 h (%4 pixels)<br>"
425 "Actual size: %5 w x %6 h (%7 pixels)"
426 "</td></tr>" )
427 .arg( testName )
428 .arg( expectedImage.width() ).arg( expectedImage.height() ).arg( mMatchTarget )
429 .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
430 mReport += QString( "<tr><td colspan=2>\n"
431 "Expected Duration : <= %1 (0 indicates not specified)<br>"
432 "Actual Duration : %2 ms<br></td></tr>" )
433 .arg( mElapsedTimeTarget )
434 .arg( mElapsedTime );
435
436 // limit image size in page to something reasonable
437 int imgWidth = 420;
438 int imgHeight = 280;
439 if ( ! expectedImage.isNull() )
440 {
441 imgWidth = std::min( expectedImage.width(), imgWidth );
442 imgHeight = expectedImage.height() * imgWidth / expectedImage.width();
443 }
444
445 const QString renderedImageFileName = QFileInfo( mRenderedImageFile ).fileName();
446 const QString diffImageFileName = QFileInfo( mDiffImageFile ).fileName();
447 const QString myImagesString = QString(
448 "<tr>"
449 "<td colspan=2>Compare expected and actual result</td>"
450 "<td>Difference (all blue is good, any red is bad)</td>"
451 "</tr>\n<tr>"
452 "<td colspan=2 id=\"td-%1-%7\"></td>\n"
453 "<td align=center><img width=%5 height=%6 src=\"%2\"></td>\n"
454 "</tr>"
455 "</table>\n"
456 "<script>\naddComparison(\"td-%1-%7\",\"%3\",\"file://%4\",%5,%6);\n</script>\n"
457 "<p>If the new image looks good, create or update a test mask with<br>"
458 "<code>scripts/generate_test_mask_image.py \"%8\" \"%9\"</code>" )
459 .arg( testName,
460 diffImageFileName,
461 renderedImageFileName,
462 referenceImageFile )
463 .arg( imgWidth ).arg( imgHeight )
464 .arg( QUuid::createUuid().toString().mid( 1, 6 ),
465 referenceImageFile,
467 );
468
469 QString prefix;
470 if ( !mControlPathPrefix.isNull() )
471 {
472 prefix = QStringLiteral( " (prefix %1)" ).arg( mControlPathPrefix );
473 }
474
475 //
476 // Put the same info to debug too
477 //
478
479 if ( expectedImage.width() != myResultImage.width() || expectedImage.height() != myResultImage.height() )
480 {
481 qDebug( "Expected size: %dw x %dh", expectedImage.width(), expectedImage.height() );
482 qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
483 if ( hasMask )
484 qDebug( "Mask size: %dw x %dh", maskImage.width(), maskImage.height() );
485 }
486
487 if ( mMatchTarget != myPixelCount )
488 {
489 qDebug( "Test image and result image for %s are different dimensions", testName.toLocal8Bit().constData() );
490
491 if ( std::abs( expectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
492 std::abs( expectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
493 {
494 emitDashMessage( "Rendered Image " + testName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
495 emitDashMessage( "Expected Image " + testName + prefix, QgsDartMeasurement::ImagePng, referenceImageFile );
496
497 mReport += QLatin1String( "<tr><td colspan=3>" );
498 mReport += "<font color=red>Expected image and result image for " + testName + " are different dimensions - FAILING!</font>";
499 mReport += QLatin1String( "</td></tr>" );
500
501 const QString diffSizeImagesString = QString(
502 "<tr>"
503 "<td colspan=3>Compare expected and actual result</td>"
504 "</tr>\n<tr>"
505 "<td align=center><img src=\"%1\"></td>\n"
506 "<td align=center><img width=%3 height=%4 src=\"%2\"></td>\n"
507 "</tr>"
508 "</table>\n" )
509 .arg(
510 renderedImageFileName,
511 referenceImageFile )
512 .arg( imgWidth ).arg( imgHeight );
513
514 mReport += diffSizeImagesString;
515 performPostTestActions( flags );
516 return mResult;
517 }
518 else
519 {
520 mReport += QLatin1String( "<tr><td colspan=3>" );
521 mReport += "Expected image and result image for " + testName + " are different dimensions, but within tolerance";
522 mReport += QLatin1String( "</td></tr>" );
523 }
524 }
525
526 if ( expectedImage.format() == QImage::Format_Indexed8 )
527 {
528 if ( myResultImage.format() != QImage::Format_Indexed8 )
529 {
530 emitDashMessage( "Rendered Image " + testName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
531 emitDashMessage( "Expected Image " + testName + prefix, QgsDartMeasurement::ImagePng, referenceImageFile );
532
533 qDebug() << "Expected image and result image for " << testName << " have different formats (8bit format is expected) - FAILING!";
534
535 mReport += QLatin1String( "<tr><td colspan=3>" );
536 mReport += "<font color=red>Expected image and result image for " + testName + " have different formats (8bit format is expected) - FAILING!</font>";
537 mReport += QLatin1String( "</td></tr>" );
538 mReport += myImagesString;
539 performPostTestActions( flags );
540 return mResult;
541 }
542
543 // When we compute the diff between the 2 images, we use constScanLine expecting a QRgb color
544 // but this method returns color table index for 8 bit image, not color.
545 // So we convert the 2 images in 32 bits so the diff works correctly
546 myResultImage = myResultImage.convertToFormat( QImage::Format_ARGB32 );
547 expectedImage = expectedImage.convertToFormat( QImage::Format_ARGB32 );
548 }
549
550
551 //
552 // Now iterate through them counting how many
553 // dissimilar pixel values there are
554 //
555
556 const int maxHeight = std::min( expectedImage.height(), myResultImage.height() );
557 const int maxWidth = std::min( expectedImage.width(), myResultImage.width() );
558
559 mMismatchCount = 0;
560 const int colorTolerance = static_cast< int >( mColorTolerance );
561 for ( int y = 0; y < maxHeight; ++y )
562 {
563 const QRgb *expectedScanline = reinterpret_cast< const QRgb * >( expectedImage.constScanLine( y ) );
564 const QRgb *resultScanline = reinterpret_cast< const QRgb * >( myResultImage.constScanLine( y ) );
565 const QRgb *maskScanline = ( hasMask && maskImage.height() > y ) ? reinterpret_cast< const QRgb * >( maskImage.constScanLine( y ) ) : nullptr;
566 QRgb *diffScanline = reinterpret_cast< QRgb * >( myDifferenceImage.scanLine( y ) );
567
568 for ( int x = 0; x < maxWidth; ++x )
569 {
570 const int maskTolerance = ( maskScanline && maskImage.width() > x ) ? qRed( maskScanline[ x ] ) : 0;
571 const int pixelTolerance = std::max( colorTolerance, maskTolerance );
572 if ( pixelTolerance == 255 )
573 {
574 //skip pixel
575 continue;
576 }
577
578 const QRgb myExpectedPixel = expectedScanline[x];
579 const QRgb myActualPixel = resultScanline[x];
580 if ( pixelTolerance == 0 )
581 {
582 if ( myExpectedPixel != myActualPixel )
583 {
584 ++mMismatchCount;
585 diffScanline[ x ] = qRgb( 255, 0, 0 );
586 }
587 }
588 else
589 {
590 if ( std::abs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
591 std::abs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
592 std::abs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
593 std::abs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
594 {
595 ++mMismatchCount;
596 diffScanline[ x ] = qRgb( 255, 0, 0 );
597 }
598 }
599 }
600 }
601
602 //
603 // Send match result to debug
604 //
605 if ( mMismatchCount > mismatchCount )
606 {
607 emitDashMessage( "Rendered Image " + testName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
608 emitDashMessage( "Expected Image " + testName + prefix, QgsDartMeasurement::ImagePng, referenceImageFile );
609
610 qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, mismatchCount );
611
612 //
613 //save the diff image to disk
614 //
615 myDifferenceImage.save( mDiffImageFile );
616 emitDashMessage( "Difference Image " + testName + prefix, QgsDartMeasurement::ImagePng, mDiffImageFile );
617 }
618
619 //
620 // Send match result to report
621 //
622 mReport += QStringLiteral( "<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
623 .arg( mMismatchCount ).arg( mMatchTarget ).arg( mismatchCount ).arg( mColorTolerance );
624
625 //
626 // And send it to CDash
627 //
628 if ( mMismatchCount > 0 )
629 {
630 emitDashMessage( QStringLiteral( "Mismatch Count" ), QgsDartMeasurement::Integer, QStringLiteral( "%1/%2" ).arg( mMismatchCount ).arg( mMatchTarget ) );
631 }
632
633 if ( mMismatchCount <= mismatchCount )
634 {
635 mReport += QLatin1String( "<tr><td colspan = 3>\n" );
636 mReport += "Test image and result image for " + testName + " are matched<br>";
637 mReport += QLatin1String( "</td></tr>" );
638 if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
639 {
640 //test failed because it took too long...
641 qDebug( "Test failed because render step took too long" );
642 mReport += QLatin1String( "<tr><td colspan = 3>\n" );
643 mReport += QLatin1String( "<font color=red>Test failed because render step took too long</font>" );
644 mReport += QLatin1String( "</td></tr>" );
645 mReport += myImagesString;
646 performPostTestActions( flags );
647 return mResult;
648 }
649 else
650 {
651 mReport += myImagesString;
652 mResult = true;
653 performPostTestActions( flags );
654 return mResult;
655 }
656 }
657
658 mReport += QLatin1String( "<tr><td colspan=3></td></tr>" );
659 emitDashMessage( QStringLiteral( "Image mismatch" ), QgsDartMeasurement::Text, "Difference image did not match any known anomaly or mask."
660 " If you feel the difference image should be considered an anomaly "
661 "you can do something like this\n"
662 "cp '" + mDiffImageFile + "' " + controlImagePath() + mControlName +
663 "/\nIf it should be included in the mask run\n"
664 "scripts/generate_test_mask_image.py '" + referenceImageFile + "' '" + mRenderedImageFile + "'\n" );
665
666 mReport += QLatin1String( "<tr><td colspan = 3>\n" );
667 mReport += "<font color=red>Test image and result image for " + testName + " are mismatched</font><br>";
668 mReport += QLatin1String( "</td></tr>" );
669 mReport += myImagesString;
670
671 performPostTestActions( flags );
672 return mResult;
673}
@ Antialiasing
Enable anti-aliasing for map rendering.
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.
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 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.
void setFlag(Qgis::MapSettingsFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
Q_DECL_DEPRECATED 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).
static QDir testReportDir()
Returns the directory to use for generating a test report.
static bool shouldGenerateReport()
Returns true if a test report should be generated given the current environment.
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)
bool runTest(const QString &testName, unsigned int mismatchCount=0, QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Render checker flags.
QString imageToHash(const QString &imageFile)
Gets an md5 hash that uniquely identifies an image.
@ AvoidExportingRenderedImage
Avoids exporting rendered images to reports.
QString report(bool ignoreSuccess=true) const
Returns the HTML report describing the results of the test run.
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that opacity is visible without requiring a tran...
bool compareImages(const QString &testName, unsigned int mismatchCount=0, const QString &renderedImageFile=QString(), QgsRenderChecker::Flags flags=QgsRenderChecker::Flags())
Test using two arbitrary images (map renderer will not be used)
QgsRenderChecker()
Constructor for QgsRenderChecker.
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.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:3448