preface

Because there was a business scene before that needed the server to dynamically generate pictures and save them for packaging and download, I looked up a lot of information on the Internet, but found that there were few related materials and articles about the node server to generate pictures. Of course, for the server to dynamically synthesize pictures, you need to use other technology stacks, such as: Java, Python, PHP, etc., have more plug-in support. This article mainly introduces the use of Canvas dynamic composite pictures based on Node service

The development environment

  • System environment: MAC or Linux

  • Scaffolding: Egg + TS

Mkdir egg-ts-canvas-template &&cdNPM init egg --type=tsCopy the code
  • Using plug-ins:

    canvas

    npm install canvas --save || yarn add canvas
    Copy the code

    The node service uses the environment that depends on the corresponding server. The system environment is as follows:

    system The command
    OS X brew install pkg-config cairo pango libpng jpeg giflib librsvg
    Ubuntu Sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
    Fedora sudo yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel
    Solaris pkgin install cairo pango pkg-config xproto renderproto kbproto xextproto
    OpenBSD doas pkg_add cairo pango png jpeg giflib
    Windows See the wiki
    Others See the wiki

    Archiver: compressed file plug-in

    npm install archiver --save || yarn add archiver
    Copy the code

The development of actual combat

  • Project directory

Core file directory: / app/controller/canvas. Ts - routing/canvas controller/app/public / * - store generated image file path and compressed file download file path, For external access /app/types/* ---- Data types of request parameters /app/utils/canvas.ts ---- Usage of canvas /app/utils/zip.ts ---- Usage of compressed filesCopy the code
  • Canvas uses code parsing

Step 1: Create canvas Canvas

/** * load background image and default font ** @param {string} [url=""]
   * @param {string} [fontFamily=""]
   * @returns
   * @memberof CanvasHandle
   */
  async initBgImgAndFonts(url: string = "", fontFamily: string = "") {
    if (url === "") {
      return "Picture address cannot be empty"; } try { registerFont(path.join(__dirname, `.. /public/fonts/${fontFamily}.ttf`), {
        family: "fonts"}); Const img: any = await this.loadImg(url); Const canvas = createCanvas(img.width, img.height); // createCanvas canvas const canvas = createCanvas(img.width, img.height); const ctx = canvas.getContext("2d"); Ctx. drawImage(img, 0, 0, img.width, img.height);return {
        ctx,
        canvas
      };
    } catch (error) {
      returnerror; }}Copy the code

Step 2: Load the network image

