DevUI is a team with both design and engineering perspectives, serving huawei DevCloud platform and huawei internal background systems, as well as designers and front-end engineers. Add devui Helper (Devui-Official) DevUIHelper plugin: Devuihelper-lsp (welcome Star)

The introduction

Sometimes users want to share our report page to other channels, such as email, PPT, etc., so they need to take screenshots every time. First, it is very troublesome, and second, the size of the screenshot is different.

Is there a way to provide a download report page function in the page, users just need to click the button, will automatically download the current report page in the form of pictures?

Html2canvas library can help us to do this, without background support, pure browser screenshots, even if the page has a scroll bar is no problem, cut out the picture is very clear.

This library takes a very long time to maintain, with its first release as early as September 8, 2013, prior to the first release of Vue (December 8, 2013).

As of December 18, 2020, HTML2Canvas library has gained 22.3K star on Github and 506K weekly downloads on NPM. It’s amazing!

The last submission was on August 9, 2020, and the author is still enthusiastically maintaining the library and TypeScript refactoring, but the author is very conservative. Even after seven years of continuous maintenance, he mentions in his README that the library is still experimental and not recommended for production.

I’ve actually been using this library in production for quite some time, so this article will take a look at this amazing JavaScript library and see how it implements browser-side screenshots.

1 How to Use it

Before introducing the principle of HTML2Canvas, let’s take a look at how to use it. It’s really very easy to use, almost one minute to get started.

Using HTML2Canvas requires the following 3 steps:

  1. The installation
  2. The introduction of
  3. call

Step 1: install

npm i html2canvas
Copy the code

Step 2: introduction

Introduce HTML2Canvas into any modern framework project

import html2canvas from 'html2canvas';
Copy the code

Take screenshots and download them

Html2canvas is a function that can be called directly after the page is rendered.

View rendered events: 1. Angular ngAfterViewInit method 2. React componentDidMount method 3. Vue Mounted methodCopy the code

You can pass in just one argument, the DOM element that you want to capture. This function returns a Promise object that can be retrieved from the canvas in its then method and converted into a picture by calling the Canvas’s toDataURL method.

Once we get the URL of the image, we can

  1. Put it in<img>The SRC attribute of the tag is displayed on the web page.
  2. You can also put it in<a>Tag to download the image to your local disk.

We choose the latter.

