diff --git a/examples/demo_make_renderer.html b/examples/demo_make_renderer.html
new file mode 100644
index 000000000..34769dc76
--- /dev/null
+++ b/examples/demo_make_renderer.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+ Demo of rendering multiple elements to canvas
+ Source container
+
+
+ First element to render
+
+
+ Second element to render
+
+
+ Target container
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index f972f7112..0ba01d097 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,8 @@
"requires": true,
"packages": {
"": {
- "version": "1.4.0",
+ "name": "html2canvas",
+ "version": "1.4.1",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
diff --git a/src/dom/document-cloner.ts b/src/dom/document-cloner.ts
index 08faa298d..2165e708d 100644
--- a/src/dom/document-cloner.ts
+++ b/src/dom/document-cloner.ts
@@ -47,7 +47,7 @@ const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
export class DocumentCloner {
private readonly scrolledElements: [Element, number, number][];
- private readonly referenceElement: HTMLElement;
+ private readonly referenceElement: HTMLElement | null;
clonedReferenceElement?: HTMLElement;
private readonly documentElement: HTMLElement;
private readonly counters: CounterState;
@@ -55,18 +55,26 @@ export class DocumentCloner {
constructor(
private readonly context: Context,
- element: HTMLElement,
- private readonly options: CloneConfigurations
+ element: HTMLElement | null | undefined,
+ private readonly options: CloneConfigurations,
+ ownerDocument?: Document
) {
this.scrolledElements = [];
- this.referenceElement = element;
this.counters = new CounterState();
this.quoteDepth = 0;
- if (!element.ownerDocument) {
- throw new Error('Cloned element does not have an owner document');
+ this.referenceElement = element ?? null;
+ let derivedOwnerDocument: Document;
+ if (ownerDocument != null) {
+ derivedOwnerDocument = ownerDocument;
+ } else if (element != null) {
+ if (!element.ownerDocument) {
+ throw new Error('Cloned element does not have an owner document');
+ }
+ derivedOwnerDocument = element.ownerDocument;
+ } else {
+ throw new Error('Either element or provided owner document should be defined');
}
-
- this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false) as HTMLElement;
+ this.documentElement = this.cloneNode(derivedOwnerDocument.documentElement, false) as HTMLElement;
}
toIFrame(ownerDocument: Document, windowSize: Bounds): Promise {
@@ -108,7 +116,7 @@ export class DocumentCloner {
const referenceElement = this.clonedReferenceElement;
- if (typeof referenceElement === 'undefined') {
+ if (typeof referenceElement === 'undefined' && this.referenceElement != null) {
return Promise.reject(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
}
@@ -120,7 +128,7 @@ export class DocumentCloner {
await imagesReady(documentClone);
}
- if (typeof onclone === 'function') {
+ if (typeof onclone === 'function' && referenceElement) {
return Promise.resolve()
.then(() => onclone(documentClone, referenceElement))
.then(() => iframe);
@@ -131,8 +139,10 @@ export class DocumentCloner {
documentClone.open();
documentClone.write(`${serializeDoctype(document.doctype)}`);
- // Chrome scrolls the parent document for some reason after the write to the cloned window???
- restoreOwnerScroll(this.referenceElement.ownerDocument, scrollX, scrollY);
+ if (this.referenceElement != null) {
+ // Chrome scrolls the parent document for some reason after the write to the cloned window???
+ restoreOwnerScroll(this.referenceElement.ownerDocument, scrollX, scrollY);
+ }
documentClone.replaceChild(documentClone.adoptNode(this.documentElement), documentClone.documentElement);
documentClone.close();
diff --git a/src/index.ts b/src/index.ts
index 348b050fb..6e8ddbff0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -148,6 +148,130 @@ const renderElement = async (element: HTMLElement, opts: Partial): Prom
return canvas;
};
+type ElementFetcher = (clonedContainer: HTMLIFrameElement) => HTMLElement[];
+
+type PreparedRenderer = {
+ renderElements: (elementFetcher: ElementFetcher) => AsyncGenerator;
+};
+
+function makeRenderer(ownerDocument: Document, opts: Partial): PreparedRenderer {
+ const defaultView = ownerDocument.defaultView;
+
+ if (!defaultView) {
+ throw new Error(`Document is not attached to a Window`);
+ }
+
+ const resourceOptions = {
+ allowTaint: opts.allowTaint ?? false,
+ imageTimeout: opts.imageTimeout ?? 15000,
+ proxy: opts.proxy,
+ useCORS: opts.useCORS ?? false
+ };
+
+ const contextOptions = {
+ logging: opts.logging ?? true,
+ cache: opts.cache,
+ ...resourceOptions
+ };
+
+ const windowOptions = {
+ windowWidth: opts.windowWidth ?? defaultView.innerWidth,
+ windowHeight: opts.windowHeight ?? defaultView.innerHeight,
+ scrollX: opts.scrollX ?? defaultView.pageXOffset,
+ scrollY: opts.scrollY ?? defaultView.pageYOffset
+ };
+
+ const windowBounds = new Bounds(
+ windowOptions.scrollX,
+ windowOptions.scrollY,
+ windowOptions.windowWidth,
+ windowOptions.windowHeight
+ );
+
+ const context = new Context(contextOptions, windowBounds);
+ const foreignObjectRendering = opts.foreignObjectRendering ?? false;
+
+ const cloneOptions: CloneConfigurations = {
+ allowTaint: opts.allowTaint ?? false,
+ onclone: opts.onclone,
+ ignoreElements: opts.ignoreElements,
+ inlineImages: foreignObjectRendering,
+ copyStyles: foreignObjectRendering
+ };
+
+ context.logger.debug(
+ `Starting document clone with size ${windowBounds.width}x${
+ windowBounds.height
+ } scrolled to ${-windowBounds.left},${-windowBounds.top}`
+ );
+
+ const documentCloner = new DocumentCloner(context, null, cloneOptions, ownerDocument);
+
+ return {
+ renderElements: async function* (elementFetcher: ElementFetcher): AsyncGenerator {
+ const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
+
+ const clonedElements = elementFetcher(container);
+
+ for (const clonedElement of clonedElements) {
+ const {width, height, left, top} =
+ isBodyElement(clonedElement) || isHTMLElement(clonedElement)
+ ? parseDocumentSize(clonedElement.ownerDocument)
+ : parseBounds(context, clonedElement);
+
+ const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
+
+ const renderOptions: RenderConfigurations = {
+ canvas: opts.canvas,
+ backgroundColor,
+ scale: opts.scale ?? defaultView.devicePixelRatio ?? 1,
+ x: (opts.x ?? 0) + left,
+ y: (opts.y ?? 0) + top,
+ width: opts.width ?? Math.ceil(width),
+ height: opts.height ?? Math.ceil(height)
+ };
+
+ let canvas;
+
+ if (foreignObjectRendering) {
+ context.logger.debug(`Document cloned, using foreign object rendering`);
+ const renderer = new ForeignObjectRenderer(context, renderOptions);
+ canvas = await renderer.render(clonedElement);
+ } else {
+ context.logger.debug(
+ `Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`
+ );
+
+ context.logger.debug(`Starting DOM parsing`);
+ const root = parseTree(context, clonedElement);
+
+ if (backgroundColor === root.styles.backgroundColor) {
+ root.styles.backgroundColor = COLORS.TRANSPARENT;
+ }
+
+ context.logger.debug(
+ `Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`
+ );
+
+ const renderer = new CanvasRenderer(context, renderOptions);
+ canvas = await renderer.render(root);
+ }
+
+ yield canvas;
+ }
+
+ if (opts.removeContainer ?? true) {
+ if (!DocumentCloner.destroy(container)) {
+ context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
+ }
+ }
+
+ context.logger.debug(`Finished rendering`);
+ }
+ };
+}
+html2canvas.makeRenderer = makeRenderer;
+
const parseBackgroundColor = (context: Context, element: HTMLElement, backgroundColorOverride?: string | null) => {
const ownerDocument = element.ownerDocument;
// http://www.w3.org/TR/css3-background/#special-backgrounds