From 2b8389cb64820b47300aae6b229aa1f64973a7c5 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sun, 19 Jan 2014 21:05:07 +0200 Subject: [PATCH] Make image loading to work on top of Promises/polyfill --- .jshintrc | 3 +- Gruntfile.js | 11 +---- src/core.js | 13 +++++- src/imagecontainer.js | 14 +++++++ src/imageloader.js | 56 +++++++++++++++++++++++++ src/nodecontainer.js | 98 +++++++++++++++++++++++++++++++++++++++++++ src/support.js | 5 +++ tests/test.js | 4 +- 8 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 src/imagecontainer.js create mode 100644 src/imageloader.js diff --git a/.jshintrc b/.jshintrc index 49f3650..b6120bd 100644 --- a/.jshintrc +++ b/.jshintrc @@ -13,5 +13,6 @@ "globals": { "jQuery": true }, - "predef": ["NodeContainer", "StackingContext", "TextContainer", "CanvasRenderer", "Renderer", "Support"] + "predef": ["NodeContainer", "StackingContext", "TextContainer", "ImageLoader", "CanvasRenderer", "Renderer", "Support", "bind", "Promise", + "ImageContainer", "ProxyImageContainer", "DummyImageContainer"] } diff --git a/Gruntfile.js b/Gruntfile.js index e6034e7..d8f3107 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -21,16 +21,7 @@ module.exports = function(grunt) { concat: { dist: { src: [ - 'src/Core.js', - 'src/Font.js', - 'src/Generate.js', - 'src/Queue.js', - 'src/Parse.js', - 'src/Preload.js', - 'src/Renderer.js', - 'src/Support.js', - 'src/Util.js', - 'src/renderers/Canvas.js' + 'src/**/*.js' ], dest: 'build/<%= pkg.name %>.js' }, diff --git a/src/core.js b/src/core.js index 2d8b0ee..589b498 100644 --- a/src/core.js +++ b/src/core.js @@ -34,13 +34,16 @@ function NodeParser(element, renderer, options) { this.range = null; this.stack = new StackingContext(true, 1, element.ownerDocument, null); var parent = new NodeContainer(element, null); - parent.blockFormattingContext = parent; this.nodes = [parent].concat(this.getChildren(parent)).filter(function(container) { return container.visible = container.isElementVisible(); }); + this.imageLoader = new ImageLoader(this.nodes.filter(isElement), options, this.support); this.createStackingContexts(); this.sortStackingContexts(this.stack); - this.parse(this.stack); + + this.imageLoader.ready.then(bind(function() { + this.parse(this.stack); + }, this)); } NodeParser.prototype.getChildren = function(parentContainer) { @@ -518,3 +521,9 @@ function zIndexSort(a, b) { function hasOpacity(container) { return container.css("opacity") < 1; } + +function bind(callback, context) { + return function() { + return callback.apply(context, arguments); + }; +} diff --git a/src/imagecontainer.js b/src/imagecontainer.js new file mode 100644 index 0000000..1b77c6d --- /dev/null +++ b/src/imagecontainer.js @@ -0,0 +1,14 @@ +function ImageContainer(src, cors) { + this.src = src; + this.image = new Image(); + var image = this.image; + this.promise = new Promise(function(resolve, reject) { + image.onload = resolve; + image.onerror = reject; + if (cors) { + image.crossOrigin = "anonymous"; + } + image.src = src; + }); +} + diff --git a/src/imageloader.js b/src/imageloader.js new file mode 100644 index 0000000..b0a2ad4 --- /dev/null +++ b/src/imageloader.js @@ -0,0 +1,56 @@ +function ImageLoader(nodes, options, support) { + this.link = null; + this.options = options; + this.support = support; + this.origin = window.location.protocol + window.location.host; + this.images = nodes.reduce(bind(this.findImages, this), []); + this.ready = Promise.all(this.images.map(this.getPromise)); +} + +ImageLoader.prototype.findImages = function(images, container) { + var backgrounds = container.parseBackgroundImages(); + var backgroundImages = backgrounds.filter(this.isImageBackground).map(this.getBackgroundUrl).filter(this.imageExists(images)).map(this.loadImage, this); + return images.concat(backgroundImages); +}; + +ImageLoader.prototype.getBackgroundUrl = function(imageData) { + return imageData.args[0]; +}; + +ImageLoader.prototype.isImageBackground = function(imageData) { + return imageData.method === "url"; +}; + +ImageLoader.prototype.loadImage = function(src) { + if (src.match(/data:image\/.*;base64,/i)) { + return new ImageContainer(src.replace(/url\(['"]{0,}|['"]{0,}\)$/ig, ''), false); + } else if (this.isSameOrigin(src) || this.options.allowTaint === true) { + return new ImageContainer(src, false); + } else if (this.support.cors && !this.options.allowTaint && this.options.useCORS) { + return new ImageContainer(src, true); + } else if (this.options.proxy) { + return new ProxyImageContainer(src); + } else { + return new DummyImageContainer(src); + } +}; + +ImageLoader.prototype.imageExists = function(images) { + return function(newImage) { + return !images.some(function(image) { + return image.src !== newImage.src; + }); + }; +}; + +ImageLoader.prototype.isSameOrigin = function(url) { + var link = this.link || (this.link = document.createElement("a")); + link.href = url; + link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/ + var origin = link.protocol + link.host; + return (origin === this.origin); +}; + +ImageLoader.prototype.getPromise = function(container) { + return container.promise; +}; diff --git a/src/nodecontainer.js b/src/nodecontainer.js index 6f09cc0..3468eb9 100644 --- a/src/nodecontainer.js +++ b/src/nodecontainer.js @@ -47,3 +47,101 @@ NodeContainer.prototype.fontWeight = function() { } return weight; }; + +NodeContainer.prototype.parseBackgroundImages = function() { + var whitespace = ' \r\n\t', + method, definition, prefix, prefix_i, block, results = [], + mode = 0, numParen = 0, quote, args; + var appendResult = function() { + if(method) { + if (definition.substr(0, 1) === '"') { + definition = definition.substr(1, definition.length - 2); + } + if (definition) { + args.push(definition); + } + if (method.substr(0, 1) === '-' && (prefix_i = method.indexOf('-', 1 ) + 1) > 0) { + prefix = method.substr(0, prefix_i); + method = method.substr(prefix_i); + } + results.push({ + prefix: prefix, + method: method.toLowerCase(), + value: block, + args: args, + image: null + }); + } + args = []; + method = prefix = definition = block = ''; + }; + args = []; + method = prefix = definition = block = ''; + this.css("backgroundImage").split("").forEach(function(c) { + if (mode === 0 && whitespace.indexOf(c) > -1) { + return; + } + switch(c) { + case '"': + if(!quote) { + quote = c; + } + else if(quote === c) { + quote = null; + } + break; + case '(': + if(quote) { + break; + } else if(mode === 0) { + mode = 1; + block += c; + return; + } else { + numParen++; + } + break; + case ')': + if (quote) { + break; + } else if(mode === 1) { + if(numParen === 0) { + mode = 0; + block += c; + appendResult(); + return; + } else { + numParen--; + } + } + break; + + case ',': + if (quote) { + break; + } else if(mode === 0) { + appendResult(); + return; + } else if (mode === 1) { + if (numParen === 0 && !method.match(/^url$/i)) { + args.push(definition); + definition = ''; + block += c; + return; + } + } + break; + } + + block += c; + if (mode === 0) { + method += c; + } else { + definition += c; + } + }); + + appendResult(); + + return results; +}; diff --git a/src/support.js b/src/support.js index c66f742..f4a95c5 100644 --- a/src/support.js +++ b/src/support.js @@ -1,5 +1,6 @@ function Support() { this.rangeBounds = this.testRangeBounds(); + this.cors = this.testCORS(); } Support.prototype.testRangeBounds = function() { @@ -26,3 +27,7 @@ Support.prototype.testRangeBounds = function() { return support; }; + +Support.prototype.testCORS = function() { + return typeof((new Image()).crossOrigin) !== "undefined"; +}; diff --git a/tests/test.js b/tests/test.js index 27fdda5..c7641d1 100644 --- a/tests/test.js +++ b/tests/test.js @@ -11,9 +11,9 @@ var h2cSelector, h2cOptions; document.write(srcStart + '/tests/assets/jquery-1.6.2.js' + scrEnd); document.write(srcStart + '/tests/assets/jquery.plugin.html2canvas.js' + scrEnd); - var html2canvas = ['nodecontainer', 'stackingcontext', 'textcontainer', 'support', 'core', 'renderer', 'renderers/canvas'], i; + var html2canvas = ['nodecontainer', 'stackingcontext', 'textcontainer', 'support', 'imagecontainer', 'imageloader', 'core', 'renderer', 'renderers/canvas'], i; for (i = 0; i < html2canvas.length; ++i) { - document.write(srcStart + '/new/' + html2canvas[i] + '.js?' + Math.random() + scrEnd); + document.write(srcStart + '/src/' + html2canvas[i] + '.js?' + Math.random() + scrEnd); } window.onload = function() { h2cSelector = [document.body];