“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

Why write

Recently, when I was working on a company project, I encountered the need to export pictures to a specific area on the web page. In the process of implementation, I encountered some pits and learned some lessons in the process of filling pits, so I decided to record them for future review.

I met html2canvas

First of all, the browser does not provide a native screen capture API for JS to use, so it has to be implemented in a “curvewise” way. At present, the most mature wheel in the community about screenshots is HTML2Canvas, which has 2.3W star. It provides a simple API out of the box, which can help us to easily achieve screenshots. The sample code given by the official website is posted below, from which you can see that it is very easy to use

<div id="capture" style="padding: 10px; background: #f5da55">
    <h4 style="color: #000; ">Hello world!</h4>
</div>

/ *... * /

html2canvas(document.querySelector("#capture")).then(canvas= > {
    document.body.appendChild(canvas)
})
Copy the code

You can also pass a second parameter, such as HTML2Canvas (Element, options), to customize the rendering result. The optional properties are listed below

The name of the The default value describe
allowTaint false Whether to allow cross-domain images to be rendered to the canvas
backgroundColor #ffffff The background color of the canvas. If the background needs to be set to transparent, set it tonull
canvas null Specifies to use an existing Canvas instance on the page
foreignObjectRendering false Support in the browserforeignObjectCase, whether to renderforeignObjectThe contents of the
ignoreElements (element) => false Specifies the elements to be ignored for rendering
onclone null A hook function that is triggered when the dom of a page area has been cloned. This hook function allows you to modify the cloned DOM to change the rendering result without affecting the original page
proxy null Address used to broker cross-domain image resource loading
scale window.devicePixelRatio The scale of the render
width Element width The width of the canvas
height Element height The height of the canvas
x Element x-offset Adjust the x coordinate of the origin of canvas
y Element y-offset Adjust the y coordinate of the origin of canvas

You can make html2Canvas ignore elements by adding the attribute data-html2Cancanvas -ignore to the element. This is a quick way to use the ignoreElements option and is highly recommended

Here is a brief description of its working principle. Html2canvas will clone the DOM of the page area into a backup, and then collect the DOM information in the backup, parse it into specific types of data, and then draw the content on the page to the canvas through these data, and finally return the canvas instance. A flow chart is posted below for easy understanding

There are some limitations to our use of the library, listed below

  1. When there are images in the page area, if the images belong to non-cross-domain resources, then the images can be rendered to the canvas and the content in the canvas can be exported normally. If the images belong to cross-domain resources, there are two cases:
  • With allowTaint set to false, images will not be rendered to the canvas and the contents of the canvas will be exported
  • When allowTaint is true, the image will be rendered to the canvas, but the canvas will be marked as Tainted, making it impossible to export its contents

For cross-domain image processing, the official solution is to add proxy

  1. Since it is not a real screenshot, but a dom transformation, the final effect will not be 100% restored to the page. The reason is that HTML2Canvas does not support some CSS styles, which leads to the difference. The following are the styles that are not supported by the latest version (1.0.0)
  • background-blend-mode
  • border-image
  • box-decoration-break
  • box-shadow
  • filter
  • font-variant-ligatures
  • mix-blend-mode
  • object-fit
  • repeating-linear-gradient()
  • writing-mode
  • zoom

