Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions examples/demo_make_renderer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
.container {
padding: 20px;
border: 1px dashed red;
display: flex;
flex-direction: column;
gap: 20px;
}

.toRender {
background: darkseagreen;
border: 1px solid green;
padding: 20px;
}
</style>

<style>

</style>
</head>
<body>
<h2>Demo of rendering multiple elements to canvas</h2>
<h3>Source container</h3>
<div class="container" id="source">
<div class="toRender" >
First element to render
</div>
<div class="toRender" >
Second element to render
</div>
</div>
<h3>Target container</h3>
<div class="container" id="target"></div>
<script type="text/javascript" src="../dist/html2canvas.js"></script>
<script type="text/javascript">
async function main() {
const renderer = html2canvas.makeRenderer(document, {})

const canvasIter = renderer.renderElements((clonedContainer) => {
return [...clonedContainer.ownerDocument.querySelectorAll('#source .toRender')]
})
for await (const canvas of canvasIter) {
document.getElementById('target').appendChild(canvas)
}
}
main().catch(e => {
console.error(e);
})
</script>
</body>
</html>
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 22 additions & 12 deletions src/dom/document-cloner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,34 @@ 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;
private quoteDepth: number;

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<HTMLIFrameElement> {
Expand Down Expand Up @@ -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`);
}

Expand All @@ -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);
Expand All @@ -131,8 +139,10 @@ export class DocumentCloner {

documentClone.open();
documentClone.write(`${serializeDoctype(document.doctype)}<html></html>`);
// 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();

Expand Down
124 changes: 124 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,130 @@ const renderElement = async (element: HTMLElement, opts: Partial<Options>): Prom
return canvas;
};

type ElementFetcher = (clonedContainer: HTMLIFrameElement) => HTMLElement[];

type PreparedRenderer = {
renderElements: (elementFetcher: ElementFetcher) => AsyncGenerator<HTMLCanvasElement>;
};

function makeRenderer(ownerDocument: Document, opts: Partial<Options>): 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<HTMLCanvasElement> {
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
Expand Down