/** * @param {string} [url=""] Error * @returns * @memberof CanvasHandle */ Async loadImg(URL: String =) when the URL is an empty string"") {
    const loadingHandle = (url = "") = > {if (url === "") {
        return "Picture address cannot be empty";
      }
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => {
          resolve({
            status: 1,
            img,
            msg: ""
          });
        };
        img.onerror = () =>
          reject({
            status: 0,
            msg: `${url}Image load failed '}); http.get(url, res => {if (res.statusCode === 200) {
            const chunks = [];
            res.on("error", (err: any) => {
              reject(err);
            });
            res.on("data", (chunk: never) => {
              chunks.push(chunk);
            });
            res.on("end", () => {
              img.src = Buffer.concat(chunks);
            });
          } else {
            reject({
              status: 0,
              msg: `${url}Image load failed '}); }}); }); }; const result: any = await loadingHandle(url);if ((result.status = 1)) {
      return result.img;
    } else{ new Error(result.msg); }}Copy the code

Step 3: Draw pictures and text

/** * @param {*} CTX * @param {listVal} data * @memberof CanvasHandle */ Async CanvasHandle (CTX: any, data: listVal) { ctx.font = `${data.fontSize}px fonts`; ctx.fillText(data.text, data.x, data.y); ** @param {*} CTX * @param {listVal} data * @memberof CanvasHandle */ async publicImg(CTX: any, data: listVal) { const img: any = await this.loadImg(data.imgSrc); console.log(img); ctx.drawImage( img, data.x, data.y, data.w || img.width, data.h || img.height ); }Copy the code

Step 4: Export the composite image

Canvas * @param {*} canvas * @param {string} [imgName= '${new Date().valueOf()}`]
   * @param {string} [imgFolderName=`${new Date().valueOf()}`]
   * @returns
   * @memberof CanvasHandle
   */
  createFile(
    canvas: any,
    imgName: string = `${new Date().valueOf()}`,
    imgFolderName: string = `${new Date().valueOf()}`) {returnNew Promise(resolve => {// check if there is a folder with the same name, delete it, create it if there is no folderif(! fs.existsSync( path.join(__dirname, `.. /public/images/${imgFolderName}`) ) ) { fs.mkdirSync(path.join(__dirname, `.. /public/images/${imgFolderName}`)); } const out = fs.createWriteStream( path.join(__dirname, `.. /public/images/${imgFolderName}/${imgName}.png`) ); Const stream = Canvas.pngStream (); stream.on("data", chunk => {
        out.write(chunk);
      });
      stream.on("end", () => {
        console.log("saved png");
        resolve(imgName);
      });
    });
  }
Copy the code

  • Zip uses code parsing

I’m not going to explain it in detail here, but I’m going to post the code and look at the archiver API

/** * compression method ** @param {string} fileName compressed fileName * @param {string} folderName compressed target folderName * @param {Boolean} [deleteFolder=false] Whether to delete the compressed folder * @returns */ const zipHandle = (fileName: string = '${new Date().valueOf()}`,
  folderName: string,
  deleteFolder: boolean = falseConst archive = archiver() => {try {const archive = archiver("zip", {zlib: {level: 7} // set the compression level.}); Const Output = fs.createWritestream (path.join(__dirname, './.. /public/zip/${fileName}.zip`) ); Const deleteFolderRecursive = (path: string) => {if (fs.existsSync(path)) {
        fs.readdirSync(path).forEach((file: string) => {
          const curPath = path + "/" + file;
          if (fs.lstatSync(curPath).isDirectory()) {
            // recurse
            deleteFolderRecursive(curPath);
          } else{ // delete file fs.unlinkSync(curPath); }}); fs.rmdirSync(path); }}; // Create file exit output.on("close", () => {
      console.log(archive.pointer() + " total bytes");
      console.log(
        "archiver has been finalized and the output file descriptor has closed."
      );
      if(deleteFolder) { deleteFolderRecursive( path.join(__dirname, `./.. /public/images/${folderName}`)); }}); // Output.on ("end", () => {
      console.log("Data has been drained"); }); Archive. On ("warning", (err: any) => {
      if (err.code === "ENOENT") {
        console.log(err);
        // log warning
      } else{ // throw error throw err; }}); Archive. On ()"error".function(err) { throw err; }); // Start compression, passing the text path archive.pipe(output); archive.directory( path.join(__dirname, `./.. /public/images/${folderName}`),
      false
    );

    archive.finalize();
    return `/public/zip/${fileName}.zip`; } catch (error) { new Error(error); }};Copy the code
  • Controller Code Parsing
public async index() { const { ctx } = this; try { const params: paramsVal = ctx.request.body; // Call the Canvas method to generate the image const result = await canvashandle.init (params); // Determine the return valueif(result.status === 1) {// Determine whether the file needs to be compressedif (params.zip) {
          const zipResult = zipHandle(
            params.zipName,
            result.data.imgFolderName,
            params.deleteFolder
          );
          if(zipResult) {// Return success message ctx.body = {zip: zipResult... result.data };return;
          }
          ctx.body = {
            code: -1,
            data: "",
            msg: "File compression failed"
          };
          return; } // ctx.body = {code: 0, data: result.data, MSG:"success"
        };
      } elseCtx. body = {code: -1, data:"", msg: result.msg }; } } catch (error) { ctx.response.status = 500; ctx.body = error; }}Copy the code
  • The service call
Test case: URL:'http://localhost:7001/canvas'Method: post, body: {"bgSrc": "http://qgyc-system.oss-cn-hangzhou.aliyuncs.com/card/bg.png"// Background image"zip": false// Whether to compress"zipName": "123", // Compressed file size"deleteFolder": false, // Whether to delete the compressed target file"data": [// array information {"fileName": "123"// Name of the current image. The image is in PNG format"list": [{"type": "text"// Render type, text: text,img: image"text": "123"// Text content"fontSize": 36, // Text font"x": 100, // x coordinates"y": 100 // y coordinates}, {"type": "img"."imgSrc": "http://qgyc-system.oss-cn-hangzhou.aliyuncs.com/card/default.jpg"// Image address"w": 200,
          "h": 200,
          "x": 200,
          "y": 200}]}]}Copy the code

successful

  • Generate images only

  • Generate the image and compress it

Failure to report errors

The last


Thank you for reading, I hope this article is helpful to you, hee hee. As usually less articles to share, if there is something wrong with the code or logic of the article, please give us a lot of advice. In the future, there will be more practical tool class articles 😊, common progress, get rich at an early date 💰

Making address:

portal