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