node服务 node-canvas 图片合并 echarts图表 绘制方框

效果图:
image

环境及依赖配置

node版本参考:v14.18.3
npm版本参考:6.14.15

环境安装

Mac系统 需要先将Homebrew升级到与系统匹配的版本

系统 命令
Mac OS X brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman
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 And Others wiki

Mac OS X v10.11+:如果系统版本Mac OS X v10.11+并且在编译时遇到问题,请运行以下命令:xcode-select--install

node-canvas安装

1.这里推荐使用yarn,npm可能会遇到权限问题,可使用 npm --build-from-source --unsafe-perm install 尝试(百度只搜到这个方法,亲测无效)
2.为防止node-gyp这个大坑的一系列报错和问题
先全局安装node-gyp

npm -g node-gyp 

3.安装canvas的时候需要从源代码去构建,否则会安装失败或报错,需要在安装是添加一些选项,根据对应的的包管理工具,使用对应的命令

npm: npm install canvas --build-from-source
yarn: npm_config_build_from_source=true yarn add canvas
pnpm: npm_config_build_from_source=true pnpm add canvas

node-canvas源码地址

安装过程中还可能遇到很多告警,如不影响编译安装,可不理会

echarts服务端渲染

代码:
echarts图表配置line.js


var echartLine = (data) => {
    var xData = [], yData = [];
    for (let index = 0; index < data.length; index++) {
        const item = data[index];
        xData.push(item.name)
        yData.push(item.value)
    }
    var option = {
        backgroundColor: "rgba(255,255,255,0.8)",
        grid: {
            left: "10%",
            right: "10%",
            top: "10%",
            bottom: "10%",
            containLabel: true,
            show: true,
            borderColor: "rgba(0,0,0,0)",
        },
        xAxis: {
            type: "category",
            boundaryGap: false,
            offset: 5,
            axisLabel: {
                // color: "rgba(198, 211, 236, 1)",
                fontSize: 12,
                fontFamily: "PingFangSC-Regular, PingFang SC",
                showMaxLabel: false,
            },
            axisLine: {
                show: false,
            },
            axisTick: {
                show: false,
            },
            splitLine: {
                show: false,
                lineStyle: {
                    color: "rgba(64, 72, 106, 1)",
                },
            },
            data: xData,
        },
        yAxis: {
            type: "value",
            offset: 3,
            splitLine: {
                show: true,
                lineStyle: {
                    color: "rgba(64, 72, 106, 1)",
                },
            },
            axisLine: {
                show: false,
            },
            axisTick: {
                show: false,
            },
            axisLabel: {
                // color: "rgba(198, 211, 236, 1)",
                fontSize: 12,
                fontFamily: "PingFangSC-Regular, PingFang SC",
            },
        },
        series: [
            {
                type: "line",
                smooth: true,
                // color: "rgba(98, 208, 255, 1)",
                data: yData,
            },
        ],
    }
    return option
}
module.exports = echartLine;

路由文件index.js

const express = require('express');
const router = express.Router();
const echarts = require('echarts');
const ECHARTS_LINE = require("../echarts/line");
const { createCanvas, Image } = require('canvas');

let resultData = {
    status: 0,
    data: null,
    msg: ""
}
let echartsColor = [
    "#74D690",
    "#ff0000",
    "#1AA7E8",
    "#BDE5F8",
    "#5273E0",
    "#32CEDC"
]

