preceptorjs/taxi

View on GitHub
lib/helpers/screenshot.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

var logMethods = require('../log');
var type = require('../type');
var when = require('../when');

var screenshotScripts = require('../scripts/screenshot');

var PNGImage = require('pngjs-image');
var fs = require('fs');

module.exports = Screenshot;

/**
 * Screenshot object
 *
 * @constructor
 * @class Screenshot
 * @module WebDriver
 * @submodule Helpers
 * @param {Driver} driver
 */
function Screenshot (driver) {
    this._driver = driver;
}


/////////////////////
// Private Methods //
/////////////////////

/**
 * Logs a method call by an event
 *
 * @param {object} event
 * @method _logMethodCall
 * @private
 */
Screenshot.prototype._logMethodCall = function (event) {
    event.target = 'Screenshot';
    this._driver._logMethodCall(event);
};


/**
 * Performs a context dependent JSON request for the current session.
 * The result is parsed for errors.
 *
 * @method _requestJSON
 * @private
 * @param {String} method
 * @param {String} path
 * @param {*} [body]
 * @return {*}
 */
Screenshot.prototype._requestJSON = function (method, path, body) {
    return this._driver._requestJSON(method, path, body);
};


////////////////////
// Public Methods //
////////////////////

/**
 * Gets the driver object.
 * Direct-access. No need to wait.
 *
 * @return {Driver}
 */
Screenshot.prototype.getDriver = function () {
    return this._driver;
};


/**
 * Takes a screenshot as-is from the webdriver protocol
 *
 * @method takeRawScreenshot
 * @param {Object} [options]
 * @return {Buffer}
 */
//Screenshot.prototype.id = 0; //TODO: Turn on for debugging
Screenshot.prototype.takeRawScreenshot = function (options) {

    return when(this._driver._requestJSON('GET', '/screenshot', undefined, options), function (base64Data) {
        var buffer = new Buffer(base64Data, 'base64');
        //var browserName = this.getDriver().deviceName() != '' ? this.getDriver().deviceName() : this.getDriver().browserName();
        //Screenshot.prototype.id++;
        //fs.writeFileSync(__dirname + '/' + browserName + " " + this.getDriver().deviceOrientation() + " " + this.getDriver().browserVersion() + " " + this.getDriver().platform() + " " + Screenshot.prototype.id + '.png', buffer);
        return buffer;
    }.bind(this));
};


/**
 * Takes a screenshot pre-processed and converted into an image
 *
 * @method takeProcessedScreenshot
 * @param {Object} [options]
 * @return {PNGImage}
 */
Screenshot.prototype.takeProcessedScreenshot = function (options) {

    return when(this.takeRawScreenshot(options), function (buffer) {
        return this._screenshotPrePrecessing(PNGImage.loadImageSync(buffer));
    }.bind(this));
};


/**
 * Gets the max. allowed resolution for one screenshot. If the document exceeds this resolution, then
 * the screenshot will be stitched together from multiple smaller screenshots called sections.
 *
 * @method getMaxImageResolution
 * @return {int}
 */
Screenshot.prototype.getMaxImageResolution = function () {
    return this.getDriver().utils().resolve(this.getDriver().getValue('maxImageResolution'));
};


/**
 * Takes a screenshot of the whole document
 *
 * @method documentScreenshot
 * @param {object} [options]
 * @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {ActiveWindow|Element} [options.context] Context of screenshot
 * @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
 * @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @return {Buffer}
 */
Screenshot.prototype.documentScreenshot = function (options) {

    return this._takeScreenshot(function (initData) {
        return {
            x: 0,
            y: 0,
            width: initData.document.width,
            height: initData.document.height
        };
    }, options);
};

/**
 * Takes a screenshot of the current view-port
 *
 * @method viewPortScreenshot
 * @param {object} [options]
 * @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {ActiveWindow|Element} [options.context] Context of screenshot
 * @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
 * @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @return {Buffer}
 */
Screenshot.prototype.viewPortScreenshot = function (options) {

    return this._takeScreenshot(function (initData) {
        return {
            x: initData.viewPort.x,
            y: initData.viewPort.y,
            width: initData.viewPort.width,
            height: initData.viewPort.height
        };
    }, options);
};

