├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── blink-diff └── blink-diff2 ├── examples ├── YDN.png ├── YDN_Color.png ├── YDN_Color_output.png ├── YDN_Missing.png ├── YDN_Missing_output.png ├── YDN_Multi.png ├── YDN_Multi_output.png ├── YDN_Sort.png ├── YDN_Sort_output.png ├── YDN_Swap.png ├── YDN_Swap_output.png ├── YDN_Upper.png ├── YDN_Upper_output.png └── example.sh ├── images └── composition.png ├── index.js ├── index2.js ├── lib ├── compatibility.js ├── configuration │ ├── anchor.js │ ├── atoms │ │ ├── color.js │ │ ├── limit.js │ │ ├── rect.js │ │ └── threshold.js │ ├── base.js │ ├── blockOut.js │ ├── config.js │ ├── image.js │ ├── output.js │ ├── pixelComparison.js │ ├── shift.js │ ├── structure │ │ ├── device.js │ │ ├── device │ │ │ ├── app.js │ │ │ ├── identity.js │ │ │ ├── screen.js │ │ │ └── size.js │ │ ├── document.js │ │ ├── domElement.js │ │ ├── screenshot.js │ │ ├── structure.js │ │ ├── version.js │ │ └── viewPort.js │ └── structureComparison.js ├── constants.js ├── defaultConfig.js ├── image.js ├── pixelComparator.js └── scripts │ └── structure.js ├── package.json ├── test ├── .gitignore ├── index.js ├── test1.png └── test2.png └── yuidoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | artifacts/ 3 | node_modules/ 4 | **/npm-debug.log 5 | **/ynpm-debug.log 6 | tmp/ 7 | CVS/ 8 | .DS_Store 9 | .*.swp 10 | .svn 11 | *~ 12 | .com.apple.timemachine.supported 13 | .idea/ 14 | docs/ 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | artifacts/ 3 | **/npm-debug.log 4 | **/ynpm-debug.log 5 | .DS_Store 6 | *~ 7 | test/ 8 | tests/ 9 | docs/ 10 | examples/ 11 | screwdriver/ 12 | Makefile 13 | .idea/ 14 | docs/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | - "0.11" 6 | - "0.12" 7 | - "iojs" 8 | 9 | branches: 10 | except: 11 | - gh-pages 12 | 13 | script: 14 | - npm test 15 | 16 | after_script: 17 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 18 | - CODECLIMATE_REPO_TOKEN=005d1100902d63403afbfae7ab20d9dfc97145bfc0b428cf141057767cc2f5d9 ./node_modules/codeclimate-test-reporter/bin/codeclimate.js < ./coverage/lcov.info 19 | 20 | notifications: 21 | webhooks: 22 | urls: 23 | - https://webhooks.gitter.im/e/5ba2780eb32dc366def2 24 | on_success: change # options: [always|never|change] default: always 25 | on_failure: always # options: [always|never|change] default: always 26 | on_start: false # default: false 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | v1.0.11 5 | * Bugfix option loading for issues #27 & #28 6 | 7 | v1.0.11 - 05/26/15 8 | * Bugfix when calling PNGImage.readImage - scope was missing 9 | 10 | v1.0.10 - 04/21/15 11 | * Update PNGjs-Image (with additional synchronous behavior) 12 | * Add runSync for synchronous comparison 13 | 14 | v1.0.9 - 03/29/15 15 | * Update dependencies 16 | 17 | v1.0.8 - 03/29/15 18 | * Cleanup 19 | * Added code-Climate, coveralls, and others 20 | 21 | v1.0.7 - 12/06/14 22 | * Block-out areas + color definition 23 | 24 | v1.0.6 - 12/05/14 25 | * Bugfix: Using image as default instead of string when loading image 26 | 27 | v1.0.4 - 11/28/14 28 | * Added limit for image output, being able to determine when an output should be created 29 | 30 | v1.0.3 - 11/28/14 31 | * Added feature to load image from Buffer 32 | * Added input image cropping 33 | * Added perceptual comparison with perceptual color-space 34 | * Added gamma correction for perceptual comparison 35 | 36 | v1.0.2 - 11/09/14 37 | * Added shift feature for to prevent anti-aliasing and sub-pixeling issues 38 | 39 | v1.0.1 - Initial release (11/04/14) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Yahoo! Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Blink-Diff 2 | ========== 3 | 4 | A lightweight image comparison tool 5 | 6 | [![Build Status](https://img.shields.io/travis/yahoo/blink-diff.svg)](http://travis-ci.org/yahoo/blink-diff) 7 | [![Coveralls Coverage](https://img.shields.io/coveralls/yahoo/blink-diff.svg)](https://coveralls.io/r/yahoo/blink-diff) 8 | [![Code Climate Grade](https://img.shields.io/codeclimate/github/yahoo/blink-diff.svg)](https://codeclimate.com/github/yahoo/blink-diff) 9 | 10 | [![NPM version](https://badge.fury.io/js/blink-diff.svg)](https://www.npmjs.com/package/blink-diff) 11 | [![NPM License](https://img.shields.io/npm/l/blink-diff.svg)](https://www.npmjs.com/package/blink-diff) 12 | 13 | [![NPM](https://nodei.co/npm/blink-diff.png?downloads=true&stars=true)](https://www.npmjs.com/package/blink-diff) 14 | [![NPM](https://nodei.co/npm-dl/blink-diff.png?months=3&height=2)](https://www.npmjs.com/package/blink-diff) 15 | 16 | [![Coverage Report](https://img.shields.io/badge/Coverage_Report-Available-blue.svg)](http://yahoo.github.io/blink-diff/coverage/lcov-report/) 17 | [![API Documentation](https://img.shields.io/badge/API_Documentation-Available-blue.svg)](http://yahoo.github.io/blink-diff/docs/) 18 | 19 | [![Gitter Support](https://img.shields.io/badge/Support-Gitter_IM-yellow.svg)](https://gitter.im/preceptorjs/support) 20 | 21 | **Table of Contents** 22 | * [Installation](#installation) 23 | * [Usage](#usage) 24 | * [Command-Line Usage](#command-line-usage) 25 | * [Object Usage](#object-usage) 26 | * [Cropping](#cropping) 27 | * [Perceptual Comparison](#perceptual-comparison) 28 | * [Logging](#logging) 29 | * [Block-Out](#block-out) 30 | * [Examples](#examples) 31 | * [API-Documentation](#api-documentation) 32 | * [Tests](#tests) 33 | * [Project Focus](#project-focus) 34 | * [Project Naming](#project-naming) 35 | * [Contributions](#contributions) 36 | * [Contributors](#contributers) 37 | * [Third-party libraries](#third-party-libraries) 38 | * [License](#license) 39 | 40 | 41 | ##Image Comparison and Result 42 | ![Composition](https://raw.githubusercontent.com/yahoo/blink-diff/master/images/composition.png) 43 | 44 | ##Installation 45 | 46 | Install this module with the following command: 47 | ```shell 48 | npm install blink-diff 49 | ``` 50 | 51 | Add the module to your ```package.json``` dependencies: 52 | ```shell 53 | npm install --save blink-diff 54 | ``` 55 | Add the module to your ```package.json``` dev-dependencies: 56 | ```shell 57 | npm install --save-dev blink-diff 58 | ``` 59 | 60 | ##Usage 61 | 62 | The package can be used in two different ways: 63 | * per command line 64 | * through an object 65 | 66 | ###Command-Line usage 67 | 68 | The command-line tool can be found in the ```bin``` directory. You can run the application with 69 | 70 | ```shell 71 | blink-diff --output .png .png .png 72 | ``` 73 | Use ```image1``` and ```image2``` as the images you want to compare. 74 | Only PNGs are supported at this point. 75 | 76 | 77 | The command-line tool exposes a couple of flags and parameters for the comparison: 78 | ``` 79 | --verbose Turn on verbose mode 80 | --debug Turn on debug mode - leaving all filters and modifications on the result 81 | --threshold p Number of pixels/percent 'p' below which differences are ignored 82 | --threshold-type t 'pixel' and 'percent' as type of threshold. (default: pixel) 83 | --delta p Max. distance colors in the 4 dimensional color-space without triggering a difference. (default: 20) 84 | --copyImageA Copies first image to output as base. (default: true) 85 | --copyImageB Copies second image to output as base. 86 | --no-copy Doesn't copy anything to output as base. 87 | --output o Write difference to the file 'o' 88 | --filter f Filters f (separated with comma) that will be applied before the comparison. 89 | --no-composition Turns the composition feature off 90 | --compose-ltr Compose output image from left to right 91 | --compose-ttb Compose output image from top to bottom 92 | --hide-shift Hides shift highlighting (default: false) 93 | --h-shift Acceptable horizontal shift of pixel. (default: 0) 94 | --v-shift Acceptable vertical shift of pixel. (default: 0) 95 | --block-out x,y,w,h Block-out area. Can be repeated multiple times. 96 | --version Print version 97 | --help This help 98 | ``` 99 | 100 | 101 | ###Object usage 102 | 103 | The package can also be used directly in code, without going through the command-line. 104 | 105 | **Example:** 106 | ```javascript 107 | var diff = new BlinkDiff({ 108 | imageAPath: 'path/to/first/image', // Use file-path 109 | imageBPath: 'path/to/second/image', 110 | 111 | thresholdType: BlinkDiff.THRESHOLD_PERCENT, 112 | threshold: 0.01, // 1% threshold 113 | 114 | imageOutputPath: 'path/to/output/image' 115 | }); 116 | 117 | diff.run(function (error, result) { 118 | if (error) { 119 | throw error; 120 | } else { 121 | console.log(diff.hasPassed(result.code) ? 'Passed' : 'Failed'); 122 | console.log('Found ' + result.differences + ' differences.'); 123 | } 124 | }); 125 | ``` 126 | 127 | All the parameters that were available in the command-line tool are also available through the class constructor, however they might use slightly different wording. The class exposes additional parameters that are not available from the command-line: 128 | * ```imageAPath``` Defines the path to the first image that should be compared (required; imageAPath or imageA is required - see example below) 129 | * ```imageA``` Supplies first image that should be compared (required; imageAPath or imageA is required - see example below) - This can be a PNGImage instance or a Buffer instance with PNG data 130 | * ```imageBPath``` Defines the path to the second image that should be compared (required; imageBPath or imageB is required - see example below) 131 | * ```imageB``` Supplies second image that should be compared (required; imageBPath or imageB is required - see example below) - This can be a PNGImage instance or a Buffer instance with PNG data 132 | * ```imageOutputPath``` Defines the path to the output-file. If you leaves this one off, then this feature is turned-off. 133 | * ```imageOutputLimit``` Defines when an image output should be created. This can be for different images, similar or different images, or all comparisons. (default: BlinkDiff.OUTPUT_ALL) 134 | * ```verbose``` Verbose output (default: false) 135 | * ```thresholdType``` Type of threshold check. This can be BlinkDiff.THRESHOLD_PIXEL and BlinkDiff.THRESHOLD_PERCENT (default: BlinkDiff.THRESHOLD_PIXEL) 136 | * ```threshold``` Number of pixels/percent p below which differences are ignored (default: 500) - For percentage thresholds: 1 = 100%, 0.2 = 20% 137 | * ```delta``` Distance between the color coordinates in the 4 dimensional color-space that will not trigger a difference. (default: 20) 138 | * ```outputMaskRed``` Red intensity for the difference highlighting in the output file (default: 255) 139 | * ```outputMaskGreen``` Green intensity for the difference highlighting in the output file (default: 0) 140 | * ```outputMaskBlue``` Blue intensity for the difference highlighting in the output file (default: 0) 141 | * ```outputMaskAlpha``` Alpha intensity for the difference highlighting in the output file (default: 255) 142 | * ```outputMaskOpacity``` Opacity of the pixel for the difference highlighting in the output file (default: 0.7 - slightly transparent) 143 | * ```outputShiftRed``` Red intensity for the shift highlighting in the output file (default: 255) 144 | * ```outputShiftGreen``` Green intensity for the shift highlighting in the output file (default: 165) 145 | * ```outputShiftBlue``` Blue intensity for the shift highlighting in the output file (default: 0) 146 | * ```outputShiftAlpha``` Alpha intensity for the shift highlighting in the output file (default: 255) 147 | * ```outputShiftOpacity``` Opacity of the pixel for the shift highlighting in the output file (default: 0.7 - slightly transparent) 148 | * ```outputBackgroundRed``` Red intensity for the background in the output file (default: 0) 149 | * ```outputBackgroundGreen``` Green intensity for the background in the output file (default: 0) 150 | * ```outputBackgroundBlue``` Blue intensity for the background in the output file (default: 0) 151 | * ```outputBackgroundAlpha``` Alpha intensity for the background in the output file (default: undefined) 152 | * ```outputBackgroundOpacity``` Opacity of the pixel for the background in the output file (default: 0.6 - transparent) 153 | * ```blockOut``` Object or list of objects with coordinates that should be blocked before testing. 154 | * ```blockOutRed``` Red intensity for the block-out in the output file (default: 0) This color will only be visible in the result when debug-mode is turned on. 155 | * ```blockOutGreen``` Green intensity for the block-out in the output file (default: 0) This color will only be visible in the result when debug-mode is turned on. 156 | * ```blockOutBlue``` Blue intensity for the block-out in the output file (default: 0) This color will only be visible in the result when debug-mode is turned on. 157 | * ```blockOutAlpha``` Alpha intensity for the block-out in the output file (default: 255) 158 | * ```blockOutOpacity``` Opacity of the pixel for the block-out in the output file (default: 1.0) 159 | * ```copyImageAToOutput``` Copies the first image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the first image. (default) 160 | * ```copyImageBToOutput``` Copies the second image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the second image. 161 | * ```filter``` Filters that will be applied before the comparison. Available filters are: blur, grayScale, lightness, luma, luminosity, sepia 162 | * ```debug``` When set, then the applied filters will be shown on the output image. (default: false) 163 | * ```composition``` Creates as output a composition of all three images (approved, highlight, and build) (default: true) 164 | * ```composeLeftToRight``` Creates comparison-composition from left to right, otherwise it lets decide the app on what is best 165 | * ```composeTopToBottom``` Creates comparison-composition from top to bottom, otherwise it lets decide the app on what is best 166 | * ```hShift``` Horizontal shift for possible antialiasing (default: 2) Set to 0 to turn this off. 167 | * ```vShift``` Vertical shift for possible antialiasing (default: 2) Set to 0 to turn this off. 168 | * ```hideShift``` Uses the background color for "highlighting" shifts. (default: false) 169 | * ```cropImageA``` Cropping for first image (default: no cropping) - Format: { x:, y:, width:, height: } 170 | * ```cropImageB``` Cropping for second image (default: no cropping) - Format: { x:, y:, width:, height: } 171 | * ```perceptual``` Turn the perceptual comparison mode on. See below for more information. 172 | * ```gamma``` Gamma correction for all colors (will be used as base) (default: none) - Any value here will turn the perceptual comparison mode on 173 | * ```gammaR``` Gamma correction for red - Any value here will turn the perceptual comparison mode on 174 | * ```gammaG``` Gamma correction for green - Any value here will turn the perceptual comparison mode on 175 | * ```gammaB``` Gamma correction for blue - Any value here will turn the perceptual comparison mode on 176 | 177 | **Example:** 178 | ```javascript 179 | var firstImage = PNGImage.readImage('path/to/first/image', function (err) { 180 | 181 | if (err) { 182 | throw err; 183 | } 184 | 185 | var diff = new BlinkDiff({ 186 | imageA: srcImage, // Use already loaded image for first image 187 | imageBPath: 'path/to/second/image', // Use file-path to select image 188 | 189 | delta: 50, // Make comparison more tolerant 190 | 191 | outputMaskRed: 0, 192 | outputMaskBlue: 255, // Use blue for highlighting differences 193 | 194 | hideShift: true, // Hide anti-aliasing differences - will still determine but not showing it 195 | 196 | imageOutputPath: 'path/to/output/image' 197 | }); 198 | 199 | diff.run(function (error, result) { 200 | if (error) { 201 | throw error; 202 | } else { 203 | console.log(diff.hasPassed(result.code) ? 'Passed' : 'Failed'); 204 | console.log('Found ' + result.differences + ' differences.'); 205 | } 206 | }); 207 | }); 208 | ``` 209 | 210 | ####Cropping 211 | Images can be cropped before they are compared by using the ```cropImageA``` or ```cropImageB``` parameters. Single values can be left off, and the system will calculate the correct dimensions. However, ```x```/```y``` coordinates have priority over ```width```/```height``` as the position are usually more important than the dimensions - image will also be clipped by the system when needed. 212 | 213 | ####Perceptual Comparison 214 | The perceptual comparison mode considers the perception of colors in the human brain. It transforms all the colors into a human perception color-space, which is quite different to the typical physical bound RGB color-space. There, in the perceptual color-space, the distance between colors is according to the human perception and should therefore closer resemble the differences a human would perceive seeing the images. 215 | 216 | ####Logging 217 | 218 | By default, the logger doesn't log events anywhere, but you can change this behavior by overwriting ```blinkDiff.log```: 219 | 220 | ```javascript 221 | var blinkDiff = new BlinkDiff({ 222 | ... 223 | }); 224 | 225 | blinkDiff.log = function (text) { 226 | // Do whatever you want to do 227 | }; 228 | 229 | ... 230 | ``` 231 | 232 | ####Block-Out 233 | Sometimes, it is necessary to block-out some specific areas in an image that should be ignored for comparisons. For example, this can be IDs or even time-labels that change with the time. Adding block-outs to images may decrease false positives and therefore stabilizes these comparisons. 234 | 235 | The color of the block-outs can be selected by the API parameters. However, the block-out areas will not be visible by default - they are hidden even though they are used. To make them visible, turn the debug-mode on. 236 | 237 | ##Examples 238 | 239 | There are some examples in the ```examples``` folder, in which I used screenshots of YDN to check for visual regressions (and made some manual modifications to the dom to make differences appear ;-)). 240 | You can find examples for: 241 | * Color changes in ```YDN_Color``` 242 | * Missing DOM elements in ```YDN_Missing``` (including some anti-aliasing) 243 | * Multiple differences in ```YDN_Multi``` 244 | * Disrupted sorting in ```YDN_Sort``` 245 | * Swapped items in ```YDN_Swap``` (including block-out areas) 246 | * Text capitalization in ```YDN_Upper``` 247 | 248 | All screenshots were compared to ```YDN.png```, a previously approved screenshot without a regression. 249 | Each of the regressions has the screenshot and the output result, highlighting the differences. 250 | 251 | ##API-Documentation 252 | 253 | Generate the documentation with following command: 254 | ```shell 255 | npm run docs 256 | ``` 257 | The documentation will be generated in the ```docs``` folder of the module root. 258 | 259 | ##Tests 260 | 261 | Run the tests with the following command: 262 | ```shell 263 | npm run test 264 | ``` 265 | The code-coverage will be written to the ```coverage``` folder in the module root. 266 | 267 | ##Project Focus 268 | There are three types of image comparisons: 269 | * Pixel-by-pixel - Used to compare low-frequency images like screenshots from web-sites, making sure that small styling differences trigger 270 | * Perceptual - Used to compare image creation applications, for example rendering engines and photo manipulation applications that are taking the human perception into account, ignoring differences a human probably would not see 271 | * Context - Used to see if parts of images are missing or are severely distorted, but accepts smaller and/or perceptual differences 272 | 273 | Blink-Diff was initially created to compare screenshots. These images are generally low-frequency, meaning larger areas with the same color and less gradients than in photos. The pixel-by-pixel comparison was chosen as it will trigger for differences that a human might not be able to see. We believe that a bug is still a bug even if a human won't see it - a regression might have happened that wasn't intended. 274 | A perceptual comparison would not trigger small differences, possibly missing problems that could get worse down the road. 275 | Pixel-by-pixel comparisons have the reputation of triggering too often, adding manual labor, checking images by hand. Blink-Diff was created to keep this in mind and was optimized to reduce false-positives by taking sub-pixeling and anti-aliasing into account. Additional features like thresholds and the pythagorean distance calculation in the four dimensional color-space makes sure that this won't happen too often. Additionally, filters can be applied to the images, for example to compare luminosity of pixels and not the saturation thereof. 276 | Blink-Diff also supports partially the perceptual comparison that can be turned on when supplying ```perceptual=true```. Then, the colors will be compared in accordance with the human perception and not according to the physical world. High-frequency filters, however, are not yet supported. 277 | 278 | ##Project Naming 279 | The name comes from the [Blink comparator](http://en.wikipedia.org/wiki/Blink_comparator) that was used in Astronomy to recognize differences in multiple photos, taking a picture of the same area in the sky over consecutive days, months, or years. Most notably, it was used to discover Pluto. 280 | 281 | ##Contributions 282 | Feel free to create an issue or create a pull-request if you have an idea on how to improve blink-diff. We are pretty relaxed on the contribution rules; add tests for your pull-requests when possible, but it is also ok if there are none - we'll add them for you. We are trying to improve blink-diff as much as possible, and this can only be done by contributions from the community. 283 | 284 | Also, even if you simply gave us an idea for a feature and did not actually write the code, we will still add you as the Contributor down below since it probably wouldn't be there without you. So, keep them coming! 285 | 286 | ##Contributors 287 | * [sarbbottam](https://github.com/sarbbottam) 288 | * [koola](https://github.com/koola) 289 | * [jeffposnick](https://github.com/jeffposnick) 290 | * [a-nwhitmont](https://github.com/a-nwhitmont) 291 | * [azu](https://github.com/azu) 292 | * [bradex](https://github.com/bradex) 293 | 294 | ##Third-party libraries 295 | 296 | The following third-party libraries are used by this module: 297 | 298 | ###Dependencies 299 | * promise: https://github.com/then/promise 300 | * pngjs-image: https://github.com/yahoo/pngjs-image 301 | 302 | ###Dev-Dependencies 303 | * chai: http://chaijs.com 304 | * coveralls: https://github.com/cainus/node-coveralls 305 | * codeclimate-test-reporter: https://github.com/codeclimate/javascript-test-reporter 306 | * istanbul: https://github.com/gotwarlost/istanbul 307 | * mocha: https://github.com/visionmedia/mocha 308 | * sinon: http://sinonjs.org 309 | * sinon-chai: https://github.com/domenic/sinon-chai 310 | * yuidocjs: https://github.com/yui/yuidoc 311 | 312 | ##License 313 | 314 | The MIT License 315 | 316 | Copyright 2014-2015 Yahoo Inc. 317 | -------------------------------------------------------------------------------- /bin/blink-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright 2014-2015 Yahoo! Inc. 4 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 5 | 6 | var BlinkDiff = require('../index'); 7 | 8 | var options, diff; 9 | 10 | try { 11 | 12 | printLicense(); 13 | 14 | options = parseArgs(process.argv.slice(1)); 15 | 16 | diff = new BlinkDiff(options); 17 | if (options.verbose) { 18 | diff.verbose = true; 19 | } 20 | 21 | // Setup console logger 22 | diff.log = function (text) { 23 | if (this.verbose) { 24 | console.log(text); 25 | } 26 | }; 27 | 28 | if (diff.verbose) { 29 | console.time('Time'); 30 | } 31 | diff.run(function (err, result) { 32 | 33 | if (err) { 34 | throw err; 35 | } 36 | 37 | var passed = diff.hasPassed(result.code); 38 | 39 | if (diff.verbose) { 40 | console.timeEnd('Time'); 41 | 42 | console.log('Differences:', result.differences, '(' + Math.round((result.differences / result.dimension) * 10000) / 100 + '%)'); 43 | } 44 | 45 | if (passed) { 46 | if (diff.verbose) { 47 | console.log("PASS"); 48 | } 49 | } else { 50 | console.log("FAIL"); 51 | } 52 | 53 | process.exit(passed ? 0 : 1); 54 | }); 55 | 56 | } catch (exception) { 57 | console.error(exception.message); 58 | process.exit(1); 59 | } 60 | 61 | /** 62 | * Prints the license 63 | */ 64 | function printLicense () { 65 | console.log("Blink-Diff " + BlinkDiff.version); 66 | console.log("Copyright (C) 2014 Yahoo! Inc."); 67 | } 68 | 69 | /** 70 | * Prints the help info 71 | */ 72 | function printHelp () { 73 | console.log("Usage: blink-diff "); 74 | console.log(""); 75 | console.log(" Compares image1 and image2."); 76 | console.log(""); 77 | console.log(" Options:"); 78 | console.log(" --verbose Turn on verbose mode"); 79 | console.log(" --debug Turn on debug mode - leaving all filters and modifications on the result"); 80 | console.log(" --threshold p Number of pixels/percent 'p' below which differences are ignored"); 81 | console.log(" --threshold-type t 'pixel' and 'percent' as type of threshold. (default: pixel)"); 82 | console.log(" --delta p Max. distance colors in the 4 dimensional color-space without triggering a difference. (default: 20)"); 83 | console.log(" --copyImageA Copies first image to output as base. (default: true)"); 84 | console.log(" --copyImageB Copies second image to output as base."); 85 | console.log(" --no-copy Doesn't copy anything to output as base."); 86 | console.log(" --output o Write difference to the file 'o'"); 87 | console.log(" --filter f Filters f (separated with comma) that will be applied before the comparison."); 88 | console.log(" --no-composition Turns the composition feature off"); 89 | console.log(" --compose-ltr Compose output image from left to right"); 90 | console.log(" --compose-ttb Compose output image from top to bottom"); 91 | console.log(" --hide-shift Hides shift highlighting (default: false)"); 92 | console.log(" --h-shift Acceptable horizontal shift of pixel. (default: 0)"); 93 | console.log(" --v-shift Acceptable vertical shift of pixel. (default: 0)"); 94 | console.log(" --block-out x,y,w,h Block-out area. Can be repeated multiple times."); 95 | console.log(" --version Print version"); 96 | console.log(" --help This help"); 97 | console.log(""); 98 | } 99 | 100 | /** 101 | * Parses the arguments and returns an option list 102 | * 103 | * @param {string[]} argv 104 | * @return {object} 105 | */ 106 | function parseArgs (argv) { 107 | 108 | var i, temporary, imageCount = 0, argLength = argv.length, options = {}; 109 | 110 | if (argLength <= 1) { 111 | printHelp(); 112 | process.exit(1); 113 | } 114 | 115 | options.blockOut = []; 116 | 117 | for (i = 1; i < argLength; i++) { 118 | 119 | try { 120 | 121 | if (argv[i] == "--help") { 122 | printHelp(); 123 | process.exit(0); 124 | 125 | } else if (argv[i] == "--verbose") { 126 | options.verbose = true; 127 | 128 | } else if (argv[i] == "--debug") { 129 | options.debug = true; 130 | 131 | } else if (argv[i] == "--no-composition") { 132 | options.composition = false; 133 | 134 | } else if (argv[i] == "--compose-ltr") { 135 | options.composeLeftToRight = true; 136 | 137 | } else if (argv[i] == "--compose-ttb") { 138 | options.composeTopToBottom = true; 139 | 140 | } else if (argv[i] == "--no-copy") { 141 | options.copyImageAToOutput = false; 142 | options.copyImageBToOutput = false; 143 | 144 | } else if (argv[i] == "--hide-shift") { 145 | options.hideShift = true; 146 | 147 | } else if (argv[i] == "--copyImageA") { 148 | options.copyImageAToOutput = true; 149 | options.copyImageBToOutput = false; 150 | 151 | } else if (argv[i] == "--copyImageB") { 152 | options.copyImageAToOutput = false; 153 | options.copyImageBToOutput = true; 154 | 155 | } else if (argv[i] == "--threshold-type") { 156 | if (++i < argLength) { 157 | if (argv[i] === 'pixel') { 158 | options.thresholdType = BlinkDiff.THRESHOLD_PIXEL; 159 | 160 | } else if (argv[i] === 'percent') { 161 | options.thresholdType = BlinkDiff.THRESHOLD_PERCENT; 162 | 163 | } else { 164 | throw new Error("--threshold-type can be either 'pixel' or 'percent'"); 165 | } 166 | } 167 | 168 | } else if (argv[i] == "--threshold") { 169 | if (++i < argLength) { 170 | 171 | temporary = parseFloat(argv[i]); 172 | if (temporary < 0) { 173 | throw new Error("--threshold must be positive"); 174 | } 175 | options.threshold = temporary; 176 | } 177 | 178 | } else if (argv[i] == "--h-shift") { 179 | if (++i < argLength) { 180 | 181 | temporary = parseInt(argv[i], 10); 182 | if (temporary < 0) { 183 | throw new Error("--h-shift must be positive"); 184 | } 185 | options.hShift = temporary; 186 | } 187 | 188 | } else if (argv[i] == "--v-shift") { 189 | if (++i < argLength) { 190 | 191 | temporary = parseInt(argv[i], 10); 192 | if (temporary < 0) { 193 | throw new Error("--v-shift must be positive"); 194 | } 195 | options.vShift = temporary; 196 | } 197 | 198 | } else if (argv[i] == "--delta") { 199 | if (++i < argLength) { 200 | 201 | temporary = parseFloat(argv[i]); 202 | if (temporary < 0) { 203 | throw new Error("--delta must be positive"); 204 | } 205 | options.delta = temporary; 206 | } 207 | 208 | } else if (argv[i] == "--block-out") { 209 | if (++i < argLength) { 210 | 211 | temporary = argv[i].split(','); 212 | if (temporary.length < 2) { 213 | throw new Error("--block-out should at least have the x and y coordinate"); 214 | } 215 | options.blockOut.push({ 216 | x: parseInt(temporary[0], 10), 217 | y: parseInt(temporary[1], 10), 218 | width: parseInt(temporary[2], 10), 219 | height: parseInt(temporary[3], 10) 220 | }); 221 | } 222 | 223 | } else if (argv[i] == "--filter") { 224 | if (++i < argLength) { 225 | options.filter = argv[i].split(','); 226 | } 227 | 228 | } else if (argv[i] == "--output") { 229 | if (++i < argLength) { 230 | options.imageOutputPath = argv[i]; 231 | } 232 | 233 | } else if (argv[i] == "--version") { 234 | console.log("Blink-Diff " + BlinkDiff.version); 235 | 236 | } else if (imageCount < 2) { 237 | 238 | ++imageCount; 239 | if (imageCount == 1) { 240 | options.imageAPath = argv[i]; 241 | } else { 242 | options.imageBPath = argv[i]; 243 | } 244 | 245 | } else { 246 | console.log('Warning: parameter "' + argv[i] + '" ignored. Unknown.'); 247 | } 248 | 249 | } catch (exception) { 250 | var reason = (exception.message !== '') ? "; " + exception.message : ''; 251 | throw new Error("Invalid argument '" + argv[i] + "' for " + argv[i - 1] + reason); 252 | } 253 | } 254 | 255 | if (!options.imageAPath || !options.imageBPath) { 256 | throw new Error("Please specify two images."); 257 | } 258 | 259 | return options; 260 | } 261 | -------------------------------------------------------------------------------- /bin/blink-diff2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright 2014-2015 Yahoo! Inc. 4 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 5 | 6 | var BlinkDiff = require('../index2'); 7 | var Compatibility = require('../lib/compatibility'); 8 | 9 | try { 10 | 11 | printLicense(); 12 | 13 | var options = parseArgs(process.argv.slice(1)); 14 | 15 | var compatibility = new Compatibility(options); 16 | 17 | var diff = new BlinkDiff(compatibility.generate()); 18 | if (options.verbose) { 19 | diff.verbose = true; 20 | } 21 | 22 | // Setup console logger 23 | diff.log = function (text) { 24 | if (this.verbose) { 25 | console.log(text); 26 | } 27 | }; 28 | 29 | if (diff.verbose) { 30 | console.time('Time'); 31 | } 32 | var result = diff.process(); 33 | 34 | var passed = diff.hasPassed(result.code); 35 | 36 | if (diff.verbose) { 37 | console.timeEnd('Time'); 38 | 39 | console.log('Differences:', result.differences, '(' + Math.round((result.differences / result.dimension) * 10000) / 100 + '%)'); 40 | } 41 | 42 | if (passed) { 43 | if (diff.verbose) { 44 | console.log("PASS"); 45 | } 46 | } else { 47 | console.log("FAIL"); 48 | } 49 | 50 | process.exit(passed ? 0 : 1); 51 | 52 | } catch (exception) { 53 | console.error(exception.message); 54 | process.exit(1); 55 | } 56 | 57 | /** 58 | * Prints the license 59 | */ 60 | function printLicense () { 61 | console.log("Blink-Diff " + BlinkDiff.version); 62 | console.log("Copyright (C) 2014 Yahoo! Inc."); 63 | } 64 | 65 | /** 66 | * Prints the help info 67 | */ 68 | function printHelp () { 69 | console.log("Usage: blink-diff "); 70 | console.log(""); 71 | console.log(" Compares image1 and image2."); 72 | console.log(""); 73 | console.log(" Options:"); 74 | console.log(" --verbose Turn on verbose mode"); 75 | console.log(" --debug Turn on debug mode - leaving all filters and modifications on the result"); 76 | console.log(" --threshold p Number of pixels/percent 'p' below which differences are ignored"); 77 | console.log(" --threshold-type t 'pixel' and 'percent' as type of threshold. (default: pixel)"); 78 | console.log(" --delta p Max. distance colors in the 4 dimensional color-space without triggering a difference. (default: 20)"); 79 | console.log(" --copyImageA Copies first image to output as base. (default: true)"); 80 | console.log(" --copyImageB Copies second image to output as base."); 81 | console.log(" --no-copy Doesn't copy anything to output as base."); 82 | console.log(" --output o Write difference to the file 'o'"); 83 | console.log(" --filter f Filters f (separated with comma) that will be applied before the comparison."); 84 | console.log(" --no-composition Turns the composition feature off"); 85 | console.log(" --compose-ltr Compose output image from left to right"); 86 | console.log(" --compose-ttb Compose output image from top to bottom"); 87 | console.log(" --hide-shift Hides shift highlighting (default: false)"); 88 | console.log(" --h-shift Acceptable horizontal shift of pixel. (default: 0)"); 89 | console.log(" --v-shift Acceptable vertical shift of pixel. (default: 0)"); 90 | console.log(" --block-out x,y,w,h Block-out area. Can be repeated multiple times."); 91 | console.log(" --version Print version"); 92 | console.log(" --help This help"); 93 | console.log(""); 94 | } 95 | 96 | /** 97 | * Parses the arguments and returns an option list 98 | * 99 | * @param {string[]} argv 100 | * @return {object} 101 | */ 102 | function parseArgs (argv) { 103 | 104 | var i, temporary, imageCount = 0, argLength = argv.length, options = {}; 105 | 106 | if (argLength <= 1) { 107 | printHelp(); 108 | process.exit(1); 109 | } 110 | 111 | options.blockOut = []; 112 | 113 | for (i = 1; i < argLength; i++) { 114 | 115 | try { 116 | 117 | if (argv[i] == "--help") { 118 | printHelp(); 119 | process.exit(0); 120 | 121 | } else if (argv[i] == "--verbose") { 122 | options.verbose = true; 123 | 124 | } else if (argv[i] == "--debug") { 125 | options.debug = true; 126 | 127 | } else if (argv[i] == "--no-composition") { 128 | options.composition = false; 129 | 130 | } else if (argv[i] == "--compose-ltr") { 131 | options.composeLeftToRight = true; 132 | 133 | } else if (argv[i] == "--compose-ttb") { 134 | options.composeTopToBottom = true; 135 | 136 | } else if (argv[i] == "--no-copy") { 137 | options.copyImageAToOutput = false; 138 | options.copyImageBToOutput = false; 139 | 140 | } else if (argv[i] == "--hide-shift") { 141 | options.hideShift = true; 142 | 143 | } else if (argv[i] == "--copyImageA") { 144 | options.copyImageAToOutput = true; 145 | options.copyImageBToOutput = false; 146 | 147 | } else if (argv[i] == "--copyImageB") { 148 | options.copyImageAToOutput = false; 149 | options.copyImageBToOutput = true; 150 | 151 | } else if (argv[i] == "--threshold-type") { 152 | if (++i < argLength) { 153 | if (argv[i] === 'pixel') { 154 | options.thresholdType = BlinkDiff.THRESHOLD_PIXEL; 155 | 156 | } else if (argv[i] === 'percent') { 157 | options.thresholdType = BlinkDiff.THRESHOLD_PERCENT; 158 | 159 | } else { 160 | throw new Error("--threshold-type can be either 'pixel' or 'percent'"); 161 | } 162 | } 163 | 164 | } else if (argv[i] == "--threshold") { 165 | if (++i < argLength) { 166 | 167 | temporary = parseFloat(argv[i]); 168 | if (temporary < 0) { 169 | throw new Error("--threshold must be positive"); 170 | } 171 | options.threshold = temporary; 172 | } 173 | 174 | } else if (argv[i] == "--h-shift") { 175 | if (++i < argLength) { 176 | 177 | temporary = parseInt(argv[i], 10); 178 | if (temporary < 0) { 179 | throw new Error("--h-shift must be positive"); 180 | } 181 | options.hShift = temporary; 182 | } 183 | 184 | } else if (argv[i] == "--v-shift") { 185 | if (++i < argLength) { 186 | 187 | temporary = parseInt(argv[i], 10); 188 | if (temporary < 0) { 189 | throw new Error("--v-shift must be positive"); 190 | } 191 | options.vShift = temporary; 192 | } 193 | 194 | } else if (argv[i] == "--delta") { 195 | if (++i < argLength) { 196 | 197 | temporary = parseFloat(argv[i]); 198 | if (temporary < 0) { 199 | throw new Error("--delta must be positive"); 200 | } 201 | options.delta = temporary; 202 | } 203 | 204 | } else if (argv[i] == "--block-out") { 205 | if (++i < argLength) { 206 | 207 | temporary = argv[i].split(','); 208 | if (temporary.length < 2) { 209 | throw new Error("--block-out should at least have the x and y coordinate"); 210 | } 211 | options.blockOut.push({ 212 | x: parseInt(temporary[0], 10), 213 | y: parseInt(temporary[1], 10), 214 | width: parseInt(temporary[2], 10), 215 | height: parseInt(temporary[3], 10) 216 | }); 217 | } 218 | 219 | } else if (argv[i] == "--filter") { 220 | if (++i < argLength) { 221 | options.filter = argv[i].split(','); 222 | } 223 | 224 | } else if (argv[i] == "--output") { 225 | if (++i < argLength) { 226 | options.imageOutputPath = argv[i]; 227 | } 228 | 229 | } else if (argv[i] == "--version") { 230 | console.log("Blink-Diff " + BlinkDiff.version); 231 | 232 | } else if (imageCount < 2) { 233 | 234 | ++imageCount; 235 | if (imageCount == 1) { 236 | options.imageAPath = argv[i]; 237 | } else { 238 | options.imageBPath = argv[i]; 239 | } 240 | 241 | } else { 242 | console.log('Warning: parameter "' + argv[i] + '" ignored. Unknown.'); 243 | } 244 | 245 | } catch (exception) { 246 | var reason = (exception.message !== '') ? "; " + exception.message : ''; 247 | throw new Error("Invalid argument '" + argv[i] + "' for " + argv[i - 1] + reason); 248 | } 249 | } 250 | 251 | if (!options.imageAPath || !options.imageBPath) { 252 | throw new Error("Please specify two images."); 253 | } 254 | 255 | return options; 256 | } 257 | -------------------------------------------------------------------------------- /examples/YDN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN.png -------------------------------------------------------------------------------- /examples/YDN_Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Color.png -------------------------------------------------------------------------------- /examples/YDN_Color_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Color_output.png -------------------------------------------------------------------------------- /examples/YDN_Missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Missing.png -------------------------------------------------------------------------------- /examples/YDN_Missing_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Missing_output.png -------------------------------------------------------------------------------- /examples/YDN_Multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Multi.png -------------------------------------------------------------------------------- /examples/YDN_Multi_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Multi_output.png -------------------------------------------------------------------------------- /examples/YDN_Sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Sort.png -------------------------------------------------------------------------------- /examples/YDN_Sort_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Sort_output.png -------------------------------------------------------------------------------- /examples/YDN_Swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Swap.png -------------------------------------------------------------------------------- /examples/YDN_Swap_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Swap_output.png -------------------------------------------------------------------------------- /examples/YDN_Upper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Upper.png -------------------------------------------------------------------------------- /examples/YDN_Upper_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/examples/YDN_Upper_output.png -------------------------------------------------------------------------------- /examples/example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --output YDN_Upper_output.png YDN.png YDN_Upper.png 4 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --output YDN_Missing_output.png YDN.png YDN_Missing.png 5 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --block-out 520,750,75,100 --block-out 50,50,100,100 --output YDN_Swap_output.png YDN.png YDN_Swap.png 6 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --output YDN_Color_output.png YDN.png YDN_Color.png 7 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --output YDN_Sort_output.png YDN.png YDN_Sort.png 8 | ../bin/blink-diff --verbose --compose-ltr --hide-shift --delta 25 --output YDN_Multi_output.png YDN.png YDN_Multi.png 9 | -------------------------------------------------------------------------------- /images/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/images/composition.png -------------------------------------------------------------------------------- /index2.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var PNGImage = require('pngjs-image'), 5 | Config = require('./lib/configuration/config'), 6 | Image = require('./lib/image'), 7 | PixelComparator = require('./lib/pixelComparator'), 8 | constants = require('./lib/constants'), 9 | Base = require('preceptor-core').Base; 10 | 11 | /** 12 | * @class BlinkDiff 13 | * @extends Base 14 | * @module Compare 15 | * 16 | * @property {Config} _configuration 17 | * @property {PNGImage} _outputImage 18 | * @property {PNGImage} _highlightImage 19 | */ 20 | var BlinkDiff = Base.extend( 21 | 22 | /** 23 | * Constructor 24 | * 25 | * @constructor 26 | * @param {object} options 27 | */ 28 | function (options) { 29 | options = options || {}; 30 | options.blinkDiff = this; 31 | this._configuration = new Config(options); 32 | }, 33 | 34 | { 35 | /** 36 | * Gets the configuration 37 | * 38 | * @method getConfig 39 | * @return {Config} 40 | */ 41 | getConfig: function () { 42 | return this._configuration; 43 | }, 44 | 45 | /** 46 | * Gets the output image 47 | * 48 | * @method getOutputImage 49 | * @return {PNGImage} 50 | */ 51 | getOutputImage: function () { 52 | return this._outputImage; 53 | }, 54 | 55 | /** 56 | * Gets the highlight image 57 | * 58 | * @method getHighlightImage 59 | * @return {PNGImage} 60 | */ 61 | getHighlightImage: function () { 62 | return this._highlightImage; 63 | }, 64 | 65 | 66 | /** 67 | * Logs events to the console 68 | * 69 | * @method log 70 | * @param {string} text 71 | */ 72 | log: function (text) { 73 | if (this._configuration.isVerboseMode()) { 74 | console.log(text); 75 | } 76 | }, 77 | 78 | /** 79 | * Clips the images to the lower resolution of both, if needed 80 | * 81 | * @private 82 | * @method _clip 83 | * @param {PNGImage} imageA Source image 84 | * @param {PNGImage} imageB Destination image 85 | */ 86 | _clip: function (imageA, imageB) { 87 | 88 | var minWidth, minHeight; 89 | 90 | if ((imageA.getWidth() != imageB.getWidth()) || (imageA.getHeight() != imageB.getHeight())) { 91 | 92 | minWidth = Math.min(imageA.getWidth(), imageB.getWidth()); 93 | minHeight = Math.min(imageA.getHeight(), imageB.getHeight()); 94 | 95 | this.log("Clipping to " + minWidth + " x " + minHeight); 96 | 97 | imageA.clip(0, 0, minWidth, minHeight); 98 | imageB.clip(0, 0, minWidth, minHeight); 99 | } 100 | }, 101 | 102 | /** 103 | * Has comparison passed? 104 | * 105 | * @method hasPassed 106 | * @param {int} result Comparison result-code 107 | * @return {boolean} 108 | */ 109 | hasPassed: function (result) { 110 | return ((result !== constants.RESULT_DIFFERENT) && (result !== constants.RESULT_UNKNOWN)); 111 | }, 112 | 113 | /** 114 | * Runs the comparison synchronously 115 | * 116 | * @method process 117 | * @return {Object} Result of comparison { code, differences, dimension, width, height } 118 | */ 119 | process: function () { 120 | 121 | // Catch all image errors 122 | PNGImage.log = function (text) { 123 | this.log('ERROR: ' + text); 124 | throw new Error('ERROR: ' + text); 125 | }.bind(this); 126 | 127 | var config = this.getConfig(), 128 | 129 | dimension, 130 | flagField, 131 | imageA = config.getImageA().getProcessedImage(), 132 | imageB = config.getImageB().getProcessedImage(), 133 | compareImageA, compareImageB, 134 | 135 | highlightImage, 136 | outputImage, 137 | differences = 0, 138 | shifts = 0, 139 | i, index, color, 140 | exported = false, 141 | code = constants.RESULT_UNKNOWN; 142 | 143 | this._highlightImage = null; 144 | this._outputImage = null; 145 | 146 | this._clip(imageA, imageB); 147 | 148 | dimension = imageA.getWidth() * imageB.getWidth(); 149 | flagField = new Buffer(dimension); 150 | flagField.fill(0); 151 | 152 | config.getComparisons().forEach(function (comparison, index) { 153 | 154 | this.log('Apply comparison #' + index); 155 | 156 | compareImageA = Image.processImage(PNGImage.copyImage(imageA), comparison); 157 | compareImageB = Image.processImage(PNGImage.copyImage(imageB), comparison); 158 | 159 | var pixelCompare = new PixelComparator(compareImageA, compareImageB, config); 160 | pixelCompare.compare(comparison, flagField); 161 | 162 | }.bind(this)); 163 | 164 | if (config.isDebugMode()) { // In debug-mode? Export comparison image 165 | 166 | this.log('In debug-mode'); 167 | 168 | imageA = compareImageA || imageA; 169 | imageB = compareImageB || imageB; 170 | } 171 | 172 | highlightImage = PNGImage.createImage(imageA.getWidth(), imageA.getHeight()); 173 | 174 | outputImage = PNGImage.createImage(imageA.getWidth(), imageA.getHeight()); 175 | config.getOutput().copyImage(imageA, imageB, outputImage); 176 | 177 | // Draw and count flag-field 178 | for(i = 0; i < dimension; i++) { 179 | 180 | index = i * 4; 181 | 182 | // Count 183 | if (flagField[i] & 1 == 1) { 184 | differences++; 185 | } 186 | if (flagField[i] & 2 == 2) { 187 | shifts++; 188 | } 189 | 190 | // Draw 191 | if (flagField[i] & 1 == 1) { 192 | color = config.getDiffColor(); 193 | 194 | } else if (flagField[i] & 2 == 2) { 195 | color = config.getIgnoreColor(); 196 | 197 | } else { 198 | color = config.getBackgroundColor(); 199 | } 200 | 201 | outputImage.setAtIndex(index, color.getColor(true, true)); 202 | highlightImage.setAtIndex(index, color.getColor(false, false)); 203 | } 204 | 205 | // Create composition if requested 206 | this._highlightImage = highlightImage; 207 | this._outputImage = config.getOutput().createComposition(imageA, imageB, outputImage); 208 | 209 | // Result 210 | if (differences == 0) { 211 | this.log("Images are identical or near identical"); 212 | code = constants.RESULT_IDENTICAL; 213 | 214 | } else if (config.getThreshold().isAboveThreshold(differences, dimension)) { 215 | this.log("Images are visibly different"); 216 | this.log(differences + " pixels are different"); 217 | code = constants.RESULT_DIFFERENT; 218 | 219 | } else { 220 | this.log("Images are similar"); 221 | this.log(differences + " pixels are different"); 222 | code = constants.RESULT_SIMILAR; 223 | } 224 | 225 | // Need to write to the filesystem? 226 | if (config.getOutput().withinOutputLimit(code)) { 227 | if (config.getOutput().writeImage(this._outputImage)) { 228 | this.log("Wrote differences to " + config.getOutput().getImagePath()); 229 | exported = true; 230 | } 231 | } 232 | 233 | return { 234 | code: code, 235 | differences: differences, 236 | shifts: shifts, 237 | dimension: dimension, 238 | width: imageA.getWidth(), 239 | height: imageA.getWidth(), 240 | highlightImage: highlightImage, 241 | outputImage: outputImage, 242 | exported: exported 243 | }; 244 | } 245 | }, 246 | 247 | { 248 | /** 249 | * Version 250 | * 251 | * @static 252 | * @property version 253 | * @type {string} 254 | */ 255 | version: require('./package.json').version 256 | } 257 | ); 258 | 259 | module.exports = BlinkDiff; 260 | -------------------------------------------------------------------------------- /lib/compatibility.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | 6 | var constants = require('./constants'); 7 | var assert = require('assert'); 8 | 9 | /** 10 | * @class Compatibility 11 | * @extends Base 12 | * @module Compare 13 | * 14 | * @property {object} _options 15 | */ 16 | var Compatibility = Base.extend( 17 | 18 | /** 19 | * Constructor for the compatibility object 20 | * 21 | * @constructor 22 | * @param {object} options 23 | * @param {PNGImage|Buffer} options.imageA Image object of first image 24 | * @param {string} options.imageAPath Path to first image 25 | * @param {PNGImage|Buffer} options.imageB Image object of second image 26 | * @param {string} options.imageBPath Path to second image 27 | * @param {string} [options.imageOutputPath=undefined] Path to output image file 28 | * @param {int} [options.imageOutputLimit=constants.OUTPUT_ALL] Determines when an image output is created 29 | * @param {string} [options.thresholdType=constants.THRESHOLD_PIXEL] Defines the threshold of the comparison 30 | * @param {int} [options.threshold=500] Threshold limit according to the comparison limit. 31 | * @param {number} [options.delta=20] Distance between the color coordinates in the 4 dimensional color-space that will not trigger a difference. 32 | * @param {int} [options.outputMaskRed=255] Value to set for red on difference pixel. 'Undefined' will not change the value. 33 | * @param {int} [options.outputMaskGreen=0] Value to set for green on difference pixel. 'Undefined' will not change the value. 34 | * @param {int} [options.outputMaskBlue=0] Value to set for blue on difference pixel. 'Undefined' will not change the value. 35 | * @param {int} [options.outputMaskAlpha=255] Value to set for the alpha channel on difference pixel. 'Undefined' will not change the value. 36 | * @param {float} [options.outputMaskOpacity=0.7] Strength of masking the pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. 37 | * @param {int} [options.outputShiftRed=255] Value to set for red on shifted pixel. 'Undefined' will not change the value. 38 | * @param {int} [options.outputShiftGreen=165] Value to set for green on shifted pixel. 'Undefined' will not change the value. 39 | * @param {int} [options.outputShiftBlue=0] Value to set for blue on shifted pixel. 'Undefined' will not change the value. 40 | * @param {int} [options.outputShiftAlpha=255] Value to set for the alpha channel on shifted pixel. 'Undefined' will not change the value. 41 | * @param {float} [options.outputShiftOpacity=0.7] Strength of masking the shifted pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. 42 | * @param {int} [options.outputBackgroundRed=0] Value to set for red as background. 'Undefined' will not change the value. 43 | * @param {int} [options.outputBackgroundGreen=0] Value to set for green as background. 'Undefined' will not change the value. 44 | * @param {int} [options.outputBackgroundBlue=0] Value to set for blue as background. 'Undefined' will not change the value. 45 | * @param {int} [options.outputBackgroundAlpha=undefined] Value to set for the alpha channel as background. 'Undefined' will not change the value. 46 | * @param {float} [options.outputBackgroundOpacity=0.6] Strength of masking the pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. 47 | * @param {object|object[]} [options.blockOut] Object or list of objects with coordinates of blocked-out areas. 48 | * @param {int} [options.blockOutRed=0] Value to set for red on blocked-out pixel. 'Undefined' will not change the value. 49 | * @param {int} [options.blockOutGreen=0] Value to set for green on blocked-out pixel. 'Undefined' will not change the value. 50 | * @param {int} [options.blockOutBlue=0] Value to set for blue on blocked-out pixel. 'Undefined' will not change the value. 51 | * @param {int} [options.blockOutAlpha=255] Value to set for the alpha channel on blocked-out pixel. 'Undefined' will not change the value. 52 | * @param {float} [options.blockOutOpacity=1.0] Strength of masking the blocked-out pixel. 1.0 means that the full color will be used; anything less will mix-in the original pixel. 53 | * @param {boolean} [options.copyImageAToOutput=true] Copies the first image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the first image. 54 | * @param {boolean} [options.copyImageBToOutput=false] Copies the second image to the output image before the comparison begins. This will make sure that the output image will highlight the differences on the second image. 55 | * @param {string[]} [options.filter=[]] Filters that will be applied before the comparison. Available filters are: blur, grayScale, lightness, luma, luminosity, sepia 56 | * @param {boolean} [options.debug=false] When set, then the applied filters will be shown on the output image. 57 | * @param {boolean} [options.composition=true] Should a composition be created to compare? 58 | * @param {boolean} [options.composeLeftToRight=false] Create composition from left to right, otherwise let it decide on its own whats best 59 | * @param {boolean} [options.composeTopToBottom=false] Create composition from top to bottom, otherwise let it decide on its own whats best 60 | * @param {boolean} [options.hideShift=false] Hides shift highlighting by using the background color instead 61 | * @param {int} [options.hShift=2] Horizontal shift for possible antialiasing 62 | * @param {int} [options.vShift=2] Vertical shift for possible antialiasing 63 | * @param {object} [options.cropImageA=null] Cropping for first image (default: no cropping) 64 | * @param {int} [options.cropImageA.x=0] Coordinate for left corner of cropping region 65 | * @param {int} [options.cropImageA.y=0] Coordinate for top corner of cropping region 66 | * @param {int} [options.cropImageA.width] Width of cropping region (default: Width that is left) 67 | * @param {int} [options.cropImageA.height] Height of cropping region (default: Height that is left) 68 | * @param {object} [options.cropImageB=null] Cropping for second image (default: no cropping) 69 | * @param {int} [options.cropImageB.x=0] Coordinate for left corner of cropping region 70 | * @param {int} [options.cropImageB.y=0] Coordinate for top corner of cropping region 71 | * @param {int} [options.cropImageB.width] Width of cropping region (default: Width that is left) 72 | * @param {int} [options.cropImageB.height] Height of cropping region (default: Height that is left) 73 | * @param {boolean} [options.perceptual=false] Turns perceptual comparison on 74 | * @param {float} [options.gamma] Gamma correction for all colors 75 | * @param {float} [options.gammaR] Gamma correction for red 76 | * @param {float} [options.gammaG] Gamma correction for green 77 | * @param {float} [options.gammaB] Gamma correction for blue 78 | */ 79 | function (options) { 80 | this._options = options; 81 | }, 82 | 83 | { 84 | /** 85 | * Generates a configuration object 86 | * 87 | * @method generate 88 | * @return {object} 89 | */ 90 | generate: function () { 91 | 92 | var options = this._options, 93 | blockOuts, 94 | comparison, 95 | config; 96 | 97 | assert.ok(options.imageAPath || options.imageA, "Image A not given."); 98 | assert.ok(options.imageBPath || options.imageB, "Image B not given."); 99 | 100 | options.blockOut = options.blockOut || []; 101 | if (typeof options.blockOut != 'object' && (options.blockOut.length !== undefined)) { 102 | options.blockOut = [options.blockOut]; 103 | } 104 | 105 | blockOuts = []; 106 | options.blockOut.forEach(function (blockOut) { 107 | blockOuts.push({ 108 | visible: true, 109 | area: { 110 | left: blockOut.x, 111 | top: blockout.y, 112 | width: blockout.width, 113 | height: blockout.height 114 | }, 115 | color: { 116 | red: options.blockOutRed || 0, 117 | green: options.blockOutGreen || 0, 118 | blue: options.blockOutBlue || 0, 119 | alpha: options.blockOutAlpha || 255, 120 | opacity: options.blockOutOpacity || 1.0 121 | } 122 | }); 123 | }); 124 | 125 | comparison = { 126 | type: 'pixel', 127 | colorDelta: options.delta || 20, 128 | 129 | gamma: { 130 | red: options.gamma || options.gammaR, 131 | green: options.gamma || options.gammaG, 132 | blue: options.gamma || options.gammaB 133 | }, 134 | perceptual: !!options.perceptual, 135 | filters: options.filter || [], 136 | 137 | shift: { 138 | active: !options.hideShift, 139 | horizontal: options.hShift || 2, 140 | vertical: options.vShift || 2 141 | }, 142 | 143 | blockOuts: blockOuts, 144 | 145 | areaImageA: { 146 | left: 0, 147 | top: 0, 148 | width: null, 149 | height: null 150 | }, 151 | areaImageB: { 152 | left: 0, 153 | top: 0, 154 | width: null, 155 | height: null 156 | } 157 | }; 158 | 159 | config = { 160 | 161 | debug: !!options.debug, 162 | verbose: !!options.debug, 163 | 164 | imageA: { 165 | image: options.imageA || options.imageAPath, 166 | crop: undefined 167 | }, 168 | imageB: { 169 | image: options.imageB || options.imageBPath, 170 | crop: undefined 171 | }, 172 | 173 | comparisons: [ comparison ], 174 | 175 | threshold: { 176 | type: options.thresholdType || constants.THRESHOLD_PIXEL, 177 | value: options.threshold || 500 178 | }, 179 | 180 | diffColor: { 181 | red: options.outputMaskRed || 255, 182 | green: options.outputMaskGreen || 0, 183 | blue: options.outputMaskBlue || 0, 184 | alpha: options.outputMaskAlpha || 255, 185 | opacity: options.outputMaskOpacity || 0.7 186 | }, 187 | 188 | backgroundColor: { 189 | red: options.outputBackgroundRed || 0, 190 | green: options.outputBackgroundGreen || 0, 191 | blue: options.outputBackgroundBlue || 0, 192 | alpha: options.outputBackgroundAlpha, 193 | opacity: options.outputBackgroundOpacity || 0.6 194 | }, 195 | ignoreColor: { 196 | red: options.outputShiftRed || 200, 197 | green: options.outputShiftGreen || 100, 198 | blue: options.outputShiftBlue || 0, 199 | alpha: options.outputShiftAlpha || 255, 200 | opacity: options.outputShiftOpacity || 0.7 201 | }, 202 | 203 | output: { 204 | imagePath: options.imageOutputPath, 205 | limit: options.imageOutputLimit || constants.OUTPUT_ALL, 206 | composition: options.composeLeftToRight ? constants.COMPOSITION_LEFT_TO_RIGHT : (options.composeTopToBottom ? constants.COMPOSITION_TOP_TO_BOTTOM : constants.COMPOSITION_AUTO), 207 | copyImage: options.copyImageBToOutput ? constants.COPY_IMAGE_B : constants.COPY_IMAGE_A 208 | } 209 | }; 210 | 211 | if (options.cropImageA) { 212 | config.imageA.crop = { 213 | left: options.cropImageA.x, 214 | top: options.cropImageA.y, 215 | width: options.cropImageA.width, 216 | height: options.cropImageA.height 217 | }; 218 | } 219 | if (options.cropImageB) { 220 | config.imageB.crop = { 221 | left: options.cropImageB.x, 222 | top: options.cropImageB.y, 223 | width: options.cropImageB.width, 224 | height: options.cropImageB.height 225 | }; 226 | } 227 | 228 | return config; 229 | } 230 | } 231 | ); 232 | 233 | module.exports = Compatibility; 234 | -------------------------------------------------------------------------------- /lib/configuration/anchor.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var Threshold = require('./atoms/threshold'); 8 | 9 | var constants = require('../constants'); 10 | 11 | /** 12 | * @class Anchor 13 | * @extends Base 14 | * @module Configuration 15 | * 16 | * @property {string} _type 17 | * @property {string} _position 18 | * @property {Threshold} _threshold 19 | */ 20 | var Anchor = Base.extend( 21 | 22 | /** 23 | * Anchor constructor 24 | * 25 | * @param {object} options 26 | * @param {string} options.type 27 | * @param {string} options.position 28 | * @param {object|Threshold} options.threshold 29 | * @constructor 30 | */ 31 | function (options) { 32 | this.__super(options); 33 | 34 | options = utils.deepExtend({ 35 | type: constants.ANCHOR_TYPE_WIDTH, 36 | position: constants.ANCHOR_POSITION_RELATIVE, 37 | threshold: {} 38 | }, [options]); 39 | 40 | this.setType(options.type); 41 | this.setPositioning(options.position); 42 | this.setThreshold(options.threshold); 43 | }, 44 | 45 | { 46 | /** 47 | * Gets the type of the anchor 48 | * 49 | * @method getType 50 | * @return {string} 51 | */ 52 | getType: function () { 53 | return this._type; 54 | }, 55 | 56 | /** 57 | * Sets the type of the anchor 58 | * 59 | * @method setType 60 | * @param {string} value 61 | */ 62 | setType: function (value) { 63 | this._type = value; 64 | }, 65 | 66 | 67 | /** 68 | * Gets the positioning of the anchor 69 | * 70 | * @method getPositioning 71 | * @return {string} 72 | */ 73 | getPositioning: function () { 74 | return this._position; 75 | }, 76 | 77 | /** 78 | * Sets the positioning of the anchor 79 | * 80 | * @method setPositioning 81 | * @param {string} value 82 | */ 83 | setPositioning: function (value) { 84 | this._position = value; 85 | }, 86 | 87 | 88 | /** 89 | * Gets the threshold 90 | * 91 | * @method getThreshold 92 | * @return {Threshold} 93 | */ 94 | getThreshold: function () { 95 | return this._threshold; 96 | }, 97 | 98 | /** 99 | * Sets the threshold 100 | * 101 | * @method setThreshold 102 | * @param {object|Threshold} value 103 | */ 104 | setThreshold: function (value) { 105 | this._threshold = this._parseObject(value, Threshold, "threshold"); 106 | } 107 | }, 108 | 109 | { 110 | /** 111 | * @property TYPE 112 | * @type {string} 113 | * @static 114 | */ 115 | TYPE: 'CONFIGURATION_ANCHOR' 116 | } 117 | ); 118 | 119 | module.exports = Anchor; 120 | -------------------------------------------------------------------------------- /lib/configuration/atoms/color.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | /** 8 | * @class Color 9 | * @extends Base 10 | * @module Configuration 11 | * @submodule Atoms 12 | * 13 | * @property {number} _red 14 | * @property {number} _green 15 | * @property {number} _blue 16 | * @property {number} _alpha 17 | * @property {float} _opacity 18 | */ 19 | var Color = Base.extend( 20 | 21 | /** 22 | * Color constructor 23 | * 24 | * @param {object} options 25 | * @param {number} options.red Red channel 26 | * @param {number} options.green Green channel 27 | * @param {number} options.blue Blue channel 28 | * @param {number} options.alpha Alpha Channel 29 | * @param {float} options.opacity Opacity of color 30 | * @constructor 31 | */ 32 | function (options) { 33 | this.__super(options); 34 | 35 | options = utils.deepExtend({ 36 | red: 0, 37 | green: 0, 38 | blue: 0, 39 | alpha: 255, 40 | opacity: 1.0 41 | }, [options]); 42 | 43 | this.setRed(options.red); 44 | this.setGreen(options.green); 45 | this.setBlue(options.blue); 46 | this.setAlpha(options.alpha); 47 | this.setOpacity(options.opacity); 48 | }, 49 | 50 | { 51 | /** 52 | * Gets the red channel 53 | * 54 | * @method getRed 55 | * @return {number} 56 | */ 57 | getRed: function () { 58 | return this._red; 59 | }, 60 | 61 | /** 62 | * Sets the red channel 63 | * 64 | * @method setRed 65 | * @param {number} value 66 | */ 67 | setRed: function (value) { 68 | this._red = value; 69 | }, 70 | 71 | 72 | /** 73 | * Gets the green channel 74 | * 75 | * @method getGreen 76 | * @return {number} 77 | */ 78 | getGreen: function () { 79 | return this._green; 80 | }, 81 | 82 | /** 83 | * Sets the green channel 84 | * 85 | * @method setGreen 86 | * @param {number} value 87 | */ 88 | setGreen: function (value) { 89 | this._green = value; 90 | }, 91 | 92 | 93 | /** 94 | * Gets the blue channel 95 | * 96 | * @method getBlue 97 | * @return {number} 98 | */ 99 | getBlue: function () { 100 | return this._blue; 101 | }, 102 | 103 | /** 104 | * Sets the blue channel 105 | * 106 | * @method setBlue 107 | * @param {number} value 108 | */ 109 | setBlue: function (value) { 110 | this._blue = value; 111 | }, 112 | 113 | 114 | /** 115 | * Gets the alpha channel 116 | * 117 | * @method getAlpha 118 | * @return {number} 119 | */ 120 | getAlpha: function () { 121 | return this._alpha; 122 | }, 123 | 124 | /** 125 | * Sets the alpha channel 126 | * 127 | * @method setAlpha 128 | * @param {number} value 129 | */ 130 | setAlpha: function (value) { 131 | this._alpha = value; 132 | }, 133 | 134 | 135 | /** 136 | * Gets the opacity of the color 137 | * 138 | * @method getOpacity 139 | * @return {float} 140 | */ 141 | getOpacity: function () { 142 | return this._opacity; 143 | }, 144 | 145 | /** 146 | * Sets the opacity of the color 147 | * 148 | * @method setOpacity 149 | * @param {float} value 150 | */ 151 | setOpacity: function (value) { 152 | this._opacity = value; 153 | }, 154 | 155 | 156 | /** 157 | * Gets all color channels as one color object 158 | * 159 | * @method getColor 160 | * @param {boolean} [alpha=false] Output alpha channel? 161 | * @param {boolean} [opacity=false] Output opacity? 162 | * @return {{red: (number), green: (number), blue: (number)[, alpha: (number)][, opacity: (float)]}} 163 | */ 164 | getColor: function (alpha, opacity) { 165 | 166 | var result = { 167 | red: this.getRed(), 168 | green: this.getGreen(), 169 | blue: this.getBlue() 170 | }; 171 | 172 | if (alpha) { 173 | result.alpha = this.getAlpha(); 174 | } 175 | 176 | if (opacity) { 177 | result.opacity = this.getOpacity(); 178 | } 179 | 180 | return result; 181 | }, 182 | 183 | /** 184 | * Gets all color channels as one color object with short descriptions 185 | * 186 | * @method getShortColor 187 | * @param {boolean} [alpha=false] Output alpha channel? 188 | * @param {boolean} [opacity=false] Output opacity? 189 | * @return {{r: (number), g: (number), b: (number)[, a: (number)][, o: (float)]}} 190 | */ 191 | getShortColor: function (alpha, opacity) { 192 | 193 | var result = { 194 | r: this.getRed(), 195 | g: this.getGreen(), 196 | b: this.getBlue() 197 | }; 198 | 199 | 200 | if (alpha) { 201 | result.a = this.getAlpha(); 202 | } 203 | 204 | if (opacity) { 205 | result.o = this.getOpacity(); 206 | } 207 | 208 | return result; 209 | } 210 | }, 211 | 212 | { 213 | /** 214 | * @property TYPE 215 | * @type {string} 216 | * @static 217 | */ 218 | TYPE: 'CONFIGURATION_ATOM_COLOR' 219 | } 220 | ); 221 | 222 | module.exports = Color; 223 | -------------------------------------------------------------------------------- /lib/configuration/atoms/limit.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var constants = require('../../constants'); 8 | 9 | /** 10 | * @class Limit 11 | * @extends Base 12 | * @module Configuration 13 | * @submodule Atoms 14 | * 15 | * @property {string} _type 16 | * @property {number} _value 17 | */ 18 | var Limit = Base.extend( 19 | 20 | /** 21 | * Limit constructor 22 | * 23 | * @param {object} options 24 | * @param {string} options.type Type of limit 25 | * @param {string} options.context Context of the limit 26 | * @param {number} options.value Value of limit 27 | * @constructor 28 | */ 29 | function (options) { 30 | this.__super(options); 31 | 32 | options = utils.deepExtend({ 33 | type: "min", 34 | context: "width", 35 | value: 0 36 | }, [options]); 37 | 38 | this.setType(options.type); 39 | this.setContext(options.context); 40 | this.setValue(options.value); 41 | }, 42 | 43 | { 44 | /** 45 | * Gets the type of the limit 46 | * 47 | * @method getType 48 | * @return {string} 49 | */ 50 | getType: function () { 51 | return this._type; 52 | }, 53 | 54 | /** 55 | * Sets the type of the limit 56 | * 57 | * @method setType 58 | * @param {string} value 59 | */ 60 | setType: function (value) { 61 | this._type = value; 62 | }, 63 | 64 | 65 | /** 66 | * Gets the context of the limit 67 | * 68 | * @method getContext 69 | * @return {string} 70 | */ 71 | getContext: function () { 72 | return this._context; 73 | }, 74 | 75 | /** 76 | * Sets the context of the limit 77 | * 78 | * @method setContext 79 | * @param {string} value 80 | */ 81 | setContext: function (value) { 82 | this._context = value; 83 | }, 84 | 85 | 86 | /** 87 | * Gets the value of the limit 88 | * 89 | * @method getValue 90 | * @return {number} 91 | */ 92 | getValue: function () { 93 | return this._value; 94 | }, 95 | 96 | /** 97 | * Sets the value of the limit 98 | * 99 | * @method setValue 100 | * @param {number} value 101 | */ 102 | setValue: function (value) { 103 | this._value = value; 104 | }, 105 | 106 | 107 | /** 108 | * Is percentage threshold? 109 | * 110 | * @method isPercentage 111 | * @return {boolean} 112 | */ 113 | isMin: function () { 114 | return (this.getType() == constants.LIMIT_TYPE_MIN); 115 | }, 116 | 117 | /** 118 | * Is pixel threshold? 119 | * 120 | * @method isPixel 121 | * @return {boolean} 122 | */ 123 | isMax: function () { 124 | return (this.getType() == constants.LIMIT_TYPE_MAX); 125 | } 126 | }, 127 | 128 | { 129 | /** 130 | * @property TYPE 131 | * @type {string} 132 | * @static 133 | */ 134 | TYPE: 'CONFIGURATION_ATOM_LIMIT' 135 | } 136 | ); 137 | 138 | module.exports = Limit; 139 | -------------------------------------------------------------------------------- /lib/configuration/atoms/rect.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | /** 8 | * @class Rect 9 | * @extends Base 10 | * @module Configuration 11 | * @submodule Atoms 12 | * 13 | * @property {int} _left 14 | * @property {int} _top 15 | * @property {int} _width 16 | * @property {int} _height 17 | */ 18 | var Rect = Base.extend( 19 | 20 | /** 21 | * Rect constructor 22 | * 23 | * @param {object} options 24 | * @param {int} options.left Offset from the left corner 25 | * @param {int} options.top Offset from the top corner 26 | * @param {int} options.width Width 27 | * @param {int} options.height Height 28 | * @param {int} [options.x] Offset from the left corner 29 | * @param {int} [options.y] Offset from the top corner 30 | * @constructor 31 | */ 32 | function (options) { 33 | this.__super(options); 34 | 35 | options = utils.deepExtend({}, [options]); 36 | 37 | this.setLeft(options.left || options.x); 38 | this.setTop(options.top || options.y); 39 | this.setWidth(options.width); 40 | this.setHeight(options.height); 41 | }, 42 | 43 | { 44 | /** 45 | * Gets the offset from the left corner 46 | * 47 | * @method getLeft 48 | * @return {int} 49 | */ 50 | getLeft: function () { 51 | return this._left; 52 | }, 53 | 54 | /** 55 | * Sets the offset from the left corner 56 | * 57 | * @method setLeft 58 | * @param {int} value 59 | */ 60 | setLeft: function (value) { 61 | this._left = value; 62 | }, 63 | 64 | 65 | /** 66 | * Gets the offset from the top corner 67 | * 68 | * @method getTop 69 | * @return {int} 70 | */ 71 | getTop: function () { 72 | return this._top; 73 | }, 74 | 75 | /** 76 | * Sets the offset from the top corner 77 | * 78 | * @method setTop 79 | * @param {int} value 80 | */ 81 | setTop: function (value) { 82 | this._top = value; 83 | }, 84 | 85 | 86 | /** 87 | * Gets the width 88 | * 89 | * @method getWidth 90 | * @return {int} 91 | */ 92 | getWidth: function () { 93 | return this._width; 94 | }, 95 | 96 | /** 97 | * Sets the width 98 | * 99 | * @method setWidth 100 | * @param {int} value 101 | */ 102 | setWidth: function (value) { 103 | this._width = value; 104 | }, 105 | 106 | 107 | /** 108 | * Gets the height 109 | * 110 | * @method getHeight 111 | * @return {int} 112 | */ 113 | getHeight: function () { 114 | return this._height; 115 | }, 116 | 117 | /** 118 | * Sets the height 119 | * 120 | * @method setHeight 121 | * @param {int} value 122 | */ 123 | setHeight: function (value) { 124 | this._height = value; 125 | }, 126 | 127 | 128 | /** 129 | * Gets boundaries as rect structure 130 | * 131 | * @method getRect 132 | * @return {{left: (int), top: (int), width: (int), height: (int)}} 133 | */ 134 | getRect: function () { 135 | return { 136 | left: this.getLeft(), 137 | top: this.getTop(), 138 | width: this.getWidth(), 139 | height: this.getHeight() 140 | }; 141 | }, 142 | 143 | /** 144 | * Gets boundaries as coordinate structure 145 | * 146 | * @method getCoordinates 147 | * @return {{x: (int), y: (int), width: (int), height: (int)}} 148 | */ 149 | getCoordinates: function () { 150 | return { 151 | x: this.getLeft(), 152 | y: this.getTop(), 153 | width: this.getWidth(), 154 | height: this.getHeight() 155 | }; 156 | }, 157 | 158 | /** 159 | * Checks if a coordinate is within the bounds of the rect 160 | * 161 | * @method inBounds 162 | * @param {int} x X-coordinate 163 | * @param {int} y Y-coordinate 164 | */ 165 | inBounds: function (x, y) { 166 | if ((x < this.getLeft()) || (x > this.getLeft() + this.getWidth())) { 167 | return false; 168 | } 169 | if ((y < this.getTop()) || (y > this.getTop() + this.getHeight())) { 170 | return false; 171 | } 172 | return true; 173 | }, 174 | 175 | /** 176 | * Limits the rect coordinates to a specified rect 177 | * 178 | * Note: 179 | * Priority is on the x/y coordinates, and not on the size since the size will then be removed anyways. 180 | * 181 | * @method limitCoordinates 182 | * @param {Rect} outerRect Outer rect 183 | * @return {Rect} 184 | */ 185 | limitCoordinates: function (outerRect) { 186 | 187 | var outerRectCoord = outerRect.getRect(); 188 | 189 | // Set values if none given 190 | this._left = this._left || outerRectCoord.left; 191 | this._top = this._top || outerRectCoord.top; 192 | this._width = this._width || outerRectCoord.width; 193 | this._height = this._height || outerRectCoord.height; 194 | 195 | // Check negative values 196 | this._left = Math.max(0, this._left); 197 | this._top = Math.max(0, this._top); 198 | this._width = Math.max(1, this._width); 199 | this._height = Math.max(1, this._height); 200 | 201 | // Check dimensions 202 | this._left = Math.min(this._left, outerRectCoord.width - 1); // -1 to make sure that there is an image 203 | this._top = Math.min(this._top, outerRectCoord.height - 1); 204 | this._width = Math.min(this._width, outerRectCoord.width - this._left); 205 | this._height = Math.min(this._height, outerRectCoord.height - this._top); 206 | 207 | // Make sure that the this is at least one pixel by one pixel 208 | this._width = Math.max(1, this._width); 209 | this._height = Math.max(1, this._height); 210 | 211 | return this; 212 | }, 213 | 214 | /** 215 | * Clones the rect 216 | * 217 | * @method clone 218 | * @return {Rect} 219 | */ 220 | clone: function () { 221 | return new Rect({ 222 | left: this.getLeft(), 223 | top: this.getTop(), 224 | width: this.getWidth(), 225 | height: this.getHeight() 226 | }); 227 | } 228 | }, 229 | 230 | { 231 | /** 232 | * @property TYPE 233 | * @type {string} 234 | * @static 235 | */ 236 | TYPE: 'CONFIGURATION_ATOM_RECT' 237 | } 238 | ); 239 | 240 | module.exports = Rect; 241 | -------------------------------------------------------------------------------- /lib/configuration/atoms/threshold.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var constants = require('../../constants'); 8 | 9 | /** 10 | * @class Threshold 11 | * @extends Base 12 | * @module Configuration 13 | * @submodule Atoms 14 | * 15 | * @property {string} _type 16 | * @property {number} _value 17 | */ 18 | var Threshold = Base.extend( 19 | 20 | /** 21 | * Threshold constructor 22 | * 23 | * @param {object} options 24 | * @param {string} options.type Type of threshold 25 | * @param {number} options.value Value of threshold 26 | * @constructor 27 | */ 28 | function (options) { 29 | this.__super(options); 30 | 31 | options = utils.deepExtend({ 32 | type: 'pixel', 33 | value: 1 34 | }, [options]); 35 | 36 | this.setType(options.type); 37 | this.setValue(options.value); 38 | }, 39 | 40 | { 41 | /** 42 | * Gets the type of the threshold 43 | * 44 | * @method getType 45 | * @return {string} 46 | */ 47 | getType: function () { 48 | return this._type; 49 | }, 50 | 51 | /** 52 | * Sets the type of the threshold 53 | * 54 | * @method setType 55 | * @param {string} value 56 | */ 57 | setType: function (value) { 58 | this._type = value; 59 | }, 60 | 61 | 62 | /** 63 | * Gets the value of the threshold 64 | * 65 | * @method getValue 66 | * @return {number} 67 | */ 68 | getValue: function () { 69 | return this._value; 70 | }, 71 | 72 | /** 73 | * Sets the value of the threshold 74 | * 75 | * @method setValue 76 | * @param {number} value 77 | */ 78 | setValue: function (value) { 79 | this._value = value; 80 | }, 81 | 82 | 83 | /** 84 | * Is the difference above the set threshold? 85 | * 86 | * @method isAboveThreshold 87 | * @param {int} items 88 | * @param {int} [total] 89 | * @return {boolean} 90 | */ 91 | isAboveThreshold: function (items, total) { 92 | 93 | if (this.isPixel() && (this.getValue() <= items)) { 94 | return true; 95 | 96 | } else if (this.getValue() <= (items / total)) { 97 | return true; 98 | } 99 | 100 | return false; 101 | }, 102 | 103 | 104 | /** 105 | * Is percentage threshold? 106 | * 107 | * @method isPercentage 108 | * @return {boolean} 109 | */ 110 | isPercentage: function () { 111 | return (this.getType() == constants.THRESHOLD_PERCENT); 112 | }, 113 | 114 | /** 115 | * Is pixel threshold? 116 | * 117 | * @method isPixel 118 | * @return {boolean} 119 | */ 120 | isPixel: function () { 121 | return (this.getType() == constants.THRESHOLD_PIXEL); 122 | } 123 | }, 124 | 125 | { 126 | /** 127 | * @property TYPE 128 | * @type {string} 129 | * @static 130 | */ 131 | TYPE: 'CONFIGURATION_ATOM_THRESHOLD' 132 | } 133 | ); 134 | 135 | module.exports = Threshold; 136 | -------------------------------------------------------------------------------- /lib/configuration/base.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var CoreBase = require('preceptor-core').Base; 5 | 6 | /** 7 | * @class Base 8 | * @extends CoreBase 9 | * @module Configuration 10 | */ 11 | var Base = CoreBase.extend( 12 | 13 | /** 14 | * Base constructor 15 | * 16 | * @constructor 17 | * @param {object} options 18 | */ 19 | function (options) { 20 | this._blinkDiff = options.blinkDiff; 21 | }, 22 | 23 | { 24 | /** 25 | * Logs events to the blink-diff instance 26 | * 27 | * @method log 28 | * @param {string} text 29 | */ 30 | log: function (text) { 31 | if (this._blinkDiff) { 32 | this._blinkDiff.log(this.constructor.TYPE + ': ' + text); 33 | } 34 | }, 35 | 36 | /** 37 | * Parses object information 38 | * 39 | * @method _parseObject 40 | * @param {object|Image} value 41 | * @param {object} Constr Constructor of data-type 42 | * @param {string} typeStr Textual type description of object 43 | * @return {object} 44 | * @private 45 | */ 46 | _parseObject: function (value, Constr, typeStr) { 47 | 48 | if (typeof value == 'object' && !(value instanceof Constr)) { 49 | value.blinkDiff = this._blinkDiff; 50 | value = new Constr(value); 51 | } 52 | 53 | if (value instanceof Constr) { 54 | return value; 55 | } else { 56 | throw new Error('Unknown ' + typeStr + ' descriptor.'); 57 | } 58 | } 59 | }, 60 | 61 | { 62 | /** 63 | * @property TYPE 64 | * @type {string} 65 | * @static 66 | */ 67 | TYPE: 'CONFIGURATION_BASE' 68 | } 69 | ); 70 | 71 | module.exports = Base; 72 | -------------------------------------------------------------------------------- /lib/configuration/blockOut.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var Color = require('./atoms/color'); 8 | var Rect = require('./atoms/rect'); 9 | 10 | /** 11 | * @class BlockOut 12 | * @extends Base 13 | * @module Configuration 14 | * 15 | * @property {boolean} _visible 16 | * @property {Color} _color 17 | * @property {Rect} _area 18 | */ 19 | var BlockOut = Base.extend( 20 | 21 | /** 22 | * BlockOut constructor 23 | * 24 | * @param {object} options 25 | * @param {boolean} options.visible 26 | * @param {object|Color} options.color 27 | * @param {object|Rect} options.area 28 | * @constructor 29 | */ 30 | function (options) { 31 | this.__super(options); 32 | 33 | options = utils.deepExtend({ 34 | visible: false, 35 | color: {}, 36 | area: {} 37 | }, [options]); 38 | 39 | this.setVisibility(options.visible); 40 | this.setColor(options.color); 41 | this.setAreaRect(options.area); 42 | }, 43 | 44 | { 45 | /** 46 | * Is block-out visible in final result 47 | * 48 | * @method isVisible 49 | * @return {boolean} 50 | */ 51 | isVisible: function () { 52 | return this._visible; 53 | }, 54 | 55 | /** 56 | * Sets the visibility of the block-out in final result 57 | * 58 | * @method setVisibility 59 | * @param {boolean} value 60 | */ 61 | setVisibility: function (value) { 62 | this._visible = !!value; 63 | }, 64 | 65 | 66 | /** 67 | * Gets the color 68 | * 69 | * @method getColor 70 | * @return {Color} 71 | */ 72 | getColor: function () { 73 | return this._color; 74 | }, 75 | 76 | /** 77 | * Sets the color 78 | * 79 | * @method setColor 80 | * @param {object|Color} value 81 | */ 82 | setColor: function (value) { 83 | this._color = this._parseObject(value, Color, 'color'); 84 | }, 85 | 86 | 87 | /** 88 | * Gets the area rectangle 89 | * 90 | * @method getAreaRect 91 | * @return {Rect} 92 | */ 93 | getAreaRect: function () { 94 | return this._area; 95 | }, 96 | 97 | /** 98 | * Sets the area rectangle 99 | * 100 | * @method setAreaRect 101 | * @param {object|Rect} value 102 | */ 103 | setAreaRect: function (value) { 104 | this._area = this._parseObject(value, Rect, 'rect'); 105 | }, 106 | 107 | 108 | /** 109 | * Post-process image after creating result image base 110 | * 111 | * @method processImage 112 | * @param {PNGImage} image 113 | * @return {PNGImage} 114 | */ 115 | processImage: function (image) { 116 | this._blockOut(image); 117 | return image; 118 | }, 119 | 120 | /** 121 | * Blocks-out an area of the image 122 | * 123 | * @method _blockOut 124 | * @param {PNGImage} image 125 | * @private 126 | */ 127 | _blockOut: function (image) { 128 | 129 | var rect, 130 | coord, 131 | color; 132 | 133 | rect = this.getAreaRect().clone(); 134 | rect.limitCoordinates(new Rect({ 135 | x: 0, 136 | y: 0, 137 | width: image.getWidth(), 138 | height: image.getHeight() 139 | })); 140 | 141 | coord = rect.getCoordinates(); 142 | color = this.getColor().getColor(true, true); 143 | 144 | image.fillRect(coord.x, coord.y, coord.width, coord.height, color); 145 | } 146 | }, 147 | 148 | { 149 | /** 150 | * @property TYPE 151 | * @type {string} 152 | * @static 153 | */ 154 | TYPE: 'CONFIGURATION_BLOCK_OUT' 155 | } 156 | ); 157 | 158 | module.exports = BlockOut; 159 | -------------------------------------------------------------------------------- /lib/configuration/config.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var Image = require('./image'); 8 | var PixelComparison = require('./pixelComparison'); 9 | var StructureComparison = require('./structureComparison'); 10 | var Threshold = require('./atoms/threshold'); 11 | var Color = require('./atoms/color'); 12 | var Output = require('./output'); 13 | 14 | var constants = require('../constants'); 15 | 16 | /** 17 | * @class Config 18 | * @extends Base 19 | * @module Configuration 20 | * 21 | * @property {boolean} debug 22 | * @property {boolean} verbose 23 | * @property {Image} _imageA 24 | * @property {Image} _imageB 25 | * @property {PixelComparison[]|StructureComparison[]} _comparisons 26 | * @property {Threshold} _threshold 27 | * @property {Color} _diffColor 28 | * @property {Color} _backgroundColor 29 | * @property {Color} _ignoreColor 30 | * @property {Output} _output 31 | */ 32 | var Config = Base.extend( 33 | 34 | /** 35 | * Config constructor 36 | * 37 | * @param {object} options 38 | * @param {boolean} options.debug 39 | * @param {boolean} options.verbose 40 | * @param {object|Image} options.imageA 41 | * @param {object|Image} options.imageB 42 | * @param {object[]|PixelComparison[]|StructureComparison[]} options.comparisons 43 | * @param {object|Threshold} options.threshold 44 | * @param {object|Color} options.diffColor 45 | * @param {object|Color} options.backgroundColor 46 | * @param {object|Color} options.ignoreColor 47 | * @param {object|Output} options.output 48 | * @constructor 49 | */ 50 | function (options) { 51 | this.__super(options); 52 | 53 | options = utils.deepExtend({ 54 | debug: false, 55 | verbose: false, 56 | 57 | imageA: {}, 58 | imageB: {}, 59 | 60 | comparisons: [], 61 | 62 | threshold: {}, 63 | 64 | diffColor: {}, 65 | backgroundColor: {}, 66 | ignoreColor: {}, 67 | 68 | output: {} 69 | }, [options]); 70 | 71 | this.setDebugMode(options.debug); 72 | this.setVerboseMode(options.verbose); 73 | 74 | this.setImageA(options.imageA); 75 | this.setImageB(options.imageB); 76 | 77 | this.setComparisons(options.comparisons); 78 | 79 | this.setThreshold(options.threshold); 80 | 81 | this.setDiffColor(options.diffColor); 82 | this.setBackgroundColor(options.backgroundColor); 83 | this.setIgnoreColor(options.ignoreColor); 84 | 85 | this.setOutput(options.output); 86 | }, 87 | 88 | { 89 | /** 90 | * Is in debug-mode? 91 | * 92 | * @method isDebugMode 93 | * @return {boolean} 94 | */ 95 | isDebugMode: function () { 96 | return this._debug; 97 | }, 98 | 99 | /** 100 | * Sets the debug-mode 101 | * 102 | * @method setDebugMode 103 | * @param {boolean} value 104 | */ 105 | setDebugMode: function (value) { 106 | this._debug = !!value; 107 | }, 108 | 109 | 110 | /** 111 | * Is in verbose-mode? 112 | * 113 | * @method isVerboseMode 114 | * @return {boolean} 115 | */ 116 | isVerboseMode: function () { 117 | return this._verbose; 118 | }, 119 | 120 | /** 121 | * Sets the verbose-mode 122 | * 123 | * @method setVerboseMode 124 | * @param {boolean} value 125 | */ 126 | setVerboseMode: function (value) { 127 | this._verbose = !!value; 128 | }, 129 | 130 | 131 | /** 132 | * Gets the first image 133 | * 134 | * @method getImageA 135 | * @return {Image} 136 | */ 137 | getImageA: function () { 138 | return this._imageA; 139 | }, 140 | 141 | /** 142 | * Sets the first image 143 | * 144 | * @method setImageA 145 | * @param {object|Image} value 146 | */ 147 | setImageA: function (value) { 148 | this._imageA = this._parseObject(value, Image, 'image A'); 149 | }, 150 | 151 | 152 | /** 153 | * Gets the second image 154 | * 155 | * @method getImageB 156 | * @return {Image} 157 | */ 158 | getImageB: function () { 159 | return this._imageB; 160 | }, 161 | 162 | /** 163 | * Sets the second image 164 | * 165 | * @method setImageB 166 | * @param {object|Image} value 167 | */ 168 | setImageB: function (value) { 169 | this._imageB = this._parseObject(value, Image, 'image B'); 170 | }, 171 | 172 | 173 | /** 174 | * Gets the comparisons 175 | * 176 | * @method getComparisons 177 | * @return {PixelComparison[]|StructureComparison[]} 178 | */ 179 | getComparisons: function () { 180 | return this._comparisons; 181 | }, 182 | 183 | /** 184 | * Sets the comparisons 185 | * 186 | * @method setComparisons 187 | * @param {object[]|PixelComparison[]|StructureComparison[]} value 188 | */ 189 | setComparisons: function (value) { 190 | 191 | var list = []; 192 | 193 | value.forEach(function (comparison) { 194 | list.push(this._parseComparison(comparison)); 195 | }.bind(this)); 196 | 197 | this._comparisons = list; 198 | }, 199 | 200 | 201 | /** 202 | * Gets the threshold 203 | * 204 | * @method getThreshold 205 | * @return {Threshold} 206 | */ 207 | getThreshold: function () { 208 | return this._threshold; 209 | }, 210 | 211 | /** 212 | * Sets the threshold 213 | * 214 | * @method setThreshold 215 | * @param {object|Threshold} value 216 | */ 217 | setThreshold: function (value) { 218 | this._threshold = this._parseObject(value, Threshold, 'threshold'); 219 | }, 220 | 221 | 222 | /** 223 | * Gets the diff-color 224 | * 225 | * @method getDiffColor 226 | * @return {Color} 227 | */ 228 | getDiffColor: function () { 229 | return this._diffColor; 230 | }, 231 | 232 | /** 233 | * Sets the diff-color 234 | * 235 | * @method setDiffColor 236 | * @param {object|Color} value 237 | */ 238 | setDiffColor: function (value) { 239 | this._diffColor = this._parseObject(value, Color, 'color'); 240 | }, 241 | 242 | 243 | /** 244 | * Gets the background-color 245 | * 246 | * @method getBackgroundColor 247 | * @return {Color} 248 | */ 249 | getBackgroundColor: function () { 250 | return this._backgroundColor; 251 | }, 252 | 253 | /** 254 | * Sets the background-color 255 | * 256 | * @method setBackgroundColor 257 | * @param {object|Color} value 258 | */ 259 | setBackgroundColor: function (value) { 260 | this._backgroundColor = this._parseObject(value, Color, 'color'); 261 | }, 262 | 263 | 264 | /** 265 | * Gets the ignore-color 266 | * 267 | * @method getIgnoreColor 268 | * @return {Color} 269 | */ 270 | getIgnoreColor: function () { 271 | return this._ignoreColor; 272 | }, 273 | 274 | /** 275 | * Sets the ignore-color 276 | * 277 | * @method setIgnoreColor 278 | * @param {object|Color} value 279 | */ 280 | setIgnoreColor: function (value) { 281 | this._ignoreColor = this._parseObject(value, Color, 'color'); 282 | }, 283 | 284 | 285 | /** 286 | * Gets the output 287 | * 288 | * @method getOutput 289 | * @return {Output} 290 | */ 291 | getOutput: function () { 292 | return this._output; 293 | }, 294 | 295 | /** 296 | * Sets the output 297 | * 298 | * @method setOutput 299 | * @param {object|Output} value 300 | */ 301 | setOutput: function (value) { 302 | this._output = this._parseObject(value, Output, 'output'); 303 | }, 304 | 305 | 306 | /** 307 | * Parses comparison information 308 | * 309 | * @method _parseComparison 310 | * @param {object|PixelComparison|StructureComparison} value Comparison or comparison-description 311 | * @returns {PixelComparison|StructureComparison} 312 | * @private 313 | */ 314 | _parseComparison: function (value) { 315 | 316 | var Constr = null, 317 | text = "comparison"; 318 | 319 | if (value.type === constants.COMPARISON_PIXEL) { 320 | Constr = PixelComparison; 321 | text = 'pixel-comparison'; 322 | 323 | } else if (value.type === constants.COMPARISON_STRUCTURE) { 324 | Constr = StructureComparison; 325 | text = 'structural-comparison' 326 | } 327 | 328 | return this._parseObject(value, Constr, text); 329 | } 330 | }, 331 | 332 | { 333 | /** 334 | * @property TYPE 335 | * @type {string} 336 | * @static 337 | */ 338 | TYPE: 'CONFIGURATION_CONFIG' 339 | } 340 | ); 341 | 342 | module.exports = Config; 343 | -------------------------------------------------------------------------------- /lib/configuration/image.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var fs = require('fs'); 5 | 6 | var Base = require('./base'); 7 | var PNGImage = require('pngjs-image'); 8 | var utils = require('preceptor-core').utils; 9 | 10 | var Rect = require('./atoms/rect'); 11 | var Structure = require('./structure/structure'); 12 | 13 | /** 14 | * @class Image 15 | * @extends Base 16 | * @module Configuration 17 | * 18 | * @property {PNGImage} _image 19 | * @property {Structure} _structure 20 | * @property {Rect} _crop 21 | * 22 | * @property {boolean} _alreadyCropped 23 | * @property {PNGImage} _croppedImage 24 | */ 25 | var Image = Base.extend( 26 | 27 | /** 28 | * Image constructor 29 | * 30 | * @param {object} options 31 | * @param {string|PNGImage} options.image Image 32 | * @param {object|Structure} options.structure Structure of the image as a DOM 33 | * @param {object|Rect} [options.crop] Cropping of the image 34 | * @constructor 35 | */ 36 | function (options) { 37 | this.__super(options); 38 | 39 | options = utils.deepExtend({ 40 | structure: null, 41 | image: null, 42 | crop: {} 43 | }, [options]); 44 | 45 | if (options.structure) { 46 | this.setStructure(options.structure); 47 | } 48 | this.setImage(options.image); 49 | 50 | this.setCropRect(options.crop); 51 | }, 52 | 53 | { 54 | /** 55 | * Gets the image 56 | * 57 | * @method getImage 58 | * @return {PNGImage} 59 | */ 60 | getImage: function () { 61 | return this._image; 62 | }, 63 | 64 | /** 65 | * Sets the image 66 | * 67 | * @method setImage 68 | * @param {string|PNGImage} image 69 | */ 70 | setImage: function (image) { 71 | 72 | if (typeof image == 'string') { 73 | image = this._readImage(image); 74 | 75 | } else if (typeof image == 'buffer') { 76 | image = this._loadImage(image); 77 | } 78 | 79 | if (image instanceof PNGImage) { 80 | this._image = image; 81 | this._alreadyCropped = false; 82 | this._croppedImage = null; 83 | } else { 84 | throw new Error('Unknown image format.'); 85 | } 86 | }, 87 | 88 | /** 89 | * Reads the image from the FS 90 | * 91 | * @param {string} path 92 | * @return {PNGImage} 93 | * @private 94 | */ 95 | _readImage: function (path) { 96 | this.log('Read image: ' + path); 97 | return this._loadImage(fs.readFileSync(path)); 98 | }, 99 | 100 | /** 101 | * Loads the image from a buffer 102 | * 103 | * @param {Buffer} blob 104 | * @return {PNGImage} 105 | * @private 106 | */ 107 | _loadImage: function (blob) { 108 | 109 | var decoder, 110 | data, 111 | headerChunk, 112 | structureChunk, 113 | width, height, 114 | image, 115 | Decoder = PNGImage.Decoder; 116 | 117 | this.log('Load image'); 118 | decoder = new Decoder(); 119 | data = decoder.decode(blob, { strict: false }); 120 | 121 | headerChunk = decoder.getHeaderChunk(); 122 | width = headerChunk.getWidth(); 123 | height = headerChunk.getHeight(); 124 | 125 | // Load structure when embedded 126 | if (decoder.hasChunksOfType('stRT')) { 127 | structureChunk = decoder.getFirstChunk('stRT'); 128 | 129 | if ((structureChunk.getDataType() == 'BLDF') && 130 | (structureChunk.getMajor() == 1) && 131 | (structureChunk.getMinor() == 0) && 132 | structureChunk.getContent()) 133 | { 134 | this.log('Found structural data'); 135 | this.setStructure(structureChunk.getContent()); 136 | } 137 | } 138 | 139 | image = new PNG({ 140 | width: width, 141 | height: height 142 | }); 143 | data.copy(image.data, 0, 0, data.length); 144 | 145 | return new PNGImage(image); 146 | }, 147 | 148 | 149 | /** 150 | * Gets the structure of the image as a DOM 151 | * 152 | * @method getStructure 153 | * @return {Structure} 154 | */ 155 | getStructure: function () { 156 | return this._structure; 157 | }, 158 | 159 | /** 160 | * Sets the structure of the image as a DOM 161 | * 162 | * @method setStructure 163 | * @return {object|Structure} 164 | */ 165 | setStructure: function (value) { 166 | this._structure = this._parseObject(value, Structure, 'structure'); 167 | }, 168 | 169 | 170 | /** 171 | * Is image cropped? 172 | * 173 | * @method isCropped 174 | * @return {boolean} 175 | */ 176 | isCropped: function () { 177 | return !!this._crop; 178 | }, 179 | 180 | /** 181 | * Gets the cropping rectangle 182 | * 183 | * @method getCropRect 184 | * @return {Rect} 185 | */ 186 | getCropRect: function () { 187 | return this._crop; 188 | }, 189 | 190 | /** 191 | * Sets the cropping rectangle 192 | * 193 | * @method setCropRect 194 | * @param {object|Rect} value 195 | */ 196 | setCropRect: function (value) { 197 | this._crop = this._parseObject(value, Rect, 'rect'); 198 | this._alreadyCropped = false; 199 | this._croppedImage = null; 200 | }, 201 | 202 | 203 | /** 204 | * Gets the processed image 205 | * 206 | * @method getProcessedImage 207 | * @return {PNGImage} 208 | */ 209 | getProcessedImage: function () { 210 | 211 | var rect, coord; 212 | 213 | if (!this.isCropped()) { 214 | return PNGImage.copyImage(this.getImage()); 215 | } 216 | 217 | if (!this._alreadyCropped) { 218 | 219 | this.log('Cropping image'); 220 | rect = this.getCropRect().clone(); 221 | rect.limitCoordinates({ 222 | left: 0, 223 | top: 0, 224 | width: this.getImage().getWidth(), 225 | height: this.getImage().getHeight() 226 | }); 227 | 228 | coord = rect.getCoordinates(); 229 | 230 | this._croppedImage = PNGImage.copyImage(this.getImage()); 231 | this._croppedImage.clip(coord.x, coord.y, coord.width, coord.height); 232 | 233 | this._alreadyCropped = true; 234 | } 235 | 236 | return this._croppedImage; 237 | } 238 | }, 239 | 240 | { 241 | /** 242 | * @property TYPE 243 | * @type {string} 244 | * @static 245 | */ 246 | TYPE: 'CONFIGURATION_IMAGE' 247 | } 248 | ); 249 | 250 | module.exports = Image; 251 | -------------------------------------------------------------------------------- /lib/configuration/output.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var constants = require('../constants'); 8 | 9 | /** 10 | * @class Output 11 | * @extends Base 12 | * @module Configuration 13 | * 14 | * @property {string} _imagePath 15 | * #property {int} _limit 16 | * @property {int} _composition 17 | * @property {int} _copyImage 18 | */ 19 | var Output = Base.extend( 20 | 21 | /** 22 | * Output constructor 23 | * 24 | * @param {object} options 25 | * @param {string} options.imagePath Path to the output image 26 | * @param {int} options.limit Limiting options for output image 27 | * @param {int} options.composition Composition options for output image 28 | * @param {int} options.copyImage Copy-image options for output image 29 | * @constructor 30 | */ 31 | function (options) { 32 | this.__super(options); 33 | 34 | options = utils.deepExtend({ 35 | imagePath: null, 36 | limit: constants.OUTPUT_DIFFERENT, 37 | composition: constants.COMPOSITION_AUTO, 38 | copyImage: constants.COPY_IMAGE_B 39 | }, [options]); 40 | 41 | this.setImagePath(options.imagePath); 42 | 43 | this.setLimitMode(options.limit); 44 | this.setCompositionMode(options.composition); 45 | this.setCopyImage(options.copyImage); 46 | }, 47 | 48 | { 49 | /** 50 | * Gets the path of the output image 51 | * 52 | * @method getImagePath 53 | * @return {string} 54 | */ 55 | getImagePath: function () { 56 | return this._imagePath; 57 | }, 58 | 59 | /** 60 | * Sets the path of the output image 61 | * 62 | * @method setImagePath 63 | * @param {string} path 64 | */ 65 | setImagePath: function (path) { 66 | this._imagePath = path; 67 | }, 68 | 69 | /** 70 | * Writes the image to a file if a path was given 71 | * 72 | * @method writeImage 73 | * @param {PNGImage} image 74 | * @return {boolean} Written? 75 | */ 76 | writeImage: function (image) { 77 | if (this.getImagePath()) { 78 | image.writeImageSync(this.getImagePath()); 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | }, 84 | 85 | 86 | /** 87 | * Gets the limit mode 88 | * 89 | * @method getLimitMode 90 | * @return {int} 91 | */ 92 | getLimitMode: function () { 93 | return this._limit; 94 | }, 95 | 96 | /** 97 | * Sets the limit mode 98 | * 99 | * @method setLimitMode 100 | * @param {int} value 101 | */ 102 | setLimitMode: function (value) { 103 | this._limit = value; 104 | }, 105 | 106 | 107 | /** 108 | * Is there an output limit? 109 | * 110 | * @method hasLimit 111 | * @return {boolean} 112 | */ 113 | hasLimit: function () { 114 | return (this.getLimitMode() != constants.OUTPUT_ALL); 115 | }, 116 | 117 | /** 118 | * Is the output-limit for different images only? 119 | * 120 | * @method isLimitDifferent 121 | * @return {boolean} 122 | */ 123 | isLimitDifferent: function () { 124 | return (this.getLimitMode() == constants.OUTPUT_DIFFERENT); 125 | }, 126 | 127 | /** 128 | * Is the output-limit for similar and different images? 129 | * 130 | * @method isLimitSimilar 131 | * @return {boolean} 132 | */ 133 | isLimitSimilar: function () { 134 | return (this.getLimitMode() == constants.OUTPUT_SIMILAR); 135 | }, 136 | 137 | /** 138 | * Determines if a result-code is within the output limits 139 | * 140 | * @method withinOutputLimit 141 | * @param resultCode 142 | * @return {boolean} 143 | */ 144 | withinOutputLimit: function (resultCode) { 145 | if (!this.hasLimit()) { 146 | return true; 147 | } else { 148 | return resultCode <= this.getLimitMode(); 149 | } 150 | }, 151 | 152 | 153 | /** 154 | * Gets the composition mode 155 | * 156 | * @method getCompositionMode 157 | * @return {int} 158 | */ 159 | getCompositionMode: function () { 160 | return this._composition; 161 | }, 162 | 163 | /** 164 | * Sets the composition mode 165 | * 166 | * @method setCompositionMode 167 | * @param {int} value 168 | */ 169 | setCompositionMode: function (value) { 170 | this._composition = value; 171 | }, 172 | 173 | 174 | /** 175 | * Is composition off? 176 | * 177 | * @method isCompositionOff 178 | * @return {boolean} 179 | */ 180 | isCompositionOff: function () { 181 | return (this.getCompositionMode() == constants.COMPOSITION_OFF); 182 | }, 183 | 184 | /** 185 | * Is composition in auto-mode? 186 | * 187 | * @method isAutoComposition 188 | * @return {boolean} 189 | */ 190 | isAutoComposition: function () { 191 | return (this.getCompositionMode() == constants.COMPOSITION_AUTO); 192 | }, 193 | 194 | /** 195 | * Is composition from left to right? 196 | * 197 | * @method isCompositionLeftToRight 198 | * @return {boolean} 199 | */ 200 | isCompositionLeftToRight: function () { 201 | return (this.getCompositionMode() == constants.COMPOSITION_LEFT_TO_RIGHT); 202 | }, 203 | 204 | /** 205 | * Is composition from top to bottom? 206 | * 207 | * @method isCompositionTopToBottom 208 | * @return {boolean} 209 | */ 210 | isCompositionTopToBottom: function () { 211 | return (this.getCompositionMode() == constants.COMPOSITION_TOP_TO_BOTTOM); 212 | }, 213 | 214 | /** 215 | * Creates a composition according to the configuration 216 | * 217 | * @method createComposition 218 | * @param {PNGImage} imageA 219 | * @param {PNGImage} imageB 220 | * @param {PNGImage} imageOutput 221 | * @return {PNGImage} 222 | */ 223 | createComposition: function (imageA, imageB, imageOutput) { 224 | 225 | var width, height, image = imageOutput; 226 | 227 | if (!this.isCompositionOff()) { 228 | this.log('Create composition'); 229 | 230 | width = Math.max(imageA.getWidth(), imageB.getWidth()); 231 | height = Math.max(imageA.getHeight(), imageB.getHeight()); 232 | 233 | if (((width > height) && this.isAutoComposition()) || this.isCompositionTopToBottom()) { 234 | image = PNGImage.createImage(width, height * 3); 235 | 236 | imageA.getImage().bitblt(image.getImage(), 0, 0, imageA.getWidth(), imageA.getHeight(), 0, 0); 237 | imageOutput.getImage().bitblt(image.getImage(), 0, 0, imageOutput.getWidth(), imageOutput.getHeight(), 0, height); 238 | imageB.getImage().bitblt(image.getImage(), 0, 0, imageB.getWidth(), imageB.getHeight(), 0, height * 2); 239 | } else { 240 | image = PNGImage.createImage(width * 3, height); 241 | 242 | imageA.getImage().bitblt(image.getImage(), 0, 0, imageA.getWidth(), imageA.getHeight(), 0, 0); 243 | imageOutput.getImage().bitblt(image.getImage(), 0, 0, imageOutput.getWidth(), imageOutput.getHeight(), width, 0); 244 | imageB.getImage().bitblt(image.getImage(), 0, 0, imageB.getWidth(), imageB.getHeight(), width * 2, 0); 245 | } 246 | } 247 | 248 | return image; 249 | }, 250 | 251 | 252 | /** 253 | * Gets the copy-image mode 254 | * 255 | * @method getCopyImage 256 | * @return {int} 257 | */ 258 | getCopyImage: function () { 259 | return this._copyImage; 260 | }, 261 | 262 | /** 263 | * Sets the copy-image mode 264 | * 265 | * @method setCopyImage 266 | * @param {int} value 267 | */ 268 | setCopyImage: function (value) { 269 | this._copyImage = value; 270 | }, 271 | 272 | 273 | /** 274 | * Should any image be copied as base for the result? 275 | * 276 | * @method shouldCopyImage 277 | * @return {boolean} 278 | */ 279 | shouldCopyImage: function () { 280 | return (this.getCopyImage() != constants.COPY_IMAGE_OFF); 281 | }, 282 | 283 | /** 284 | * Should image A be copied as base of result? 285 | * 286 | * @method shouldCopyImageA 287 | * @return {boolean} 288 | */ 289 | shouldCopyImageA: function () { 290 | return (this.getCopyImage() == constants.COPY_IMAGE_A); 291 | }, 292 | 293 | /** 294 | * Should image B be copied as base of result? 295 | * 296 | * @method shouldCopyImageB 297 | * @return {boolean} 298 | */ 299 | shouldCopyImageB: function () { 300 | return (this.getCopyImage() == constants.COPY_IMAGE_B); 301 | }, 302 | 303 | 304 | /** 305 | * Copies the image to the output-image as defined in the configuration 306 | * 307 | * @method copyImage 308 | * @param {PNGImage} imageA 309 | * @param {PNGImage} imageB 310 | * @param {PNGImage} imageOutput 311 | */ 312 | copyImage: function (imageA, imageB, imageOutput) { 313 | 314 | if (this.shouldCopyImageA()) { 315 | imageA.getImage().bitblt(imageOutput.getImage(), 0, 0, imageA.getWidth(), imageA.getHeight(), 0, 0); 316 | 317 | } else if (this.shouldCopyImageB()) { 318 | imageB.getImage().bitblt(imageOutput.getImage(), 0, 0, imageB.getWidth(), imageB.getHeight(), 0, 0); 319 | } 320 | } 321 | }, 322 | 323 | { 324 | /** 325 | * @property TYPE 326 | * @type {string} 327 | * @static 328 | */ 329 | TYPE: 'CONFIGURATION_OUTPUT' 330 | } 331 | ); 332 | 333 | module.exports = Output; 334 | -------------------------------------------------------------------------------- /lib/configuration/pixelComparison.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var Color = require('./atoms/color'); 8 | var Rect = require('./atoms/rect'); 9 | var Shift = require('./shift'); 10 | var BlockOut = require('./blockOut'); 11 | 12 | /** 13 | * @class PixelComparison 14 | * @extends Base 15 | * @module Configuration 16 | * 17 | * @property {string} _type 18 | * @property {number} _colorDelta 19 | * @property {number} _colorDeltaSquared 20 | * @property {Color} _gamma 21 | * @property {boolean} _perceptual 22 | * @property {string[]} _filters 23 | * @property {Shift} _shift 24 | * @property {BlockOut[]} _blockOuts 25 | * @property {Rect} _areaImageA 26 | * @property {Rect} _areaImageB 27 | */ 28 | var PixelComparison = Base.extend( 29 | 30 | /** 31 | * Pixel-comparison constructor 32 | * 33 | * @param {object} options 34 | * @param {string} options.type Type of comparison 35 | * @param {number} options.colorDelta Max. delta of colors before triggering difference 36 | * @param {object|Color} options.gamma Gamma correction for perceptual comparisons 37 | * @param {boolean} options.perceptual Active/deactivate perceptual comparison 38 | * @param {string[]} options.filters Comparison filters (i.e. "blur" to reduce sub-pixel issues) 39 | * @param {object|Shift} options.shift Pixel shifting 40 | * @param {object[]|BlockOut[]} options.blockOuts List of areas to block-out 41 | * @param {object|Rect} [options.areaImageA] Area of comparison in Image A 42 | * @param {object|Rect} [options.areaImageB] Area of comparison in Image B 43 | * @constructor 44 | */ 45 | function (options) { 46 | this.__super(options); 47 | 48 | options = utils.deepExtend({ 49 | type: "pixel", 50 | colorDelta: 20, 51 | gamma: null, 52 | perceptual: false, 53 | filters: [], 54 | shift: {}, 55 | blockOuts: [] 56 | }, [options]); 57 | 58 | this.setType(options.type); 59 | this.setColorDelta(options.colorDelta); 60 | 61 | if (options.gamma) { 62 | this.setGamma(options.gamma); 63 | } 64 | this.setPerceptual(options.perceptual); 65 | this.setFilters(options.filters); 66 | 67 | this.setShift(options.shift); 68 | 69 | this.setBlockOuts(options.blockOuts); 70 | 71 | if (options.areaImageA) { 72 | this.setAreaImageA(options.areaImageA); 73 | } 74 | if (options.areaImageB) { 75 | this.setAreaImageB(options.areaImageB); 76 | } 77 | }, 78 | 79 | { 80 | /** 81 | * Gets the comparison type 82 | * 83 | * @method getType 84 | * @return {string} 85 | */ 86 | getType: function () { 87 | return this._type; 88 | }, 89 | 90 | /** 91 | * Sets the comparison type 92 | * 93 | * @method setType 94 | * @param {string} value 95 | */ 96 | setType: function (value) { 97 | this._type = value; 98 | }, 99 | 100 | 101 | /** 102 | * Gets the max. delta for color differences before triggering it 103 | * 104 | * @method getColorDelta 105 | * @return {number} 106 | */ 107 | getColorDelta: function () { 108 | return this._colorDelta; 109 | }, 110 | 111 | /** 112 | * Gets the max. delta for color differences before triggering it 113 | * This value is squared to reduce Math.sqrt during comparison. 114 | * 115 | * @method getColorDeltaSquared 116 | * @return {number} 117 | */ 118 | getColorDeltaSquared: function () { 119 | return this._colorDeltaSquared; 120 | }, 121 | 122 | /** 123 | * Sets the max. delta for color differences before triggering it 124 | * 125 | * @method setColorDelta 126 | * @param {number} value 127 | */ 128 | setColorDelta: function (value) { 129 | this._colorDelta = value; 130 | this._colorDeltaSquared = Math.pow(value, 2); 131 | }, 132 | 133 | 134 | /** 135 | * Is gamma available? 136 | * 137 | * @method hasGamma 138 | * @return {boolean} 139 | */ 140 | hasGamma: function () { 141 | return !!this._gamma; 142 | }, 143 | 144 | /** 145 | * Gets the gamma 146 | * 147 | * @method getGamma 148 | * @return {Color} 149 | */ 150 | getGamma: function () { 151 | return this._gamma; 152 | }, 153 | 154 | /** 155 | * Sets the gamma 156 | * 157 | * @method setGamma 158 | * @param {object|Color} value 159 | */ 160 | setGamma: function (value) { 161 | this._gamma = this._parseObject(value, Color, 'color'); 162 | }, 163 | 164 | 165 | /** 166 | * Is the comparison perceptual? 167 | * 168 | * @method isPerceptual 169 | * @return {boolean} 170 | */ 171 | isPerceptual: function () { 172 | return this._perceptual; 173 | }, 174 | 175 | /** 176 | * Sets if the comparison is perceptual 177 | * 178 | * @method setPerceptual 179 | * @param {boolean} value 180 | */ 181 | setPerceptual: function (value) { 182 | this._perceptual = !!value; 183 | }, 184 | 185 | 186 | /** 187 | * Gets all filters that will be applied to the images before comparison 188 | * 189 | * @method getFilters 190 | * @return {string[]} 191 | */ 192 | getFilters: function () { 193 | return this._filters; 194 | }, 195 | 196 | /** 197 | * Sets all filters that should be applied to the images before comparison 198 | * 199 | * @method setFilters 200 | * @param {string[]} value 201 | */ 202 | setFilters: function (value) { 203 | this._filters = value; 204 | }, 205 | 206 | 207 | /** 208 | * Gets the shift 209 | * 210 | * @method getShift 211 | * @return {Shift} 212 | */ 213 | getShift: function () { 214 | return this._shift; 215 | }, 216 | 217 | /** 218 | * Sets the shift 219 | * 220 | * @method setShift 221 | * @param {object|Shift} value 222 | */ 223 | setShift: function (value) { 224 | this._shift = this._parseObject(value, Shift, 'shift'); 225 | }, 226 | 227 | 228 | /** 229 | * Gets the block-outs 230 | * 231 | * @method getBlockOuts 232 | * @return {BlockOut[]} 233 | */ 234 | getBlockOuts: function () { 235 | return this._blockOuts; 236 | }, 237 | 238 | /** 239 | * Sets the block-outs 240 | * 241 | * @method setBlockOuts 242 | * @param {object[]|BlockOut[]} value 243 | */ 244 | setBlockOuts: function (value) { 245 | 246 | var list = []; 247 | 248 | value.forEach(function (blockOut) { 249 | list.push(this._parseObject(blockOut, BlockOut, 'block-out')); 250 | }.bind(this)); 251 | 252 | this._blockOuts = list; 253 | }, 254 | 255 | 256 | /** 257 | * Gets the area rectangle for image A 258 | * 259 | * @method getAreaImageA 260 | * @return {Rect} 261 | */ 262 | getAreaImageA: function () { 263 | return this._areaImageA; 264 | }, 265 | 266 | /** 267 | * Sets the area rectangle for image A 268 | * 269 | * @method setAreaImageA 270 | * @param {object|Rect} value 271 | */ 272 | setAreaImageA: function (value) { 273 | this._areaImageA = this._parseObject(value, Rect, 'rect A'); 274 | }, 275 | 276 | 277 | /** 278 | * Gets the area rectangle for image B 279 | * 280 | * @method getAreaImageB 281 | * @return {Rect} 282 | */ 283 | getAreaImageB: function () { 284 | return this._areaImageB; 285 | }, 286 | 287 | /** 288 | * Sets the area rectangle for image B 289 | * 290 | * @method setAreaImageB 291 | * @param {object|Rect} value 292 | */ 293 | setAreaImageB: function (value) { 294 | this._areaImageB = this._parseObject(value, Rect, 'rect B'); 295 | } 296 | }, 297 | 298 | { 299 | /** 300 | * @property TYPE 301 | * @type {string} 302 | * @static 303 | */ 304 | TYPE: 'CONFIGURATION_PIXEL_COMPARISON' 305 | } 306 | ); 307 | 308 | module.exports = PixelComparison; 309 | -------------------------------------------------------------------------------- /lib/configuration/shift.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | /** 8 | * @class Shift 9 | * @extends Base 10 | * @module Configuration 11 | * 12 | * @property {boolean} _active 13 | * @property {int} _horizontal 14 | * @property {int} _vertical 15 | * @property {boolean} _visible 16 | */ 17 | var Shift = Base.extend( 18 | 19 | /** 20 | * Shift constructor 21 | * 22 | * @param {object} options 23 | * @param {boolean} options.active Shift activated? 24 | * @param {int} options.horizontal Max. horizontal shift 25 | * @param {int} options.vertical Max. vertical shift 26 | * @param {boolean} options.visible Visible 27 | * @constructor 28 | */ 29 | function (options) { 30 | this.__super(options); 31 | 32 | options = utils.deepExtend({ 33 | active: true, 34 | horizontal: 2, 35 | vertical: 2, 36 | visible: true 37 | }, [options]); 38 | 39 | if (options.active) { 40 | this.activate(); 41 | } else { 42 | this.deactivate(); 43 | } 44 | 45 | this.setHorizontal(options.horizontal); 46 | this.setVertical(options.vertical); 47 | 48 | this.setVisibility(options.visible); 49 | }, 50 | 51 | { 52 | /** 53 | * Is shifting activated? 54 | * 55 | * @method isActive 56 | * @return {boolean} 57 | */ 58 | isActive: function () { 59 | return this._active; 60 | }, 61 | 62 | /** 63 | * Activates shift highlighting 64 | * 65 | * @method activate 66 | */ 67 | activate: function () { 68 | this._activate = true; 69 | }, 70 | 71 | /** 72 | * Deactivates shift highlighting 73 | * 74 | * @method deactivate 75 | */ 76 | deactivate: function () { 77 | this._active = false; 78 | }, 79 | 80 | 81 | /** 82 | * Gets the max. horizontal shift 83 | * 84 | * @method getHorizontal 85 | * @return {int} 86 | */ 87 | getHorizontal: function () { 88 | return this._horizontal; 89 | }, 90 | 91 | /** 92 | * Sets the max. horizontal shift 93 | * 94 | * @method setHorizontal 95 | * @param {int} value 96 | */ 97 | setHorizontal: function (value) { 98 | this._horizontal = value; 99 | }, 100 | 101 | 102 | /** 103 | * Gets the max. vertical shift 104 | * 105 | * @method getVertical 106 | * @return {int} 107 | */ 108 | getVertical: function () { 109 | return this._vertical; 110 | }, 111 | 112 | /** 113 | * Sets the max. vertical shift 114 | * 115 | * @method setVertical 116 | * @param {int} value 117 | */ 118 | setVertical: function (value) { 119 | this._vertical = value; 120 | }, 121 | 122 | 123 | /** 124 | * Is shift visible in final result 125 | * 126 | * @method isVisible 127 | * @return {boolean} 128 | */ 129 | isVisible: function () { 130 | return this._visible; 131 | }, 132 | 133 | /** 134 | * Sets the visibility of the shift in final result 135 | * 136 | * @method setVisibility 137 | * @param {boolean} value 138 | */ 139 | setVisibility: function (value) { 140 | this._visible = !!value; 141 | } 142 | }, 143 | 144 | { 145 | /** 146 | * @property TYPE 147 | * @type {string} 148 | * @static 149 | */ 150 | TYPE: 'CONFIGURATION_SHIFT' 151 | } 152 | ); 153 | 154 | module.exports = Shift; 155 | -------------------------------------------------------------------------------- /lib/configuration/structure/device.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | var Rect = require('../atoms/rect'); 7 | 8 | /** 9 | * @class Device 10 | * @extends Base 11 | * @module Configuration 12 | * @submodule Structure 13 | * 14 | * @property {boolean} _stitched 15 | * @property {Rect} _section 16 | */ 17 | var Device = Base.extend( 18 | 19 | /** 20 | * Device constructor 21 | * 22 | * @param {object} options 23 | * @param {boolean} options.stitched 24 | * @param {object|Rect} options.section 25 | * @constructor 26 | */ 27 | function (options) { 28 | this.__super(options); 29 | 30 | this.setStitched(options.stitched); 31 | this.setSection(options.section); 32 | }, 33 | 34 | { 35 | /** 36 | * Is screenshot stitched? 37 | * 38 | * @method isStitched 39 | * @return {boolean} 40 | */ 41 | isStitched: function () { 42 | return this._stitched; 43 | }, 44 | 45 | /** 46 | * Sets if the screenshot was stitched 47 | * 48 | * @method setStitched 49 | * @param {boolean} value 50 | */ 51 | setStitched: function (value) { 52 | this._stitched = value; 53 | }, 54 | 55 | 56 | /** 57 | * Gets the section of the complete screenshot that is represented by the image-data 58 | * 59 | * @method getSection 60 | * @return {Rect} 61 | */ 62 | getSection: function () { 63 | return this._section; 64 | }, 65 | 66 | /** 67 | * Sets the section of the complete screenshot that is represented by the image-data 68 | * 69 | * @method setSection 70 | * @param {object|Rect} value 71 | */ 72 | setSection: function (value) { 73 | this._section = this._parseObject(value, Rect, 'rect'); 74 | } 75 | }, 76 | 77 | { 78 | /** 79 | * @property TYPE 80 | * @type {string} 81 | * @static 82 | */ 83 | TYPE: 'CONFIGURATION_STRUCTURE_DEVICE' 84 | } 85 | ); 86 | 87 | module.exports = Device; 88 | -------------------------------------------------------------------------------- /lib/configuration/structure/device/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../../base'); 5 | 6 | /** 7 | * @class App 8 | * @extends Base 9 | * @module Configuration 10 | * @submodule Structure 11 | * 12 | * @property {string} _codeName 13 | * @property {string} _name 14 | * @property {string} _version 15 | * @property {string} _buildId 16 | */ 17 | var App = Base.extend( 18 | 19 | /** 20 | * App constructor 21 | * 22 | * @param {object} options 23 | * @param {string} options.codeName 24 | * @param {string} options.name 25 | * @param {string} options.version 26 | * @param {string} options.buildId 27 | * @constructor 28 | */ 29 | function (options) { 30 | this.__super(options); 31 | 32 | this.setCodeName(options.codeName); 33 | this.setName(options.name); 34 | this.setVersion(options.version); 35 | this.setBuildId(options.buildId); 36 | }, 37 | 38 | { 39 | /** 40 | * Gets the code-name 41 | * 42 | * @method getCodeName 43 | * @return {string} 44 | */ 45 | getCodeName: function () { 46 | return this._codeName; 47 | }, 48 | 49 | /** 50 | * Sets the code-name 51 | * 52 | * @method setCodeName 53 | * @param {string} value 54 | */ 55 | setCodeName: function (value) { 56 | this._codeName = value; 57 | }, 58 | 59 | 60 | /** 61 | * Gets the name 62 | * 63 | * @method getName 64 | * @return {string} 65 | */ 66 | getName: function () { 67 | return this._name; 68 | }, 69 | 70 | /** 71 | * Sets the name 72 | * 73 | * @method setName 74 | * @param {string} value 75 | */ 76 | setName: function (value) { 77 | this._name = value; 78 | }, 79 | 80 | 81 | /** 82 | * Gets the version 83 | * 84 | * @method getVersion 85 | * @return {string} 86 | */ 87 | getVersion: function () { 88 | return this._version; 89 | }, 90 | 91 | /** 92 | * Sets the version 93 | * 94 | * @method setVersion 95 | * @param {string} value 96 | */ 97 | setVersion: function (value) { 98 | this._version = value; 99 | }, 100 | 101 | 102 | /** 103 | * Gets the build-id 104 | * 105 | * @method getBuildId 106 | * @return {string} 107 | */ 108 | getBuildId: function () { 109 | return this._buildId; 110 | }, 111 | 112 | /** 113 | * Sets the build-id 114 | * 115 | * @method setBuildId 116 | * @param {string} value 117 | */ 118 | setBuildId: function (value) { 119 | this._buildId = value; 120 | } 121 | }, 122 | 123 | { 124 | /** 125 | * @property TYPE 126 | * @type {string} 127 | * @static 128 | */ 129 | TYPE: 'CONFIGURATION_STRUCTURE_DEVICE_APP' 130 | } 131 | ); 132 | 133 | module.exports = App; 134 | -------------------------------------------------------------------------------- /lib/configuration/structure/device/identity.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../../base'); 5 | 6 | /** 7 | * @class Identity 8 | * @extends Base 9 | * @module Configuration 10 | * @submodule Structure 11 | * 12 | * @property {string} _name 13 | * @property {string} _sub 14 | */ 15 | var Identity = Base.extend( 16 | 17 | /** 18 | * Identity constructor 19 | * 20 | * @param {object} options 21 | * @param {string} options.name 22 | * @param {string} options.sub 23 | * @constructor 24 | */ 25 | function (options) { 26 | this.__super(options); 27 | 28 | this.setName(options.name); 29 | this.setSub(options.sub); 30 | }, 31 | 32 | { 33 | /** 34 | * Gets the name 35 | * 36 | * @method getName 37 | * @return {string} 38 | */ 39 | getName: function () { 40 | return this._name; 41 | }, 42 | 43 | /** 44 | * Sets the name 45 | * 46 | * @method setName 47 | * @param {string} value 48 | */ 49 | setName: function (value) { 50 | this._name = value; 51 | }, 52 | 53 | 54 | /** 55 | * Gets the sub 56 | * 57 | * @method getSub 58 | * @return {string} 59 | */ 60 | getSub: function () { 61 | return this._sub; 62 | }, 63 | 64 | /** 65 | * Sets the sub 66 | * 67 | * @method setSub 68 | * @param {string} value 69 | */ 70 | setSub: function (value) { 71 | this._sub = value; 72 | } 73 | }, 74 | 75 | { 76 | /** 77 | * @property TYPE 78 | * @type {string} 79 | * @static 80 | */ 81 | TYPE: 'CONFIGURATION_STRUCTURE_DEVICE_IDENTITY' 82 | } 83 | ); 84 | 85 | module.exports = Identity; 86 | -------------------------------------------------------------------------------- /lib/configuration/structure/device/screen.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Size = require('./size'); 5 | 6 | /** 7 | * @class Screen 8 | * @extends Size 9 | * @module Configuration 10 | * @submodule Structure 11 | * 12 | * @property {number} _pixelRatio 13 | * @property {int} _colorDepth 14 | * @property {boolean} _fullScreen 15 | */ 16 | var Screen = Size.extend( 17 | 18 | /** 19 | * Screen constructor 20 | * 21 | * @param {object} options 22 | * @param {number} options.pixelRatio 23 | * @param {int} options.colorDepth 24 | * @param {boolean} options.fullScreen 25 | * @constructor 26 | */ 27 | function (options) { 28 | this.__super(options); 29 | 30 | this.setPixelRatio(options.pixelRatio); 31 | this.setColorDepth(options.colorDepth); 32 | this.setFullScreen(options.fullScreen); 33 | }, 34 | 35 | { 36 | /** 37 | * Gets the pixel-ratio 38 | * 39 | * @method getPixelRatio 40 | * @return {number} 41 | */ 42 | getPixelRatio: function () { 43 | return this._pixelRatio; 44 | }, 45 | 46 | /** 47 | * Sets the pixel-ratio 48 | * 49 | * @method setPixelRatio 50 | * @param {number} value 51 | */ 52 | setPixelRatio: function (value) { 53 | this._pixelRatio = value; 54 | }, 55 | 56 | 57 | /** 58 | * Gets the color-depth 59 | * 60 | * @method getColorDepth 61 | * @return {int} 62 | */ 63 | getColorDepth: function () { 64 | return this._colorDepth; 65 | }, 66 | 67 | /** 68 | * Sets the color-depth 69 | * 70 | * @method setColorDepth 71 | * @param {int} value 72 | */ 73 | setColorDepth: function (value) { 74 | this._colorDepth = value; 75 | }, 76 | 77 | 78 | /** 79 | * If the application was full-screen when the screenshot was taken 80 | * 81 | * @method wasFullScreen 82 | * @return {boolean} 83 | */ 84 | wasFullScreen: function () { 85 | return this._fullScreen; 86 | }, 87 | 88 | /** 89 | * Sets if the application was in full-screen when the screenshot was taken 90 | * 91 | * @method setFullScreen 92 | * @param {boolean} value 93 | */ 94 | setFullScreen: function (value) { 95 | this._fullScreen = value; 96 | } 97 | }, 98 | 99 | { 100 | /** 101 | * @property TYPE 102 | * @type {string} 103 | * @static 104 | */ 105 | TYPE: 'CONFIGURATION_STRUCTURE_DEVICE_SCREEN' 106 | } 107 | ); 108 | 109 | module.exports = Screen; 110 | -------------------------------------------------------------------------------- /lib/configuration/structure/device/size.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../../base'); 5 | 6 | /** 7 | * @class Size 8 | * @extends Base 9 | * @module Configuration 10 | * @submodule Structure 11 | * 12 | * @property {int} _width 13 | * @property {int} _height 14 | */ 15 | var Size = Base.extend( 16 | 17 | /** 18 | * Size constructor 19 | * 20 | * @param {object} options 21 | * @param {int} options.width 22 | * @param {int} options.height 23 | * @constructor 24 | */ 25 | function (options) { 26 | this.__super(options); 27 | 28 | this.setWidth(options.width); 29 | this.setHeight(options.height); 30 | }, 31 | 32 | { 33 | /** 34 | * Gets the width 35 | * 36 | * @method getWidth 37 | * @return {int} 38 | */ 39 | getWidth: function () { 40 | return this._width; 41 | }, 42 | 43 | /** 44 | * Sets the width 45 | * 46 | * @method setWidth 47 | * @param {int} value 48 | */ 49 | setWidth: function (value) { 50 | this._width = value; 51 | }, 52 | 53 | 54 | /** 55 | * Gets the height 56 | * 57 | * @method getHeight 58 | * @return {int} 59 | */ 60 | getHeight: function () { 61 | return this._height; 62 | }, 63 | 64 | /** 65 | * Sets the height 66 | * 67 | * @method setHeight 68 | * @param {int} value 69 | */ 70 | setHeight: function (value) { 71 | this._height = value; 72 | } 73 | }, 74 | 75 | { 76 | /** 77 | * @property TYPE 78 | * @type {string} 79 | * @static 80 | */ 81 | TYPE: 'CONFIGURATION_STRUCTURE_DEVICE_SIZE' 82 | } 83 | ); 84 | 85 | module.exports = Size; 86 | -------------------------------------------------------------------------------- /lib/configuration/structure/document.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | var Size = require('./device/size'); 7 | 8 | /** 9 | * @class document 10 | * @extends Base 11 | * @module Configuration 12 | * @submodule Structure 13 | * 14 | * @property {string} _title 15 | * @property {string} _url 16 | * @property {string} _referrer 17 | * @property {Size} _size 18 | */ 19 | var Document = Base.extend( 20 | 21 | /** 22 | * Document constructor 23 | * 24 | * @param {object} options 25 | * @param {string} options.title 26 | * @param {string} options.url 27 | * @param {string} options.referrer 28 | * @param {object|Size} options.size 29 | * @constructor 30 | */ 31 | function (options) { 32 | this.__super(options); 33 | 34 | this.setTitle(options.title); 35 | this.setUrl(options.url); 36 | this.setReferrer(options.referrer); 37 | this.setSize(options.size); 38 | }, 39 | 40 | { 41 | /** 42 | * Gets the title of the document 43 | * 44 | * @method getTitle 45 | * @return {string} 46 | */ 47 | getTitle: function () { 48 | return this._title; 49 | }, 50 | 51 | /** 52 | * Sets the title of the document 53 | * 54 | * @method setTitle 55 | * @param {string} value 56 | */ 57 | setTitle: function (value) { 58 | this._title = value; 59 | }, 60 | 61 | 62 | /** 63 | * Gets the url of the document 64 | * 65 | * @method getUrl 66 | * @return {string} 67 | */ 68 | getUrl: function () { 69 | return this._url; 70 | }, 71 | 72 | /** 73 | * Sets the url of the document 74 | * 75 | * @method setUrl 76 | * @param {string} value 77 | */ 78 | setUrl: function (value) { 79 | this._url = value; 80 | }, 81 | 82 | 83 | /** 84 | * Gets the referrer of the document 85 | * 86 | * @method getReferrer 87 | * @return {string} 88 | */ 89 | getReferrer: function () { 90 | return this._referrer; 91 | }, 92 | 93 | /** 94 | * Sets the referrer of the document 95 | * 96 | * @method setReferrer 97 | * @param {string} value 98 | */ 99 | setReferrer: function (value) { 100 | this._referrer = value; 101 | }, 102 | 103 | 104 | /** 105 | * Gets the size of the document 106 | * 107 | * @method getSize 108 | * @return {Size} 109 | */ 110 | getSize: function () { 111 | return this._size; 112 | }, 113 | 114 | /** 115 | * Sets the size of the document 116 | * 117 | * @method setSize 118 | * @param {object|Size} value 119 | */ 120 | setSize: function (value) { 121 | this._size = this._parseObject(value, Size, 'size'); 122 | } 123 | }, 124 | 125 | { 126 | /** 127 | * @property TYPE 128 | * @type {string} 129 | * @static 130 | */ 131 | TYPE: 'CONFIGURATION_STRUCTURE_DOCUMENT' 132 | } 133 | ); 134 | 135 | module.exports = Document; 136 | -------------------------------------------------------------------------------- /lib/configuration/structure/domElement.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | var Rect = require('../atoms/rect'); 7 | 8 | /** 9 | * @class DomElement 10 | * @extends Base 11 | * @module Configuration 12 | * @submodule Structure 13 | * 14 | * @property {string} _id 15 | * @property {DomElement} _parent 16 | * @property {string} _tagName 17 | * @property {string[]} _classes 18 | * @property {Rect} _rect 19 | * @property {DomElement[]} _nodes 20 | */ 21 | var DomElement = Base.extend( 22 | 23 | /** 24 | * Dom-Element constructor 25 | * 26 | * @param {object} options 27 | * @param {string} options.id 28 | * @param {DomElement} options.parent 29 | * @param {string} options.tagName 30 | * @param {string[]} options.classes 31 | * @param {object|Rect} options.rect 32 | * @param {object[]|DomElement[]} options.nodes 33 | * @constructor 34 | */ 35 | function (options) { 36 | this.__super(options); 37 | 38 | this.setId(options.id); 39 | this.setTagName(options.tagName); 40 | this.setClasses(options.classes); 41 | this.setRect(options.rect); 42 | this.setNodes(options.nodes); 43 | }, 44 | 45 | { 46 | /** 47 | * Gets the id of the dom-element 48 | * 49 | * @method getId 50 | * @return {string} 51 | */ 52 | getId: function () { 53 | return this._id; 54 | }, 55 | 56 | /** 57 | * Sets the id of the dom-element 58 | * 59 | * @method setId 60 | * @param {string} value 61 | */ 62 | setId: function (value) { 63 | this._id = value; 64 | }, 65 | 66 | 67 | /** 68 | * Gets the parent of this DOM-element 69 | * 70 | * @method getParent 71 | * @return {DomElement} 72 | */ 73 | getParent: function () { 74 | return this._parent; 75 | }, 76 | 77 | /** 78 | * Sets the parent of this DOM-element 79 | * 80 | * @method setParent 81 | * @param {DomElement} value 82 | */ 83 | setParent: function (value) { 84 | this._parent = value; 85 | }, 86 | 87 | /** 88 | * Does this DOM-element have a parent? 89 | * 90 | * @return {boolean} 91 | */ 92 | hasParent: function () { 93 | return !!this.getParent(); 94 | }, 95 | 96 | 97 | /** 98 | * Gets the tag-name of the dom-element 99 | * 100 | * @method getTagName 101 | * @return {string} 102 | */ 103 | getTagName: function () { 104 | return this._tagName; 105 | }, 106 | 107 | /** 108 | * Sets the tag-name of the dom-element 109 | * 110 | * @method setTagName 111 | * @param {string} value 112 | */ 113 | setTagName: function (value) { 114 | this._tagName = value; 115 | }, 116 | 117 | 118 | /** 119 | * Gets the classes of the dom-element 120 | * 121 | * @method getClasses 122 | * @return {string[]} 123 | */ 124 | getClasses: function () { 125 | return this._classes; 126 | }, 127 | 128 | /** 129 | * Sets the classes of the dom-element 130 | * 131 | * @method setClasses 132 | * @param {string[]} value 133 | */ 134 | setClasses: function (value) { 135 | this._classes = value; 136 | }, 137 | 138 | 139 | /** 140 | * Gets the position and dimension of the dom-element 141 | * 142 | * @method getRect 143 | * @return {Rect} 144 | */ 145 | getRect: function () { 146 | return this._rect; 147 | }, 148 | 149 | /** 150 | * Sets the position and dimension of the dom-element 151 | * 152 | * @method setRect 153 | * @param {object|Rect} value 154 | */ 155 | setRect: function (value) { 156 | this._rect = this._parseObject(value, Rect, 'rect'); 157 | }, 158 | 159 | 160 | /** 161 | * Gets the nodes of the dom-element 162 | * 163 | * @method getNodes 164 | * @return {DomElement[]} 165 | */ 166 | getNodes: function () { 167 | return this._nodes; 168 | }, 169 | 170 | /** 171 | * Sets the nodes of the dom-element 172 | * 173 | * @method setNodes 174 | * @param {object|DomElement[]} value 175 | */ 176 | setNodes: function (value) { 177 | 178 | var list = []; 179 | 180 | value.forEach(function (node) { 181 | node.parent = this; 182 | list.push(this._parseObject(node, DomElement, 'dom-element')); 183 | }.bind(this)); 184 | 185 | this._nodes = list; 186 | }, 187 | 188 | 189 | /** 190 | * Gets the selector for this DOM-element 191 | * 192 | * @param {string} [suffix] 193 | * @return {string} 194 | */ 195 | getSelector: function (suffix) { 196 | 197 | var id = this.getId(), 198 | tagName = this.getTagName(), 199 | classes = this.getClasses(), 200 | result; 201 | 202 | if (id) { 203 | return '#' + id + ' ' + suffix; 204 | 205 | } else if (classes && classes.length > 0) { 206 | result = '.' + classes.join('.') + ' ' + suffix; 207 | 208 | if (tagName) { 209 | result = tagName + result; 210 | } 211 | 212 | } else { 213 | result = tagName + ' ' + suffix; 214 | } 215 | 216 | if (this.hasParent()) { 217 | return this.getParent().getSelector(result); 218 | } else { 219 | return result; 220 | } 221 | }, 222 | 223 | /** 224 | * Finds a DOM-element at an absolute coordinate 225 | * 226 | * @method findElementAt 227 | * @param {int} x X-coordinate 228 | * @param {int} y Y-coordinate 229 | */ 230 | findElementAt: function (x, y) { 231 | 232 | var nodes, 233 | result, 234 | i; 235 | 236 | if (this.getRect().inBounds(x, y)) { 237 | return this; 238 | 239 | } else { 240 | nodes = this.getNodes(); 241 | 242 | if (nodes) { 243 | 244 | // Travers from the back so that overlapping elements will be selected first 245 | for (i = nodes.length; i >= 0; i--) { 246 | result = nodes[i].findElementAt(x, y); 247 | 248 | if (result) { 249 | return result; 250 | } 251 | } 252 | } 253 | 254 | return null; 255 | } 256 | } 257 | }, 258 | 259 | { 260 | /** 261 | * @property TYPE 262 | * @type {string} 263 | * @static 264 | */ 265 | TYPE: 'CONFIGURATION_STRUCTURE_DOM_ELEMENT' 266 | } 267 | ); 268 | 269 | module.exports = DomElement; 270 | -------------------------------------------------------------------------------- /lib/configuration/structure/screenshot.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | var Rect = require('../atoms/rect'); 7 | 8 | /** 9 | * @class Screenshot 10 | * @extends Base 11 | * @module Configuration 12 | * @submodule Structure 13 | * 14 | * @property {boolean} _stitched 15 | * @property {Rect} _section 16 | */ 17 | var Screenshot = Base.extend( 18 | 19 | /** 20 | * Screenshot constructor 21 | * 22 | * @param {object} options 23 | * @param {boolean} options.stitched 24 | * @param {object|Rect} options.section 25 | * @constructor 26 | */ 27 | function (options) { 28 | this.__super(options); 29 | 30 | this.setStitched(options.stitched); 31 | this.setSection(options.section); 32 | }, 33 | 34 | { 35 | /** 36 | * Is screenshot stitched? 37 | * 38 | * @method isStitched 39 | * @return {boolean} 40 | */ 41 | isStitched: function () { 42 | return this._stitched; 43 | }, 44 | 45 | /** 46 | * Sets if the screenshot was stitched 47 | * 48 | * @method setStitched 49 | * @param {boolean} value 50 | */ 51 | setStitched: function (value) { 52 | this._stitched = value; 53 | }, 54 | 55 | 56 | /** 57 | * Gets the section of the complete screenshot that is represented by the image-data 58 | * 59 | * @method getSection 60 | * @return {Rect} 61 | */ 62 | getSection: function () { 63 | return this._section; 64 | }, 65 | 66 | /** 67 | * Sets the section of the complete screenshot that is represented by the image-data 68 | * 69 | * @method setSection 70 | * @param {object|Rect} value 71 | */ 72 | setSection: function (value) { 73 | this._section = value; 74 | } 75 | }, 76 | 77 | { 78 | /** 79 | * @property TYPE 80 | * @type {string} 81 | * @static 82 | */ 83 | TYPE: 'CONFIGURATION_STRUCTURE_SCREENSHOT' 84 | } 85 | ); 86 | 87 | module.exports = Screenshot; 88 | -------------------------------------------------------------------------------- /lib/configuration/structure/structure.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | var Version = require('./version'); 7 | var Device = require('./device'); 8 | var Document = require('./document'); 9 | var ViewPort = require('./viewPort'); 10 | var Screenshot = require('./screenshot'); 11 | var DomElement = require('./domElement'); 12 | 13 | /** 14 | * @class Structure 15 | * @extends Base 16 | * @module Configuration 17 | * @submodule Structure 18 | * 19 | * @property {Version} _version 20 | * @property {Device} _device 21 | * @property {Document} _document 22 | * @property {ViewPort} _viewPort 23 | * @property {Screenshot} _screenshot 24 | * @property {DomElement} _dom 25 | */ 26 | var Structure = Base.extend( 27 | 28 | /** 29 | * Structure constructor 30 | * 31 | * @param {object} options 32 | * @param {object|Version} options.version 33 | * @param {object|Device} options.device 34 | * @param {object|Document} options.document 35 | * @param {object|ViewPort} options.viewPort 36 | * @param {object|Screenshot} options.screenshot 37 | * @param {object|DomElement} options.dom 38 | * @constructor 39 | */ 40 | function (options) { 41 | this.__super(options); 42 | 43 | this.setVersion(options.version); 44 | this.setDevice(options.device); 45 | this.setDocument(options.document); 46 | this.setViewPort(options.viewPort); 47 | this.setScreenshot(options.screenshot); 48 | this.setDom(options.dom); 49 | }, 50 | 51 | { 52 | /** 53 | * Gets the version of the structure format 54 | * 55 | * @method getVersion 56 | * @return {Version} 57 | */ 58 | getVersion: function () { 59 | return this._version; 60 | }, 61 | 62 | /** 63 | * Sets the version of the structure format 64 | * 65 | * @method setVersion 66 | * @param {object|Version} value 67 | */ 68 | setVersion: function (value) { 69 | this._version = this._parseObject(value, Version, 'version'); 70 | }, 71 | 72 | 73 | /** 74 | * Gets the device from where the data was recorded 75 | * 76 | * @method getDevice 77 | * @return {Device} 78 | */ 79 | getDevice: function () { 80 | return this._device; 81 | }, 82 | 83 | /** 84 | * Sets the device from where the data was recorded 85 | * 86 | * @method setDevice 87 | * @param {object|Device} value 88 | */ 89 | setDevice: function (value) { 90 | this._device = this._parseObject(value, Device, 'device'); 91 | }, 92 | 93 | 94 | /** 95 | * Gets the document 96 | * 97 | * @method getDocument 98 | * @return {Document} 99 | */ 100 | getDocument: function () { 101 | return this._document; 102 | }, 103 | 104 | /** 105 | * Sets the document 106 | * 107 | * @method setDocument 108 | * @param {object|Document} value 109 | */ 110 | setDocument: function (value) { 111 | this._document = this._parseObject(value, Document, 'document'); 112 | }, 113 | 114 | 115 | /** 116 | * Gets the view-port 117 | * 118 | * @method getViewPort 119 | * @return {ViewPort} 120 | */ 121 | getViewPort: function () { 122 | return this._viewPort; 123 | }, 124 | 125 | /** 126 | * Sets the view-port 127 | * 128 | * @method setViewPort 129 | * @param {object|ViewPort} value 130 | */ 131 | setViewPort: function (value) { 132 | this._viewPort = this._parseObject(value, ViewPort, 'view-port'); 133 | }, 134 | 135 | 136 | /** 137 | * Gets the screenshot information 138 | * 139 | * @method getScreenshot 140 | * @return {Screenshot} 141 | */ 142 | getScreenshot: function () { 143 | return this._screenshot; 144 | }, 145 | 146 | /** 147 | * Sets the screenshot information 148 | * 149 | * @method setScreenshot 150 | * @param {object|Screenshot} value 151 | */ 152 | setScreenshot: function (value) { 153 | this._screenshot = this._parseObject(value, Screenshot, 'screenshot'); 154 | }, 155 | 156 | 157 | /** 158 | * Gets the DOM 159 | * 160 | * @method getDom 161 | * @return {DomElement} 162 | */ 163 | getDom: function () { 164 | return this._dom; 165 | }, 166 | 167 | /** 168 | * Sets the DOM 169 | * 170 | * @method setDom 171 | * @param {object|DomElement} value 172 | */ 173 | setDom: function (value) { 174 | this._dom = this._parseObject(value, DomElement, 'DOM'); 175 | } 176 | }, 177 | 178 | { 179 | /** 180 | * @property TYPE 181 | * @type {string} 182 | * @static 183 | */ 184 | TYPE: 'CONFIGURATION_STRUCTURE' 185 | } 186 | ); 187 | 188 | module.exports = Structure; 189 | -------------------------------------------------------------------------------- /lib/configuration/structure/version.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('../base'); 5 | 6 | /** 7 | * @class Version 8 | * @extends Base 9 | * @module Configuration 10 | * @submodule Structure 11 | * 12 | * @property {int} _major 13 | * @property {int} _minor 14 | */ 15 | var Version = Base.extend( 16 | 17 | /** 18 | * Version constructor 19 | * 20 | * @param {object} options 21 | * @param {int} options.major 22 | * @param {int} options.minor 23 | * @constructor 24 | */ 25 | function (options) { 26 | this.__super(options); 27 | 28 | this.setMajor(options.major); 29 | this.setMinor(options.minor); 30 | }, 31 | 32 | { 33 | /** 34 | * Gets the major part of the version 35 | * 36 | * @method getMajor 37 | * @return {int} 38 | */ 39 | getMajor: function () { 40 | return this._major; 41 | }, 42 | 43 | /** 44 | * Sets the major part of the version 45 | * 46 | * @method setMajor 47 | * @param {int} value 48 | */ 49 | setMajor: function (value) { 50 | this._major = value; 51 | }, 52 | 53 | 54 | /** 55 | * Gets the minor part of the version 56 | * 57 | * @method getMinor 58 | * @return {int} 59 | */ 60 | getMinor: function () { 61 | return this._minor; 62 | }, 63 | 64 | /** 65 | * Sets the minor part of the version 66 | * 67 | * @method setMinor 68 | * @param {int} value 69 | */ 70 | setMinor: function (value) { 71 | this._minor = value; 72 | } 73 | }, 74 | 75 | { 76 | /** 77 | * @property TYPE 78 | * @type {string} 79 | * @static 80 | */ 81 | TYPE: 'CONFIGURATION_STRUCTURE_VERSION' 82 | } 83 | ); 84 | 85 | module.exports = Version; 86 | -------------------------------------------------------------------------------- /lib/configuration/structure/viewPort.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Rect = require('../atoms/rect'); 5 | 6 | /** 7 | * @class ViewPort 8 | * @extends Rect 9 | * @module Configuration 10 | * @submodule Structure 11 | */ 12 | var ViewPort = Rect.extend( 13 | 14 | /** 15 | * View-port constructor 16 | * 17 | * @param {object} options 18 | * @constructor 19 | */ 20 | function (options) { 21 | this.__super(options); 22 | }, 23 | 24 | { 25 | }, 26 | 27 | { 28 | /** 29 | * @property TYPE 30 | * @type {string} 31 | * @static 32 | */ 33 | TYPE: 'CONFIGURATION_STRUCTURE_VIEWPORT' 34 | } 35 | ); 36 | 37 | module.exports = ViewPort; 38 | -------------------------------------------------------------------------------- /lib/configuration/structureComparison.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('./base'); 5 | var utils = require('preceptor-core').utils; 6 | 7 | var Color = require('./atoms/color'); 8 | var Rect = require('./atoms/rect'); 9 | var Shift = require('./shift'); 10 | var BlockOut = require('./blockOut'); 11 | 12 | /** 13 | * @class StructureComparison 14 | * @extends Base 15 | * @module Configuration 16 | * 17 | * @property {string} _type 18 | * @property {string} _selector 19 | * @property {Anchor[]} _anchors 20 | * @property {Limit[]} _limits 21 | */ 22 | var StructureComparison = Base.extend( 23 | 24 | /** 25 | * Structural comparison constructor 26 | * 27 | * @param {object} options 28 | * @param {string} options.type Type of comparison 29 | * @param {string} options.selector Selector for checked DOM-element 30 | * @param {object[]|Anchor[]} options.anchors Anchors for DOM-element 31 | * @param {object[]|Limit[]} options.limits Limits for selecting DOM-elements 32 | * @constructor 33 | */ 34 | function (options) { 35 | this.__super(options); 36 | 37 | options = utils.deepExtend({ 38 | type: "structure", 39 | selector: null, 40 | anchors: [], 41 | limits: [] 42 | }, [options]); 43 | 44 | this.setType(options.type); 45 | this.setSelector(options.selector); 46 | 47 | this.setAnchors(options.anchors); 48 | this.setLimits(options.limits); 49 | }, 50 | 51 | { 52 | /** 53 | * Gets the comparison type 54 | * 55 | * @method getType 56 | * @return {string} 57 | */ 58 | getType: function () { 59 | return this._type; 60 | }, 61 | 62 | /** 63 | * Sets the comparison type 64 | * 65 | * @method setType 66 | * @param {string} value 67 | */ 68 | setType: function (value) { 69 | this._type = value; 70 | }, 71 | 72 | 73 | /** 74 | * Gets the selector for a DOM element 75 | * 76 | * @method getSelector 77 | * @return {string} 78 | */ 79 | getSelector: function () { 80 | return this._selector; 81 | }, 82 | 83 | /** 84 | * Sets the selector for a DOM element 85 | * 86 | * @method setSelector 87 | * @param {string} value 88 | */ 89 | setSelector: function (value) { 90 | this._selector = value; 91 | }, 92 | 93 | 94 | /** 95 | * Gets the anchors for DOM-elements 96 | * 97 | * @method getAnchors 98 | * @return {Anchor[]} 99 | */ 100 | getAnchors: function () { 101 | return this._anchors; 102 | }, 103 | 104 | /** 105 | * Sets the anchors for DOM-elements 106 | * 107 | * @method setAnchors 108 | * @param {object[]|Anchor[]} value 109 | */ 110 | setAnchors: function (value) { 111 | 112 | var list = []; 113 | 114 | value.forEach(function (entry) { 115 | list.push(this._parseObject(entry, Anchor, 'anchor')); 116 | }.bind(this)); 117 | 118 | this._anchors = list; 119 | }, 120 | 121 | 122 | /** 123 | * Gets the limitations for selection 124 | * 125 | * @method getLimits 126 | * @return {Limit[]} 127 | */ 128 | getLimits: function () { 129 | return this._limits; 130 | }, 131 | 132 | /** 133 | * Sets the limitations or selection 134 | * 135 | * @method setLimits 136 | * @param {object[]|Limit[]} value 137 | */ 138 | setLimits: function (value) { 139 | 140 | var list = []; 141 | 142 | value.forEach(function (entry) { 143 | list.push(this._parseObject(entry, Limit, 'limit')); 144 | }.bind(this)); 145 | 146 | this._limits = list; 147 | } 148 | }, 149 | 150 | { 151 | /** 152 | * @property TYPE 153 | * @type {string} 154 | * @static 155 | */ 156 | TYPE: 'CONFIGURATION_STRUCTURE_COMPARISON' 157 | } 158 | ); 159 | 160 | module.exports = StructureComparison; 161 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class constants 3 | * @type {object} 4 | */ 5 | module.exports = { 6 | 7 | /** 8 | * Anchor-type left 9 | * 10 | * @static 11 | * @property ANCHOR_TYPE_LEFT 12 | * @type {string} 13 | */ 14 | ANCHOR_TYPE_LEFT: 'left', 15 | 16 | /** 17 | * Anchor-type right 18 | * 19 | * @static 20 | * @property ANCHOR_TYPE_RIGHT 21 | * @type {string} 22 | */ 23 | ANCHOR_TYPE_RIGHT: 'right', 24 | 25 | /** 26 | * Anchor-type top 27 | * 28 | * @static 29 | * @property ANCHOR_TYPE_TOP 30 | * @type {string} 31 | */ 32 | ANCHOR_TYPE_TOP: 'top', 33 | 34 | /** 35 | * Anchor-type bottom 36 | * 37 | * @static 38 | * @property ANCHOR_TYPE_BOTTOM 39 | * @type {string} 40 | */ 41 | ANCHOR_TYPE_BOTTOM: 'bottom', 42 | 43 | /** 44 | * Anchor-type width 45 | * 46 | * @static 47 | * @property ANCHOR_TYPE_WIDTH 48 | * @type {string} 49 | */ 50 | ANCHOR_TYPE_WIDTH: 'width', 51 | 52 | /** 53 | * Anchor-type height 54 | * 55 | * @static 56 | * @property ANCHOR_TYPE_HEIGHT 57 | * @type {string} 58 | */ 59 | ANCHOR_TYPE_HEIGHT: 'height', 60 | 61 | /** 62 | * Anchor-type horizontal (includes width, left, and right) 63 | * 64 | * @static 65 | * @property ANCHOR_TYPE_HORIZONTAL 66 | * @type {string} 67 | */ 68 | ANCHOR_TYPE_HORIZONTAL: 'horizontal', 69 | 70 | /** 71 | * Anchor-type vertical (includes height, top, and bottom) 72 | * 73 | * @static 74 | * @property ANCHOR_TYPE_VERTICAL 75 | * @type {string} 76 | */ 77 | ANCHOR_TYPE_VERTICAL: 'vertical', 78 | 79 | 80 | /** 81 | * Relative positioning 82 | * 83 | * @static 84 | * @property ANCHOR_POSITION_RELATIVE 85 | * @type {string} 86 | */ 87 | ANCHOR_POSITION_RELATIVE: 'relative', 88 | 89 | /** 90 | * Absolute positioning 91 | * 92 | * @static 93 | * @property ANCHOR_POSITION_ABSOLUTE 94 | * @type {string} 95 | */ 96 | ANCHOR_POSITION_ABSOLUTE: 'absolute', 97 | 98 | 99 | /** 100 | * Limit-type min 101 | * 102 | * @static 103 | * @property LIMIT_TYPE_MIN 104 | * @type {string} 105 | */ 106 | LIMIT_TYPE_MIN: 'min', 107 | 108 | /** 109 | * Limit-type max 110 | * 111 | * @static 112 | * @property LIMIT_TYPE_MAX 113 | * @type {string} 114 | */ 115 | LIMIT_TYPE_MAX: 'max', 116 | 117 | 118 | /** 119 | * Limit-context left 120 | * 121 | * @static 122 | * @property LIMIT_CONTEXT_LEFT 123 | * @type {string} 124 | */ 125 | LIMIT_CONTEXT_LEFT: 'left', 126 | 127 | /** 128 | * Limit-context right 129 | * 130 | * @static 131 | * @property LIMIT_CONTEXT_RIGHT 132 | * @type {string} 133 | */ 134 | LIMIT_CONTEXT_RIGHT: 'right', 135 | 136 | /** 137 | * Limit-context top 138 | * 139 | * @static 140 | * @property LIMIT_CONTEXT_TOP 141 | * @type {string} 142 | */ 143 | LIMIT_CONTEXT_TOP: 'top', 144 | 145 | /** 146 | * Limit-context bottom 147 | * 148 | * @static 149 | * @property LIMIT_CONTEXT_BOTTOM 150 | * @type {string} 151 | */ 152 | LIMIT_CONTEXT_BOTTOM: 'bottom', 153 | 154 | /** 155 | * Limit-context width 156 | * 157 | * @static 158 | * @property LIMIT_CONTEXT_WIDTH 159 | * @type {string} 160 | */ 161 | LIMIT_CONTEXT_WIDTH: 'width', 162 | 163 | /** 164 | * Limit-context height 165 | * 166 | * @static 167 | * @property LIMIT_CONTEXT_HEIGHT 168 | * @type {string} 169 | */ 170 | LIMIT_CONTEXT_HEIGHT: 'height', 171 | 172 | 173 | /** 174 | * Threshold-type for pixel 175 | * 176 | * @static 177 | * @property THRESHOLD_PIXEL 178 | * @type {string} 179 | */ 180 | THRESHOLD_PIXEL: 'pixel', 181 | 182 | /** 183 | * Threshold-type for percent of all pixels 184 | * 185 | * @static 186 | * @property THRESHOLD_PERCENT 187 | * @type {string} 188 | */ 189 | THRESHOLD_PERCENT: 'percent', 190 | 191 | 192 | /** 193 | * Comparison-type for pixel 194 | * 195 | * @static 196 | * @property COMPARISON_PIXEL 197 | * @type {string} 198 | */ 199 | COMPARISON_PIXEL: 'pixel', 200 | 201 | /** 202 | * Comparison-type for structure 203 | * 204 | * @static 205 | * @property COMPARISON_STRUCTURE 206 | * @type {string} 207 | */ 208 | COMPARISON_STRUCTURE: 'structure', 209 | 210 | 211 | /** 212 | * Unknown result of the comparison 213 | * 214 | * @static 215 | * @property RESULT_UNKNOWN 216 | * @type {int} 217 | */ 218 | RESULT_UNKNOWN: 0, 219 | 220 | /** 221 | * The images are too different 222 | * 223 | * @static 224 | * @property RESULT_DIFFERENT 225 | * @type {int} 226 | */ 227 | RESULT_DIFFERENT: 10, 228 | 229 | /** 230 | * The images are very similar, but still below the threshold 231 | * 232 | * @static 233 | * @property RESULT_SIMILAR 234 | * @type {int} 235 | */ 236 | RESULT_SIMILAR: 20, 237 | 238 | /** 239 | * The images are identical (or near identical) 240 | * 241 | * @static 242 | * @property RESULT_IDENTICAL 243 | * @type {int} 244 | */ 245 | RESULT_IDENTICAL: 30, 246 | 247 | 248 | /** 249 | * Create output when images are different 250 | * 251 | * @static 252 | * @property OUTPUT_DIFFERENT 253 | * @type {int} 254 | */ 255 | OUTPUT_DIFFERENT: 10, 256 | 257 | /** 258 | * Create output when images are similar or different 259 | * 260 | * @static 261 | * @property OUTPUT_SIMILAR 262 | * @type {int} 263 | */ 264 | OUTPUT_SIMILAR: 20, 265 | 266 | /** 267 | * Force output of all comparisons 268 | * 269 | * @static 270 | * @property OUTPUT_ALL 271 | * @type {int} 272 | */ 273 | OUTPUT_ALL: 100, 274 | 275 | 276 | /** 277 | * Composition is off 278 | * 279 | * @static 280 | * @property COMPOSITION_OFF 281 | * @type {int} 282 | */ 283 | COMPOSITION_OFF: 0, 284 | 285 | /** 286 | * Automatic composition depending on the resolutions of the images 287 | * 288 | * @static 289 | * @property COMPOSITION_AUTO 290 | * @type {int} 291 | */ 292 | COMPOSITION_AUTO: 1, 293 | 294 | /** 295 | * Composition from left to right 296 | * 297 | * @static 298 | * @property COMPOSITION_LEFT_TO_RIGHT 299 | * @type {int} 300 | */ 301 | COMPOSITION_LEFT_TO_RIGHT: 2, 302 | 303 | /** 304 | * Composition from top to bottom 305 | * 306 | * @static 307 | * @property COMPOSITION_TOP_TO_BOTTOM 308 | * @type {int} 309 | */ 310 | COMPOSITION_TOP_TO_BOTTOM: 3, 311 | 312 | 313 | /** 314 | * Do not copy any image to the result 315 | * 316 | * @static 317 | * @property COPY_IMAGE_OFF 318 | * @type {int} 319 | */ 320 | COPY_IMAGE_OFF: 0, 321 | 322 | /** 323 | * Copy image A as base for result 324 | * 325 | * @static 326 | * @property COPY_IMAGE_A 327 | * @type {int} 328 | */ 329 | COPY_IMAGE_A: 1, 330 | 331 | /** 332 | * Copy image B as base for result 333 | * 334 | * @static 335 | * @property COPY_IMAGE_B 336 | * @type {int} 337 | */ 338 | COPY_IMAGE_B: 2 339 | }; 340 | -------------------------------------------------------------------------------- /lib/defaultConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | debug: true, 4 | verbose: false, 5 | 6 | imageA: { 7 | image: "", 8 | crop: { 9 | left: 0, 10 | top: 0, 11 | width: 100, 12 | height: 100 13 | } 14 | }, 15 | imageB: { 16 | image: "", 17 | crop: { 18 | left: 0, 19 | top: 0, 20 | width: 100, 21 | height: 100 22 | } 23 | }, 24 | 25 | comparisons: [ 26 | { 27 | type: 'structure', 28 | 29 | selector: "body div.test", // * 30 | 31 | anchors:[ 32 | { 33 | type: "width", // height, left, right, top, bottom, horizontal, vertical 34 | position: "absolute", // relative 35 | threshold: { 36 | type: 'pixel', 37 | value: 5 38 | } 39 | } 40 | ], 41 | 42 | limits: [ 43 | { 44 | type: "min", // max 45 | context: "width", // height 46 | value: 50 47 | } 48 | ] 49 | }, 50 | { 51 | type: 'pixel', 52 | colorDelta: 5, 53 | 54 | gamma: { 55 | red: 0, 56 | green: 0, 57 | blue: 0 58 | }, 59 | perceptual: true, 60 | filters: ['blur'], 61 | 62 | shift: { 63 | active: true, 64 | horizontal: 2, 65 | vertical: 2, 66 | visible: true 67 | }, 68 | 69 | blockOuts: [ 70 | { 71 | visible: true, 72 | area: { 73 | left: 0, 74 | top: 0, 75 | width: 100, 76 | height: 100 77 | }, 78 | color: { 79 | red: 0, 80 | green: 0, 81 | blue: 0, 82 | alpha: 0, 83 | opacity: 0 84 | } 85 | } 86 | ], 87 | 88 | areaImageA: { 89 | left: 0, 90 | top: 0, 91 | width: null, 92 | height: null 93 | }, 94 | areaImageB: { 95 | left: 0, 96 | top: 0, 97 | width: null, 98 | height: null 99 | } 100 | } 101 | ], 102 | 103 | threshold: { 104 | type: 'pixel', 105 | value: 500 106 | }, 107 | 108 | diffColor: { 109 | red: 0, 110 | green: 0, 111 | blue: 0, 112 | alpha: 0, 113 | opacity: 0 114 | }, 115 | 116 | backgroundColor: { 117 | red: 0, 118 | green: 0, 119 | blue: 0, 120 | alpha: 0, 121 | opacity: 0 122 | }, 123 | ignoreColor: { 124 | red: 0, 125 | green: 0, 126 | blue: 0, 127 | alpha: 0, 128 | opacity: 0 129 | }, 130 | 131 | output: { 132 | imagePath: "", 133 | limit: OUTPUT_DIFFERENT, 134 | composition: COMPOSITION_AUTO, 135 | copyImage: COPY_IMAGE_A 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /lib/image.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | var PNGImage = require('pngjs-image'); 6 | 7 | /** 8 | * @class Image 9 | * @extends Base 10 | * @module Compare 11 | * 12 | * @property {PNGImage} _image 13 | * @property {number[]} _refWhite 14 | * @property {boolean} _gammaCorrection 15 | * @property {boolean} _perceptual 16 | * @property {boolean} _filters 17 | */ 18 | var Image = Base.extend( 19 | 20 | /** 21 | * Image constructor 22 | * 23 | * @param {object} options 24 | * @param {PNGImage} options.image Image 25 | * @constructor 26 | */ 27 | function (options) { 28 | this.__super(); 29 | 30 | this._image = options.image; 31 | 32 | this._refWhite = []; 33 | this._convertRgbToXyz([1, 1, 1, 1], 0, this._refWhite); 34 | 35 | this._gammaCorrection = false; 36 | this._perceptual = false; 37 | this._filters = false; 38 | }, 39 | 40 | { 41 | /** 42 | * Gets the image 43 | * 44 | * @method getImage 45 | * @return {PNGImage} 46 | */ 47 | getImage: function () { 48 | return this._image; 49 | }, 50 | 51 | /** 52 | * Gets the image data 53 | * 54 | * @method getData 55 | * @return {Buffer} 56 | */ 57 | getData: function () { 58 | return this.getImage()._data; 59 | }, 60 | 61 | 62 | /** 63 | * Gets the width of the image 64 | * 65 | * @method getWidth 66 | * @return {int} 67 | */ 68 | getWidth: function () { 69 | return this.getImage().getWidth(); 70 | }, 71 | 72 | /** 73 | * Gets the height of the image 74 | * 75 | * @method getHeight 76 | * @return {int} 77 | */ 78 | getHeight: function () { 79 | return this.getImage().getHeight(); 80 | }, 81 | 82 | /** 83 | * Gets the length of bytes of the image 84 | * 85 | * @method getLength 86 | * @return {int} 87 | */ 88 | getLength: function () { 89 | return this.getWidth() * this.getHeight() * 4; 90 | }, 91 | 92 | 93 | /** 94 | * Determines if gamma-correction has been applied 95 | * 96 | * @method hasGammaCorrection 97 | * @return {boolean} 98 | */ 99 | hasGammaCorrection: function () { 100 | return this._gammaCorrection; 101 | }, 102 | 103 | /** 104 | * Is image converted to a perceptual image? 105 | * 106 | * @method isPerceptual 107 | * @return {boolean} 108 | */ 109 | isPerceptual: function () { 110 | return this._perceptual; 111 | }, 112 | 113 | /** 114 | * Has applied filters 115 | * 116 | * @method hasFilters 117 | * @return {boolean} 118 | */ 119 | hasFilters: function () { 120 | return this._filters; 121 | }, 122 | 123 | 124 | /** 125 | * Applies gamma correction on the image 126 | * 127 | * @method applyGamma 128 | * @param {Color} gamma 129 | */ 130 | applyGamma: function (gamma) { 131 | 132 | var i, len, 133 | image, 134 | localGamma; 135 | 136 | if (!this._perceptual) { 137 | 138 | len = this.getLength(); 139 | image = this.getData(); 140 | localGamma = gamma.getColor(); 141 | 142 | for (i = 0; i < len; i += 4) { 143 | image[i] = Math.pow(image[i], 1 / localGamma.red); 144 | image[i + 1] = Math.pow(image[i + 1], 1 / localGamma.green); 145 | image[i + 2] = Math.pow(image[i + 2], 1 / localGamma.blue); 146 | } 147 | 148 | this._gammaCorrection = true; 149 | } 150 | }, 151 | 152 | 153 | /** 154 | * Converts the image to a perceptual color-space 155 | * 156 | * @method convertToPerceptual 157 | */ 158 | convertToPerceptual: function () { 159 | 160 | var i, len, 161 | data, 162 | pixelList, 163 | bounds; 164 | 165 | if (!this._perceptual) { 166 | 167 | len = this.getLength(); 168 | data = this.getData(); 169 | pixelList = []; 170 | 171 | bounds = [ 172 | {min: 20000000, max: -20000000}, 173 | {min: 20000000, max: -20000000}, 174 | {min: 20000000, max: -20000000} 175 | ]; 176 | 177 | for (i = 0; i < len; i += 4) { 178 | this._convertRgbToXyz(data, i, pixelList); 179 | this._convertXyzToCieLab(data, i, pixelList); 180 | 181 | bounds[0].min = Math.min(bounds[0].min, pixelList[i]); 182 | bounds[1].min = Math.min(bounds[1].min, pixelList[i + 1]); 183 | bounds[2].min = Math.min(bounds[2].min, pixelList[i + 2]); 184 | 185 | bounds[0].max = Math.max(bounds[0].max, pixelList[i]); 186 | bounds[1].max = Math.max(bounds[1].max, pixelList[i + 1]); 187 | bounds[2].max = Math.max(bounds[2].max, pixelList[i + 2]); 188 | } 189 | 190 | for (i = 0; i < len; i += 4) { 191 | data[i] = 255 * ((pixelList[i] - bounds[0].min) / bounds[0].max); 192 | data[i + 1] = 255 * ((pixelList[i + 1] - bounds[1].min) / bounds[1].max); 193 | data[i + 2] = 255 * ((pixelList[i + 2] - bounds[2].min) / bounds[2].max); 194 | } 195 | 196 | this._perceptual = true; 197 | } 198 | }, 199 | 200 | /** 201 | * Converts the color from RGB to XYZ 202 | * 203 | * @method _convertRgbToXyz 204 | * @param {Buffer} buffer 205 | * @param {int} offset 206 | * @param {int[]} output 207 | * @private 208 | */ 209 | _convertRgbToXyz: function (buffer, offset, output) { 210 | 211 | var result = [ 212 | buffer[offset] * 0.4887180 + buffer[offset + 1] * 0.3106803 + buffer[offset + 2] * 0.2006017, 213 | buffer[offset] * 0.1762044 + buffer[offset + 1] * 0.8129847 + buffer[offset + 2] * 0.0108109, 214 | buffer[offset + 1] * 0.0102048 + buffer[offset + 2] * 0.9897952, 215 | buffer[offset + 3] 216 | ]; 217 | 218 | output[offset] = result[0]; 219 | output[offset + 1] = result[1]; 220 | output[offset + 2] = result[2]; 221 | output[offset + 3] = result[3]; 222 | }, 223 | 224 | /** 225 | * Converts the color from Xyz to CieLab 226 | * 227 | * @method _convertXyzToCieLab 228 | * @param {Buffer} buffer 229 | * @param {int} offset 230 | * @param {int[]} output 231 | * @private 232 | */ 233 | _convertXyzToCieLab: function (buffer, offset, output) { 234 | 235 | var c1, c2, c3; 236 | 237 | function f (t) { 238 | return (t > 0.00885645167904) ? Math.pow(t, 1 / 3) : 70.08333333333263 * t + 0.13793103448276; 239 | } 240 | 241 | c1 = f(buffer[offset] / this._refWhite[0]); 242 | c2 = f(buffer[offset + 1] / this._refWhite[1]); 243 | c3 = f(buffer[offset + 2] / this._refWhite[2]); 244 | 245 | output[offset] = (116 * c2) - 16; 246 | output[offset + 1] = 500 * (c1 - c2); 247 | output[offset + 2] = 200 * (c2 - c3); 248 | output[offset + 3] = buffer[offset + 3]; 249 | }, 250 | 251 | 252 | /** 253 | * Applies a list of filters 254 | * 255 | * @method applyFilters 256 | * @param {string[]} filters 257 | */ 258 | applyFilters: function (filters) { 259 | if (!this._filters) { 260 | this.getImage().applyFilters(filters); 261 | this._filters = true; 262 | } 263 | }, 264 | 265 | 266 | /** 267 | * Processes image for the comparison configuration 268 | * 269 | * @method processImage 270 | * @param {PixelComparison|StructureComparison} comparison 271 | */ 272 | processImage: function (comparison) { 273 | 274 | if (comparison.hasGamma && comparison.hasGamma()) { 275 | this.applyGamma(comparison.getGamma()) 276 | } 277 | if (comparison.isPerceptual && comparison.isPerceptual()) { 278 | this.convertToPerceptual(); 279 | } 280 | 281 | if (comparison.getFilters) { 282 | this.applyFilters(comparison.getFilters()); 283 | } 284 | 285 | if (comparison.getBlockOuts) { // Important - do this after filtering 286 | comparison.getBlockOuts().forEach(function (blockOut) { 287 | this._image = blockOut.processImage(this.getImage()); 288 | }.bind(this)); 289 | } 290 | 291 | return this.getImage(); 292 | } 293 | }, 294 | 295 | /** 296 | * @lends Image 297 | */ 298 | { 299 | /** 300 | * Processes an image with comparison configuration 301 | * 302 | * @param {PNGImage} image 303 | * @param {PixelComparison|StructureComparison} comparison 304 | * @return {PNGImage} 305 | */ 306 | processImage: function (image, comparison) { 307 | 308 | var obj = new Image({ 309 | image: image 310 | }); 311 | 312 | obj.processImage(comparison); 313 | 314 | return obj.getImage(); 315 | } 316 | } 317 | ); 318 | 319 | module.exports = Image; 320 | -------------------------------------------------------------------------------- /lib/pixelComparator.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var Base = require('preceptor-core').Base; 5 | 6 | var Rect = require('./configuration/atoms/rect'); 7 | 8 | /** 9 | * @class PixelComparator 10 | * @extends Base 11 | * @module Compare 12 | * 13 | * @property {PNGImage} _imageA 14 | * @property {PNGImage} _imageB 15 | * @property {Config} _config 16 | */ 17 | var PixelComparator = Base.extend( 18 | 19 | /** 20 | * Constructor for the pixel comparator 21 | * 22 | * @constructor 23 | * @param {PNGImage} imageA 24 | * @param {PNGImage} imageB 25 | * @param {Config} config 26 | */ 27 | function (imageA, imageB, config) { 28 | this._imageA = imageA; 29 | this._imageB = imageB; 30 | this._config = config; 31 | }, 32 | 33 | { 34 | /** 35 | * Gets image A 36 | * 37 | * @method getImageA 38 | * @return {PNGImage} 39 | */ 40 | getImageA: function () { 41 | return this._imageA; 42 | }, 43 | 44 | /** 45 | * Gets image B 46 | * 47 | * @method getImageB 48 | * @return {PNGImage} 49 | */ 50 | getImageB: function () { 51 | return this._imageB; 52 | }, 53 | 54 | 55 | /** 56 | * Gets the configuration 57 | * 58 | * @method getConfig 59 | * @return {Config} 60 | */ 61 | getConfig: function () { 62 | return this._config; 63 | }, 64 | 65 | 66 | /** 67 | * Calculates the distance of colors in the 4 dimensional color space 68 | * 69 | * Note: The distance is squared for faster calculation. 70 | * 71 | * @method _colorDelta 72 | * @param {int} offsetA Offset in first image 73 | * @param {int} offsetB Offset in second image 74 | * @return {number} Distance 75 | * @private 76 | */ 77 | _colorDelta: function (offsetA, offsetB) { 78 | 79 | var imageA = this._imageA.getData(), 80 | imageB = this._imageB.getData(); 81 | 82 | return Math.pow(imageA[offsetA] - imageB[offsetB], 2) + 83 | Math.pow(imageA[offsetA + 1] - imageB[offsetB + 1], 2) + 84 | Math.pow(imageA[offsetA + 2] - imageB[offsetB + 2], 2) + 85 | Math.pow(imageA[offsetA + 3] - imageB[offsetB + 3], 2); 86 | }, 87 | 88 | /** 89 | * Determines the areas that needs to be compared 90 | * 91 | * @method _getAreas 92 | * @param {PixelComparison} comparison 93 | * @return {Rect[]} 94 | * @private 95 | */ 96 | _getAreas: function (comparison) { 97 | 98 | var areaA, areaB, 99 | areas = [], 100 | imageArea = new Rect({ 101 | x: 0, 102 | y: 0, 103 | width: this._imageA.getWidth(), 104 | height: this._imageA.getHeight() 105 | }); 106 | 107 | if (!comparison.getAreaImageA() || !comparison.getAreaImageB()) { 108 | areas.push(imageArea); 109 | areas.push(imageArea); 110 | 111 | } else { 112 | 113 | areaA = comparison.getAreaImageA().clone().limitCoordinates(imageArea); 114 | areaB = comparison.getAreaImageB().clone().limitCoordinates(imageArea); 115 | 116 | areaA.setWidth(Math.min(areaA.getWidth(), areaB.getWidth())); 117 | areaA.setHeight(Math.min(areaA.getHeight(), areaB.getHeight())); 118 | 119 | areaB.setWidth(areaA.getWidth()); 120 | areaB.setHeight(areaA.getHeight()); 121 | 122 | areas.push(areaA); 123 | areas.push(areaB); 124 | } 125 | 126 | return areas; 127 | }, 128 | 129 | /** 130 | * Compares two images and sets a flags in flag-field 131 | * 132 | * @method compare 133 | * @param {PixelComparison} comparison 134 | * @param {Buffer} flagField 135 | */ 136 | compare: function (comparison, flagField) { 137 | 138 | var flagFieldIndexA, dataIndexA, 139 | flagFieldIndexB, dataIndexB, 140 | x, y, delta, 141 | areas = this._getAreas(comparison), 142 | areaA = areas[0].getCoordinates(), 143 | areaB = areas[1].getCoordinates(), 144 | width = areaA.width, 145 | height = areaA.height, 146 | deltaThreshold = comparison.getColorDeltaSquared(), 147 | useImageB = this.getConfig().getOutput().shouldCopyImageB(); 148 | 149 | for (x = 0; x < width; x++) { 150 | for (y = 0; y < height; y++) { 151 | 152 | flagFieldIndexA = (x + areaA.x) + ((y + areaA.y) * width); 153 | dataIndexA = 4 * flagFieldIndexA; 154 | 155 | flagFieldIndexB = (x + areaB.x) + ((y + areaB.y) * width); 156 | dataIndexB = 4 * flagFieldIndexB; 157 | 158 | delta = this._colorDelta(dataIndexA, dataIndexB); 159 | 160 | if (delta > deltaThreshold) { 161 | 162 | if (this._shiftCompare(x, y, dataIndexA, comparison, this._imageA, areaA, this._imageB, areaB) && 163 | this._shiftCompare(x, y, dataIndexB, comparison, this._imageB, areaB, this._imageA, areaA)) { 164 | 165 | flagField[useImageB ? flagFieldIndexB : flagFieldIndexA] |= 2; 166 | } else { 167 | flagField[useImageB ? flagFieldIndexB : flagFieldIndexA] |= 1; 168 | } 169 | } 170 | } 171 | } 172 | }, 173 | 174 | /** 175 | * Comparison, covering sub-pixel shifts 176 | * 177 | * @method _shiftCompare 178 | * @param {int} x X-offset for current comparison coordinate 179 | * @param {int} y Y-offset for current comparison coordinate 180 | * @param {int} colorIndex Index of color in data 181 | * @param {PixelComparison} comparison Comparison configuration 182 | * @param {PNGImage} imageSrc Source image 183 | * @param {object} areaSrc Area that is compared in source image 184 | * @param {PNGImage} imageDst Destination image 185 | * @param {object} areaDst Area that is compared in destination image 186 | * @return {boolean} Within limits? 187 | * @private 188 | */ 189 | _shiftCompare: function (x, y, colorIndex, comparison, imageSrc, areaSrc, imageDst, areaDst) { 190 | 191 | var xOffset, xLow, xHigh, 192 | yOffset, yLow, yHigh, 193 | 194 | delta, localDeltaThreshold, 195 | dataIndexSrc, dataIndexDst, 196 | 197 | deltaThreshold = comparison.getColorDeltaSquared(), 198 | 199 | shift = comparison.getShift(), 200 | hShift = shift.getHorizontal(), 201 | vShift = shift.getVertical(), 202 | 203 | width = areaSrc.width, 204 | height = areaSrc.height; 205 | 206 | 207 | if ((hShift > 0) || (vShift > 0)) { 208 | 209 | xLow = this._calculateLowerLimit(x, 0, hShift); 210 | xHigh = this._calculateUpperLimit(x, width - 1, hShift); 211 | 212 | yLow = this._calculateLowerLimit(y, 0, vShift); 213 | yHigh = this._calculateUpperLimit(y, height - 1, vShift); 214 | 215 | for (xOffset = xLow; xOffset <= xHigh; xOffset++) { 216 | for (yOffset = yLow; yOffset <= yHigh; yOffset++) { 217 | 218 | if ((xOffset != 0) || (yOffset != 0)) { 219 | 220 | dataIndexSrc = ((x + areaSrc.x + xOffset) + ((y + areaSrc.y + yOffset) * width)) * 4; 221 | localDeltaThreshold = this._colorDelta(colorIndex, dataIndexSrc); 222 | 223 | dataIndexDst = ((x + areaDst.x + xOffset) + ((y + areaDst.y + yOffset) * width)) * 4; 224 | delta = this._colorDelta(colorIndex, dataIndexDst); 225 | 226 | if ((Math.abs(delta - localDeltaThreshold) < deltaThreshold) && 227 | (localDeltaThreshold > deltaThreshold)) { 228 | return true; 229 | } 230 | } 231 | } 232 | } 233 | } 234 | 235 | return false; 236 | }, 237 | 238 | 239 | /** 240 | * Calculates the lower limit 241 | * 242 | * @method _calculateLowerLimit 243 | * @param {int} value 244 | * @param {int} min 245 | * @param {int} shift 246 | * @return {int} 247 | * @private 248 | */ 249 | _calculateLowerLimit: function (value, min, shift) { 250 | return (value - shift) < min ? -(shift + (value - shift)) : -shift; 251 | }, 252 | 253 | /** 254 | * Calculates the upper limit 255 | * 256 | * @method _calculateUpperLimit 257 | * @param {int} value 258 | * @param {int} max 259 | * @param {int} shift 260 | * @return {int} 261 | * @private 262 | */ 263 | _calculateUpperLimit: function (value, max, shift) { 264 | return (value + shift) > max ? (max - value) : shift; 265 | } 266 | } 267 | ); 268 | 269 | module.exports = PixelComparator; 270 | -------------------------------------------------------------------------------- /lib/scripts/structure.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | function versionInfo () { 4 | return { 5 | major: 1, 6 | minor: 0 7 | }; 8 | } 9 | 10 | function deviceInfo () { 11 | return { 12 | userAgent: window.navigator.userAgent, 13 | 14 | app: { 15 | codeName: window.navigator.appCodeName, 16 | name: window.navigator.appName, 17 | version: window.navigator.appVersion, 18 | buildId: window.navigator.buildID 19 | }, 20 | 21 | languages: window.navigator.languages || [window.navigator.language], 22 | 23 | product: { 24 | name: window.navigator.product, 25 | sub: window.navigator.productSub 26 | }, 27 | vendor: { 28 | name: window.navigator.vendor, 29 | sub: window.navigator.vendorSub 30 | }, 31 | 32 | screen: { 33 | pixelRatio: window.devicePixelRatio || 1, 34 | colorDepth: window.screen.colorDepth, 35 | width: Math.min(window.screen.width, window.screen.availWidth), 36 | height: Math.min(window.screen.height, window.screen.availHeight), 37 | fullScreen: window.fullScreen 38 | } 39 | } 40 | } 41 | 42 | function documentInfo () { 43 | var de = document.documentElement, 44 | body = document.body, 45 | result = {}; 46 | 47 | result.title = document.title; 48 | result.url = window.location + ''; 49 | result.referrer = document.referrer; 50 | 51 | result.size = { 52 | width: Math.max(body.scrollWidth, body.offsetWidth, de.clientWidth, de.scrollWidth, de.offsetWidth), 53 | height: Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight) 54 | } 55 | } 56 | 57 | function viewPortInfo () { 58 | var el = document.createElement('div'), 59 | de = document.documentElement, 60 | body = document.body, 61 | result = {}; 62 | 63 | // Get current scroll-position 64 | result.x = window.pageXOffset || body.scrollLeft || de.scrollLeft; 65 | result.y = window.pageYOffset || body.scrollTop || de.scrollTop; 66 | 67 | // Get current view-port size 68 | el.style.position = "fixed"; 69 | el.style.top = 0; 70 | el.style.left = 0; 71 | el.style.bottom = 0; 72 | el.style.right = 0; 73 | de.insertBefore(el, de.firstChild); 74 | result.width = el.offsetWidth; 75 | result.height = el.offsetHeight; 76 | de.removeChild(el); 77 | 78 | return result; 79 | } 80 | 81 | function domInfo () { 82 | 83 | var capturedTags = ["A", "SPAN", "OL", "UL", "LI", "HEADER", "FOOTER", "NAV", "ARTICLE", "SECTION", "ASIDE", "DIV", "APPLET", "CANVAS", "VIDEO", "TABLE", "DETAILS", "SUMMARY", "IFRAME", "MENU", "MAIN", "FIGURE", "FIELDSET"]; 84 | 85 | function loadDOMNode (inputNode, parentNode) { 86 | 87 | var nodes, length, offset, newNode; 88 | 89 | if (capturedTags.indexOf(inputNode.tagName) !== -1) { 90 | 91 | newNode = {}; 92 | newNode.id = (inputNode.id && inputNode.id.length > 0) ? inputNode.id : null; 93 | newNode.tagName = inputNode.tagName; 94 | newNode.classes = (inputNode.className && inputNode.className.length > 0) ? inputNode.className.split(/\s/) : []; 95 | 96 | offset = absoluteOffset(inputNode); 97 | newNode.rect = { 98 | x: offset.x, 99 | y: offset.y, 100 | width: inputNode.offsetWidth, 101 | height: inputNode.offsetHeight 102 | }; 103 | 104 | newNode.nodes = []; 105 | 106 | if ((newNode.rect.width > 0) && (newNode.rect.height > 0)) { 107 | parentNode.nodes.push(newNode); 108 | parentNode = newNode; 109 | } 110 | } 111 | 112 | nodes = inputNode.children; 113 | length = inputNode.childElementCount; 114 | 115 | if (nodes && length) { 116 | for (var i = 0; i < length; i++) { 117 | loadDOMNode(nodes[i], parentNode); 118 | } 119 | } 120 | } 121 | 122 | function absoluteOffset (element) { 123 | 124 | var x = 0, y = 0; 125 | 126 | do { 127 | x += element.offsetLeft || 0; 128 | y += element.offsetTop || 0; 129 | element = element.offsetParent; 130 | } while (element); 131 | 132 | return { 133 | x: x, y: y 134 | }; 135 | } 136 | 137 | var node = {nodes:[]}; 138 | 139 | loadDOMNode(document.body, node); 140 | 141 | return node.nodes[0]; 142 | } 143 | 144 | return JSON.stringify({ 145 | version: versionInfo(), 146 | device: deviceInfo(), 147 | document: documentInfo(), 148 | viewPort: viewPortInfo(), 149 | screenshot: { 150 | stitched: false, 151 | section: { 152 | x: 0, 153 | y: 0, 154 | width: null, 155 | height: null 156 | } 157 | }, 158 | dom: domInfo() 159 | }); 160 | }; 161 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blink-diff", 3 | "version": "1.0.13", 4 | "description": "A lightweight image comparison tool", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "bugs": "https://github.com/yahoo/blink-diff/issues", 8 | "homepage": "https://github.com/yahoo/blink-diff", 9 | "bin": { 10 | "blink-diff": "bin/blink-diff" 11 | }, 12 | "author": { 13 | "name": "Marcel Erz", 14 | "email": "erz@yahoo-inc.com", 15 | "url": "http://www.marcelerz.com" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/yahoo/blink-diff.git" 20 | }, 21 | "keywords": [ 22 | "image-diff", 23 | "visual-diff", 24 | "diff", 25 | "testing", 26 | "blink", 27 | "image", 28 | "difference", 29 | "compare" 30 | ], 31 | "scripts": { 32 | "test": "istanbul cover -- _mocha --reporter spec", 33 | "docs": "yuidoc ." 34 | }, 35 | "dependencies": { 36 | "pngjs-image": "~0.11.5", 37 | "preceptor-core": "~0.10.0", 38 | "promise": "6.0.0" 39 | }, 40 | "devDependencies": { 41 | "chai": "1.9.2", 42 | "coveralls": "2.11.2", 43 | "codeclimate-test-reporter": "0.0.4", 44 | "istanbul": "0.3.2", 45 | "mocha": "1.21.4", 46 | "sinon": "1.12.2", 47 | "sinon-chai": "2.7.0", 48 | "yuidocjs": "0.3.50" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | tmp.png -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Yahoo! Inc. 2 | // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. 3 | 4 | var BlinkDiff = require('../'); 5 | var PNGImage = require('pngjs-image'); 6 | var Promise = require('promise'); 7 | var fs = require('fs'); 8 | var expect = require('chai').expect; 9 | 10 | function generateImage (type) { 11 | var image; 12 | 13 | switch (type) { 14 | case "small-1": 15 | image = PNGImage.createImage(2, 2); 16 | image.setAt(0, 0, {red: 10, green: 20, blue: 30, alpha: 40}); 17 | image.setAt(0, 1, {red: 50, green: 60, blue: 70, alpha: 80}); 18 | image.setAt(1, 0, {red: 90, green: 100, blue: 110, alpha: 120}); 19 | image.setAt(1, 1, {red: 130, green: 140, blue: 150, alpha: 160}); 20 | break; 21 | 22 | case "small-2": 23 | image = PNGImage.createImage(2, 2); 24 | image.setAt(0, 0, {red: 210, green: 220, blue: 230, alpha: 240}); 25 | image.setAt(0, 1, {red: 10, green: 20, blue: 30, alpha: 40}); 26 | image.setAt(1, 0, {red: 50, green: 60, blue: 70, alpha: 80}); 27 | image.setAt(1, 1, {red: 15, green: 25, blue: 35, alpha: 45}); 28 | break; 29 | 30 | case "small-3": 31 | image = PNGImage.createImage(2, 2); 32 | break; 33 | 34 | case "medium-1": 35 | image = PNGImage.createImage(3, 3); 36 | image.setAt(0, 0, {red: 130, green: 140, blue: 150, alpha: 160}); 37 | image.setAt(0, 1, {red: 170, green: 180, blue: 190, alpha: 200}); 38 | image.setAt(0, 2, {red: 210, green: 220, blue: 230, alpha: 240}); 39 | image.setAt(1, 0, {red: 15, green: 25, blue: 35, alpha: 45}); 40 | image.setAt(1, 1, {red: 55, green: 65, blue: 75, alpha: 85}); 41 | image.setAt(1, 2, {red: 95, green: 105, blue: 115, alpha: 125}); 42 | image.setAt(2, 0, {red: 10, green: 20, blue: 30, alpha: 40}); 43 | image.setAt(2, 1, {red: 50, green: 60, blue: 70, alpha: 80}); 44 | image.setAt(2, 2, {red: 90, green: 100, blue: 110, alpha: 120}); 45 | break; 46 | 47 | case "medium-2": 48 | image = PNGImage.createImage(3, 3); 49 | image.setAt(0, 0, {red: 95, green: 15, blue: 165, alpha: 26}); 50 | image.setAt(0, 1, {red: 15, green: 225, blue: 135, alpha: 144}); 51 | image.setAt(0, 2, {red: 170, green: 80, blue: 210, alpha: 2}); 52 | image.setAt(1, 0, {red: 50, green: 66, blue: 23, alpha: 188}); 53 | image.setAt(1, 1, {red: 110, green: 120, blue: 63, alpha: 147}); 54 | image.setAt(1, 2, {red: 30, green: 110, blue: 10, alpha: 61}); 55 | image.setAt(2, 0, {red: 190, green: 130, blue: 180, alpha: 29}); 56 | image.setAt(2, 1, {red: 10, green: 120, blue: 31, alpha: 143}); 57 | image.setAt(2, 2, {red: 155, green: 165, blue: 15, alpha: 185}); 58 | break; 59 | 60 | case "slim-1": 61 | image = PNGImage.createImage(1, 3); 62 | image.setAt(0, 0, {red: 15, green: 225, blue: 135, alpha: 144}); 63 | image.setAt(0, 1, {red: 170, green: 80, blue: 210, alpha: 2}); 64 | image.setAt(0, 2, {red: 50, green: 66, blue: 23, alpha: 188}); 65 | break; 66 | 67 | case "slim-2": 68 | image = PNGImage.createImage(3, 1); 69 | image.setAt(0, 0, {red: 15, green: 225, blue: 135, alpha: 144}); 70 | image.setAt(1, 0, {red: 170, green: 80, blue: 210, alpha: 2}); 71 | image.setAt(2, 0, {red: 50, green: 66, blue: 23, alpha: 188}); 72 | break; 73 | } 74 | 75 | return image; 76 | } 77 | 78 | function compareBuffer (buf1, buf2) { 79 | 80 | if (buf1.length !== buf2.length) { 81 | return false; 82 | } 83 | 84 | for (var i = 0, len = buf1.length; i < len; i++) { 85 | if (buf1[i] !== buf2[i]) { 86 | return false; 87 | } 88 | } 89 | 90 | return true; 91 | } 92 | 93 | describe('Blink-Diff', function () { 94 | 95 | describe('Default values', function () { 96 | 97 | beforeEach(function () { 98 | this.instance = new BlinkDiff({ 99 | imageA: "image-a", imageAPath: "path to image-a", imageB: "image-b", imageBPath: "path to image-b", 100 | 101 | composition: false 102 | }); 103 | }); 104 | 105 | it('should have the right values for imageA', function () { 106 | expect(this.instance._imageA).to.be.equal("image-a"); 107 | }); 108 | 109 | it('should have the right values for imageAPath', function () { 110 | expect(this.instance._imageAPath).to.be.equal("path to image-a"); 111 | }); 112 | 113 | it('should have the right values for imageB', function () { 114 | expect(this.instance._imageB).to.be.equal("image-b"); 115 | }); 116 | 117 | it('should have the right values for imageBPath', function () { 118 | expect(this.instance._imageBPath).to.be.equal("path to image-b"); 119 | }); 120 | 121 | it('should not have a value for imageOutputPath', function () { 122 | expect(this.instance._imageOutputPath).to.be.undefined; 123 | }); 124 | 125 | it('should not have a value for thresholdType', function () { 126 | expect(this.instance._thresholdType).to.be.equal("pixel"); 127 | }); 128 | 129 | it('should not have a value for threshold', function () { 130 | expect(this.instance._threshold).to.be.equal(500); 131 | }); 132 | 133 | it('should not have a value for delta', function () { 134 | expect(this.instance._delta).to.be.equal(20); 135 | }); 136 | 137 | it('should not have a value for outputMaskRed', function () { 138 | expect(this.instance._outputMaskRed).to.be.equal(255); 139 | }); 140 | 141 | it('should not have a value for outputMaskGreen', function () { 142 | expect(this.instance._outputMaskGreen).to.be.equal(0); 143 | }); 144 | 145 | it('should not have a value for outputMaskBlue', function () { 146 | expect(this.instance._outputMaskBlue).to.be.equal(0); 147 | }); 148 | 149 | it('should not have a value for outputMaskAlpha', function () { 150 | expect(this.instance._outputMaskAlpha).to.be.equal(255); 151 | }); 152 | 153 | it('should not have a value for outputMaskOpacity', function () { 154 | expect(this.instance._outputMaskOpacity).to.be.equal(0.7); 155 | }); 156 | 157 | it('should not have a value for outputBackgroundRed', function () { 158 | expect(this.instance._outputBackgroundRed).to.be.equal(0); 159 | }); 160 | 161 | it('should not have a value for outputBackgroundGreen', function () { 162 | expect(this.instance._outputBackgroundGreen).to.be.equal(0); 163 | }); 164 | 165 | it('should not have a value for outputBackgroundBlue', function () { 166 | expect(this.instance._outputBackgroundBlue).to.be.equal(0); 167 | }); 168 | 169 | it('should not have a value for outputBackgroundAlpha', function () { 170 | expect(this.instance._outputBackgroundAlpha).to.be.undefined; 171 | }); 172 | 173 | it('should not have a value for outputBackgroundOpacity', function () { 174 | expect(this.instance._outputBackgroundOpacity).to.be.equal(0.6); 175 | }); 176 | 177 | it('should not have a value for copyImageAToOutput', function () { 178 | expect(this.instance._copyImageAToOutput).to.be.true; 179 | }); 180 | 181 | it('should not have a value for copyImageBToOutput', function () { 182 | expect(this.instance._copyImageBToOutput).to.be.false; 183 | }); 184 | 185 | it('should not have a value for filter', function () { 186 | expect(this.instance._filter).to.be.empty; 187 | }); 188 | 189 | it('should not have a value for debug', function () { 190 | expect(this.instance._debug).to.be.false; 191 | }); 192 | 193 | describe('Special cases', function () { 194 | 195 | beforeEach(function () { 196 | this.instance = new BlinkDiff({ 197 | imageA: "image-a", imageB: "image-b" 198 | }); 199 | }); 200 | 201 | it('should have the images', function () { 202 | expect(this.instance._imageA).to.be.equal("image-a"); 203 | expect(this.instance._imageB).to.be.equal("image-b"); 204 | }); 205 | }) 206 | }); 207 | 208 | describe('Methods', function () { 209 | 210 | beforeEach(function () { 211 | this.instance = new BlinkDiff({ 212 | imageA: "image-a", imageAPath: "path to image-a", imageB: "image-b", imageBPath: "path to image-b" 213 | }); 214 | }); 215 | 216 | describe('hasPassed', function () { 217 | 218 | it('should pass when identical', function () { 219 | expect(this.instance.hasPassed(BlinkDiff.RESULT_IDENTICAL)).to.be.true; 220 | }); 221 | 222 | it('should pass when similar', function () { 223 | expect(this.instance.hasPassed(BlinkDiff.RESULT_SIMILAR)).to.be.true; 224 | }); 225 | 226 | it('should not pass when unknown', function () { 227 | expect(this.instance.hasPassed(BlinkDiff.RESULT_UNKNOWN)).to.be.false; 228 | }); 229 | 230 | it('should not pass when different', function () { 231 | expect(this.instance.hasPassed(BlinkDiff.RESULT_DIFFERENT)).to.be.false; 232 | }); 233 | }); 234 | 235 | describe('_colorDelta', function () { 236 | it('should calculate the delta', function () { 237 | var color1 = { 238 | c1: 23, c2: 87, c3: 89, c4: 234 239 | }, color2 = { 240 | c1: 84, c2: 92, c3: 50, c4: 21 241 | }; 242 | 243 | expect(this.instance._colorDelta(color1, color2)).to.be.within(225.02, 225.03); 244 | }); 245 | }); 246 | 247 | describe('_loadImage', function () { 248 | 249 | beforeEach(function () { 250 | this.image = generateImage('medium-2'); 251 | }); 252 | 253 | describe('from Image', function () { 254 | 255 | it('should use already loaded image', function () { 256 | var result = this.instance._loadImage("pathToFile", this.image); 257 | 258 | expect(result).to.be.an.instanceof(PNGImage); 259 | expect(result).to.be.equal(this.image) 260 | }); 261 | }); 262 | 263 | describe('from Path', function () { 264 | 265 | it('should load image when only path given', function (done) { 266 | var result = this.instance._loadImage(__dirname + '/test2.png'); 267 | 268 | expect(result).to.be.an.instanceof(Promise); 269 | 270 | result.then(function (image) { 271 | var compare = compareBuffer(image.getImage().data, this.image.getImage().data); 272 | expect(compare).to.be.true; 273 | done(); 274 | }.bind(this)).then(null, function (err) { 275 | done(err); 276 | }); 277 | }); 278 | }); 279 | 280 | describe('from Buffer', function () { 281 | 282 | beforeEach(function () { 283 | this.buffer = fs.readFileSync(__dirname + '/test2.png'); 284 | }); 285 | 286 | it('should load image from buffer if given', function () { 287 | var result = this.instance._loadImage("pathToFile", this.buffer); 288 | 289 | expect(result).to.be.an.instanceof(Promise); 290 | 291 | result.then(function (image) { 292 | var compare = compareBuffer(image.getImage().data, this.image.getImage().data); 293 | expect(compare).to.be.true; 294 | done(); 295 | }.bind(this)).then(null, function (err) { 296 | done(err); 297 | }); 298 | }); 299 | }); 300 | }); 301 | 302 | describe('_copyImage', function () { 303 | 304 | it('should copy the image', function () { 305 | var image1 = generateImage('small-1'), image2 = generateImage('small-2'); 306 | 307 | this.instance._copyImage(image1, image2); 308 | 309 | expect(image1.getAt(0, 0)).to.be.equal(image2.getAt(0, 0)); 310 | expect(image1.getAt(0, 1)).to.be.equal(image2.getAt(0, 1)); 311 | expect(image1.getAt(1, 0)).to.be.equal(image2.getAt(1, 0)); 312 | expect(image1.getAt(1, 1)).to.be.equal(image2.getAt(1, 1)); 313 | }); 314 | }); 315 | 316 | describe('_correctDimensions', function () { 317 | 318 | describe('Missing Values', function () { 319 | 320 | it('should correct missing x values', function () { 321 | var rect = {y: 23, width: 42, height: 57}; 322 | 323 | this.instance._correctDimensions(300, 200, rect); 324 | 325 | expect(rect.x).to.be.equal(0); 326 | expect(rect.y).to.be.equal(23); 327 | expect(rect.width).to.be.equal(42); 328 | expect(rect.height).to.be.equal(57); 329 | }); 330 | 331 | it('should correct missing y values', function () { 332 | var rect = {x: 10, width: 42, height: 57}; 333 | 334 | this.instance._correctDimensions(300, 200, rect); 335 | 336 | expect(rect.x).to.be.equal(10); 337 | expect(rect.y).to.be.equal(0); 338 | expect(rect.width).to.be.equal(42); 339 | expect(rect.height).to.be.equal(57); 340 | }); 341 | 342 | it('should correct missing width values', function () { 343 | var rect = {x: 10, y: 23, height: 57}; 344 | 345 | this.instance._correctDimensions(300, 200, rect); 346 | 347 | expect(rect.x).to.be.equal(10); 348 | expect(rect.y).to.be.equal(23); 349 | expect(rect.width).to.be.equal(290); 350 | expect(rect.height).to.be.equal(57); 351 | }); 352 | 353 | it('should correct missing height values', function () { 354 | var rect = {x: 10, y: 23, width: 42}; 355 | 356 | this.instance._correctDimensions(300, 200, rect); 357 | 358 | expect(rect.x).to.be.equal(10); 359 | expect(rect.y).to.be.equal(23); 360 | expect(rect.width).to.be.equal(42); 361 | expect(rect.height).to.be.equal(177); 362 | }); 363 | 364 | it('should correct all missing values', function () { 365 | var rect = {}; 366 | 367 | this.instance._correctDimensions(300, 200, rect); 368 | 369 | expect(rect.x).to.be.equal(0); 370 | expect(rect.y).to.be.equal(0); 371 | expect(rect.width).to.be.equal(300); 372 | expect(rect.height).to.be.equal(200); 373 | }); 374 | }); 375 | 376 | describe('Negative Values', function () { 377 | 378 | it('should correct negative x values', function () { 379 | var rect = {x: -10, y: 23, width: 42, height: 57}; 380 | 381 | this.instance._correctDimensions(300, 200, rect); 382 | 383 | expect(rect.x).to.be.equal(0); 384 | expect(rect.y).to.be.equal(23); 385 | expect(rect.width).to.be.equal(42); 386 | expect(rect.height).to.be.equal(57); 387 | }); 388 | 389 | it('should correct negative y values', function () { 390 | var rect = {x: 10, y: -23, width: 42, height: 57}; 391 | 392 | this.instance._correctDimensions(300, 200, rect); 393 | 394 | expect(rect.x).to.be.equal(10); 395 | expect(rect.y).to.be.equal(0); 396 | expect(rect.width).to.be.equal(42); 397 | expect(rect.height).to.be.equal(57); 398 | }); 399 | 400 | it('should correct negative width values', function () { 401 | var rect = {x: 10, y: 23, width: -42, height: 57}; 402 | 403 | this.instance._correctDimensions(300, 200, rect); 404 | 405 | expect(rect.x).to.be.equal(10); 406 | expect(rect.y).to.be.equal(23); 407 | expect(rect.width).to.be.equal(0); 408 | expect(rect.height).to.be.equal(57); 409 | }); 410 | 411 | it('should correct negative height values', function () { 412 | var rect = {x: 10, y: 23, width: 42, height: -57}; 413 | 414 | this.instance._correctDimensions(300, 200, rect); 415 | 416 | expect(rect.x).to.be.equal(10); 417 | expect(rect.y).to.be.equal(23); 418 | expect(rect.width).to.be.equal(42); 419 | expect(rect.height).to.be.equal(0); 420 | }); 421 | 422 | it('should correct all negative values', function () { 423 | var rect = {x: -10, y: -23, width: -42, height: -57}; 424 | 425 | this.instance._correctDimensions(300, 200, rect); 426 | 427 | expect(rect.x).to.be.equal(0); 428 | expect(rect.y).to.be.equal(0); 429 | expect(rect.width).to.be.equal(0); 430 | expect(rect.height).to.be.equal(0); 431 | }); 432 | }); 433 | 434 | describe('Dimensions', function () { 435 | 436 | it('should correct too big x values', function () { 437 | var rect = {x: 1000, y: 23, width: 42, height: 57}; 438 | 439 | this.instance._correctDimensions(300, 200, rect); 440 | 441 | expect(rect.x).to.be.equal(299); 442 | expect(rect.y).to.be.equal(23); 443 | expect(rect.width).to.be.equal(1); 444 | expect(rect.height).to.be.equal(57); 445 | }); 446 | 447 | it('should correct too big y values', function () { 448 | var rect = {x: 10, y: 2300, width: 42, height: 57}; 449 | 450 | this.instance._correctDimensions(300, 200, rect); 451 | 452 | expect(rect.x).to.be.equal(10); 453 | expect(rect.y).to.be.equal(199); 454 | expect(rect.width).to.be.equal(42); 455 | expect(rect.height).to.be.equal(1); 456 | }); 457 | 458 | it('should correct too big width values', function () { 459 | var rect = {x: 11, y: 23, width: 4200, height: 57}; 460 | 461 | this.instance._correctDimensions(300, 200, rect); 462 | 463 | expect(rect.x).to.be.equal(11); 464 | expect(rect.y).to.be.equal(23); 465 | expect(rect.width).to.be.equal(289); 466 | expect(rect.height).to.be.equal(57); 467 | }); 468 | 469 | it('should correct too big height values', function () { 470 | var rect = {x: 11, y: 23, width: 42, height: 5700}; 471 | 472 | this.instance._correctDimensions(300, 200, rect); 473 | 474 | expect(rect.x).to.be.equal(11); 475 | expect(rect.y).to.be.equal(23); 476 | expect(rect.width).to.be.equal(42); 477 | expect(rect.height).to.be.equal(177); 478 | }); 479 | 480 | it('should correct too big width and height values', function () { 481 | var rect = {x: 11, y: 23, width: 420, height: 570}; 482 | 483 | this.instance._correctDimensions(300, 200, rect); 484 | 485 | expect(rect.x).to.be.equal(11); 486 | expect(rect.y).to.be.equal(23); 487 | expect(rect.width).to.be.equal(289); 488 | expect(rect.height).to.be.equal(177); 489 | }); 490 | }); 491 | 492 | describe('Border Dimensions', function () { 493 | 494 | it('should correct too big x values', function () { 495 | var rect = {x: 300, y: 23, width: 42, height: 57}; 496 | 497 | this.instance._correctDimensions(300, 200, rect); 498 | 499 | expect(rect.x).to.be.equal(299); 500 | expect(rect.y).to.be.equal(23); 501 | expect(rect.width).to.be.equal(1); 502 | expect(rect.height).to.be.equal(57); 503 | }); 504 | 505 | it('should correct too big y values', function () { 506 | var rect = {x: 10, y: 200, width: 42, height: 57}; 507 | 508 | this.instance._correctDimensions(300, 200, rect); 509 | 510 | expect(rect.x).to.be.equal(10); 511 | expect(rect.y).to.be.equal(199); 512 | expect(rect.width).to.be.equal(42); 513 | expect(rect.height).to.be.equal(1); 514 | }); 515 | 516 | it('should correct too big width values', function () { 517 | var rect = {x: 11, y: 23, width: 289, height: 57}; 518 | 519 | this.instance._correctDimensions(300, 200, rect); 520 | 521 | expect(rect.x).to.be.equal(11); 522 | expect(rect.y).to.be.equal(23); 523 | expect(rect.width).to.be.equal(289); 524 | expect(rect.height).to.be.equal(57); 525 | }); 526 | 527 | it('should correct too big height values', function () { 528 | var rect = {x: 11, y: 23, width: 42, height: 177}; 529 | 530 | this.instance._correctDimensions(300, 200, rect); 531 | 532 | expect(rect.x).to.be.equal(11); 533 | expect(rect.y).to.be.equal(23); 534 | expect(rect.width).to.be.equal(42); 535 | expect(rect.height).to.be.equal(177); 536 | }); 537 | }); 538 | }); 539 | 540 | describe('_crop', function () { 541 | 542 | beforeEach(function () { 543 | this.croppedImage = generateImage('medium-1'); 544 | this.expectedImage = generateImage('medium-1'); 545 | }); 546 | 547 | it('should crop image', function () { 548 | this.instance._crop("Medium-1", this.croppedImage, {x: 1, y: 2, width: 2, height: 1}); 549 | 550 | expect(this.croppedImage.getWidth()).to.be.equal(2); 551 | expect(this.croppedImage.getHeight()).to.be.equal(1); 552 | 553 | expect(this.croppedImage.getAt(0, 0)).to.be.equal(this.expectedImage.getAt(1, 2)); 554 | expect(this.croppedImage.getAt(1, 0)).to.be.equal(this.expectedImage.getAt(2, 2)); 555 | }); 556 | }); 557 | 558 | describe('_clip', function () { 559 | 560 | it('should clip the image small and medium', function () { 561 | var image1 = generateImage('small-1'), image2 = generateImage('medium-2'); 562 | 563 | this.instance._clip(image1, image2); 564 | 565 | expect(image1.getWidth()).to.be.equal(image2.getWidth()); 566 | expect(image1.getHeight()).to.be.equal(image2.getHeight()); 567 | }); 568 | 569 | it('should clip the image medium and small', function () { 570 | var image1 = generateImage('medium-1'), image2 = generateImage('small-2'); 571 | 572 | this.instance._clip(image1, image2); 573 | 574 | expect(image1.getWidth()).to.be.equal(image2.getWidth()); 575 | expect(image1.getHeight()).to.be.equal(image2.getHeight()); 576 | }); 577 | 578 | it('should clip the image slim-1 and medium', function () { 579 | var image1 = generateImage('slim-1'), image2 = generateImage('medium-1'); 580 | 581 | this.instance._clip(image1, image2); 582 | 583 | expect(image1.getWidth()).to.be.equal(image2.getWidth()); 584 | expect(image1.getHeight()).to.be.equal(image2.getHeight()); 585 | }); 586 | 587 | it('should clip the image slim-2 and medium', function () { 588 | var image1 = generateImage('slim-2'), image2 = generateImage('medium-1'); 589 | 590 | this.instance._clip(image1, image2); 591 | 592 | expect(image1.getWidth()).to.be.equal(image2.getWidth()); 593 | expect(image1.getHeight()).to.be.equal(image2.getHeight()); 594 | }); 595 | 596 | it('should clip the image small and small', function () { 597 | var image1 = generateImage('small-2'), image2 = generateImage('small-1'); 598 | 599 | this.instance._clip(image1, image2); 600 | 601 | expect(image1.getWidth()).to.be.equal(image2.getWidth()); 602 | expect(image1.getHeight()).to.be.equal(image2.getHeight()); 603 | }); 604 | }); 605 | 606 | describe('isAboveThreshold', function () { 607 | 608 | describe('Pixel threshold', function () { 609 | 610 | beforeEach(function () { 611 | this.instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL; 612 | this.instance._threshold = 50; 613 | }); 614 | 615 | it('should be below threshold', function () { 616 | expect(this.instance.isAboveThreshold(49)).to.be.false; 617 | }); 618 | 619 | it('should be above threshold on border', function () { 620 | expect(this.instance.isAboveThreshold(50)).to.be.true; 621 | }); 622 | 623 | it('should be above threshold', function () { 624 | expect(this.instance.isAboveThreshold(51)).to.be.true; 625 | }); 626 | }); 627 | 628 | describe('Percent threshold', function () { 629 | 630 | beforeEach(function () { 631 | this.instance._thresholdType = BlinkDiff.THRESHOLD_PERCENT; 632 | this.instance._threshold = 0.1; 633 | }); 634 | 635 | it('should be below threshold', function () { 636 | expect(this.instance.isAboveThreshold(9, 100)).to.be.false; 637 | }); 638 | 639 | it('should be above threshold on border', function () { 640 | expect(this.instance.isAboveThreshold(10, 100)).to.be.true; 641 | }); 642 | 643 | it('should be above threshold', function () { 644 | expect(this.instance.isAboveThreshold(11, 100)).to.be.true; 645 | }); 646 | }); 647 | }); 648 | 649 | describe('Comparison', function () { 650 | 651 | beforeEach(function () { 652 | this.image1 = generateImage('small-1'); 653 | this.image2 = generateImage('small-2'); 654 | this.image3 = generateImage('small-3'); 655 | this.image4 = generateImage('small-1'); 656 | 657 | this.maskColor = { 658 | red: 123, green: 124, blue: 125, alpha: 126 659 | }; 660 | this.shiftColor = { 661 | red: 200, green: 100, blue: 0, alpha: 113 662 | }; 663 | this.backgroundMaskColor = { 664 | red: 31, green: 33, blue: 35, alpha: 37 665 | }; 666 | }); 667 | 668 | describe('_pixelCompare', function () { 669 | 670 | it('should have no differences with a zero dimension', function () { 671 | var result, deltaThreshold = 10, width = 0, height = 0, hShift = 0, vShift = 0; 672 | 673 | result = this.instance._pixelCompare(this.image1, this.image2, this.image3, deltaThreshold, width, height, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 674 | 675 | expect(result).to.be.equal(0); 676 | }); 677 | 678 | it('should have all differences', function () { 679 | var result, deltaThreshold = 10, width = 2, height = 2, hShift = 0, vShift = 0; 680 | 681 | result = this.instance._pixelCompare(this.image1, this.image2, this.image3, deltaThreshold, width, height, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 682 | 683 | expect(result).to.be.equal(4); 684 | }); 685 | 686 | it('should have some differences', function () { 687 | var result, deltaThreshold = 100, width = 2, height = 2, hShift = 0, vShift = 0; 688 | 689 | result = this.instance._pixelCompare(this.image1, this.image2, this.image3, deltaThreshold, width, height, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 690 | 691 | expect(result).to.be.equal(2); 692 | }); 693 | }); 694 | 695 | describe('_compare', function () { 696 | 697 | beforeEach(function () { 698 | this.instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL; 699 | this.instance._threshold = 3; 700 | }); 701 | 702 | it('should be different', function () { 703 | var result, deltaThreshold = 10, hShift = 0, vShift = 0; 704 | 705 | result = this.instance._compare(this.image1, this.image2, this.image3, deltaThreshold, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 706 | 707 | expect(result).to.be.deep.equal({ 708 | code: BlinkDiff.RESULT_DIFFERENT, differences: 4, dimension: 4, width: 2, height: 2 709 | }); 710 | }); 711 | 712 | it('should be similar', function () { 713 | var result, deltaThreshold = 100, hShift = 0, vShift = 0; 714 | 715 | result = this.instance._compare(this.image1, this.image2, this.image3, deltaThreshold, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 716 | 717 | expect(result).to.be.deep.equal({ 718 | code: BlinkDiff.RESULT_SIMILAR, differences: 2, dimension: 4, width: 2, height: 2 719 | }); 720 | }); 721 | 722 | it('should be identical', function () { 723 | var result, deltaThreshold = 10, hShift = 0, vShift = 0; 724 | 725 | result = this.instance._compare(this.image1, this.image4, this.image3, deltaThreshold, this.maskColor, this.shiftColor, this.backgroundMaskColor, hShift, vShift); 726 | 727 | expect(result).to.be.deep.equal({ 728 | code: BlinkDiff.RESULT_IDENTICAL, differences: 0, dimension: 4, width: 2, height: 2 729 | }); 730 | }); 731 | }); 732 | }); 733 | 734 | describe('Run', function () { 735 | 736 | beforeEach(function () { 737 | this.instance._imageA = generateImage('small-1'); 738 | this.instance._imageB = generateImage('medium-1'); 739 | 740 | this.instance._thresholdType = BlinkDiff.THRESHOLD_PIXEL; 741 | this.instance._threshold = 3; 742 | 743 | this.instance._composition = false; 744 | }); 745 | 746 | it('should crop image-a', function (done) { 747 | this.instance._cropImageA = {width: 1, height: 2}; 748 | this.instance.run(function (err, result) { 749 | if (err) { 750 | done(err); 751 | } else { 752 | try { 753 | expect(result.dimension).to.be.equal(2); 754 | done(); 755 | } catch (err) { 756 | done(err); 757 | } 758 | } 759 | }); 760 | }); 761 | 762 | it('should crop image-b', function (done) { 763 | this.instance._cropImageB = {width: 1, height: 1}; 764 | this.instance.run(function (err, result) { 765 | if (err) { 766 | done(err); 767 | } else { 768 | try { 769 | expect(result.dimension).to.be.equal(1); 770 | done(); 771 | } catch (err) { 772 | done(err); 773 | } 774 | } 775 | }); 776 | }); 777 | 778 | it('should clip image-b', function (done) { 779 | this.instance.run(function (err, result) { 780 | if (err) { 781 | done(err); 782 | } else { 783 | try { 784 | expect(result.dimension).to.be.equal(4); 785 | done(); 786 | } catch (err) { 787 | done(err); 788 | } 789 | } 790 | }); 791 | }); 792 | 793 | it('should crop and clip images', function (done) { 794 | this.instance._cropImageA = {width: 1, height: 2}; 795 | this.instance._cropImageB = {width: 1, height: 1}; 796 | this.instance.run(function (err, result) { 797 | if (err) { 798 | done(err); 799 | } else { 800 | try { 801 | expect(result.dimension).to.be.equal(1); 802 | done(); 803 | } catch (err) { 804 | done(err); 805 | } 806 | } 807 | }); 808 | }); 809 | 810 | it('should write output file', function (done) { 811 | this.instance._imageOutputPath = __dirname + '/tmp.png'; 812 | this.instance.run(function (err) { 813 | if (err) { 814 | done(err); 815 | } else { 816 | try { 817 | if (!fs.existsSync(__dirname + '/tmp.png')) { 818 | done(new Error('Could not write file.')); 819 | } else { 820 | done(); 821 | } 822 | } catch (err) { 823 | done(err); 824 | } 825 | } 826 | }); 827 | }); 828 | 829 | it('should compare image-a to image-b', function (done) { 830 | this.instance.run(function (err, result) { 831 | if (err) { 832 | done(err); 833 | } else { 834 | try { 835 | expect(result.code).to.be.equal(BlinkDiff.RESULT_DIFFERENT); 836 | done(); 837 | } catch (err) { 838 | done(err); 839 | } 840 | } 841 | }); 842 | }); 843 | 844 | it('should be black', function (done) { 845 | this.instance._delta = 1000; 846 | this.instance._copyImageAToOutput = false; 847 | this.instance._copyImageBToOutput = false; 848 | this.instance._outputBackgroundRed = 0; 849 | this.instance._outputBackgroundGreen = 0; 850 | this.instance._outputBackgroundBlue = 0; 851 | this.instance._outputBackgroundAlpha = 0; 852 | this.instance._outputBackgroundOpacity = undefined; 853 | 854 | this.instance.run(function (err) { 855 | if (err) { 856 | done(err); 857 | } else { 858 | try { 859 | expect(this.instance._imageOutput.getAt(0, 0)).to.be.equal(0); 860 | done(); 861 | } catch (err) { 862 | done(err); 863 | } 864 | } 865 | }.bind(this)); 866 | }); 867 | 868 | it('should copy image-a to output by default', function (done) { 869 | this.instance._delta = 1000; 870 | this.instance._outputBackgroundRed = undefined; 871 | this.instance._outputBackgroundGreen = undefined; 872 | this.instance._outputBackgroundBlue = undefined; 873 | this.instance._outputBackgroundAlpha = undefined; 874 | this.instance._outputBackgroundOpacity = undefined; 875 | 876 | this.instance.run(function (err) { 877 | if (err) { 878 | done(err); 879 | } else { 880 | try { 881 | expect(this.instance._imageOutput.getAt(0, 0)).to.be.equal(this.instance._imageA.getAt(0, 0)); 882 | done(); 883 | } catch (err) { 884 | done(err); 885 | } 886 | } 887 | }.bind(this)); 888 | }); 889 | 890 | it('should copy image-a to output', function (done) { 891 | this.instance._delta = 1000; 892 | this.instance._copyImageAToOutput = true; 893 | this.instance._copyImageBToOutput = false; 894 | this.instance._outputBackgroundRed = undefined; 895 | this.instance._outputBackgroundGreen = undefined; 896 | this.instance._outputBackgroundBlue = undefined; 897 | this.instance._outputBackgroundAlpha = undefined; 898 | this.instance._outputBackgroundOpacity = undefined; 899 | 900 | this.instance.run(function (err) { 901 | if (err) { 902 | done(err); 903 | } else { 904 | try { 905 | expect(this.instance._imageOutput.getAt(0, 0)).to.be.equal(this.instance._imageA.getAt(0, 0)); 906 | done(); 907 | } catch (err) { 908 | done(err); 909 | } 910 | } 911 | }.bind(this)); 912 | }); 913 | 914 | it('should copy image-b to output', function (done) { 915 | this.instance._delta = 1000; 916 | this.instance._copyImageAToOutput = false; 917 | this.instance._copyImageBToOutput = true; 918 | this.instance._outputBackgroundRed = undefined; 919 | this.instance._outputBackgroundGreen = undefined; 920 | this.instance._outputBackgroundBlue = undefined; 921 | this.instance._outputBackgroundAlpha = undefined; 922 | this.instance._outputBackgroundOpacity = undefined; 923 | 924 | this.instance.run(function (err) { 925 | if (err) { 926 | done(err); 927 | } else { 928 | try { 929 | expect(this.instance._imageOutput.getAt(0, 0)).to.be.equal(this.instance._imageB.getAt(0, 0)); 930 | done(); 931 | } catch (err) { 932 | done(err); 933 | } 934 | } 935 | }.bind(this)); 936 | }); 937 | 938 | it('should run as promise', function (done) { 939 | var promise = this.instance.runWithPromise(); 940 | 941 | expect(promise).to.be.instanceof(Promise); 942 | promise.then(function (result) { 943 | expect(result.code).to.be.equal(BlinkDiff.RESULT_DIFFERENT); 944 | done(); 945 | }).then(null, function (err) { 946 | done(err); 947 | }); 948 | }); 949 | }); 950 | 951 | describe('Color-Conversion', function () { 952 | 953 | it('should convert RGB to XYZ', function () { 954 | var color = this.instance._convertRgbToXyz({c1: 92 / 255, c2: 255 / 255, c3: 162 / 255, c4: 1}); 955 | 956 | expect(color.c1).to.be.closeTo(0.6144431682352941, 0.0001); 957 | expect(color.c2).to.be.closeTo(0.8834245847058824, 0.0001); 958 | expect(color.c3).to.be.closeTo(0.6390158682352941, 0.0001); 959 | expect(color.c4).to.be.closeTo(1, 0.0001); 960 | }); 961 | 962 | it('should convert Xyz to CIELab', function () { 963 | var color = this.instance._convertXyzToCieLab({ 964 | c1: 0.6144431682352941, c2: 0.8834245847058824, c3: 0.6390158682352941, c4: 1 965 | }); 966 | 967 | expect(color.c1).to.be.closeTo(95.30495102757038, 0.0001); 968 | expect(color.c2).to.be.closeTo(-54.68933740774734, 0.0001); 969 | expect(color.c3).to.be.closeTo(19.63870174748623, 0.0001); 970 | expect(color.c4).to.be.closeTo(1, 0.0001); 971 | }); 972 | }); 973 | }); 974 | }); 975 | -------------------------------------------------------------------------------- /test/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/test/test1.png -------------------------------------------------------------------------------- /test/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/blink-diff/644140869afddf2a1b343919ce246f0f00100398/test/test2.png -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "outdir": "docs", 4 | "linkNatives": true, 5 | "exclude": "test,bin" 6 | } 7 | } 8 | --------------------------------------------------------------------------------