Using HTML Canvas generate poster and problems solving


Ruoyan Meng

The idea of generating a poster by HTML Canvas comes from a personal project called Lyrics-Crawler, which helps Spotify users fetch lyrics easily. To help users sharing pieces of lyrics I decided to generate lyrics card.

Convert DOM elements into SVG or intto Canvas then download as raster (PNG or JPEG) images are the most common methods for poster generating. Js libraries dom-to-image and html2canvas have provided solutions to the above methods. However, the picture generated by these two libraries is unsatisfied. So I start programming my own method.

My idea follows drawing required elements to Canvas then using web API HTMLCanvasElement.toDataURL() save canvas as pictures.

There are two main problems encountered during the implementation process:

1. The generated image and text are blurry

2. HTML5 canvas.fillText() won't do line breaks

Solution will talk later.

The lyrics card is generated dynamically, the width can be set in advance. To get the height of it, using React Refs to get the DOM size after the first render.

                
                import React, { Component } from "react";
                class LyricsPic extends Component {
                    constructor(props) {
                        super(props);
                            this.state = {
                                dimensions: null,
                                };
                        }
                        componentDidMount() {
                            this.setState({
                                dimensions: {
                                    width: this.card.offsetWidth,
                                    height: this.card.offsetHeight,
                                    },
                                });
                            }
                        render() {
                            let card =
                                <div className='card' id='card' ref={el => { this.card = el }}>
                                </div>
                                return (
                                    {card}
                                    <canvas id="canvas" ref={el => { this.canvas = el }} style={{ display: 'none' }}></canvas>
                                )
                            }
                        
                    

After getting the dimension information, using it to draw a corresponding Canvas.

                        
                    // *2 to render high-resolution pic, depends on your needs
                    this.canvas.width = dimensions.width*2;
                    this.canvas.height = dimensions.height*2;
                    this.canvas.style.width = dimensions.width*2 + "px";
                    this.canvas.style.height = dimensions.height*2 + "px";
                    var ctx = this.canvas.getContext('2d')
                        
                    

Then load the image and fill text in Canvas. Set image crossOrigin "anonymous" to allow cross-origin downloading. Adjust text position for every line you add.

                    
                    //load image
                    var imageObj = new Image();
                    imageObj.crossOrigin = "anonymous";
                    imageObj.onload = () => {
                        ctx.drawImage(imageObj, padding, padding, imageSize, imageSize * imageObj.height / imageObj.width);
                    }
                    
                    //fill text
                    ctx.font = '23px "Roboto Mono"';
                    ctx.fillText("hello world", padding, currentHeight, width);
                    
                

Problem 1. Now every element are in its right place, but the Canvas looks blurry. I'm using Mac and 4K screen, to fit the high-resolution requiement, first get the pixel ratio of current device, then using ratio redraw the canvas.

                        
                        var getPixelRatio = function (context) {
                            var backingStore = context.backingStorePixelRatio ||
                            context.webkitBackingStorePixelRatio ||
                            context.mozBackingStorePixelRatio ||
                            context.msBackingStorePixelRatio ||
                            context.oBackingStorePixelRatio ||
                            context.backingStorePixelRatio || 1;
                            return (window.devicePixelRatio || 1) / backingStore;
                        };
                        var ratio = getPixelRatio(ctx);
                        
                        // Canvas zoom ratio
                        this.canvas.width = dimensions.width * ratio *2;
                        this.canvas.height = dimensions.height * ratio *2;
                        this.canvas.style.width = dimensions.width *2+ "px";
                        this.canvas.style.height = dimensions.height *2+ "px";
                
                        // After the canvas is enlarged, the corresponding drawing canvas should also be enlarged
                        var ctx = this.canvas.getContext('2d')
                        ctx.scale(ratio, ratio)
                        
                    

Problem 2. Break the line based on the width of your choice, first measure the text wide using ctx.measureText(text).width, then calculate the ratio of text wide and line width, from this get how many words in this line, recursively, break text into different lines.

                        
                        let lyricsWidth = Math.round(ctx.measureText(item).width);

                        if (lyricsWidth>=imageSize){
                            let itemWordNum = item.split(" ").length
                            while (item!==""){
                                let wordNum = Math.round(imageSize/lyricsWidth*item.split(" ").length);
                                if(wordNum >= itemWordNum){
                                    currentHeight = currentHeight + lineheight;
                                    ctx.fillText(item, padding, currentHeight, imageSize);
                                    item = "";
                                    break
                                }else{
                                    currentHeight = currentHeight + lineheight;
                                    let newline = item.split(" ").slice(0,wordNum).join(" ");
                                    ctx.fillText(newline, padding, currentHeight, imageSize);
                                    item = item.split(" ").slice(wordNum, lyricsWidth).join(" ");
                                    lyricsWidth = Math.round(ctx.measureText(item).width);
                                }
                            }
                        }else{
                            currentHeight = currentHeight + lineheight;
                            ctx.fillText(item, padding, currentHeight, imageSize)
                        }   
                        
                    

Now the Canvas is ready! Write a function to make it downloadable.

                    
                    <a id="download"  onClick={el=>this.download_img(el)} >Download Lyrics Pic</a>  
                    
                    download_img = (el) => {
                        let time = new Date().getTime()
                        var canvas = document.getElementById('canvas');
                        var image = canvas.toDataURL("image/png");
                        el.target.download = name + "_"+ time + ".png"
                        el.target.href = image;
                    };                  
                        
                    

Tada, all done!

Check Lyrics-Crawler here!