Branch data Line data Source code
1 : : /*************************************************************************** 2 : : qgsrenderchecker.h - check maprender output against an expected image 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 : : #ifndef QGSRENDERCHECKER_H 17 : : #define QGSRENDERCHECKER_H 18 : : 19 : : #include "qgis_core.h" 20 : : #include "qgis_sip.h" 21 : : #include <QDir> 22 : : #include <QString> 23 : : #include <QRegExp> 24 : : #include <QList> 25 : : 26 : : #include "qgslogger.h" 27 : : #include "qgsmapsettings.h" 28 : : #include "qgsdartmeasurement.h" 29 : : 30 : : class QImage; 31 : : 32 : : /** 33 : : * \ingroup core 34 : : * \brief This is a helper class for unit tests that need to 35 : : * write an image and compare it to an expected result 36 : : * or render time. 37 : : */ 38 : 6 : class CORE_EXPORT QgsRenderChecker 39 : : { 40 : : public: 41 : : 42 : : /** 43 : : * Constructor for QgsRenderChecker. 44 : : */ 45 : : QgsRenderChecker(); 46 : : 47 : : /** 48 : : * Returns the base path containing the reference images. 49 : : * 50 : : * This defaults to an internal QGIS test data path, but can be changed via setControlImagePath(). 51 : : * 52 : : * \see setControlImagePath() 53 : : */ 54 : : QString controlImagePath() const; 55 : : 56 : : /** 57 : : * Sets the base \a path containing the reference images. 58 : : * 59 : : * \see controlImagePath() 60 : : * \since QGIS 3.18 61 : : */ 62 : : void setControlImagePath( const QString &path ); 63 : : 64 : : /** 65 : : * Returns the HTML report describing the results of the test run. 66 : : */ 67 : 6 : QString report() { return mReport; } 68 : : 69 : : /** 70 : : * Returns the percent of pixels which matched the control image. 71 : : */ 72 : : float matchPercent() const 73 : : { 74 : : return static_cast<float>( mMismatchCount ) / 75 : : static_cast<float>( mMatchTarget ) * 100; 76 : : } 77 : : 78 : : /** 79 : : * Returns the number of pixels which did not match the control image. 80 : : */ 81 : : unsigned int mismatchCount() const { return mMismatchCount; } 82 : : 83 : : /** 84 : : * Returns the total number of pixels in the control image. 85 : : */ 86 : : unsigned int matchTarget() const { return mMatchTarget; } 87 : : 88 : : /** 89 : : * Returns the total elapsed time for the rendering test. 90 : : * 91 : : * \note this only records time for actual render part. 92 : : */ 93 : : int elapsedTime() { return mElapsedTime; } 94 : : void setElapsedTimeTarget( int target ) { mElapsedTimeTarget = target; } 95 : : 96 : : /** 97 : : * Sets the base directory \a name for the control image (with control image path 98 : : * suffixed). 99 : : * 100 : : * The path to the image will be constructed like this: 101 : : * controlImagePath() + '/' + control name + '/' + control name + '.' + extension ('png' by default) 102 : : */ 103 : : void setControlName( const QString &name ); 104 : : 105 : : /** 106 : : * Sets file extension for the control image. By default it is "png" 107 : : * \since QGIS 3.20 108 : : */ 109 : : void setControlExtension( const QString &extension ) { mControlExtension = extension; } 110 : : 111 : : /** 112 : : * Sets the path prefix where the control images are kept. 113 : : * This will be appended to controlImagePath(). 114 : : */ 115 : 0 : void setControlPathPrefix( const QString &name ) { mControlPathPrefix = name + '/'; } 116 : : 117 : : void setControlPathSuffix( const QString &name ); 118 : : 119 : : //! Gets an md5 hash that uniquely identifies an image 120 : : QString imageToHash( const QString &imageFile ); 121 : : 122 : : /** 123 : : * Sets the file name of the rendered image generated by the test. 124 : : */ 125 : 6 : void setRenderedImage( const QString &imageFileName ) { mRenderedImageFile = imageFileName; } 126 : : 127 : : /** 128 : : * Returns the path of the rendered image generated by the test. 129 : : * 130 : : * This method will return either the path set with setRenderedImage() or generated in runTest(). 131 : : */ 132 : 0 : QString renderedImage() const { return mRenderedImageFile; } 133 : : 134 : : //! \since QGIS 2.4 135 : : void setMapSettings( const QgsMapSettings &mapSettings ); 136 : : 137 : : /** 138 : : * Set tolerance for color components used by runTest() and compareImages(). 139 : : * Default value is 0. 140 : : * \param colorTolerance is maximum difference for each color component 141 : : * including alpha to be considered correct. 142 : : * \since QGIS 2.1 143 : : */ 144 : 0 : void setColorTolerance( unsigned int colorTolerance ) { mColorTolerance = colorTolerance; } 145 : : 146 : : /** 147 : : * Sets the largest allowable difference in size between the rendered and the expected image. 148 : : * \param xTolerance x tolerance in pixels 149 : : * \param yTolerance y tolerance in pixels 150 : : * \since QGIS 2.12 151 : : */ 152 : 0 : void setSizeTolerance( int xTolerance, int yTolerance ) { mMaxSizeDifferenceX = xTolerance; mMaxSizeDifferenceY = yTolerance; } 153 : : 154 : : /** 155 : : * Test using renderer to generate the image to be compared. 156 : : * \param testName - to be used as the basis for writing a file to 157 : : * e.g. /tmp/theTestName.png 158 : : * \param mismatchCount - defaults to 0 - the number of pixels that 159 : : * are allowed to be different from the control image. In some cases 160 : : * rendering may be non-deterministic. This parameter allows you to account 161 : : * for that by providing a tolerance. 162 : : * \note make sure to call setExpectedImage and setMapRenderer first 163 : : */ 164 : : bool runTest( const QString &testName, unsigned int mismatchCount = 0 ); 165 : : 166 : : /** 167 : : * Test using two arbitrary images (map renderer will not be used) 168 : : * \param testName - to be used as the basis for writing a file to 169 : : * e.g. /tmp/theTestName.png 170 : : * \param mismatchCount - defaults to 0 - the number of pixels that 171 : : * are allowed to be different from the control image. In some cases 172 : : * rendering may be non-deterministic. This parameter allows you to account 173 : : * for that by providing a tolerance. 174 : : * \param renderedImageFile to optionally override the output filename 175 : : * \note: make sure to call setExpectedImage and setRenderedImage first. 176 : : */ 177 : : bool compareImages( const QString &testName, unsigned int mismatchCount = 0, const QString &renderedImageFile = QString() ); 178 : : 179 : : /** 180 : : * Test using two arbitrary images at the specified paths for equality. 181 : : * 182 : : * \since QGIS 3.18 183 : : */ 184 : : bool compareImages( const QString &testName, const QString &referenceImageFile, const QString &renderedImageFile, unsigned int mismatchCount = 0 ); 185 : : 186 : : /** 187 : : * Gets a list of all the anomalies. An anomaly is a rendered difference 188 : : * file where there is some red pixel content (indicating a render check 189 : : * mismatch), but where the output was still acceptable. If the render 190 : : * diff matches one of these anomalies we will still consider it to be 191 : : * acceptable. 192 : : * \returns a bool indicating if the diff matched one of the anomaly files 193 : : */ 194 : : bool isKnownAnomaly( const QString &diffImageFile ); 195 : : 196 : : /** 197 : : * Draws a checkboard pattern for image backgrounds, so that opacity is visible 198 : : * without requiring a transparent background for the image 199 : : */ 200 : : static void drawBackground( QImage *image ); 201 : : 202 : : /** 203 : : * Returns the path to the expected image file 204 : : * 205 : : * \returns Path to the expected image file 206 : : */ 207 : : QString expectedImageFile() const { return mExpectedImageFile; } 208 : : 209 : : /** 210 : : * Call this to enable internal buffering of dash messages. You may later call 211 : : * dashMessages() to get access to the buffered messages. If disabled (default) 212 : : * dash messages will be sent immediately. 213 : : * 214 : : * \param enable Enable or disable buffering 215 : : */ 216 : 0 : void enableDashBuffering( bool enable ) { mBufferDashMessages = enable; } 217 : : 218 : : /** 219 : : * Gets access to buffered dash messages. 220 : : * Only will return something if you call enableDashBuffering( TRUE ); before. 221 : : * 222 : : * \returns buffered dash messages 223 : : */ 224 : 0 : QVector<QgsDartMeasurement> dartMeasurements() const { return mDashMessages; } 225 : : 226 : : protected: 227 : : QString mReport; 228 : : unsigned int mMatchTarget = 0; 229 : : int mElapsedTime = 0; 230 : : QString mRenderedImageFile; 231 : : QString mExpectedImageFile; 232 : : 233 : : private: 234 : : void emitDashMessage( const QgsDartMeasurement &dashMessage ); 235 : : void emitDashMessage( const QString &name, QgsDartMeasurement::Type type, const QString &value ); 236 : : 237 : : QString mBasePath; 238 : : 239 : : QString mControlName; 240 : : unsigned int mMismatchCount = 0; 241 : : unsigned int mColorTolerance = 0; 242 : : int mMaxSizeDifferenceX = 0; 243 : : int mMaxSizeDifferenceY = 0; 244 : : int mElapsedTimeTarget = 0; 245 : : QgsMapSettings mMapSettings; 246 : 6 : QString mControlExtension = QStringLiteral( "png" ); 247 : : QString mControlPathPrefix; 248 : : QString mControlPathSuffix; 249 : : QVector<QgsDartMeasurement> mDashMessages; 250 : : bool mBufferDashMessages = false; 251 : : }; // class QgsRenderChecker 252 : : 253 : : 254 : : /** 255 : : * Compare two WKT strings with some tolerance 256 : : * \param a first WKT string 257 : : * \param b second WKT string 258 : : * \param tolerance tolerance to use (optional, defaults to 0.000001) 259 : : * \returns bool indicating if the WKT are sufficiently equal 260 : : */ 261 : : 262 : : inline bool compareWkt( const QString &a, const QString &b, double tolerance = 0.000001 ) 263 : : { 264 : : QgsDebugMsg( QStringLiteral( "a:%1 b:%2 tol:%3" ).arg( a, b ).arg( tolerance ) ); 265 : : QRegExp re( "-?\\d+(?:\\.\\d+)?(?:[eE]\\d+)?" ); 266 : : 267 : : QString a0( a ), b0( b ); 268 : : a0.replace( re, QStringLiteral( "#" ) ); 269 : : b0.replace( re, QStringLiteral( "#" ) ); 270 : : 271 : : QgsDebugMsg( QStringLiteral( "a0:%1 b0:%2" ).arg( a0, b0 ) ); 272 : : 273 : : if ( a0 != b0 ) 274 : : return false; 275 : : 276 : : QList<double> al, bl; 277 : : 278 : : int pos; 279 : : for ( pos = 0; ( pos = re.indexIn( a, pos ) ) != -1; pos += re.matchedLength() ) 280 : : { 281 : : al << re.cap( 0 ).toDouble(); 282 : : } 283 : : for ( pos = 0; ( pos = re.indexIn( b, pos ) ) != -1; pos += re.matchedLength() ) 284 : : { 285 : : bl << re.cap( 0 ).toDouble(); 286 : : } 287 : : 288 : : if ( al.size() != bl.size() ) 289 : : return false; 290 : : 291 : : for ( int i = 0; i < al.size(); i++ ) 292 : : { 293 : : if ( !qgsDoubleNear( al[i], bl[i], tolerance ) ) 294 : : return false; 295 : : } 296 : : 297 : : return true; 298 : : } 299 : : 300 : : #endif