html2canvas(document.querySelector('.main')).then(canvas => { const link = document.createElement('a'); // Create an instance of a hyperlink object const event = new MouseEvent('click'); // Create an instance of the mouse event link.download = 'button.png '; // Set the name of the image to download link.href = canvas.todataurl (); // Set the URL of the image to the href of the hyperlink. // Trigger the hyperlink click event});Copy the code

Is it very simple?

parameter

Taking a quick look at its API, the function is signed as follows:

html2canvas(element: HTMLElement, options: object): Promise<HTMLCanvasElement>
Copy the code

The options object has the following optional values:

Name Default Description
allowTaint false Whether cross-domain images are allowed to contaminate the canvas
backgroundColor #ffffff Canvas background color, set to “null” (transparent) if not specified in DOM
canvas null Use the existing “Canvas” element as the base for your drawing
foreignObjectRendering false Whether to render with ForeignObject (if supported by the browser)
imageTimeout 15000 Timeout for loading images in milliseconds, set to 0 to disable timeout
ignoreElements (element) => false Removes the matching element from the rendering
logging true Enable logging for debugging purposes
onclone null The callback function, called when the document is cloned for rendering, can be used to modify what is to be rendered without affecting the original source document.
proxy null Proxy URL used to load cross-domain images. If set to empty (default), cross-domain images will not be loaded
removeContainer true Whether to clear cloned DOM elements temporarily created by HTML2Canvas
scale window.devicePixelRatio The zoom scale used for rendering, which defaults to browser device pixel ratio
useCORS false Whether to try to load images from the server using CORS
width Element width canvasThe width of the
height Element height canvasThe height of the
x Element x-offset canvasThe X-axis position of theta
y Element y-offset canvasY position of theta
scrollX Element scrollX The X-axis position used to render the element (for example, if the element is usedposition: fixed)
scrollY Element scrollY The Y-axis position used to render the element (for example, if the element is usedposition: fixed)
windowWidth Window.innerWidth The width of the window used to render elements, which can affect things like media queries
windowHeight Window.innerHeight The height of the window used to render elements, which can affect things like media queries

Ignore elements

Options has an ignoreElements parameter that can be used to ignore elements that are removed from the rendering process. Another way to ignore elements is to add data-html2Canvas-ignore to the elements that need to be ignored.

<div data-html2canvas-ignore>Ignore element</div>
Copy the code

2 Basic Principles

After introducing the use of HTML2Canvas, let’s first understand its basic principle, and then analyze the details of implementation.

The basic principle is as simple as reading the structure and style information of rendered DOM elements, and then building screenshots based on that information and rendering them on a Canvas canvas.

It cannot bypass the browser’s content policy restrictions, and you need to set up a proxy to render cross-domain images.

3 main process HTML2Canvas method

The basic principle is very simple, but there are a lot of things in the source code, we step by step, first find the entrance, and then slowly debugging, go through the general process.

Finding entry files

To pull the source code, there are several ways to find the entry file:

  • Method 1: The simplest method is a direct global searchhtml2canvasThis method is inefficient and hit-or-miss and not recommended
  • Method two: Introduce the library in the project, call it, run, and debug at the interrupt point in front of the method, generally finding the entry file exactly, recommended
  • Method three: See if there iswebpack.config.jsorrollup.config.jsThe build tool configuration file, and then in the configuration file to find the exact entry file (usually isentryorinputProperties), recommend
  • Method 4: directly scan the directory structure, the general entry file insrc/core/packagesAnd so on. The file name isindexormain“, or the name of the module, if you are experienced you can use this method to find it quickly, highly recommended

Method 1: Global search

The simplest and easiest way to think of is to search the keyword html2Canvas globally, because before we do not know the implementation of HTML2Canvas, we only have access to this one keyword.

However, if the global search is unlucky, it is likely to turn up a lot of results, which take time and effort to find entry files, such as:

42 files 285 results, very troublesome to find, not recommended.

Method 2: Break point

Make a breakpoint where HTML2Canvas is called.

Then, when executing at the breakpoint, click the small downward arrow to enter the method.

Because in the development environment, it’s pretty easy to see where the entry file and the entry method are. Here’s the HTML2Canvas file, which is actually a post-build file, but the context of that file gives us the information to find the entry method, and here we find the renderElement method.

At this point we can try the global search method, fortunately found directly 😄

Method 3: Find the configuration file

Finding configuration files is also a matter of thumb. Configuration files usually have a. Config suffix.

Build tools The configuration file
Webpack webpack.config.js
Rollup rollup.config.js
Gulp glupfile.config.js
Grunt Gruntfile.js

Configuration files found, entry files are generally easy to find

Method 4:

The main entry SRC /index.ts is easy to find when we glance at the directory structure

Start from the main entrance

We’ve already found entry methods in the SRC /index.ts file. Start with the main entry, sort out the general call relationships, get a basic picture, and then dive into the details.

The entry method does almost nothing but returns the result of a call to the other method, renderElement.

// entry method const html2canvas = (Element: HTMLElement, options: Partial< options > = {}): Promise<HTMLCanvasElement> => { return renderElement(element, options); };Copy the code

Following the call relationships, we quickly teased out the following simple flame diagram (with method annotations)

There are two main things to note about this simple fire chart:

  1. The simple flame diagram only gives us a rough idea of the process, which is neither detailed nor comprehensive, and requires further analysis of key methods
  2. RenderStackContent is the core method of the entire HTML2Canvas4 Render layered contentA separate analysis in one chapter

Render the DOM element specified in the page to the renderElement in the off-screen canvas

We have a basic understanding of the main flow of HTML2Canvas through a simple flame diagram. Let’s analyze it layer by layer, starting with the renderElement method.

The main purpose of this method is to render the DOM element specified in the page into an off-screen canvas and return the rendered canvas to the user.

It mainly does the following:

  1. The options passed in by the user are parsed and merged with the default options to obtain renderOptions configuration data for rendering
  2. The DOM element passed in is parsed to fetch node information and style information, which is passed to the canvasRenderer instance along with the renderOptions configuration in the previous step to draw the off-screen canvas
  3. The canvasRenderer will render the DOM element passed in by the user to an off-screen canvas, which we can fetch in the callback of the then method, according to the browser’s rules for rendering layered content

The core code for the renderElement method is as follows:

const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => { const renderOptions = {... defaultOptions, ... opts}; Const renderer = new CanvasRenderer(renderOptions); Const root = parseTree(Element); // Parse the DOM element passed in by the user (in order not to affect the original DOM, a new DOM element is actually cloned), get node information return await renderer.render(root); // The canvasRenderer instance renders the DOM element content to the off-screen canvas based on the node information parsed and the browser's rules for rendering layered content.Copy the code

The logic of merging configuration is relatively simple, so let’s skip it and focus on parsing node information (parseTree) and rendering off-screen canvas (renderer.render).

Parses node information parseTree

The entry to parseTree is a normal DOM element, and the return value is an ElementContainer object that contains the location of the DOM element (bounds: Width | height | left | top), data form, data such as text nodes (just a node tree information, does not contain data cascade, cascade data in the parseStackingContexts method).

The method of parsing is to recurse through the DOM tree and get the data for each node at each level.

The ElementContainer object is a tree structure that looks like this:

{bounds: {height: 1303.6875, left: 8, top: -298.5625, width: 1273}, elements: [{bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875}, cc: [{bounds: {left: 8, top: -298.5625, width: 1273, height: 1303.6875}, 1303.6875}, style: [{styles: CSSParsedDeclaration, textNodes: Array(1), Bounds: Array(0), bounds: bounds, flags: 0}, {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0}, {styles: CSSParsedDeclaration, textNodes: Array(1), elements: Array(0), bounds: Bounds, flags: 0}, {styles: CSSParsedDeclaration, textNodes: Array(3), elements: Array(2), bounds: Bounds, flags: 0}, ... ], flags: 0, styles: {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1),...}, textNodes: []}], Flags: 0, styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1),...}, textNodes: []}], Flags: 4, styles: CSSParsedDeclaration {backgroundClip: Array(1), backgroundColor: 0, backgroundImage: Array(0), backgroundOrigin: Array(1), backgroundPosition: Array(1)... }, textNodes: [] }Copy the code

