mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Implementing cropping and dimension options for rendering (Fix #1230)
This commit is contained in:
parent
ae47d901a1
commit
53dd885279
@ -4,6 +4,7 @@
|
|||||||
* Complete rewrite of library
|
* Complete rewrite of library
|
||||||
##### Breaking Changes #####
|
##### Breaking Changes #####
|
||||||
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
|
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
|
||||||
|
* Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties.
|
||||||
|
|
||||||
##### New featues / fixes #####
|
##### New featues / fixes #####
|
||||||
* Add support for scaling canvas (defaults to device pixel ratio)
|
* Add support for scaling canvas (defaults to device pixel ratio)
|
||||||
|
@ -41,13 +41,22 @@ export class Bounds {
|
|||||||
this.height = h;
|
this.height = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromClientRect(clientRect: ClientRect): Bounds {
|
static fromClientRect(clientRect: ClientRect, scrollX: number, scrollY: number): Bounds {
|
||||||
return new Bounds(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
|
return new Bounds(
|
||||||
|
clientRect.left + scrollX,
|
||||||
|
clientRect.top + scrollY,
|
||||||
|
clientRect.width,
|
||||||
|
clientRect.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseBounds = (node: HTMLElement | SVGSVGElement): Bounds => {
|
export const parseBounds = (
|
||||||
return Bounds.fromClientRect(node.getBoundingClientRect());
|
node: HTMLElement | SVGSVGElement,
|
||||||
|
scrollX: number,
|
||||||
|
scrollY: number
|
||||||
|
): Bounds => {
|
||||||
|
return Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculatePaddingBox = (bounds: Bounds, borders: Array<Border>): Bounds => {
|
export const calculatePaddingBox = (bounds: Bounds, borders: Array<Border>): Bounds => {
|
||||||
|
35
src/Clone.js
35
src/Clone.js
@ -109,6 +109,8 @@ export class DocumentCloner {
|
|||||||
const iframeKey = generateIframeKey();
|
const iframeKey = generateIframeKey();
|
||||||
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
|
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
|
||||||
|
|
||||||
|
const {width, height} = parseBounds(node, 0, 0);
|
||||||
|
|
||||||
this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
|
this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
|
||||||
.then(documentElement => {
|
.then(documentElement => {
|
||||||
return this.renderer(
|
return this.renderer(
|
||||||
@ -123,11 +125,14 @@ export class DocumentCloner {
|
|||||||
removeContainer: this.options.removeContainer,
|
removeContainer: this.options.removeContainer,
|
||||||
scale: this.options.scale,
|
scale: this.options.scale,
|
||||||
target: new CanvasRenderer(),
|
target: new CanvasRenderer(),
|
||||||
type: 'view',
|
width,
|
||||||
|
height,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
|
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
|
||||||
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
|
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
|
||||||
offsetX: documentElement.ownerDocument.defaultView.pageXOffset,
|
scrollX: documentElement.ownerDocument.defaultView.pageXOffset,
|
||||||
offsetY: documentElement.ownerDocument.defaultView.pageYOffset
|
scrollY: documentElement.ownerDocument.defaultView.pageYOffset
|
||||||
},
|
},
|
||||||
this.logger.child(iframeKey)
|
this.logger.child(iframeKey)
|
||||||
);
|
);
|
||||||
@ -338,7 +343,7 @@ const getIframeDocumentElement = (
|
|||||||
.then(html =>
|
.then(html =>
|
||||||
createIframeContainer(
|
createIframeContainer(
|
||||||
node.ownerDocument,
|
node.ownerDocument,
|
||||||
parseBounds(node)
|
parseBounds(node, 0, 0)
|
||||||
).then(cloneIframeContainer => {
|
).then(cloneIframeContainer => {
|
||||||
const cloneWindow = cloneIframeContainer.contentWindow;
|
const cloneWindow = cloneIframeContainer.contentWindow;
|
||||||
const documentClone = cloneWindow.document;
|
const documentClone = cloneWindow.document;
|
||||||
@ -411,6 +416,8 @@ export const cloneWindow = (
|
|||||||
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
|
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
|
||||||
): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => {
|
): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => {
|
||||||
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
|
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 => {
|
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
|
||||||
const cloneWindow = cloneIframeContainer.contentWindow;
|
const cloneWindow = cloneIframeContainer.contentWindow;
|
||||||
@ -422,16 +429,14 @@ export const cloneWindow = (
|
|||||||
|
|
||||||
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
|
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
|
||||||
cloner.scrolledElements.forEach(initNode);
|
cloner.scrolledElements.forEach(initNode);
|
||||||
if (options.type === 'view') {
|
cloneWindow.scrollTo(bounds.left, bounds.top);
|
||||||
cloneWindow.scrollTo(bounds.left, bounds.top);
|
if (
|
||||||
if (
|
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
||||||
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
|
||||||
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
|
) {
|
||||||
) {
|
documentClone.documentElement.style.top = -bounds.top + 'px';
|
||||||
documentClone.documentElement.style.top = -bounds.top + 'px';
|
documentClone.documentElement.style.left = -bounds.left + 'px';
|
||||||
documentClone.documentElement.style.left = -bounds.left + 'px';
|
documentClone.documentElement.style.position = 'absolute';
|
||||||
documentClone.documentElement.style.position = 'absolute';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
|
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
|
||||||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
|
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
|
||||||
@ -451,7 +456,7 @@ export const cloneWindow = (
|
|||||||
documentClone.open();
|
documentClone.open();
|
||||||
documentClone.write('<!DOCTYPE html><html></html>');
|
documentClone.write('<!DOCTYPE html><html></html>');
|
||||||
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
// 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.replaceChild(
|
||||||
documentClone.adoptNode(cloner.documentElement),
|
documentClone.adoptNode(cloner.documentElement),
|
||||||
documentClone.documentElement
|
documentClone.documentElement
|
||||||
|
@ -92,7 +92,7 @@ const testForeignObject = document => {
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
const greenImageSrc = canvas.toDataURL();
|
const greenImageSrc = canvas.toDataURL();
|
||||||
img.src = greenImageSrc;
|
img.src = greenImageSrc;
|
||||||
const svg = createForeignObjectSVG(size, size, img);
|
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||||
ctx.fillStyle = 'red';
|
ctx.fillStyle = 'red';
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ const testForeignObject = document => {
|
|||||||
node.style.height = `${size}px`;
|
node.style.height = `${size}px`;
|
||||||
// Firefox 55 does not render inline <img /> tags
|
// Firefox 55 does not render inline <img /> tags
|
||||||
return isGreenPixel(data)
|
return isGreenPixel(data)
|
||||||
? loadSerializedSVG(createForeignObjectSVG(size, size, node))
|
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||||
: Promise.reject(false);
|
: Promise.reject(false);
|
||||||
})
|
})
|
||||||
.then(img => {
|
.then(img => {
|
||||||
|
@ -94,6 +94,8 @@ export default class NodeContainer {
|
|||||||
this.index = index;
|
this.index = index;
|
||||||
this.childNodes = [];
|
this.childNodes = [];
|
||||||
const defaultView = node.ownerDocument.defaultView;
|
const defaultView = node.ownerDocument.defaultView;
|
||||||
|
const scrollX = defaultView.pageXOffset;
|
||||||
|
const scrollY = defaultView.pageYOffset;
|
||||||
const style = defaultView.getComputedStyle(node, null);
|
const style = defaultView.getComputedStyle(node, null);
|
||||||
const display = parseDisplay(style.display);
|
const display = parseDisplay(style.display);
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ export default class NodeContainer {
|
|||||||
// TODO move bound retrieval for all nodes to a later stage?
|
// TODO move bound retrieval for all nodes to a later stage?
|
||||||
if (node.tagName === 'IMG') {
|
if (node.tagName === 'IMG') {
|
||||||
node.addEventListener('load', () => {
|
node.addEventListener('load', () => {
|
||||||
this.bounds = parseBounds(node);
|
this.bounds = parseBounds(node, scrollX, scrollY);
|
||||||
this.curvedBounds = parseBoundCurves(
|
this.curvedBounds = parseBoundCurves(
|
||||||
this.bounds,
|
this.bounds,
|
||||||
this.style.border,
|
this.style.border,
|
||||||
@ -147,7 +149,9 @@ export default class NodeContainer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.image = getImage(node, imageLoader);
|
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.curvedBounds = parseBoundCurves(
|
||||||
this.bounds,
|
this.bounds,
|
||||||
this.style.border,
|
this.style.border,
|
||||||
|
@ -46,6 +46,8 @@ export type RenderOptions = {
|
|||||||
imageStore: ImageStore<ImageElement>,
|
imageStore: ImageStore<ImageElement>,
|
||||||
fontMetrics: FontMetrics,
|
fontMetrics: FontMetrics,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number
|
height: number
|
||||||
};
|
};
|
||||||
|
@ -33,16 +33,24 @@ export const parseTextBounds = (
|
|||||||
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value);
|
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value);
|
||||||
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints);
|
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints);
|
||||||
const length = textList.length;
|
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 = [];
|
const textBounds = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
let text = textList[i];
|
let text = textList[i];
|
||||||
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
|
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
|
||||||
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
|
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 {
|
} else {
|
||||||
const replacementNode = node.splitText(text.length);
|
const replacementNode = node.splitText(text.length);
|
||||||
textBounds.push(new TextBounds(text, getWrapperBounds(node)));
|
textBounds.push(new TextBounds(text, getWrapperBounds(node, scrollX, scrollY)));
|
||||||
node = replacementNode;
|
node = replacementNode;
|
||||||
}
|
}
|
||||||
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
|
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||||
@ -53,13 +61,13 @@ export const parseTextBounds = (
|
|||||||
return textBounds;
|
return textBounds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWrapperBounds = (node: Text): Bounds => {
|
const getWrapperBounds = (node: Text, scrollX: number, scrollY: number): Bounds => {
|
||||||
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
||||||
wrapper.appendChild(node.cloneNode(true));
|
wrapper.appendChild(node.cloneNode(true));
|
||||||
const parentNode = node.parentNode;
|
const parentNode = node.parentNode;
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
parentNode.replaceChild(wrapper, node);
|
parentNode.replaceChild(wrapper, node);
|
||||||
const bounds = parseBounds(wrapper);
|
const bounds = parseBounds(wrapper, scrollX, scrollY);
|
||||||
if (wrapper.firstChild) {
|
if (wrapper.firstChild) {
|
||||||
parentNode.replaceChild(wrapper.firstChild, wrapper);
|
parentNode.replaceChild(wrapper.firstChild, wrapper);
|
||||||
}
|
}
|
||||||
@ -68,11 +76,17 @@ const getWrapperBounds = (node: Text): Bounds => {
|
|||||||
return new Bounds(0, 0, 0, 0);
|
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();
|
const range = node.ownerDocument.createRange();
|
||||||
range.setStart(node, offset);
|
range.setStart(node, offset);
|
||||||
range.setEnd(node, offset + length);
|
range.setEnd(node, offset + length);
|
||||||
return Bounds.fromClientRect(range.getBoundingClientRect());
|
return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitWords = (codePoints: Array<number>): Array<string> => {
|
const splitWords = (codePoints: Array<number>): Array<string> => {
|
||||||
|
@ -10,7 +10,7 @@ import Renderer from './Renderer';
|
|||||||
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
|
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
|
||||||
|
|
||||||
import Feature from './Feature';
|
import Feature from './Feature';
|
||||||
import {Bounds, parseDocumentSize} from './Bounds';
|
import {Bounds} from './Bounds';
|
||||||
import {cloneWindow, DocumentCloner} from './Clone';
|
import {cloneWindow, DocumentCloner} from './Clone';
|
||||||
import {FontMetrics} from './Font';
|
import {FontMetrics} from './Font';
|
||||||
import Color, {TRANSPARENT} from './Color';
|
import Color, {TRANSPARENT} from './Color';
|
||||||
@ -23,14 +23,12 @@ export const renderElement = (
|
|||||||
const ownerDocument = element.ownerDocument;
|
const ownerDocument = element.ownerDocument;
|
||||||
|
|
||||||
const windowBounds = new Bounds(
|
const windowBounds = new Bounds(
|
||||||
options.offsetX,
|
options.scrollX,
|
||||||
options.offsetY,
|
options.scrollY,
|
||||||
options.windowWidth,
|
options.windowWidth,
|
||||||
options.windowHeight
|
options.windowHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const bounds = options.type === 'view' ? windowBounds : parseDocumentSize(ownerDocument);
|
|
||||||
|
|
||||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||||
const documentBackgroundColor = ownerDocument.documentElement
|
const documentBackgroundColor = ownerDocument.documentElement
|
||||||
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
|
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
|
||||||
@ -60,10 +58,17 @@ export const renderElement = (
|
|||||||
return cloner.imageLoader.ready().then(() => {
|
return cloner.imageLoader.ready().then(() => {
|
||||||
const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement);
|
const renderer = new ForeignObjectRenderer(cloner.clonedReferenceElement);
|
||||||
return renderer.render({
|
return renderer.render({
|
||||||
bounds,
|
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
logger,
|
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))
|
})(new DocumentCloner(element, options, logger, true, renderElement))
|
||||||
@ -81,8 +86,6 @@ export const renderElement = (
|
|||||||
|
|
||||||
const stack = NodeParser(clonedElement, imageLoader, logger);
|
const stack = NodeParser(clonedElement, imageLoader, logger);
|
||||||
const clonedDocument = clonedElement.ownerDocument;
|
const clonedDocument = clonedElement.ownerDocument;
|
||||||
const width = bounds.width;
|
|
||||||
const height = bounds.height;
|
|
||||||
|
|
||||||
if (backgroundColor === stack.container.style.background.backgroundColor) {
|
if (backgroundColor === stack.container.style.background.backgroundColor) {
|
||||||
stack.container.style.background.backgroundColor = TRANSPARENT;
|
stack.container.style.background.backgroundColor = TRANSPARENT;
|
||||||
@ -110,8 +113,10 @@ export const renderElement = (
|
|||||||
imageStore,
|
imageStore,
|
||||||
logger,
|
logger,
|
||||||
scale: options.scale,
|
scale: options.scale,
|
||||||
width,
|
x: options.x,
|
||||||
height
|
y: options.y,
|
||||||
|
width: options.width,
|
||||||
|
height: options.height
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Array.isArray(options.target)) {
|
if (Array.isArray(options.target)) {
|
||||||
|
30
src/index.js
30
src/index.js
@ -6,6 +6,7 @@ import type {RenderTarget} from './Renderer';
|
|||||||
import CanvasRenderer from './renderer/CanvasRenderer';
|
import CanvasRenderer from './renderer/CanvasRenderer';
|
||||||
import Logger from './Logger';
|
import Logger from './Logger';
|
||||||
import {renderElement} from './Window';
|
import {renderElement} from './Window';
|
||||||
|
import {parseBounds, parseDocumentSize} from './Bounds';
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
async: ?boolean,
|
async: ?boolean,
|
||||||
@ -17,11 +18,14 @@ export type Options = {
|
|||||||
removeContainer: ?boolean,
|
removeContainer: ?boolean,
|
||||||
scale: number,
|
scale: number,
|
||||||
target: RenderTarget<*>,
|
target: RenderTarget<*>,
|
||||||
type: ?string,
|
width: number,
|
||||||
|
height: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
scrollX: number,
|
||||||
|
scrollY: number,
|
||||||
windowWidth: number,
|
windowWidth: number,
|
||||||
windowHeight: number,
|
windowHeight: number
|
||||||
offsetX: number,
|
|
||||||
offsetY: number
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
|
const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
|
||||||
@ -37,6 +41,15 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
|
|||||||
const ownerDocument = element.ownerDocument;
|
const ownerDocument = element.ownerDocument;
|
||||||
const defaultView = ownerDocument.defaultView;
|
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 = {
|
const defaultOptions = {
|
||||||
async: true,
|
async: true,
|
||||||
allowTaint: false,
|
allowTaint: false,
|
||||||
@ -45,11 +58,14 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
|
|||||||
removeContainer: true,
|
removeContainer: true,
|
||||||
scale: defaultView.devicePixelRatio || 1,
|
scale: defaultView.devicePixelRatio || 1,
|
||||||
target: new CanvasRenderer(config.canvas),
|
target: new CanvasRenderer(config.canvas),
|
||||||
type: null,
|
x: left,
|
||||||
|
y: top,
|
||||||
|
width: Math.ceil(width),
|
||||||
|
height: Math.ceil(height),
|
||||||
windowWidth: defaultView.innerWidth,
|
windowWidth: defaultView.innerWidth,
|
||||||
windowHeight: defaultView.innerHeight,
|
windowHeight: defaultView.innerHeight,
|
||||||
offsetX: defaultView.pageXOffset,
|
scrollX: defaultView.pageXOffset,
|
||||||
offsetY: defaultView.pageYOffset
|
scrollY: defaultView.pageYOffset
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = renderElement(element, {...defaultOptions, ...config}, logger);
|
const result = renderElement(element, {...defaultOptions, ...config}, logger);
|
||||||
|
@ -37,9 +37,10 @@ export default class CanvasRenderer implements RenderTarget<HTMLCanvasElement> {
|
|||||||
this.canvas.style.height = `${options.height}px`;
|
this.canvas.style.height = `${options.height}px`;
|
||||||
|
|
||||||
this.ctx.scale(this.options.scale, this.options.scale);
|
this.ctx.scale(this.options.scale, this.options.scale);
|
||||||
|
this.ctx.translate(-options.x, -options.y);
|
||||||
this.ctx.textBaseline = 'bottom';
|
this.ctx.textBaseline = 'bottom';
|
||||||
options.logger.log(
|
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}`
|
.options.scale}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,31 +12,41 @@ export default class ForeignObjectRenderer {
|
|||||||
this.options = options;
|
this.options = options;
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
this.ctx = this.canvas.getContext('2d');
|
this.ctx = this.canvas.getContext('2d');
|
||||||
this.canvas.width = Math.floor(options.bounds.width * options.scale);
|
this.canvas.width = Math.floor(options.width * options.scale);
|
||||||
this.canvas.height = Math.floor(options.bounds.height * options.scale);
|
this.canvas.height = Math.floor(options.height * options.scale);
|
||||||
this.canvas.style.width = `${options.bounds.width}px`;
|
this.canvas.style.width = `${options.width}px`;
|
||||||
this.canvas.style.height = `${options.bounds.height}px`;
|
this.canvas.style.height = `${options.height}px`;
|
||||||
this.ctx.scale(this.options.scale, this.options.scale);
|
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(
|
const svg = createForeignObjectSVG(
|
||||||
options.bounds.width,
|
Math.max(options.windowWidth, options.width),
|
||||||
options.bounds.height,
|
Math.max(options.windowHeight, options.height),
|
||||||
|
options.scrollX,
|
||||||
|
options.scrollY,
|
||||||
this.element
|
this.element
|
||||||
);
|
);
|
||||||
|
|
||||||
return loadSerializedSVG(svg).then(img => {
|
return loadSerializedSVG(svg).then(img => {
|
||||||
if (options.backgroundColor) {
|
if (options.backgroundColor) {
|
||||||
this.ctx.fillStyle = options.backgroundColor.toString();
|
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;
|
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 xmlns = 'http://www.w3.org/2000/svg';
|
||||||
const svg = document.createElementNS(xmlns, 'svg');
|
const svg = document.createElementNS(xmlns, 'svg');
|
||||||
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
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, 'width', '100%');
|
||||||
foreignObject.setAttributeNS(null, 'height', '100%');
|
foreignObject.setAttributeNS(null, 'height', '100%');
|
||||||
|
foreignObject.setAttributeNS(null, 'x', x);
|
||||||
|
foreignObject.setAttributeNS(null, 'y', y);
|
||||||
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
||||||
svg.appendChild(foreignObject);
|
svg.appendChild(foreignObject);
|
||||||
|
|
||||||
|
37
tests/reftests/options/crop.html
Normal file
37
tests/reftests/options/crop.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>crop test</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<script>
|
||||||
|
h2cOptions = {
|
||||||
|
x: 250,
|
||||||
|
y: 250,
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="../../test.js"></script>
|
||||||
|
<style>
|
||||||
|
#div1 {
|
||||||
|
position: absolute;
|
||||||
|
left: 250px;
|
||||||
|
top: 250px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="div1">
|
||||||
|
great success
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
tests/reftests/options/element.html
Normal file
33
tests/reftests/options/element.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>element render test</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<script type="text/javascript" src="../../test.js"></script>
|
||||||
|
<style>
|
||||||
|
#div1 {
|
||||||
|
position: absolute;
|
||||||
|
left: 250px;
|
||||||
|
top: 250px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="div1">
|
||||||
|
great success
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var forceElement = document.querySelector('#div1');
|
||||||
|
h2cSelector = forceElement;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
54
tests/reftests/options/scroll.html
Normal file
54
tests/reftests/options/scroll.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>scroll test</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<script>
|
||||||
|
h2cOptions = {
|
||||||
|
x: 250,
|
||||||
|
y: 250,
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
scrollX: 250,
|
||||||
|
scrollY: 250
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="../../test.js"></script>
|
||||||
|
<style>
|
||||||
|
#div1 {
|
||||||
|
position: absolute;
|
||||||
|
left: 350px;
|
||||||
|
top: 250px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#div2 {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, html {
|
||||||
|
background: red;
|
||||||
|
height: 4000px;
|
||||||
|
width: 4000px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="div1">
|
||||||
|
great success
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="div2">
|
||||||
|
fixed great success
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -143,7 +143,7 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1;
|
|||||||
};
|
};
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|
||||||
h2cSelector = [document.documentElement];
|
h2cSelector = typeof h2cSelector === 'undefined' ? [document.documentElement] : h2cSelector;
|
||||||
|
|
||||||
if (window.setUp) {
|
if (window.setUp) {
|
||||||
window.setUp();
|
window.setUp();
|
||||||
|
@ -115,7 +115,7 @@ const assertPath = (result, expected, desc) => {
|
|||||||
});
|
});
|
||||||
it('Should render untainted canvas', () => {
|
it('Should render untainted canvas', () => {
|
||||||
return testContainer.contentWindow
|
return testContainer.contentWindow
|
||||||
.html2canvas(testContainer.contentWindow.document.documentElement, {
|
.html2canvas(testContainer.contentWindow.forceElement || testContainer.contentWindow.document.documentElement, {
|
||||||
removeContainer: true,
|
removeContainer: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
proxy: 'http://localhost:8081/proxy',
|
proxy: 'http://localhost:8081/proxy',
|
||||||
|
Loading…
Reference in New Issue
Block a user