/**
 * Takes a screenshot of a specific area
 *
 * @method areaScreenshot
 * @param {int} [x=0] X-coordinate for area
 * @param {int} [y=0] Y-coordinate for area
 * @param {int} [width=document.width-x] Width of area to be captured
 * @param {int} [height=document.height-y] Height of area to be captured
 * @param {object} [options]
 * @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {ActiveWindow|Element} [options.context] Context of screenshot
 * @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
 * @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @return {Buffer}
 */
Screenshot.prototype.areaScreenshot = function (x, y, width, height, options) {

    return this._takeScreenshot(function (initData) {

        var localX = x || 0,
            localY = y || 0,
            localWidth = width || initData.document.width - localX,
            localHeight = height || initData.document.height - localY;

        return {
            x: localX,
            y: localY,
            width: localWidth,
            height: localHeight
        };
    }, options);
};

/**
 * Determines the areas that should be blocked-out
 *
 * @method _determineBlockOuts
 * @param {object} area Area that will be captured
 * @param {object} context Context of the capture (ActiveWindow, Element)
 * @param {object[]|Element[]|string[]} blockOuts List of block-out areas
 * @param {object} [blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
 * @return {object[]} List of rectangular areas {x, y, width, height}
 * @private
 */
Screenshot.prototype._determineBlockOuts = function (area, context, blockOuts, blockOutColor) {

    var defaultColor = blockOutColor || { red: 0, green: 0, blue: 0 };

    return when(this.getDriver().utils().map(blockOuts, function (blockOutEl) {

        if (typeof blockOutEl === 'string') {

            return when(context.getElements(blockOutEl), function (elements) {

                return elements.map(function (el) {
                    return el.getFrame();
                });
            });

        } else if (typeof blockOutEl === 'object') {

            if (blockOutEl instanceof require('../element')) {
                return blockOutEl.getFrame();
            } else {
                return this.getDriver().utils().resolve(blockOutEl);
            }

        } else {
            throw new Error('Unknown block-out type: ' + typeof blockOutEl);
        }

    }.bind(this)), function (blockOuts) {

        var flattenList = [], reducedList;

        // Make sure that we have a flat list
        blockOuts.forEach(function (blockOut) {
            if ((typeof blockOut === 'object') && (blockOut.length !== undefined)) { // Array
                blockOut.forEach(function (item) {
                    flattenList.push(item);
                });
            } else {
                flattenList.push(blockOut);
            }
        });

        // Validate, reduce, and convert coordinates of each block-out area
        flattenList = flattenList.map(function (blockOut) {

            this._validateRectItem(blockOut);
            this._reduceRectItemToArea(blockOut, area);

            // Convert coordinates from root to captured area
            blockOut.x -= area.x;
            blockOut.y -= area.y;

            // Use default block-out color if none specified
            blockOut.color = blockOut.color || defaultColor;

            return blockOut;

        }.bind(this));

        // Remove block-outs that doesn't need to be done
        reducedList = flattenList.filter(function (blockOut) {
            return ((blockOut.width !== 0) && (blockOut.height !== 0));
        });

        return reducedList;

    }.bind(this));
};

/**
 * Reduces a frame-item to a specified area
 *
 * @method _reduceRectItemToArea
 * @param {object} item Item that should be reduced
 * @param {object} area Area to reduce it to
 * @private
 */
Screenshot.prototype._reduceRectItemToArea = function (item, area) {

    if (item.x < area.x) {
        item.width -= Math.abs(item.x - area.x);
        item.x = area.x;
    }
    if (item.x > area.x + area.width) {
        item.x = area.x + area.width;
        item.width = 0;
    }

    if (item.y < area.y) {
        item.height -= Math.abs(item.y - area.y);
        item.y = area.y;
    }
    if (item.y > area.y + area.height) {
        item.y = area.y + area.height;
        item.height = 0;
    }

    if (item.width < 0) {
        item.width = 0;
    }
    if (item.width > area.width) {
        item.width = area.width;
    }

    if (item.height < 0) {
        item.height = 0;
    }
    if (item.height > area.height) {
        item.height = area.height;
    }
};