It contains the values of each layer node:

  • Bounds – Position information (width/height, horizontal/vertical)
  • Elements – Child element information
  • Flags – Flags used to determine how to render
  • Styles – Describes the styles
  • TextNodes – Text node information

Render off-screen canvas renderer.render

With the node tree information, it can be used to render the off-screen canvas. Let’s take a look at the rendering logic.

The render logic is in the Render method of the CanvasRenderer class, which is mainly used to render layered content:

  1. Generate cascading data using the node data parsed in the previous step
  2. The DOM elements are rendered layer by layer to the off-screen canvas using the layered data of nodes, according to the browser’s rules for rendering layered data

The core code for the Render method is as follows:

async render(element: ElementContainer): Promise<HTMLCanvasElement> { /** * StackingContext { * element: ElementPaint {container: ElementContainer, effects: Array(0), curves: BoundCurves} * inlineLevel: [] * negativeZIndex: [] * nonInlineLevel: [ElementPaint] * nonPositionedFloats: [] * nonPositionedInlineLevel: [] * positiveZIndex: [StackingContext] * zeroOrAutoZIndexOrTransformedOrOpacity: [StackingContext] * } */ const stack = parseStackingContexts(element); // render this. RenderStack (stack); return this.canvas; }Copy the code

One of the

  • inlineLevel– Inline elements
  • negativeZIndex-zindex is a negative element
  • nonInlineLevel– Non-inline elements
  • nonPositionedFloats– Unpositioned floating elements
  • nonPositionedInlineLevel– Inline non-positioned elements, including inline tables and inline blocks
  • positiveZIndex– z-index Indicates the element greater than or equal to 1
  • zeroOrAutoZIndexOrTransformedOrOpacity– all have cascading context (z – index: auto | 0), transparency, less than 1 (less than 1) opacity or transform element (not transform to none)

This represents a cascade of information, based on which the order of rendering is determined, layer by layer.

ParseStackingContexts contexts resolve cascaded information in a similar way to parseTree addresses nodes, recursing the entire tree, collecting information from each layer of the tree to form a cascaded tree.

The renderStack method actually invokes the renderStackContent method, which is the most critical method in the entire rendering process and will be analyzed separately in the next chapter.

renderStackContent

Rendering DOM elements layer by layer to the off-screen canvas is the core of what HTML2Canvas does, and this is done by the renderStackContent method.

Therefore, it is necessary to focus on the analysis of the implementation principle of this method, which involves some knowledge related to CSS layout, I first do a simple introduction.

CSS cascading layout rules

By default, CSS is laid out as a stream, with no overlap between elements.

The meaning of flow layout is understandable: on a rectangular surface of water, a number of rectangular floating blocks are placed. The floating blocks float on the surface of the water and are arranged in sequence with each other, without overlapping

This is to draw them and it’s really easy to draw them one by one.

However, there are situations where this streaming layout breaks down, such as when floats and positions are used.

So you need to identify elements that are out of the normal document flow and remember their cascading information in order to render them correctly.

Those elements that depart from the normal document flow form a cascading context, which can be thought of simply as thin layers (like Photoshop layers) of DOM elements that are stacked together to create the colorful pages we see.

The cascading order of these different types of layers is as follows:

This graph is very important, and the rules of HTML2Canvas rendering DOM elements are the same. It can be considered that HTML2Canvas is an implementation of the rules described in this graph.

The detailed rules are described in the official W3 documentation, you can refer to: www.w3.org/TR/css-posi…

RenderStackContent is an implementation of CSS cascading layout rules

With that in mind, renderStackContent can be easily analyzed, and its source code is as follows:

async renderStackContent(stack: StackingContext) { // 1. The bottom is background/border await this. RenderNodeBackgroundAndBorders (stack. Element); / / 2. The second layer is for negative z - index (child of stack const. NegativeZIndex) {await this. RenderStack (child); } // 3. Block block box await this.renderNodecontent (stack.element); for (const child of stack.nonInlineLevel) { await this.renderNode(child); } / / 4. The fourth floor is a float float box for (child of stack const. NonPositionedFloats) {await this. RenderStack (child); } / / 5. The fifth layer is the inline/inline - block level box for (child of stack const. NonPositionedInlineLevel) {await this. RenderStack (child);  } for (const child of stack.inlineLevel) { await this.renderNode(child); } // (1) 'z-index: auto' or 'z-index: 0'. / / (2) 'transform: None '/ / (3) the opacity for less than 1 (child of stack const. ZeroOrAutoZIndexOrTransformedOrOpacity) {await this.renderStack(child); } // 7. Z-index for (const child of stack.positivezIndex) {await this.renderstack (child); }}Copy the code

summary

This paper mainly introduces the principle of html2Canvas browser screenshot.

First, html2Canvas is simply introduced what to do, how to use it;

Then starting from the main entrance, analyze html2Canvas rendering DOM elements of the general process (simple flame diagram);

Then according to the order of the figure of flame, which in turn perform in method of renderElement parseTree/parseStackingContextrenderer. Render three methods were analyzed, and explore the effects of these methods and principles;

Finally, through the introduction of CSS layout rules and 7 layer stacking levels, the key method renderStackContent implementation principle is naturally introduced.

Join us

We are DevUI team, welcome to come here and build elegant and efficient human-computer design/research and development system with us. Email: [email protected].

The text/DevUI Kagol

Previous articles are recommended

Baking cookies under a Waterfall: Three Steps to Quickly Locate Performance Issues

Denver “🏆 DevUI x | of 2020”

“How to build a grayscale Publishing environment”