diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2575b..f944604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Complete rewrite of library ##### Breaking Changes ##### * Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise` + * Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties. ##### New featues / fixes ##### * Add support for scaling canvas (defaults to device pixel ratio) diff --git a/src/Bounds.js b/src/Bounds.js index 34a64ee..d54d332 100644 --- a/src/Bounds.js +++ b/src/Bounds.js @@ -41,13 +41,22 @@ export class Bounds { this.height = h; } - static fromClientRect(clientRect: ClientRect): Bounds { - return new Bounds(clientRect.left, clientRect.top, clientRect.width, clientRect.height); + static fromClientRect(clientRect: ClientRect, scrollX: number, scrollY: number): Bounds { + return new Bounds( + clientRect.left + scrollX, + clientRect.top + scrollY, + clientRect.width, + clientRect.height + ); } } -export const parseBounds = (node: HTMLElement | SVGSVGElement): Bounds => { - return Bounds.fromClientRect(node.getBoundingClientRect()); +export const parseBounds = ( + node: HTMLElement | SVGSVGElement, + scrollX: number, + scrollY: number +): Bounds => { + return Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY); }; export const calculatePaddingBox = (bounds: Bounds, borders: Array): Bounds => { diff --git a/src/Clone.js b/src/Clone.js index ffd378a..3abd658 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -109,6 +109,8 @@ export class DocumentCloner { const iframeKey = generateIframeKey(); tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey); + const {width, height} = parseBounds(node, 0, 0); + this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options) .then(documentElement => { return this.renderer( @@ -123,11 +125,14 @@ export class DocumentCloner { removeContainer: this.options.removeContainer, scale: this.options.scale, target: new CanvasRenderer(), - type: 'view', + width, + height, + x: 0, + y: 0, windowWidth: documentElement.ownerDocument.defaultView.innerWidth, windowHeight: documentElement.ownerDocument.defaultView.innerHeight, - offsetX: documentElement.ownerDocument.defaultView.pageXOffset, - offsetY: documentElement.ownerDocument.defaultView.pageYOffset + scrollX: documentElement.ownerDocument.defaultView.pageXOffset, + scrollY: documentElement.ownerDocument.defaultView.pageYOffset }, this.logger.child(iframeKey) ); @@ -338,7 +343,7 @@ const getIframeDocumentElement = ( .then(html => createIframeContainer( node.ownerDocument, - parseBounds(node) + parseBounds(node, 0, 0) ).then(cloneIframeContainer => { const cloneWindow = cloneIframeContainer.contentWindow; const documentClone = cloneWindow.document; @@ -411,6 +416,8 @@ export const cloneWindow = ( renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*> ): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader]> => { const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer); + const scrollX = ownerDocument.defaultView.pageXOffset; + const scrollY = ownerDocument.defaultView.pageYOffset; return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => { const cloneWindow = cloneIframeContainer.contentWindow; @@ -422,16 +429,14 @@ export const cloneWindow = ( const iframeLoad = iframeLoader(cloneIframeContainer).then(() => { cloner.scrolledElements.forEach(initNode); - if (options.type === 'view') { - cloneWindow.scrollTo(bounds.left, bounds.top); - if ( - /(iPad|iPhone|iPod)/g.test(navigator.userAgent) && - (cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left) - ) { - documentClone.documentElement.style.top = -bounds.top + 'px'; - documentClone.documentElement.style.left = -bounds.left + 'px'; - documentClone.documentElement.style.position = 'absolute'; - } + cloneWindow.scrollTo(bounds.left, bounds.top); + if ( + /(iPad|iPhone|iPod)/g.test(navigator.userAgent) && + (cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left) + ) { + documentClone.documentElement.style.top = -bounds.top + 'px'; + documentClone.documentElement.style.left = -bounds.left + 'px'; + documentClone.documentElement.style.position = 'absolute'; } return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement || cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement || @@ -451,7 +456,7 @@ export const cloneWindow = ( documentClone.open(); documentClone.write(''); // Chrome scrolls the parent document for some reason after the write to the cloned window??? - restoreOwnerScroll(referenceElement.ownerDocument, bounds.left, bounds.top); + restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY); documentClone.replaceChild( documentClone.adoptNode(cloner.documentElement), documentClone.documentElement diff --git a/src/Feature.js b/src/Feature.js index a0117eb..4018b21 100644 --- a/src/Feature.js +++ b/src/Feature.js @@ -92,7 +92,7 @@ const testForeignObject = document => { const img = new Image(); const greenImageSrc = canvas.toDataURL(); img.src = greenImageSrc; - const svg = createForeignObjectSVG(size, size, img); + const svg = createForeignObjectSVG(size, size, 0, 0, img); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, size, size); @@ -108,7 +108,7 @@ const testForeignObject = document => { node.style.height = `${size}px`; // Firefox 55 does not render inline tags return isGreenPixel(data) - ? loadSerializedSVG(createForeignObjectSVG(size, size, node)) + ? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node)) : Promise.reject(false); }) .then(img => { diff --git a/src/NodeContainer.js b/src/NodeContainer.js index caa6907..3218ead 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -94,6 +94,8 @@ export default class NodeContainer { this.index = index; this.childNodes = []; const defaultView = node.ownerDocument.defaultView; + const scrollX = defaultView.pageXOffset; + const scrollY = defaultView.pageYOffset; const style = defaultView.getComputedStyle(node, null); const display = parseDisplay(style.display); @@ -138,7 +140,7 @@ export default class NodeContainer { // TODO move bound retrieval for all nodes to a later stage? if (node.tagName === 'IMG') { node.addEventListener('load', () => { - this.bounds = parseBounds(node); + this.bounds = parseBounds(node, scrollX, scrollY); this.curvedBounds = parseBoundCurves( this.bounds, this.style.border, @@ -147,7 +149,9 @@ export default class NodeContainer { }); } this.image = getImage(node, imageLoader); - this.bounds = IS_INPUT ? reformatInputBounds(parseBounds(node)) : parseBounds(node); + this.bounds = IS_INPUT + ? reformatInputBounds(parseBounds(node, scrollX, scrollY)) + : parseBounds(node, scrollX, scrollY); this.curvedBounds = parseBoundCurves( this.bounds, this.style.border, diff --git a/src/Renderer.js b/src/Renderer.js index d924eda..2407151 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -46,6 +46,8 @@ export type RenderOptions = { imageStore: ImageStore, fontMetrics: FontMetrics, logger: Logger, + x: number, + y: number, width: number, height: number }; diff --git a/src/TextBounds.js b/src/TextBounds.js index d4cf384..1b7f2af 100644 --- a/src/TextBounds.js +++ b/src/TextBounds.js @@ -33,16 +33,24 @@ export const parseTextBounds = ( const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value); const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints); const length = textList.length; + const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null; + const scrollX = defaultView ? defaultView.pageXOffset : 0; + const scrollY = defaultView ? defaultView.pageYOffset : 0; const textBounds = []; let offset = 0; for (let i = 0; i < length; i++) { let text = textList[i]; if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { - textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length))); + textBounds.push( + new TextBounds( + text, + getRangeBounds(node, offset, text.length, scrollX, scrollY) + ) + ); } else { const replacementNode = node.splitText(text.length); - textBounds.push(new TextBounds(text, getWrapperBounds(node))); + textBounds.push(new TextBounds(text, getWrapperBounds(node, scrollX, scrollY))); node = replacementNode; } } else if (!FEATURES.SUPPORT_RANGE_BOUNDS) { @@ -53,13 +61,13 @@ export const parseTextBounds = ( return textBounds; }; -const getWrapperBounds = (node: Text): Bounds => { +const getWrapperBounds = (node: Text, scrollX: number, scrollY: number): Bounds => { const wrapper = node.ownerDocument.createElement('html2canvaswrapper'); wrapper.appendChild(node.cloneNode(true)); const parentNode = node.parentNode; if (parentNode) { parentNode.replaceChild(wrapper, node); - const bounds = parseBounds(wrapper); + const bounds = parseBounds(wrapper, scrollX, scrollY); if (wrapper.firstChild) { parentNode.replaceChild(wrapper.firstChild, wrapper); } @@ -68,11 +76,17 @@ const getWrapperBounds = (node: Text): Bounds => { return new Bounds(0, 0, 0, 0); }; -const getRangeBounds = (node: Text, offset: number, length: number): Bounds => { +const getRangeBounds = ( + node: Text, + offset: number, + length: number, + scrollX: number, + scrollY: number +): Bounds => { const range = node.ownerDocument.createRange(); range.setStart(node, offset); range.setEnd(node, offset + length); - return Bounds.fromClientRect(range.getBoundingClientRect()); + return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY); }; const splitWords = (codePoints: Array): Array => { diff --git a/src/Window.js b/src/Window.js index 07a8b5e..46d52f7 100644 --- a/src/Window.js +++ b/src/Window.js @@ -10,7 +10,7 @@ import Renderer from './Renderer'; import ForeignObjectRenderer from './renderer/ForeignObjectRenderer'; import Feature from './Feature'; -import {Bounds, parseDocumentSize} from './Bounds'; +import {Bounds} from './Bounds'; import {cloneWindow, DocumentCloner} from './Clone'; import {FontMetrics} from './Font'; import Color, {TRANSPARENT} from './Color'; @@ -23,14 +23,12 @@ export const renderElement = ( const ownerDocument = element.ownerDocument; const windowBounds = new Bounds( - options.offsetX, - options.offsetY, + options.scrollX, + options.scrollY, options.windowWidth, options.windowHeight ); - const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument); - // http://www.w3.org/TR/css3-background/#special-backgrounds const documentBackgroundColor = ownerDocument.documentElement ? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor) @@ -60,10 +58,17 @@ export const renderElement = ( return cloner.imageLoader.ready().then(() => { const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement); return renderer.render({ - bounds, backgroundColor, logger, - scale: options.scale + scale: options.scale, + x: options.x, + y: options.y, + width: options.width, + height: options.height, + windowWidth: options.windowWidth, + windowHeight: options.windowHeight, + scrollX: options.scrollX, + scrollY: options.scrollY }); }); })(new DocumentCloner(element, options, logger, true, renderElement)) @@ -81,8 +86,6 @@ export const renderElement = ( const stack = NodeParser(clonedElement, imageLoader, logger); const clonedDocument = clonedElement.ownerDocument; - const width = bounds.width; - const height = bounds.height; if (backgroundColor === stack.container.style.background.backgroundColor) { stack.container.style.background.backgroundColor = TRANSPARENT; @@ -110,8 +113,10 @@ export const renderElement = ( imageStore, logger, scale: options.scale, - width, - height + x: options.x, + y: options.y, + width: options.width, + height: options.height }; if (Array.isArray(options.target)) { diff --git a/src/index.js b/src/index.js index 1ec3c4a..52a785c 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import type {RenderTarget} from './Renderer'; import CanvasRenderer from './renderer/CanvasRenderer'; import Logger from './Logger'; import {renderElement} from './Window'; +import {parseBounds, parseDocumentSize} from './Bounds'; export type Options = { async: ?boolean, @@ -17,11 +18,14 @@ export type Options = { removeContainer: ?boolean, scale: number, target: RenderTarget<*>, - type: ?string, + width: number, + height: number, + x: number, + y: number, + scrollX: number, + scrollY: number, windowWidth: number, - windowHeight: number, - offsetX: number, - offsetY: number + windowHeight: number }; const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { @@ -37,6 +41,15 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { const ownerDocument = element.ownerDocument; const defaultView = ownerDocument.defaultView; + const scrollX = defaultView.pageXOffset; + const scrollY = defaultView.pageYOffset; + + const isDocument = element.tagName === 'HTML' || element.tagName === 'BODY'; + + const {width, height, left, top} = isDocument + ? parseDocumentSize(ownerDocument) + : parseBounds(element, scrollX, scrollY); + const defaultOptions = { async: true, allowTaint: false, @@ -45,11 +58,14 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { removeContainer: true, scale: defaultView.devicePixelRatio || 1, target: new CanvasRenderer(config.canvas), - type: null, + x: left, + y: top, + width: Math.ceil(width), + height: Math.ceil(height), windowWidth: defaultView.innerWidth, windowHeight: defaultView.innerHeight, - offsetX: defaultView.pageXOffset, - offsetY: defaultView.pageYOffset + scrollX: defaultView.pageXOffset, + scrollY: defaultView.pageYOffset }; const result = renderElement(element, {...defaultOptions, ...config}, logger); diff --git a/src/renderer/CanvasRenderer.js b/src/renderer/CanvasRenderer.js index 6d2edff..cd860ae 100644 --- a/src/renderer/CanvasRenderer.js +++ b/src/renderer/CanvasRenderer.js @@ -37,9 +37,10 @@ export default class CanvasRenderer implements RenderTarget { this.canvas.style.height = `${options.height}px`; this.ctx.scale(this.options.scale, this.options.scale); + this.ctx.translate(-options.x, -options.y); this.ctx.textBaseline = 'bottom'; options.logger.log( - `Canvas renderer initialized (${options.width}x${options.height}) with scale ${this + `Canvas renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this .options.scale}` ); } diff --git a/src/renderer/ForeignObjectRenderer.js b/src/renderer/ForeignObjectRenderer.js index 10a7f40..4c00eec 100644 --- a/src/renderer/ForeignObjectRenderer.js +++ b/src/renderer/ForeignObjectRenderer.js @@ -12,31 +12,41 @@ export default class ForeignObjectRenderer { this.options = options; this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d'); - this.canvas.width = Math.floor(options.bounds.width * options.scale); - this.canvas.height = Math.floor(options.bounds.height * options.scale); - this.canvas.style.width = `${options.bounds.width}px`; - this.canvas.style.height = `${options.bounds.height}px`; + this.canvas.width = Math.floor(options.width * options.scale); + this.canvas.height = Math.floor(options.height * options.scale); + this.canvas.style.width = `${options.width}px`; + this.canvas.style.height = `${options.height}px`; this.ctx.scale(this.options.scale, this.options.scale); - options.logger.log(`ForeignObject renderer initialized with scale ${this.options.scale}`); + options.logger.log( + `ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${this + .options.scale}` + ); const svg = createForeignObjectSVG( - options.bounds.width, - options.bounds.height, + Math.max(options.windowWidth, options.width), + Math.max(options.windowHeight, options.height), + options.scrollX, + options.scrollY, this.element ); - return loadSerializedSVG(svg).then(img => { if (options.backgroundColor) { this.ctx.fillStyle = options.backgroundColor.toString(); - this.ctx.fillRect(0, 0, options.bounds.width, options.bounds.height); + this.ctx.fillRect(0, 0, options.width, options.height); } - this.ctx.drawImage(img, 0, 0); + this.ctx.drawImage(img, -options.x, -options.y); return this.canvas; }); } } -export const createForeignObjectSVG = (width: number, height: number, node: Node) => { +export const createForeignObjectSVG = ( + width: number, + height: number, + x: number, + y: number, + node: Node +) => { const xmlns = 'http://www.w3.org/2000/svg'; const svg = document.createElementNS(xmlns, 'svg'); const foreignObject = document.createElementNS(xmlns, 'foreignObject'); @@ -45,6 +55,8 @@ export const createForeignObjectSVG = (width: number, height: number, node: Node foreignObject.setAttributeNS(null, 'width', '100%'); foreignObject.setAttributeNS(null, 'height', '100%'); + foreignObject.setAttributeNS(null, 'x', x); + foreignObject.setAttributeNS(null, 'y', y); foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true'); svg.appendChild(foreignObject); diff --git a/tests/reftests/options/crop.html b/tests/reftests/options/crop.html new file mode 100644 index 0000000..546a607 --- /dev/null +++ b/tests/reftests/options/crop.html @@ -0,0 +1,37 @@ + + + + crop test + + + + + + + + +
+ great success +
+ + diff --git a/tests/reftests/options/element.html b/tests/reftests/options/element.html new file mode 100644 index 0000000..681e73b --- /dev/null +++ b/tests/reftests/options/element.html @@ -0,0 +1,33 @@ + + + + element render test + + + + + + + +
+ great success +
+ + + diff --git a/tests/reftests/options/scroll.html b/tests/reftests/options/scroll.html new file mode 100644 index 0000000..eb43611 --- /dev/null +++ b/tests/reftests/options/scroll.html @@ -0,0 +1,54 @@ + + + + scroll test + + + + + + + + +
+ great success +
+ +
+ fixed great success +
+ + diff --git a/tests/test.js b/tests/test.js index f2d49ef..6e8f5b4 100644 --- a/tests/test.js +++ b/tests/test.js @@ -143,7 +143,7 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1; }; })(jQuery); - h2cSelector = [document.documentElement]; + h2cSelector = typeof h2cSelector === 'undefined' ? [document.documentElement] : h2cSelector; if (window.setUp) { window.setUp(); diff --git a/tests/testrunner.js b/tests/testrunner.js index bfe6f0e..30117c3 100644 --- a/tests/testrunner.js +++ b/tests/testrunner.js @@ -115,7 +115,7 @@ const assertPath = (result, expected, desc) => { }); it('Should render untainted canvas', () => { return testContainer.contentWindow - .html2canvas(testContainer.contentWindow.document.documentElement, { + .html2canvas(testContainer.contentWindow.forceElement || testContainer.contentWindow.document.documentElement, { removeContainer: true, backgroundColor: '#ffffff', proxy: 'http://localhost:8081/proxy',