/**
 * Calidates if a rectangular item has all the required properties
 *
 * @method _validateRectItem
 * @param {object} item
 * @private
 */
Screenshot.prototype._validateRectItem = function (item) {
    ['x', 'y', 'width', 'height'].forEach(function (property) {
        if (item[property] === undefined) {
            throw new Error('The "' + property + '" property needs to be defined for item: ' + JSON.stringify(item));
        }
        if (typeof item[property] !== 'number') {
            throw new Error('The "' + property + '" property needs to be numeric: ' + JSON.stringify(item));
        }
    });
};


/**
 * Takes a screenshot of an area that will be specified through the return-value of the callback
 *
 * @method _takeScreenshot
 * @param {function} fn Function to be called to get area to be captured. Supplies the data retrieved from the client as the first parameters.
 * @param {object} [options]
 * @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {object} [options.padding] Padding for screenshots
 * @param {object} [options.padding.viewPort] Padding for view-port screenshots, ignoring areas of the view-port screenshot
 * @param {int} [options.padding.viewPort.top=0] Padding of view-port screenshot from the top
 * @param {int} [options.padding.viewPort.bottom=0] Padding of view-port screenshot from the bottom
 * @param {int} [options.padding.viewPort.left=0] Padding of view-port screenshot from the left
 * @param {int} [options.padding.viewPort.right=0] Padding of view-port screenshot from the right
 * @param {object} [options.padding.screenshot] Padding for screenshot, ignoring areas of the screenshot
 * @param {int} [options.padding.screenshot.top=0] Padding of screenshot from the top
 * @param {int} [options.padding.screenshot.bottom=0] Padding of screenshot from the bottom
 * @param {int} [options.padding.screenshot.left=0] Padding of screenshot from the left
 * @param {int} [options.padding.screenshot.right=0] Padding of screenshot from the right
 * @param {object} [options.padding.document] Padding for document screenshots, ignoring areas of the document screenshot
 * @param {int} [options.padding.document.top=0] Padding of document screenshot from the top
 * @param {int} [options.padding.document.bottom=0] Padding of document screenshot from the bottom
 * @param {int} [options.padding.document.left=0] Padding of document screenshot from the left
 * @param {int} [options.padding.document.right=0] Padding of document screenshot from the right
 * @param {ActiveWindow|Element} [options.context] Context of screenshot
 * @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
 * @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @return {Buffer}
 * @private
 */
Screenshot.prototype._takeScreenshot = function (fn, options) {

    var DevicePixelRatio = require('./devicePixelRatio'),
        devicePixelRatio = new DevicePixelRatio(this.getDriver()),

        Stitching = require('./stitching'),
        stitching = new Stitching(this.getDriver());

    return when(devicePixelRatio.getDevicePixelRatio(options), function (devicePixelRatio) {

        return when(stitching.doesNeedStitching(options), function (needsStitching) {

            return when(this.getMaxImageResolution(), function (maxImageResolution) {

                return when(this._execute(screenshotScripts.init, [needsStitching]), function (initData) {
                    initData = JSON.parse(initData);

                    var area, sections, padding;

                    // Make sure to account for the device-pixel-ratio
                    maxImageResolution *= (1 / devicePixelRatio);

                    // Make sure the document is not too wide; can fix this by increasing maxImageResolution
                    // We might need to capture only smaller areas, but we need to account also for correct behaving
                    // browsers like firefox which will always return the whole document.
                    if (initData.document.width > maxImageResolution) {
                        throw new Error('The maxImageResolution needs to be greater or equal to one time the document width.');
                    }

                    // Set defaults for padding (if none given) and validate
                    padding = this._browserPadding((options && options.padding) || {});

                    area = fn(initData);

                    // Add requested document padding
                    area.x = area.x + padding.document.left;
                    area.y = area.y + padding.document.top;
                    area.width = area.width - padding.document.left - padding.document.right;
                    area.height = area.height - padding.document.top - padding.document.bottom;

                    this._validateArea(area, padding, initData);

                    // Determine sections and view-ports to capture
                    sections = this._gatherSections(area, padding, initData, maxImageResolution, needsStitching);

                    return when(this._determineBlockOuts(area, options.context || this, options.blockOuts || [], options.blockOutColor), function (blockOuts) {
                        return when(this._takeSectionScreenshots(sections, initData, options), function () {

                            // Stitch images together
                            var image;

                            if (needsStitching) {
                                image = this._stitchImages(area, sections, devicePixelRatio);
                            } else {
                                image = sections[0].viewPorts[0].image;
                                image.clip(area.x, area.y, area.width, area.height);
                            }

                            return when(this._execute(screenshotScripts.revert, [initData]), function () {

                                // Apply block-outs (if available)
                                this._applyBlockOuts(image, blockOuts, devicePixelRatio);

                                return image.toBlobSync();

                            }.bind(this));
                        }.bind(this));
                    }.bind(this));
                }.bind(this));
            }.bind(this));
        }.bind(this));
    }.bind(this));
};

