2019-05-26 01:54:41 +03:00
|
|
|
import {Bounds, parseBounds, parseDocumentSize} from './css/layout/bounds';
|
|
|
|
import {color, Color, COLORS, isTransparent} from './css/types/color';
|
|
|
|
import {Parser} from './css/syntax/parser';
|
|
|
|
import {CloneOptions, DocumentCloner} from './dom/document-cloner';
|
|
|
|
import {isBodyElement, isHTMLElement, parseTree} from './dom/node-parser';
|
|
|
|
import {Logger} from './core/logger';
|
2019-05-30 07:11:50 +03:00
|
|
|
import {CacheStorage, ResourceOptions} from './core/cache-storage';
|
2019-05-26 01:54:41 +03:00
|
|
|
import {CanvasRenderer, RenderOptions} from './render/canvas/canvas-renderer';
|
|
|
|
import {ForeignObjectRenderer} from './render/canvas/foreignobject-renderer';
|
|
|
|
|
|
|
|
export type Options = CloneOptions &
|
2019-05-30 07:11:50 +03:00
|
|
|
RenderOptions &
|
|
|
|
ResourceOptions & {
|
2019-09-25 13:37:59 +03:00
|
|
|
backgroundColor: string | null;
|
2019-05-26 01:54:41 +03:00
|
|
|
foreignObjectRendering: boolean;
|
|
|
|
logging: boolean;
|
|
|
|
removeContainer?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
const parseColor = (value: string): Color => color.parse(Parser.create(value).parseComponentValue());
|
|
|
|
|
2019-05-30 07:11:50 +03:00
|
|
|
const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Promise<HTMLCanvasElement> => {
|
2019-05-26 01:54:41 +03:00
|
|
|
return renderElement(element, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default html2canvas;
|
|
|
|
|
2020-08-08 10:37:34 +03:00
|
|
|
if (typeof window !== 'undefined') {
|
2019-11-26 07:16:07 +03:00
|
|
|
CacheStorage.setContext(window);
|
|
|
|
}
|
2019-05-26 01:54:41 +03:00
|
|
|
|
2021-07-13 13:07:09 +03:00
|
|
|
let instanceCount = 1;
|
|
|
|
|
2019-05-30 07:11:50 +03:00
|
|
|
const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {
|
2021-07-14 15:21:57 +03:00
|
|
|
if (!element || typeof element !== 'object') {
|
2021-07-13 13:07:09 +03:00
|
|
|
return Promise.reject('Invalid element provided as first argument');
|
|
|
|
}
|
2019-05-26 01:54:41 +03:00
|
|
|
const ownerDocument = element.ownerDocument;
|
|
|
|
|
|
|
|
if (!ownerDocument) {
|
|
|
|
throw new Error(`Element is not attached to a Document`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultView = ownerDocument.defaultView;
|
|
|
|
|
|
|
|
if (!defaultView) {
|
|
|
|
throw new Error(`Document is not attached to a Window`);
|
|
|
|
}
|
|
|
|
|
2021-07-13 13:07:09 +03:00
|
|
|
const instanceName = `#${instanceCount++}`;
|
2019-05-30 07:11:50 +03:00
|
|
|
|
|
|
|
const {width, height, left, top} =
|
|
|
|
isBodyElement(element) || isHTMLElement(element) ? parseDocumentSize(ownerDocument) : parseBounds(element);
|
|
|
|
|
|
|
|
const defaultResourceOptions = {
|
2019-05-26 01:54:41 +03:00
|
|
|
allowTaint: false,
|
|
|
|
imageTimeout: 15000,
|
|
|
|
proxy: undefined,
|
2019-05-30 07:11:50 +03:00
|
|
|
useCORS: false
|
|
|
|
};
|
|
|
|
|
|
|
|
const resourceOptions: ResourceOptions = {...defaultResourceOptions, ...opts};
|
|
|
|
|
|
|
|
const defaultOptions = {
|
|
|
|
backgroundColor: '#ffffff',
|
|
|
|
cache: opts.cache ? opts.cache : CacheStorage.create(instanceName, resourceOptions),
|
|
|
|
logging: true,
|
2019-05-26 01:54:41 +03:00
|
|
|
removeContainer: true,
|
|
|
|
foreignObjectRendering: false,
|
|
|
|
scale: defaultView.devicePixelRatio || 1,
|
|
|
|
windowWidth: defaultView.innerWidth,
|
|
|
|
windowHeight: defaultView.innerHeight,
|
|
|
|
scrollX: defaultView.pageXOffset,
|
2019-05-30 07:11:50 +03:00
|
|
|
scrollY: defaultView.pageYOffset,
|
|
|
|
x: left,
|
|
|
|
y: top,
|
|
|
|
width: Math.ceil(width),
|
|
|
|
height: Math.ceil(height),
|
|
|
|
id: instanceName
|
2019-05-26 01:54:41 +03:00
|
|
|
};
|
|
|
|
|
2019-05-30 07:11:50 +03:00
|
|
|
const options: Options = {...defaultOptions, ...resourceOptions, ...opts};
|
2019-05-26 01:54:41 +03:00
|
|
|
|
|
|
|
const windowBounds = new Bounds(options.scrollX, options.scrollY, options.windowWidth, options.windowHeight);
|
|
|
|
|
2019-09-25 13:37:59 +03:00
|
|
|
Logger.create({id: instanceName, enabled: options.logging});
|
2019-05-26 01:54:41 +03:00
|
|
|
Logger.getInstance(instanceName).debug(`Starting document clone`);
|
|
|
|
const documentCloner = new DocumentCloner(element, {
|
|
|
|
id: instanceName,
|
|
|
|
onclone: options.onclone,
|
|
|
|
ignoreElements: options.ignoreElements,
|
|
|
|
inlineImages: options.foreignObjectRendering,
|
|
|
|
copyStyles: options.foreignObjectRendering
|
|
|
|
});
|
|
|
|
const clonedElement = documentCloner.clonedReferenceElement;
|
|
|
|
if (!clonedElement) {
|
|
|
|
return Promise.reject(`Unable to find element in cloned iframe`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
|
|
|
|
2019-05-30 07:11:50 +03:00
|
|
|
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
|
|
|
const documentBackgroundColor = ownerDocument.documentElement
|
|
|
|
? parseColor(getComputedStyle(ownerDocument.documentElement).backgroundColor as string)
|
|
|
|
: COLORS.TRANSPARENT;
|
|
|
|
const bodyBackgroundColor = ownerDocument.body
|
|
|
|
? parseColor(getComputedStyle(ownerDocument.body).backgroundColor as string)
|
|
|
|
: COLORS.TRANSPARENT;
|
|
|
|
|
|
|
|
const bgColor = opts.backgroundColor;
|
2019-09-25 13:37:59 +03:00
|
|
|
const defaultBackgroundColor =
|
|
|
|
typeof bgColor === 'string' ? parseColor(bgColor) : bgColor === null ? COLORS.TRANSPARENT : 0xffffffff;
|
2019-05-30 07:11:50 +03:00
|
|
|
|
|
|
|
const backgroundColor =
|
|
|
|
element === ownerDocument.documentElement
|
|
|
|
? isTransparent(documentBackgroundColor)
|
|
|
|
? isTransparent(bodyBackgroundColor)
|
|
|
|
? defaultBackgroundColor
|
|
|
|
: bodyBackgroundColor
|
|
|
|
: documentBackgroundColor
|
|
|
|
: defaultBackgroundColor;
|
2019-05-26 01:54:41 +03:00
|
|
|
|
|
|
|
const renderOptions = {
|
|
|
|
id: instanceName,
|
2019-05-30 07:11:50 +03:00
|
|
|
cache: options.cache,
|
2019-09-26 09:34:18 +03:00
|
|
|
canvas: options.canvas,
|
2019-05-26 01:54:41 +03:00
|
|
|
backgroundColor,
|
|
|
|
scale: options.scale,
|
2019-05-30 07:11:50 +03:00
|
|
|
x: options.x,
|
|
|
|
y: options.y,
|
2019-05-26 01:54:41 +03:00
|
|
|
scrollX: options.scrollX,
|
|
|
|
scrollY: options.scrollY,
|
2019-05-30 07:11:50 +03:00
|
|
|
width: options.width,
|
|
|
|
height: options.height,
|
2019-05-26 01:54:41 +03:00
|
|
|
windowWidth: options.windowWidth,
|
|
|
|
windowHeight: options.windowHeight
|
|
|
|
};
|
|
|
|
|
|
|
|
let canvas;
|
|
|
|
|
|
|
|
if (options.foreignObjectRendering) {
|
|
|
|
Logger.getInstance(instanceName).debug(`Document cloned, using foreign object rendering`);
|
|
|
|
const renderer = new ForeignObjectRenderer(renderOptions);
|
|
|
|
canvas = await renderer.render(clonedElement);
|
|
|
|
} else {
|
|
|
|
Logger.getInstance(instanceName).debug(`Document cloned, using computed rendering`);
|
|
|
|
|
2019-05-30 07:11:50 +03:00
|
|
|
CacheStorage.attachInstance(options.cache);
|
2019-05-26 01:54:41 +03:00
|
|
|
Logger.getInstance(instanceName).debug(`Starting DOM parsing`);
|
|
|
|
const root = parseTree(clonedElement);
|
|
|
|
CacheStorage.detachInstance();
|
|
|
|
|
|
|
|
if (backgroundColor === root.styles.backgroundColor) {
|
|
|
|
root.styles.backgroundColor = COLORS.TRANSPARENT;
|
|
|
|
}
|
|
|
|
|
|
|
|
Logger.getInstance(instanceName).debug(`Starting renderer`);
|
|
|
|
|
|
|
|
const renderer = new CanvasRenderer(renderOptions);
|
|
|
|
canvas = await renderer.render(root);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.removeContainer === true) {
|
2019-09-26 09:34:18 +03:00
|
|
|
if (!DocumentCloner.destroy(container)) {
|
2019-05-26 01:54:41 +03:00
|
|
|
Logger.getInstance(instanceName).error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Logger.getInstance(instanceName).debug(`Finished rendering`);
|
|
|
|
Logger.destroy(instanceName);
|
|
|
|
CacheStorage.destroy(instanceName);
|
|
|
|
return canvas;
|
|
|
|
};
|