router.post('/mergeImage', function (req, res, next) {
    let data = req.body;
    let { imgUrl, imgCoor, chartData } = data
    if (imgUrl && imgCoor && chartData) {

        let bgImg = new Image();
        bgImg.onload = function () {
            let bgWidth = bgImg.width, bgHeight = bgImg.height;
            const bgCanvas = createCanvas(bgWidth, bgHeight);
            let bgCtx = bgCanvas.getContext('2d');
            bgCtx.drawImage(bgImg, 0, 0, bgWidth, bgHeight);

            let lineChartWidth = bgWidth * 0.33
            let lineChartHeight = parseInt(lineChartWidth / 16 * 9)

            let coor = [], zuoshang = [], youshang = [], zuoxia = [], youxia = [];
            for (let index = 0; index < imgCoor.length; index++) {
                const item = imgCoor[index];
                if (item.origin && item.target && item.origin.includes(",") && item.target.includes(",")) {
                    let [originX, originY] = item.origin.split(",")
                    let [targetX, targetY] = item.target.split(",")
                    coor.push([+originX, +originY, targetX - originX, targetY - originY])
                    //筛选出四角区域内存在的方框坐标点
                    if ((+originX < lineChartWidth) && (+originY < lineChartHeight)) {
                        zuoshang.push(item)
                    }
                    if ((+targetX > bgWidth - lineChartWidth) && (+targetY > bgHeight - lineChartHeight)) {
                        youxia.push(item)
                    }
                    if (+originX < lineChartWidth && (+targetY > bgHeight - lineChartHeight)) {
                        zuoxia.push(item)
                    }
                    if ((+targetX > bgWidth - lineChartWidth) && (+originY < lineChartHeight)) {
                        youshang.push(item)
                    }
                }
            }
            for (let index = 0; index < coor.length; index++) {
                const item = coor[index];
                bgCtx.strokeStyle = echartsColor[index]
                bgCtx.lineWidth = 4;
                bgCtx.strokeRect(...item);
            }
            //为防止echarts图表和方框重合,需要动态调整echarts图片位置(四角)。默认右上角。
            let lineChartXY = []
            if (!youshang.length) {
                lineChartXY = [bgWidth - lineChartWidth, 0]
            } else if (!zuoshang.length) {
                lineChartXY = [0, 0]
            } else if (!youxia.length) {
                lineChartXY = [bgWidth - lineChartWidth, bgHeight - lineChartHeight]
            } else if (!zuoxia.length) {
                lineChartXY = [0, bgHeight - lineChartHeight]
            } else {
                lineChartXY = [bgWidth - lineChartWidth, 0]
            }
            // 生成echarts折线图
            let option = ECHARTS_LINE(chartData);
            const chartCanvas = createCanvas(lineChartWidth, lineChartHeight);
            const chart = echarts.init(chartCanvas);
            chart.setOption(option);
            chartCanvas.toBuffer(function (err, buf) {
                if (err) {
                    return res.send({ msg: "Diagram generation failed" })
                } else {
                    let chartImg = new Image()
                    chartImg.onload = function () {
                        // res.writeHead(200, {
                        //     'Content-Type': 'image/png'
                        // })
                        bgCtx.drawImage(chartImg, ...lineChartXY, lineChartWidth, lineChartHeight)
                        // res.write(bgCanvas.toBuffer('image/png'))
                        // console.log(bgCanvas.toDataURL())
                        // bgCanvas.toBuffer('image/png')
                        resultData.status = 200
                        resultData.data = bgCanvas.toDataURL()
                        let message = "keyongquyu:" + (!zuoshang.length ? "zoushang|" : "") + (!youshang.length ? "youshang|" : "") + (!youxia.length ? "youxia|" : "") + (!zuoxia.length ? "zuoxia" : "")
                        resultData.msg = message
                        res.send(resultData)
                    }
                    chartImg.onerror = function () {
                        resultData.status = 500;
                        resultData.msg = "Diagram loading failed";
                        res.send(resultData);
                    }
                    chartImg.src = buf
                }
            })
        }
        bgImg.onerror = function () {
            resultData.status = 500;
            resultData.msg = "Image loading failed";
            res.send(resultData);

        }
        bgImg.src = imgUrl
    } else {
        resultData.data = "";
        resultData.status = 400;
        resultData.msg = "必传字段不可为空";
        res.send(resultData);
        res.end();
    }
});

module.exports = router;

测试接口

let params = {
//图片地址
  imgUrl: "http://localhost:4060/images/shujia.jpg",
  //画框对角线坐标
  imgCoor: [
    { origin: "100,100", target: "150,130" },
    { origin: "180,150", target: "210,190" },
  ],
  //折线图数据
  chartData: [
    { name: "2023-06-10", value: "15" },
    { name: "2023-06-12", value: "30" },
    { name: "2023-06-13", value: "40" },
    { name: "2023-06-14", value: "20" },
    { name: "2023-06-15", value: "22" },
    { name: "2023-06-16", value: "15" },
    { name: "2023-06-17", value: "20" },
  ],
};

mergeImage(params).then((res) => {
 console.log(res)
});
posted @ 2023-06-26 11:19  Edith6  阅读(382)  评论(0编辑  收藏  举报