/**
 * Applies all areas selected to be blocked-out
 *
 * @param {PNGImage} image Image to be blocked-out
 * @param {object[]} blockOuts List of block-out areas
 * @param {number} devicePixelRatio Device pixel ratio for browser
 * @private
 */
Screenshot.prototype._applyBlockOuts = function (image, blockOuts, devicePixelRatio) {
    blockOuts.forEach(function (blockOut) {
        image.fillRect(
            Math.floor(blockOut.x * devicePixelRatio),
            Math.floor(blockOut.y * devicePixelRatio),
            Math.floor(blockOut.width * devicePixelRatio),
            Math.floor(blockOut.height * devicePixelRatio),
            blockOut.color
        );
    });
};

/**
 * Determines if the screenshots need additional padding
 *
 * Note:
 *   Overwrite this method if you want to implement standard browser padding.
 *
 * @method _browserPadding
 * @param {object} padding
 * @private
 */
Screenshot.prototype._browserPadding = function (padding) {

    var paddingViewPortTop = 0, paddingViewPortBottom = 0,
        paddingViewPortLeft = 0, paddingViewPortRight = 0,
        paddingScreenshotTop = 0, paddingScreenshotBottom = 0,
        paddingScreenshotLeft = 0, paddingScreenshotRight = 0,
        paddingDocumentTop = 0, paddingDocumentBottom = 0,
        paddingDocumentLeft = 0, paddingDocumentRight = 0,

        browserName = this.getDriver().browserName().toLowerCase(),
        browserVersion = this.getDriver().browserVersion(),
        deviceName = this.getDriver().deviceName().toLowerCase(),
        deviceOrientation = this.getDriver().deviceOrientation().toLowerCase();

    // Exceptions
    switch (browserName) {

        case 'iphone':
            paddingScreenshotTop = 65; // Browser address bar needs to be trimmed
            paddingScreenshotBottom = 2; // Browser returns view-port that is one pixel smaller than reported
            break;

        case 'internet explorer':
            // We expect for future versions to behave the same (black border on the right)
            if (browserVersion >= 10) {
                paddingDocumentRight = 4;
            }
            break;
    }

    padding.viewPort = padding.viewPort || {};
    padding.viewPort.top = padding.viewPort.top || paddingViewPortTop;
    padding.viewPort.bottom = padding.viewPort.bottom || paddingViewPortBottom;
    padding.viewPort.left = padding.viewPort.left || paddingViewPortLeft;
    padding.viewPort.right = padding.viewPort.right || paddingViewPortRight;

    padding.screenshot = padding.screenshot || {};
    padding.screenshot.top = padding.screenshot.top || paddingScreenshotTop;
    padding.screenshot.bottom = padding.screenshot.bottom || paddingScreenshotBottom;
    padding.screenshot.left = padding.screenshot.left || paddingScreenshotLeft;
    padding.screenshot.right = padding.screenshot.right || paddingScreenshotRight;

    padding.document = padding.document || {};
    padding.document.top = padding.document.top || paddingDocumentTop;
    padding.document.bottom = padding.document.bottom || paddingDocumentBottom;
    padding.document.left = padding.document.left || paddingDocumentLeft;
    padding.document.right = padding.document.right || paddingDocumentRight;

    return padding;
};

