/*! rasterizeHTML.js - v1.3.1 - 2022-01-16
* http://www.github.com/cburgmer/rasterizeHTML.js
* Copyright (c) 2022 Christoph Burgmer; Licensed MIT */
(function (root, factory) {
  if (root === undefined && window !== undefined) root = window;
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module unless amdModuleId is set
    define(["url","xmlserializer","sane-domparser-error","inlineresources"], function (a0,b1,c2,d3) {
      return (root['rasterizeHTML'] = factory(a0,b1,c2,d3));
    });
  } else if (typeof module === 'object' && module.exports) {
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory(require("url"),require("xmlserializer"),require("sane-domparser-error"),require("inlineresources"));
  } else {
    root['rasterizeHTML'] = factory(root["url"],root["xmlserializer"],root["sanedomparsererror"],root["inlineresources"]);
  }
}(this, function (url, xmlserializer, sanedomparsererror, inlineresources) {

var util = (function (url) {
    "use strict";

    var module = {};

    var uniqueIdList = [];

    module.joinUrl = function (baseUrl, relUrl) {
        if (!baseUrl) {
            return relUrl;
        }
        return url.resolve(baseUrl, relUrl);
    };

    module.getConstantUniqueIdFor = function (element) {
        // HACK, using a list results in O(n), but how do we hash e.g. a DOM node?
        if (uniqueIdList.indexOf(element) < 0) {
            uniqueIdList.push(element);
        }
        return uniqueIdList.indexOf(element);
    };

    module.clone = function (object) {
        var theClone = {},
            i;
        for (i in object) {
            if (object.hasOwnProperty(i)) {
                theClone[i] = object[i];
            }
        }
        return theClone;
    };

    var isObject = function (obj) {
        return typeof obj === "object" && obj !== null;
    };

    var isCanvas = function (obj) {
        return (
            isObject(obj) &&
            Object.prototype.toString
                .apply(obj)
                .match(/\[object (Canvas|HTMLCanvasElement)\]/i)
        );
    };

    // args: canvas, options
    module.parseOptionalParameters = function (args) {
        var parameters = {
            canvas: null,
            options: {},
        };

        if (args[0] == null || isCanvas(args[0])) {
            parameters.canvas = args[0] || null;

            parameters.options = module.clone(args[1]);
        } else {
            parameters.options = module.clone(args[0]);
        }

        return parameters;
    };

    return module;
})(url);

// Proxy objects by monkey patching
var proxies = (function (util) {
    "use strict";

    var module = {};

    var monkeyPatchInstanceMethod = function (object, methodName, proxyFunc) {
        var originalFunc = object[methodName];

        object[methodName] = function () {
            var args = Array.prototype.slice.call(arguments);

            return proxyFunc.apply(this, [args, originalFunc]);
        };

        return originalFunc;
    };

    // Bases all XHR calls on the given base URL
    module.baseUrlRespectingXhr = function (XHRObject, baseUrl) {
        var xhrConstructor = function () {
            var xhr = new XHRObject();

            monkeyPatchInstanceMethod(
                xhr,
                "open",
                function (args, originalOpen) {
                    var method = args.shift(),
                        url = args.shift(),
                        joinedUrl = util.joinUrl(baseUrl, url);

                    return originalOpen.apply(
                        this,
                        [method, joinedUrl].concat(args)
                    );
                }
            );

            return xhr;
        };

        return xhrConstructor;
    };

    // Provides a convenient way of being notified when all pending XHR calls are finished
    module.finishNotifyingXhr = function (XHRObject) {
        var totalXhrCount = 0,
            doneXhrCount = 0,
            waitingForPendingToClose = false;
        var checkAllRequestsFinished;

        var promise = new Promise(function (resolve) {
            checkAllRequestsFinished = function () {
                var pendingXhrCount = totalXhrCount - doneXhrCount;

                if (pendingXhrCount <= 0 && waitingForPendingToClose) {
                    resolve({ totalCount: totalXhrCount });
                }
            };
        });

        var xhrConstructor = function () {
            var xhr = new XHRObject();

            monkeyPatchInstanceMethod(xhr, "send", function (_, originalSend) {
                totalXhrCount += 1;
                return originalSend.apply(this, arguments);
            });

            xhr.addEventListener("load", function () {
                doneXhrCount += 1;

                checkAllRequestsFinished();
            });

            return xhr;
        };

        xhrConstructor.waitForRequestsToFinish = function () {
            waitingForPendingToClose = true;
            checkAllRequestsFinished();
            return promise;
        };

        return xhrConstructor;
    };

    return module;
})(util);

var documentUtil = (function () {
    "use strict";

    var module = {};

    var asArray = function (arrayLike) {
        return Array.prototype.slice.call(arrayLike);
    };

    module.addClassName = function (element, className) {
        element.className += " " + className;
    };

    module.addClassNameRecursively = function (element, className) {
        module.addClassName(element, className);

        if (element.parentNode !== element.ownerDocument) {
            module.addClassNameRecursively(element.parentNode, className);
        }
    };

    var changeCssRule = function (rule, newRuleText) {
        var styleSheet = rule.parentStyleSheet,
            ruleIdx = asArray(styleSheet.cssRules).indexOf(rule);

        // Exchange rule with the new text
        styleSheet.insertRule(newRuleText, ruleIdx + 1);
        styleSheet.deleteRule(ruleIdx);
    };

    var updateRuleSelector = function (rule, updatedSelector) {
        var styleDefinitions = rule.cssText.replace(/^[^\{]+/, ""),
            newRule = updatedSelector + " " + styleDefinitions;

        changeCssRule(rule, newRule);
    };

    var cssRulesToText = function (cssRules) {
        return asArray(cssRules).reduce(function (cssText, rule) {
            return cssText + rule.cssText;
        }, "");
    };

    var rewriteStyleContent = function (styleElement) {
        styleElement.textContent = cssRulesToText(styleElement.sheet.cssRules);
    };

    var addSheetPropertyToSvgStyleElement = function (svgStyleElement) {
        var doc = document.implementation.createHTMLDocument(""),
            cssStyleElement = document.createElement("style");

        cssStyleElement.textContent = svgStyleElement.textContent;
        // the style will only be parsed once it is added to a document
        doc.body.appendChild(cssStyleElement);

        svgStyleElement.sheet = cssStyleElement.sheet;
    };

    var matchingSimpleSelectorsRegex = function (simpleSelectorList) {
        return (
            "(" +
            "(?:^|[^.#:\\w])" + // start of string or not a simple selector character,
            "|" + // ... or ...
            "(?=\\W)" + // the next character parsed is not an alphabetic character (and thus a natural boundary)
            ")" +
            "(" +
            simpleSelectorList.join("|") + // one out of the given simple selectors
            ")" +
            "(?=\\W|$)"
        ); // followed either by a non-alphabetic character or the end of the string
    };

    var replaceSimpleSelectorsBy = function (
        element,
        simpleSelectorList,
        caseInsensitiveReplaceFunc
    ) {
        var selectorRegex = matchingSimpleSelectorsRegex(simpleSelectorList);

        asArray(element.querySelectorAll("style")).forEach(function (
            styleElement
        ) {
            // SVGStyleElement doesn't have a property sheet in Safari, we need some workaround here
            // more details can be found here: https://github.com/cburgmer/rasterizeHTML.js/issues/158
            if (typeof styleElement.sheet === "undefined") {
                addSheetPropertyToSvgStyleElement(styleElement);
            }

            var matchingRules = asArray(styleElement.sheet.cssRules).filter(
                function (rule) {
                    return (
                        rule.selectorText &&
                        new RegExp(selectorRegex, "i").test(rule.selectorText)
                    );
                }
            );

            if (matchingRules.length) {
                matchingRules.forEach(function (rule) {
                    var newSelector = rule.selectorText.replace(
                        new RegExp(selectorRegex, "gi"),
                        function (_, prefixMatch, selectorMatch) {
                            return (
                                prefixMatch +
                                caseInsensitiveReplaceFunc(selectorMatch)
                            );
                        }
                    );

                    if (newSelector !== rule.selectorText) {
                        updateRuleSelector(rule, newSelector);
                    }
                });

                rewriteStyleContent(styleElement);
            }
        });
    };

    module.rewriteCssSelectorWith = function (
        element,
        oldSelector,
        newSelector
    ) {
        replaceSimpleSelectorsBy(element, [oldSelector], function () {
            return newSelector;
        });
    };

    module.lowercaseCssTypeSelectors = function (element, matchingTagNames) {
        replaceSimpleSelectorsBy(element, matchingTagNames, function (match) {
            return match.toLowerCase();
        });
    };

    module.findHtmlOnlyNodeNames = function (element) {
        var treeWalker = element.ownerDocument.createTreeWalker(
                element,
                NodeFilter.SHOW_ELEMENT
            ),
            htmlNodeNames = {},
            nonHtmlNodeNames = {},
            currentTagName;

        do {
            currentTagName = treeWalker.currentNode.tagName.toLowerCase();
            if (
                treeWalker.currentNode.namespaceURI ===
                "http://www.w3.org/1999/xhtml"
            ) {
                htmlNodeNames[currentTagName] = true;
            } else {
                nonHtmlNodeNames[currentTagName] = true;
            }
        } while (treeWalker.nextNode());

        return Object.keys(htmlNodeNames).filter(function (tagName) {
            return !nonHtmlNodeNames[tagName];
        });
    };

    return module;
})();

var documentHelper = (function (documentUtil) {
    "use strict";

    var module = {};

    var asArray = function (arrayLike) {
        return Array.prototype.slice.call(arrayLike);
    };

    var cascadingAction = {
        active: true,
        hover: true,
        focus: false,
        target: false,
    };

    module.fakeUserAction = function (element, selector, action) {
        var elem = element.querySelector(selector),
            pseudoClass = ":" + action,
            fakeActionClass = "rasterizehtml" + action;
        if (!elem) {
            return;
        }

        if (cascadingAction[action]) {
            documentUtil.addClassNameRecursively(elem, fakeActionClass);
        } else {
            documentUtil.addClassName(elem, fakeActionClass);
        }
        documentUtil.rewriteCssSelectorWith(
            element,
            pseudoClass,
            "." + fakeActionClass
        );
    };

    module.persistInputValues = function (doc) {
        var inputs = doc.querySelectorAll("input"),
            textareas = doc.querySelectorAll("textarea"),
            isCheckable = function (input) {
                return input.type === "checkbox" || input.type === "radio";
            };

        asArray(inputs)
            .filter(isCheckable)
            .forEach(function (input) {
                if (input.checked) {
                    input.setAttribute("checked", "");
                } else {
                    input.removeAttribute("checked");
                }
            });

        asArray(inputs)
            .filter(function (input) {
                return !isCheckable(input);
            })
            .forEach(function (input) {
                input.setAttribute("value", input.value);
            });

        asArray(textareas).forEach(function (textarea) {
            textarea.textContent = textarea.value;
        });
    };

    module.rewriteTagNameSelectorsToLowerCase = function (element) {
        documentUtil.lowercaseCssTypeSelectors(
            element,
            documentUtil.findHtmlOnlyNodeNames(element)
        );
    };

    return module;
})(documentUtil);

var browser = (function (util, proxies, sanedomparsererror, theWindow) {
    "use strict";

    var module = {};

    var createHiddenElement = function (doc, tagName, width, height) {
        var element = doc.createElement(tagName);
        // 'display: none' doesn't cut it, as browsers seem to be lazy loading CSS
        element.style.visibility = "hidden";
        element.style.width = width + "px";
        element.style.height = height + "px";
        element.style.position = "absolute";
        element.style.top = -10000 - height + "px";
        element.style.left = -10000 - width + "px";
        // We need to add the element to the document so that its content gets loaded
        doc.getElementsByTagName("body")[0].appendChild(element);
        return element;
    };

    var wait = function (timeout) {
        if (timeout > 0) {
            return new Promise(function (resolve) {
                setTimeout(resolve, timeout);
            });
        } else {
            return Promise.resolve();
        }
    };

    module.executeJavascript = function (element, options) {
        return new Promise(function (resolve) {
            var iframe = createHiddenElement(
                    theWindow.document,
                    "iframe",
                    options.width,
                    options.height
                ),
                html = element.outerHTML,
                iframeErrorsMessages = [],
                executeJsTimeout = options.executeJsTimeout || 0;

            var cleanUp = function () {
                // We have to defer removing the iframe from the current document, see https://github.com/cburgmer/rasterizeHTML.js/issues/219
                theWindow.document
                    .getElementsByTagName("body")[0]
                    .removeChild(iframe);
            };

            var doResolve = function () {
                var doc = iframe.contentDocument;
                resolve({
                    document: doc,
                    errors: iframeErrorsMessages,
                    cleanUp: cleanUp,
                });
            };

            var xhr = iframe.contentWindow.XMLHttpRequest,
                finishNotifyXhrProxy = proxies.finishNotifyingXhr(xhr),
                baseUrlXhrProxy = proxies.baseUrlRespectingXhr(
                    finishNotifyXhrProxy,
                    options.baseUrl
                );

            iframe.onload = function () {
                wait(executeJsTimeout)
                    .then(finishNotifyXhrProxy.waitForRequestsToFinish)
                    .then(doResolve);
            };

            iframe.contentDocument.open();
            iframe.contentWindow.XMLHttpRequest = baseUrlXhrProxy;
            iframe.contentWindow.onerror = function (msg) {
                iframeErrorsMessages.push({
                    resourceType: "scriptExecution",
                    msg: msg,
                });
            };

            iframe.contentDocument.write("<!DOCTYPE html>");
            iframe.contentDocument.write(html);
            iframe.contentDocument.close();
        });
    };

    var createHiddenSandboxedIFrame = function (doc, width, height) {
        var iframe = doc.createElement("iframe");
        iframe.style.width = width + "px";
        iframe.style.height = height + "px";
        // 'display: none' doesn't cut it, as browsers seem to be lazy loading content
        iframe.style.visibility = "hidden";
        iframe.style.position = "absolute";
        iframe.style.top = -10000 - height + "px";
        iframe.style.left = -10000 - width + "px";
        // make sure content gets exact width independent of box-sizing value
        iframe.style.borderWidth = 0;
        // Don't execute JS, all we need from sandboxing is access to the iframe's document
        iframe.sandbox = "allow-same-origin";
        // Don't include a scrollbar on Linux
        iframe.scrolling = "no";
        return iframe;
    };

    var createIframeWithSizeAtZoomLevel1 = function (width, height, zoom) {
        var scaledViewportWidth = Math.floor(width / zoom),
            scaledViewportHeight = Math.floor(height / zoom);

        return createHiddenSandboxedIFrame(
            theWindow.document,
            scaledViewportWidth,
            scaledViewportHeight
        );
    };

    var calculateZoomedContentSizeAndRoundUp = function (
        actualViewport,
        requestedWidth,
        requestedHeight,
        zoom
    ) {
        return {
            width: Math.max(actualViewport.width * zoom, requestedWidth),
            height: Math.max(actualViewport.height * zoom, requestedHeight),
        };
    };

    var selectElementOrDescendant = function (element, selector) {
        var descendant = element.querySelector(selector);
        if (descendant) {
            return descendant;
        } else if (element.ownerDocument.querySelector(selector) === element) {
            return element;
        }

        throw {
            message: "Clipping selector not found",
        };
    };

    var calculateContentSize = function (
        rootElement,
        selector,
        requestedWidth,
        requestedHeight,
        zoom
    ) {
        // clientWidth/clientHeight needed for PhantomJS
        var actualViewportWidth = Math.max(
                rootElement.scrollWidth,
                rootElement.clientWidth
            ),
            actualViewportHeight = Math.max(
                rootElement.scrollHeight,
                rootElement.clientHeight
            ),
            top,
            left,
            originalWidth,
            originalHeight,
            rootFontSize,
            element,
            rect,
            contentSize;

        if (selector) {
            element = selectElementOrDescendant(rootElement, selector);

            rect = element.getBoundingClientRect();

            top = rect.top;
            left = rect.left;
            originalWidth = rect.width;
            originalHeight = rect.height;
        } else {
            top = 0;
            left = 0;
            originalWidth = actualViewportWidth;
            originalHeight = actualViewportHeight;
        }

        contentSize = calculateZoomedContentSizeAndRoundUp(
            {
                width: originalWidth,
                height: originalHeight,
            },
            requestedWidth,
            requestedHeight,
            zoom
        );

        rootFontSize = theWindow.getComputedStyle(
            rootElement.ownerDocument.documentElement
        ).fontSize;

        return {
            left: left,
            top: top,
            width: contentSize.width,
            height: contentSize.height,
            viewportWidth: actualViewportWidth,
            viewportHeight: actualViewportHeight,

            rootFontSize: rootFontSize,
        };
    };

    var findCorrelatingElement = function (element, documentClone) {
        var tagName = element.tagName;
        // Stupid but simple method: find first match. Should work for a single HTML element, and any other element given as root
        return documentClone.querySelector(tagName);
    };

    var elementToFullHtmlDocument = function (element) {
        var tagName = element.tagName.toLowerCase();
        if (tagName === "html" || tagName === "body") {
            return element.outerHTML;
        }

        // Simple hack: hide the body from sizing, otherwise browser would apply a 8px margin
        return '<body style="margin: 0;">' + element.outerHTML + "</body>";
    };

    module.calculateDocumentContentSize = function (element, options) {
        return new Promise(function (resolve, reject) {
            var zoom = options.zoom || 1,
                iframe;

            iframe = createIframeWithSizeAtZoomLevel1(
                options.width,
                options.height,
                zoom
            );
            // We need to add the element to the document so that its content gets loaded
            theWindow.document
                .getElementsByTagName("body")[0]
                .appendChild(iframe);

            iframe.onload = function () {
                var doc = iframe.contentDocument,
                    size;

                try {
                    size = calculateContentSize(
                        findCorrelatingElement(element, doc),
                        options.clip,
                        options.width,
                        options.height,
                        zoom
                    );

                    resolve(size);
                } catch (e) {
                    reject(e);
                } finally {
                    theWindow.document
                        .getElementsByTagName("body")[0]
                        .removeChild(iframe);
                }
            };

            // srcdoc doesn't work in PhantomJS yet
            iframe.contentDocument.open();
            iframe.contentDocument.write("<!DOCTYPE html>");
            iframe.contentDocument.write(elementToFullHtmlDocument(element));
            iframe.contentDocument.close();
        });
    };

    module.parseHtmlFragment = function (htmlFragment) {
        var doc = theWindow.document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = htmlFragment;

        var element = doc.querySelector("body").firstChild;

        if (!element) {
            throw "Invalid source";
        }

        return element;
    };

    var addHTMLTagAttributes = function (doc, html) {
        var attributeMatch = /<html((?:\s+[^>]*)?)>/im.exec(html),
            helperDoc =
                theWindow.document.implementation.createHTMLDocument(""),
            htmlTagSubstitute,
            i,
            elementSubstitute,
            attribute;

        if (!attributeMatch) {
            return;
        }

        htmlTagSubstitute = "<div" + attributeMatch[1] + "></div>";
        helperDoc.documentElement.innerHTML = htmlTagSubstitute;
        elementSubstitute = helperDoc.querySelector("div");

        for (i = 0; i < elementSubstitute.attributes.length; i++) {
            attribute = elementSubstitute.attributes[i];
            doc.documentElement.setAttribute(attribute.name, attribute.value);
        }
    };

    module.parseHTML = function (html) {
        // We should be using the DOMParser, but it is not supported in older browsers
        var doc = theWindow.document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = html;

        addHTMLTagAttributes(doc, html);
        return doc;
    };

    var failOnInvalidSource = function (doc) {
        try {
            return sanedomparsererror.failOnParseError(doc);
        } catch (e) {
            throw {
                message: "Invalid source",
                originalError: e,
            };
        }
    };

    module.validateXHTML = function (xhtml) {
        var p = new DOMParser(),
            doc = p.parseFromString(xhtml, "application/xml");

        failOnInvalidSource(doc);
    };

    var lastCacheDate = null;

    var getUncachableURL = function (url, cache) {
        if (cache === "none" || cache === "repeated") {
            if (lastCacheDate === null || cache !== "repeated") {
                lastCacheDate = Date.now();
            }
            return url + "?_=" + lastCacheDate;
        } else {
            return url;
        }
    };

    var doDocumentLoad = function (url, options) {
        return new Promise(function (resolve, reject) {
            var xhr = new window.XMLHttpRequest(),
                joinedUrl = util.joinUrl(options.baseUrl, url),
                augmentedUrl = getUncachableURL(joinedUrl, options.cache),
                doReject = function (e) {
                    reject({
                        message: "Unable to load page",
                        originalError: e,
                    });
                };

            xhr.addEventListener(
                "load",
                function () {
                    if (xhr.status === 200 || xhr.status === 0) {
                        resolve(xhr.responseXML);
                    } else {
                        doReject(xhr.statusText);
                    }
                },
                false
            );

            xhr.addEventListener(
                "error",
                function (e) {
                    doReject(e);
                },
                false
            );

            try {
                xhr.open("GET", augmentedUrl, true);
                xhr.responseType = "document";
                xhr.send(null);
            } catch (e) {
                doReject(e);
            }
        });
    };

    module.loadDocument = function (url, options) {
        return doDocumentLoad(url, options).then(function (doc) {
            return failOnInvalidSource(doc);
        });
    };

    return module;
})(util, proxies, sanedomparsererror, window);

var svg2image = (function (window) {
    "use strict";

    var module = {};

    var urlForSvg = function (svg, useBlobs) {
        if (useBlobs) {
            return URL.createObjectURL(
                new Blob([svg], { type: "image/svg+xml" })
            );
        } else {
            return (
                "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg)
            );
        }
    };

    var cleanUpUrl = function (url) {
        if (url instanceof Blob) {
            URL.revokeObjectURL(url);
        }
    };

    var simpleForeignObjectSvg =
        '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><foreignObject></foreignObject></svg>';

    var supportsReadingObjectFromCanvas = function (url) {
        return new Promise(function (resolve, reject) {
            var canvas = document.createElement("canvas"),
                image = new Image();

            image.onload = function () {
                var context = canvas.getContext("2d");
                try {
                    context.drawImage(image, 0, 0);
                    // This will fail in Chrome & Safari
                    canvas.toDataURL("image/png");
                    resolve(true);
                } catch (e) {
                    resolve(false);
                }
            };
            image.onerror = reject;
            image.src = url;
        });
    };

    var readingBackFromCanvasBenefitsFromOldSchoolDataUris = function () {
        // Check for work around for https://code.google.com/p/chromium/issues/detail?id=294129
        var blobUrl = urlForSvg(simpleForeignObjectSvg, true);
        return supportsReadingObjectFromCanvas(blobUrl).then(
            function (supportsReadingFromBlobs) {
                cleanUpUrl(blobUrl);
                if (supportsReadingFromBlobs) {
                    return false;
                }
                return supportsReadingObjectFromCanvas(
                    urlForSvg(simpleForeignObjectSvg, false)
                ).then(function (s) {
                    return s;
                });
            },
            function () {
                return false;
            }
        );
    };

    var supportsBlobBuilding = function () {
        if (window.Blob) {
            // Available as constructor only in newer builds for all browsers
            try {
                new Blob(["<b></b>"], { type: "text/xml" });
                return true;
            } catch (err) {}
        }
        return false;
    };

    var checkBlobSupport = function () {
        return new Promise(function (resolve, reject) {
            if (supportsBlobBuilding() && window.URL) {
                readingBackFromCanvasBenefitsFromOldSchoolDataUris().then(
                    function (doesBenefit) {
                        resolve(!doesBenefit);
                    },
                    function () {
                        reject();
                    }
                );
            } else {
                resolve(false);
            }
        });
    };

    var checkForBlobsResult;

    var checkForBlobs = function () {
        if (checkForBlobsResult === undefined) {
            checkForBlobsResult = checkBlobSupport();
        }

        return checkForBlobsResult;
    };

    var buildImageUrl = function (svg) {
        return checkForBlobs().then(function (useBlobs) {
            return urlForSvg(svg, useBlobs);
        });
    };

    module.renderSvg = function (svg) {
        return new Promise(function (resolve, reject) {
            var url,
                image,
                resetEventHandlers = function () {
                    image.onload = null;
                    image.onerror = null;
                },
                cleanUp = function () {
                    if (url) {
                        cleanUpUrl(url);
                    }
                };

            image = new Image();
            image.onload = function () {
                resetEventHandlers();
                cleanUp();

                resolve(image);
            };
            image.onerror = function () {
                cleanUp();

                reject();
            };

            buildImageUrl(svg).then(function (imageUrl) {
                url = imageUrl;
                image.src = url;
            }, reject);
        });
    };

    return module;
})(window);

var document2svg = (function (util, browser, documentHelper, xmlserializer) {
    "use strict";

    var module = {};

    var svgAttributes = function (size, zoom) {
        var zoomFactor = zoom || 1;

        var attributes = {
            width: size.width,
            height: size.height,
            "font-size": size.rootFontSize,
        };

        if (zoomFactor !== 1) {
            attributes.style =
                "transform:scale(" + zoomFactor + "); transform-origin: 0 0;";
        }

        return attributes;
    };

    var foreignObjectAttributes = function (size) {
        var closestScaledWith, closestScaledHeight, offsetX, offsetY;

        closestScaledWith = Math.round(size.viewportWidth);
        closestScaledHeight = Math.round(size.viewportHeight);

        offsetX = -size.left;
        offsetY = -size.top;

        var attributes = {
            x: offsetX,
            y: offsetY,
            width: closestScaledWith,
            height: closestScaledHeight,
        };

        return attributes;
    };

    var workAroundCollapsingMarginsAcrossSVGElementInWebKitLike = function (
        attributes
    ) {
        var style = attributes.style || "";
        attributes.style = style + "float: left;";
    };

    var workAroundSafariSometimesNotShowingExternalResources = function (
        attributes
    ) {
        /* Let's hope that works some magic. The spec says SVGLoad only fires
         * now when all externals are available.
         * http://www.w3.org/TR/SVG/struct.html#ExternalResourcesRequired */
        attributes.externalResourcesRequired = true;
    };

    var workAroundChromeShowingScrollbarsUnderLinuxIfHtmlIsOverflowScroll =
        function () {
            return '<style scoped="">html::-webkit-scrollbar { display: none; }</style>';
        };

    var serializeAttributes = function (attributes) {
        var keys = Object.keys(attributes);
        if (!keys.length) {
            return "";
        }

        return (
            " " +
            keys
                .map(function (key) {
                    return key + '="' + attributes[key] + '"';
                })
                .join(" ")
        );
    };

    var convertElementToSvg = function (element, size, zoomFactor) {
        var xhtml = xmlserializer.serializeToString(element);

        browser.validateXHTML(xhtml);

        var foreignObjectAttrs = foreignObjectAttributes(size);
        workAroundCollapsingMarginsAcrossSVGElementInWebKitLike(
            foreignObjectAttrs
        );
        workAroundSafariSometimesNotShowingExternalResources(
            foreignObjectAttrs
        );

        return (
            '<svg xmlns="http://www.w3.org/2000/svg"' +
            serializeAttributes(svgAttributes(size, zoomFactor)) +
            ">" +
            workAroundChromeShowingScrollbarsUnderLinuxIfHtmlIsOverflowScroll() +
            "<foreignObject" +
            serializeAttributes(foreignObjectAttrs) +
            ">" +
            xhtml +
            "</foreignObject>" +
            "</svg>"
        );
    };

    module.getSvgForDocument = function (element, size, zoomFactor) {
        documentHelper.rewriteTagNameSelectorsToLowerCase(element);

        return convertElementToSvg(element, size, zoomFactor);
    };

    module.drawDocumentAsSvg = function (element, options) {
        ["hover", "active", "focus", "target"].forEach(function (action) {
            if (options[action]) {
                documentHelper.fakeUserAction(element, options[action], action);
            }
        });

        return browser
            .calculateDocumentContentSize(element, options)
            .then(function (size) {
                return module.getSvgForDocument(element, size, options.zoom);
            });
    };

    return module;
})(util, browser, documentHelper, xmlserializer);

var rasterize = (function (
    util,
    browser,
    documentHelper,
    document2svg,
    svg2image,
    inlineresources
) {
    "use strict";

    var module = {};

    var generalDrawError = function (e) {
        return {
            message: "Error rendering page",
            originalError: e,
        };
    };

    var drawSvgAsImg = function (svg) {
        return svg2image.renderSvg(svg).then(
            function (image) {
                return {
                    image: image,
                    svg: svg,
                };
            },
            function (e) {
                throw generalDrawError(e);
            }
        );
    };

    var drawImageOnCanvas = function (image, canvas) {
        try {
            canvas.getContext("2d").drawImage(image, 0, 0);
        } catch (e) {
            // Firefox throws a 'NS_ERROR_NOT_AVAILABLE' if the SVG is faulty
            throw generalDrawError(e);
        }
    };

    var doDraw = function (element, canvas, options) {
        return document2svg
            .drawDocumentAsSvg(element, options)
            .then(drawSvgAsImg)
            .then(function (result) {
                if (canvas) {
                    drawImageOnCanvas(result.image, canvas);
                }

                return result;
            });
    };

    var operateJavaScriptOnDocument = function (element, options) {
        return browser
            .executeJavascript(element, options)
            .then(function (result) {
                var document = result.document;
                documentHelper.persistInputValues(document);

                return {
                    document: document,
                    errors: result.errors,
                    cleanUp: result.cleanUp,
                };
            });
    };

    module.rasterize = function (element, canvas, options) {
        var inlineOptions;

        inlineOptions = util.clone(options);
        inlineOptions.inlineScripts = options.executeJs === true;

        return inlineresources
            .inlineReferences(element, inlineOptions)
            .then(function (errors) {
                if (options.executeJs) {
                    return operateJavaScriptOnDocument(element, options).then(
                        function (result) {
                            return {
                                element: result.document.documentElement,
                                errors: errors.concat(result.errors),
                                cleanUp: result.cleanUp,
                            };
                        }
                    );
                } else {
                    return {
                        element: element,
                        errors: errors,
                        cleanUp: function () {},
                    };
                }
            })
            .then(function (result) {
                return doDraw(result.element, canvas, options).then(function (
                    drawResult
                ) {
                    result.cleanUp();

                    return {
                        image: drawResult.image,
                        svg: drawResult.svg,
                        errors: result.errors,
                    };
                });
            });
    };

    return module;
})(util, browser, documentHelper, document2svg, svg2image, inlineresources);

var rasterizeHTML = (function (util, browser, rasterize) {
    "use strict";

    var module = {};

    var getViewportSize = function (canvas, options) {
        var defaultWidth = 300,
            defaultHeight = 200,
            fallbackWidth = canvas ? canvas.width : defaultWidth,
            fallbackHeight = canvas ? canvas.height : defaultHeight,
            width = options.width !== undefined ? options.width : fallbackWidth,
            height =
                options.height !== undefined ? options.height : fallbackHeight;

        return {
            width: width,
            height: height,
        };
    };

    var constructOptions = function (params) {
        var viewport = getViewportSize(params.canvas, params.options),
            options;

        options = util.clone(params.options);
        options.width = viewport.width;
        options.height = viewport.height;

        return options;
    };

    /**
     * Draws a Document to the canvas.
     * rasterizeHTML.drawDocument( document [, canvas] [, options] ).then(function (result) { ... });
     */
    module.drawDocument = function () {
        var doc = arguments[0],
            optionalArguments = Array.prototype.slice.call(arguments, 1),
            params = util.parseOptionalParameters(optionalArguments);

        var element = doc.documentElement ? doc.documentElement : doc;

        return rasterize.rasterize(
            element,
            params.canvas,
            constructOptions(params)
        );
    };

    var drawHTML = function (html, canvas, options) {
        var doc = browser.parseHTML(html);

        return module.drawDocument(doc, canvas, options);
    };

    /**
     * Draws a HTML string to the canvas.
     * rasterizeHTML.drawHTML( html [, canvas] [, options] ).then(function (result) { ... });
     */
    module.drawHTML = function () {
        var html = arguments[0],
            optionalArguments = Array.prototype.slice.call(arguments, 1),
            params = util.parseOptionalParameters(optionalArguments);

        return drawHTML(html, params.canvas, params.options);
    };

    // work around https://bugzilla.mozilla.org/show_bug.cgi?id=925493
    var workAroundFirefoxNotLoadingStylesheetStyles = function (
        doc,
        url,
        options
    ) {
        var d = document.implementation.createHTMLDocument("");
        d.replaceChild(doc.documentElement, d.documentElement);

        var extendedOptions = options ? util.clone(options) : {};

        if (!options.baseUrl) {
            extendedOptions.baseUrl = url;
        }

        return {
            document: d,
            options: extendedOptions,
        };
    };

    var drawURL = function (url, canvas, options) {
        return browser.loadDocument(url, options).then(function (doc) {
            var workaround = workAroundFirefoxNotLoadingStylesheetStyles(
                doc,
                url,
                options
            );
            return module.drawDocument(
                workaround.document,
                canvas,
                workaround.options
            );
        });
    };

    /**
     * Draws a page to the canvas.
     * rasterizeHTML.drawURL( url [, canvas] [, options] ).then(function (result) { ... });
     */
    module.drawURL = function () {
        var url = arguments[0],
            optionalArguments = Array.prototype.slice.call(arguments, 1),
            params = util.parseOptionalParameters(optionalArguments);

        return drawURL(url, params.canvas, params.options);
    };

    return module;
})(util, browser, rasterize);

return rasterizeHTML;

}));