Note that border-radius: 50% has no effect, it must be written as a fixed value, the correct way to write: border-radius: 50px

  1. The output image will be of low resolution, so the code snippet of the solution is posted below
  /* Get the DOM node you want to transform */
  const dom = document.querySelector('.target');
  const box = window.getComputedStyle(dom);
  /* Compute the width and height of the DOM node */
  const width = parseInt(box.width, 10);
  const height = parseInt(box.height, 10);
  /* Get pixel ratio */
  const scaleBy = window.devicePixelRatio
  /* Create a custom Canvas element */
  const canvas = document.createElement('canvas');
   /* Set canvas element attribute width and height to DOM node width and height * pixel ratio */
  canvas.width = width * scaleBy;
  canvas.height = height * scaleBy;
  Set canvas CSS width and height to DOM node width and height */
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  /* Get the brush */
  const context = canvas.getContext('2d');
  /* Make everything drawn larger by a pixel ratio */
  context.scale(scaleBy, scaleBy);
  /* Pass in the custom canvas as a configuration item and start drawing */
  html2canvas(dom, {canvas, background: '#ffffff'}).then((canvas) = > {
    let url= canvas.toDataURL();
    /* In this case, the url is in the base64 format of the image and can be directly assigned to the SRC of img
    console.log(url)
  })
Copy the code

Problems with HTML2Canvas

In fact, html2Canvas can do a good job in most scenarios, but in a few scenarios, there will be more or less problems, especially when translating SVG

The usage scenario was that you needed to export the content of one of the child elements in the SVG on the page. At the beginning, the version of 1.0.0-rc.7 had been used. When I carried out export operations, there was an error reporting unable to find elements in biology iframe. Finally, I found a clue in the issue of his warehouse. From that person’s question, I speculated that it should be the version problem, so I reduced the version to 1.0.0-Rc.6, and then exported it again, no error was reported! Happy! But I only guessed the beginning, but not the end. I looked at the output picture, and there was only text content, nothing else!

So I began to explore. I found that as long as the root node of the target element is selected as the < SVG > tag, that is, the full export, then the output content is ok. If the target is set to a child node in SVG, then the output content will be incomplete. The road of HTML2Canvas is impossible, so after a hard search, I found an alternative to HTML2Canvas, that is canVG

This library is dedicated to drawing SVG to the Canvas. Compared with HTML2Canvas, this library is more professional and mature for SVG processing. I also used this library to finally complete the requirements, but I also encountered some problems in this process, which will be described in the following part

I met canvg

First, refer to the official description of the library

JavaScript SVG parser and renderer on Canvas. It takes the URL to the SVG file or the text of the SVG file, parses it in JavaScript and renders the result on Canvas.

In short, it is an engine dedicated to parsing SVG and rendering it to canvas, and provides a concise API for us to use. The following examples are provided on the website

import Canvg from 'canvg';

let v = null;

window.onload = async() = > {const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');
    /* Start the canVG engine from */
    v = await Canvg.from(ctx, './svgs/1.svg');
    /* Start SVG rendering with animations and mouse handling. */
    v.start();
};

window.onbeforeunload = () = > {
    v.stop();
};
Copy the code
import Canvg, {
    presets
} from 'canvg';

self.onmessage = async (event) => {
    const {
        width,
        height,
        svg
    } = event.data;
    const canvas = new OffscreenCanvas(width, height);
    const ctx = canvas.getContext('2d');
    const v = await Canvg.from(ctx, svg, presets.offscreen());

    /* Render only first frame, ignoring animations and mouse. */
    await v.render();

    const blob = await canvas.convertToBlob();
    const pngUrl = URL.createObjectURL(blob);

    self.postMessage({
        pngUrl
    });
};
Copy the code

In the example above, we saw the unfamiliar name of the OffscreenCanvas constructor, which is used to provide a canvas object that can be rendered off the screen, so that we can actually generate a canvas without having to createElement(‘canvas’). This avoids contaminating the Document, but this feature is not very compatible at the moment, IE and Safari do not support it at all, so we have to wait and see

Another thing to note is that there are two ways to start rendering: render and start. The difference between these two methods is that when the SVG to be drawn is dynamic, Render will only draw the content of the first frame, that is to say, the drawn image is static, while start will draw all the CONTENT and dynamic effect of SVG, that is to say, the image is dynamic, we can choose according to their needs

There are three ways to start the CANVG engine

  • new Canvg(…)
  • Canvg.from(…)
  • Canvg.fromString(…)

The difference between from and fromString is that from needs to be passed in SVG itself, while fromString needs to be passed in SVG as a string. The first parameter is the drawing context of the canvas, the second parameter is the SVG that needs to be drawn, and the third is a custom configuration option that can be used to control the rendering result of the canvas

interface IOptions {
    /** * WHATWG-compatible `fetch` function. */fetch? :typeof fetch;
    /** * XML/HTML parser from string into DOM Document. */DOMParser? :typeof DOMParser;
    /** * Window object. */
    window? : Window;/** * Whether enable the redraw. */enableRedraw? : boolean;/** * Ignore mouse events. */ignoreMouse? : boolean;/** * Ignore animations. */ignoreAnimation? : boolean;/** * Does not try to resize canvas. */ignoreDimensions? : boolean;/** * Does not clear canvas. */ignoreClear? : boolean;/** * Scales horizontally to width. */scaleWidth? : number;/** * Scales vertically to height. */scaleHeight? : number;/** * Draws at a x offset. */offsetX? : number;/** * Draws at a y offset. */offsetY? : number; forceRedraw? (): boolean;/*Will call the function on every frame, if it returns true, will redraw.*/rootEmSize? : number;/*Default `rem` size.*/emSize? : number;/* Default `em` size.*/createCanvas? :(width: number, height: number) = > HTMLCanvasElement | OffscreenCanvas;    /*Function to create new canvas.*/createImage? :(src: string, anonymousCrossOrigin? : boolean) = > Promise<CanvasImageSource>;     /* Function to create new image.*/anonymousCrossOrigin? : boolean;/* Load images anonymously.*/
}
Copy the code

Problems with CanVG

First of all, THE version I use is 3.0.7, and the problems I encounter are listed as follows:

  • Just like HTML2Canvas, CanVG does not support some CSS styles. The situation I encountered is that CanVG does not support the filter attribute, and if the attribute exists on the element, the stroke or fill of all graphics will be invalid, so I first remove the filter of all elements before conversion. Using stroke instead solves this problem
  • No error will be reported when exporting SVG in full, but an error will be reported when you want to export the contents of a child element in SVGUncaught (in promise) Error: This page contains the following errors:error on line 1 at column 2796: Namespace prefix xlink for href on image is not defined, Below is a rendering of the page up to the first error.The error message was mostly due to a lack of namespaces, so I naturally putxmlns:xlink="http://www.w3.org/1999/xlink"andxmlns="http://www.w3.org/2000/svg"Added to the root node of the target element, which in this case is<g>After searching the Internet, I found that the attributes of the namespace can only be defined in<svg>So the solution to the problem is clear. A code example of the solution is posted below
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
    canvas.setAttribute('width'.'8000px')
    canvas.setAttribute('height'.'1500px')
    canvas.setAttribute('position'.'fixed')
    canvas.setAttribute('top'.'99999999px')
    document.body.appendChild(canvas)

    /* Since the canVG library does not support the filter attribute, rectangular and circular strokes or fills will not work, so remove the filter before converting and use a stroke instead of */
    let rectArr = Array.from(container.getElementsByTagName('rect'))
    rectArr.forEach(item= >{
      item.setAttribute('stroke'.'#ccc')
      item.removeAttribute('filter')})let rootCircle = container.getElementsByTagName('circle') [0]
    rootCircle.setAttribute('stroke'.'#ccc')
    rootCircle.removeAttribute('filter')

    /* Manually add the SVG tag, adding the namespace */
    let v = await Canvg.from(ctx, `<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">${container.innerHTML.trim()}</svg>`, {
      offsetX: 3000
    })
    await v.render()
Copy the code

Here’s what the SVG namespace means: The namespace declaration XMLNS needs to be provided only once on the root tag. The declaration defines the default namespace, so the user agent knows that all descendant tags of < SVG > tags belong to the same namespace. XMLNS is the namespace for the tag, and XMLNS :xlink is the namespace for the attribute. The xlink namespace is defined here. Xlink is usually associated with the href attribute, such as xlink:href

conclusion

After a lot of agonizing, finally achieved the goal, although the process is more tortuous, but in the process of solving the problem also gained a lot of knowledge. In fact, there is a lot of knowledge in front of the screen, as far as I know can also be in the server for screen, but I have not tried, I think that should also be a very interesting and challenging scheme, there is a chance to try the next, write so much, the end, sprinkle flowers ~

Personal blog

www.carlblog.site has recently set up a personal blog, which will regularly publish articles about the summary of technical experience. I hope you are interested in it and don’t run away! ~