/**
 * Validates and corrects area that was given to capture
 *
 * @method _validateArea
 * @param {object} area
 * @param {object} padding Padding for screenshots
 * @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
 * @param {int} padding.viewPort.top Padding of view-port screenshot from the top
 * @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
 * @param {int} padding.viewPort.left Padding of view-port screenshot from the left
 * @param {int} padding.viewPort.right Padding of view-port screenshot from the right
 * @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
 * @param {int} padding.screenshot.top Padding of screenshot from the top
 * @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
 * @param {int} padding.screenshot.left Padding of screenshot from the left
 * @param {int} padding.screenshot.right Padding of screenshot from the right
 * @param {object} padding.document Padding for document screenshots, ignoring areas of the document screenshot
 * @param {int} padding.document.top Padding of document screenshot from the top
 * @param {int} padding.document.bottom Padding of document screenshot from the bottom
 * @param {int} padding.document.left Padding of document screenshot from the left
 * @param {int} padding.document.right Padding of document screenshot from the right
 * @param {object} initData
 * @private
 */
Screenshot.prototype._validateArea = function (area, padding, initData) {

    if (area.width < 0) {
        throw new Error('Width of area to capture cannot be negative.');
    }
    if (area.height < 0) {
        throw new Error('Height of area to capture cannot be negative.');
    }

    if (area.x < 0) {
        area.x = 0;
    }
    if (area.x >= initData.document.width) {
        area.x = initData.document.width - 1;
    }
    if (area.y < 0) {
        area.y = 0;
    }
    if (area.y >= initData.document.height) {
        area.y = initData.document.height - 1;
    }

    if (area.x + area.width > initData.document.width) {
        area.width = initData.document.width - area.x;
    }
    if (area.y + area.height > initData.document.height) {
        area.height = initData.document.height - area.y;
    }
};


/**
 * Calculates the sections that need to be captured on their own to be able to capture the whole document.
 *
 * This method depends on the value given to "maxImageResolution".
 *
 * @method _gatherSections
 * @param {object} area Area to capture
 * @param {object} padding Padding for screenshots
 * @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
 * @param {int} padding.viewPort.top Padding of view-port screenshot from the top
 * @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
 * @param {int} padding.viewPort.left Padding of view-port screenshot from the left
 * @param {int} padding.viewPort.right Padding of view-port screenshot from the right
 * @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
 * @param {int} padding.screenshot.top Padding of screenshot from the top
 * @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
 * @param {int} padding.screenshot.left Padding of screenshot from the left
 * @param {int} padding.screenshot.right Padding of screenshot from the right
 * @param {object} initData Data that was initially gathered from the client
 * @param {int} maxImageResolution Max. number of pixels allowed for a screenshot
 * @param {boolean} needsStitching Does the browser need stitching?
 * @return {object[]} List of sections
 * @private
 */
Screenshot.prototype._gatherSections = function (area, padding, initData, maxImageResolution, needsStitching) {

    var sections = [], section,
        sectionCount, sectionHeight,
        i, index = 0,
        yOffset,
        documentWidth,
        viewPortHeight = initData.viewPort.height - padding.viewPort.top - padding.viewPort.bottom - padding.screenshot.bottom;

    documentWidth = initData.document.width - area.x;

    // Calculate max. section height considering the max resolution to capture
    sectionHeight = Math.floor(maxImageResolution / documentWidth);

    if (needsStitching) {
        // When the screenshots are stitched together,
        // we try here to reduce the amount of screenshots per section.
        // We don't want to be wasteful so we make sure that the section border is on
        // even viewPort height; we want to avoid taking a viewPort screenshot and only use
        // the upper part of it, discarding it and requesting it for the next section.
        sectionHeight = Math.floor(sectionHeight / viewPortHeight) * viewPortHeight;
    }

    sectionCount = Math.ceil(area.height / sectionHeight);

    for (i = 0; i < sectionCount; i++) {

        yOffset = (i * sectionHeight);
        section = {
            shift: (sectionCount !== 1),
            x: area.x,
            y: area.y + yOffset, // Vertical offset for sections
            dstX: 0,
            dstY: yOffset, // Vertical offset for sections
            width: area.width,
            height: Math.min(sectionHeight, area.height - yOffset),
            viewPorts: undefined
        };

        if (needsStitching) {
            section.viewPorts = this._gatherViewPortSections(section, padding, initData, index);
            index += section.viewPorts.length;
        } else {
            section.viewPorts = [
                {
                    srcX: padding.viewPort.left + padding.screenshot.left,
                    srcY: padding.viewPort.top + padding.screenshot.top,
                    x: 0,
                    y: 0,
                    width: section.width,
                    height: section.height,
                    image: undefined,
                    index: index
                }
            ];
            index++;
        }

        sections.push(section);
    }

    return sections;
};

