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