/**
 * Calculates the sections that need to be captured when the browser is not able to return pixels outside of the view-port.
 *
 * @method _gatherViewPortSections
 * @param {object} section Section that should be captured
 * @param {object} padding Padding for screenshots
 * @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
 * @param {int} padding.viewPort.top Padding of view-port screenshot from the top
 * @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
 * @param {int} padding.viewPort.left Padding of view-port screenshot from the left
 * @param {int} padding.viewPort.right Padding of view-port screenshot from the right
 * @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
 * @param {int} padding.screenshot.top Padding of screenshot from the top
 * @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
 * @param {int} padding.screenshot.left Padding of screenshot from the left
 * @param {int} padding.screenshot.right Padding of screenshot from the right
 * @param {object} initData Data that was initially gathered from the client
 * @param {int} index Index of first view-port
 * @return {object[]}
 * @private
 */
Screenshot.prototype._gatherViewPortSections = function (section, padding, initData, index) {

    var viewPorts = [],

        sectionWidth = section.width,
        sectionHeight = section.height,

        viewPortWidth = initData.viewPort.width - padding.viewPort.left - padding.viewPort.right - padding.screenshot.right,
        viewPortHeight = initData.viewPort.height - padding.viewPort.top - padding.viewPort.bottom - padding.screenshot.bottom,

        columns = Math.ceil(sectionWidth / viewPortWidth),
        rows = Math.ceil(sectionHeight / viewPortHeight),

        offsetX, offsetY,
        x, y;

    for (y = 0; y < rows; y++) {
        for (x = 0; x < columns; x++) {

            offsetX = x * viewPortWidth;
            offsetY = y * viewPortHeight;

            viewPorts.push({
                srcX: padding.viewPort.left + padding.screenshot.left,
                srcY: padding.viewPort.top + padding.screenshot.top,
                x: offsetX,
                y: offsetY,
                width: Math.min(viewPortWidth, sectionWidth - offsetX),
                height: Math.min(viewPortHeight, sectionHeight - offsetY),
                image: undefined,
                index: index
            });

            index++;
        }
    }

    return viewPorts;
};


/**
 * Takes all the screenshots defined in section
 *
 * @method _takeSectionScreenshots
 * @param {object[]} sections List of sections to capture
 * @param {object} initData Data that was initially gathered from the client
 * @param {object} [options]
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @private
 */
Screenshot.prototype._takeSectionScreenshots = function (sections, initData, options) {

    return when(this.getDriver().utils().map(sections, function (section) {
        return this._takeViewPortScreenshots(section, section.viewPorts, initData, options);

    }.bind(this)), function () {
        return this._execute(options.completeFn);
    }.bind(this));
};

/**
 * Takes all the screenshots defined in the view-ports
 *
 * @method _takeViewPortScreenshots
 * @param {object} section Section to capture
 * @param {object[]} viewPorts List of view-ports to capture
 * @param {object} initData Data that was initially gathered from the client
 * @param {object} [options]
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @private
 */
Screenshot.prototype._takeViewPortScreenshots = function (section, viewPorts, initData, options) {

    return this.getDriver().utils().map(viewPorts, function (viewPort) {
        return this._takeViewPortScreenshot(section, viewPort, initData, options);
    }.bind(this));
};

/**
 * Takes a screenshot of the given view-port in relation to the section
 *
 * @method _takeViewPortScreenshot
 * @param {object} section Section to capture
 * @param {object} viewPort View-ports to capture
 * @param {object} initData Data that was initially gathered from the client
 * @param {object} [options]
 * @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
 * @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
 * @param {int} [options.wait=100] Wait in ms before each screenshot
 * @private
 */
Screenshot.prototype._takeViewPortScreenshot = function (section, viewPort, initData, options) {

    var offsetX = section.x + viewPort.x,
        offsetY = section.y + viewPort.y,
        viewPortIndex = viewPort.index,
        sectionHeight = section.shift ? section.height : null,
        wait = (options && options.wait) || 100;

    return when(this._execute(screenshotScripts.documentOffset, [offsetX, offsetY, sectionHeight, initData]), function () {

        // Just wait a little bit. Some browsers need time for that.
        return when(this.getDriver().utils().sleep(wait), function () {

            return when(this._execute(options.eachFn, [viewPortIndex]), function () {

                // Take screenshot with current section and view-port
                return when(this.takeProcessedScreenshot(), function (image) {

                    viewPort.image = image;

                }.bind(this));
            }.bind(this));
        }.bind(this));
    }.bind(this));
};

/**
 * Processes the screenshots before it is used for stitching
 *
 * Note:
 *  Some browser need some pre-processing like rotating the screenshot.
 *
 * @method _screenshotPrePrecessing
 * @param {PNGImage} image Image to process
 * @return {PNGImage}
 * @private
 */
Screenshot.prototype._screenshotPrePrecessing = function (image) {

    var browserName = this.getDriver().browserName().toLowerCase(),
        browserVersion = this.getDriver().browserVersion(),
        deviceName = this.getDriver().deviceName().toLowerCase(),
        deviceOrientation = this.getDriver().deviceOrientation().toLowerCase();

    // Exceptions
    switch (browserName) {

        case 'iphone':
            if (deviceOrientation === 'landscape') {
                image = image.rotateCCW();
            }
            break;
    }

    return image;
};


/**
 * Stitches all the sections and view-ports together to one final image
 *
 * @method _stitchImages
 * @param {object} area Area that should have been captured on the page
 * @param {object[]} sections List of sections that were captured
 * @param {number} devicePixelRatio Device pixel ratio for browser
 * @return {PNGImage}
 * @private
 */
Screenshot.prototype._stitchImages = function (area, sections, devicePixelRatio) {

    var image;

    image = PNGImage.createImage(
        Math.floor(area.width * devicePixelRatio),
        Math.floor(area.height * devicePixelRatio)
    );

    sections.forEach(function (section) {

        section.viewPorts.forEach(function (viewPort) {

            var offsetX = Math.floor((section.dstX + viewPort.x) * devicePixelRatio),
                offsetY = Math.floor((section.dstY + viewPort.y) * devicePixelRatio),
                width = Math.floor(viewPort.width * devicePixelRatio),
                height = Math.floor(viewPort.height * devicePixelRatio);

            viewPort.image.getImage().bitblt(
                image.getImage(),
                Math.floor(viewPort.srcX * devicePixelRatio),
                Math.floor(viewPort.srcY * devicePixelRatio),
                Math.min(width - Math.floor(section.dstX * devicePixelRatio), viewPort.image.getWidth()),
                Math.min(height - Math.floor(section.dstY * devicePixelRatio), viewPort.image.getHeight()),
                offsetX,
                offsetY
            );

            // Free-up some memory
            viewPort.image = null;
        });
    });

    // Force garbage-collection if available (can be turned on with option "--expose-gc" when running node)
    if (global.gc) {
        global.gc();
    }

    return image;
};

/**
 * Executes a script in the browser and returns the result.
 *
 * This is a convenience method for accessing the execute method.
 *
 * @method _execute
 * @param {String|Function} script
 * @param {Array} [args]
 * @return {*}
 * @private
 */
Screenshot.prototype._execute = function (script, args) {
    if (script) {
        return this.getDriver().browser().activeWindow().execute(script, args);

    } else { // Ignore script if there is nothing - might happen with screenshot requests
        return this.getDriver().utils().resolve(undefined);
    }
};

logMethods(Screenshot.prototype);