node+koa+canvas绘制出货单、收据票据的方法

JavaScript/前端
384
0
0
2023-03-07
标签   NodeJs
目录
  • 先看效果
  • 使用库
  • 首先创建服务 index.js
  • 创建一个api 提供外面可访问的接口api
  • 绘制画布

在生成票据需求中,我们会想到前端生成或者后端生成返回图片地址访问两个方法,前端生成则不需要调用接口,而后端是在完成整个流程时就进行生成然后把上传的地址保存数据库

先看效果

下面我们就使用node +koa+canvas后端生成图片的方法进行生成

使用库

1、node

2、canvas npm install canvas

3、koa npm install koa

4、mime-types npm install mime-types -S

首先创建服务 index.js

把用到的库都导入进去,当然如何创建node项目我这就不做过多的描述,创建成功后,直接使用 node index.js 就可以启动服务了

const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/
 
//....这里需要做很多事
 
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)

创建一个api 提供外面可访问的接口api

在末尾加了一个供外面访问的接口,启动服务后 访问localhost:3000/img 就可以访问了

const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/
 
//....这里需要做很多事
 
router.get('/img', async (ctx) => { });
 
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)

ok 服务已经好了,正片开始,瓜子饮料矿泉水,前面的麻烦让一让,

首先我没得知道,票据单有哪些内容

1、标题:编号,日期,地址;这些都是文字,所以我没得绘制文字

2、表格:表头,内容,线条;表格就是线条堆积而成的,内容就是文字,这里就得绘制线条,文字

3、尾部:文字,印章,签名;这里需要绘制文字,图片两个

总的来说,我们想要绘制出一张票据单,得要绘制文字,绘制线条,通过线条与文字结合生成表格,最后添加印章与签名照片

绘制画布

1、给画布设置长宽

  const width = 700
  const height = 460
  const canvas = createCanvas(width, height)
  const context = canvas.getContext('2d')

2、创建画布 给画布添加背景颜色

  const createMyCanvas=()=>{
      context.fillStyle = '#a2bcd3'
      context.fillRect(0, 0, width, height)
  }

画布添加文字函数

  /**
    * @writeTxt: canvas 写入字内容
    * @param {str} t 内容
    * @param {str} s 字体大小 粗体
    * @param {arr} p 写入文字的位置
    * @param {arr} a 写入文字对齐方式 默认 居中
    * @param {obj} c 写入文字颜色 默认 #000
    */
  const writeTxt = (t, s='normal bold 12px', p, a = 'center', c = '#000') => {
      if (!t) {
        return;
      }
      context.font = `${s} 黑体`;
      context.fillStyle = c;
      context.textAlign = a;
      context.textDecoration='underline'
      context.textBaseline = 'middle';
      context.fillText(t, p[0], p[1]);
  }

画布绘表格线条函数

/**
    * @drawLine: 画table线
    * @param list {arr} 表格竖线x轴位置
    * @param tlist {arr} 表格需要填写文字 无文字 填 ''
    * @param startHei {num} 开始画线的高度
    * @param lineWidth {num} 横线的长度
    * @param n {num} 行数
    * @param txtHei {num} 文字位置调整
    * @param isTrue {boolean} 是否为物资列表
    */
  const drawLine = (list, tlist, startHei, lineWidth, n, txtHei = 14, isTrue = false) => {
      for (let i = 0; i < n; i++) {
          for (let y in list) {
              if (+y !== 0) {
                  const poi = list[y] - (list[y] - list[y - 1]) / 2;
                  writeTxt(tlist[i][y - 1], '12px', [poi, startHei + txtHei + 30 * i])
              }
              context.moveTo(list[y], startHei);
              context.lineTo(list[y], startHei + 30 * (i + 1));
          }
          if (isTrue) {
                   const mtY = startHei + 30 * n;
                   if (i == 0) {
                       context.moveTo(10, startHei + 30 * i);
                       context.lineTo(690, startHei + 30 * i);
                   }
                   context.moveTo(10, mtY);
                   context.lineTo(690, mtY);
          }
          context.moveTo(10, startHei + 30 * i);
          context.lineTo(lineWidth, startHei + 30 * i);
      }
      if (isTrue) {
          const mtY = startHei + 30 * n;
          context.moveTo(10, mtY);
          context.lineTo(690, mtY);
      }
      context.strokeStyle = '#5a5a59';
      context.stroke();
  }

绘制表格

/**
* @drawTable: 画表格
  */
  const drawTable = () => {
 
      const titleArr = [10, 100, 290, 360, 430, 500, 600, 690];
      const titleTxtArr = [
        ['货号', '名称及规格', '单位', '数量', '单价', '金额', '备注']
      ]
      const goodsTxtArr = [
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', ''],
          ['', '', '', '', '', '', '']
      ]
      const bottomArr=[10,100,690]
      const bottomTxtArr=[
        ['合计大写', ' 拾 万 仟 佰 拾 元 角 分 ¥ ']
      ]
      drawLine(titleArr, titleTxtArr, 120, 690, 1, 16)
      drawLine(titleArr, goodsTxtArr, 151, 690, goodsTxtArr.length, 16, true)
      drawLine(bottomArr, bottomTxtArr, 301, 690, bottomTxtArr.length, 16,true)
  }

绘制图片,这里绘制图片其实就是绘制印章

/**
* 添加图片
* @param imgPath 图片路径 和图片所在位置
* @returns {Promise<void>}
  */
  const drawImg = async (imgPath = [{imgUrl: '', position: []}]) => {
    let len = imgPath.length
    for (let i = 0; i < len; i++) {
      const image = await loadImage(imgPath[i].imgUrl)
      context.drawImage(image, ...imgPath[i].position)
    }
 
  }

ok,相关绘制的函数已经好了,那么接下来就是进行排版了

1、首先的是头部的标题,单位,位置 编号,和时间

//创建画布
createMyCanvas()
//开始绘制
context.beginPath()
writeTxt('送 货 单', 'normal bold 30px', [370, 30])
writeTxt('No', '20px', [450, 34])
writeTxt('收货单地址:XXXXX', '14px', [12, 70], 'left')
writeTxt('地     址:XXXXXXXXXXX', '14px', [12, 100], 'left')
writeTxt('2022 年 9 月 22 日', '14px', [680, 100], 'right')

2、表格部分,绘制表头,表格,表尾

这里直接调用绘制表格的函数就可以了

3、票据尾部,签章,签字

writeTxt('收货单位及经手人(签章):', '14px', [12, 350], 'left')
writeTxt('送货单位及经手人(签章):', '14px', [400, 350],)
const imgList = [
    {
        // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
        imgUrl: path.join(__dirname + '/reject.png'),
        position: [180, 350, 90, 80]
    },
    {
        imgUrl: path.join(__dirname + '/pass.png'),
        // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
        position: [500, 350, 90, 80]
    },
]
//绘制签章
await drawImg(imgList)
//签名
writeTxt('井底的蜗牛', '24px', [240, 370],)
writeTxt('井底的蜗牛', '24px', [550, 370],)

到这里,完整的票据就好了

完整代码

const Koa = require('koa')
const app = new Koa()
const {createCanvas, loadImage, Image} = require('canvas');
const qr = require('qr-image');
const router = require('koa-router')(); /*引入是实例化路由 推荐*/
 
const path = require("path")
const fs = require("fs")
 
const width = 700
const height = 460
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
 
/**
 * @writeTxt: canvas 写入字内容
 * @param {str} t 内容
 * @param {str} s 字体大小 粗体
 * @param {arr} p 写入文字的位置
 * @param {arr} a 写入文字对齐方式 默认 居中
 * @param {obj} c 写入文字颜色 默认 #000
 */
const writeTxt = (t, s = 'normal bold 12px', p, a = 'center', c = '#000') => {
    if (!t) {
        return;
    }
    context.font = `${s} 黑体`;
    context.fillStyle = c;
    context.textAlign = a;
    context.textDecoration = 'underline'
    context.textBaseline = 'middle';
    context.fillText(t, p[0], p[1]);
}
/**
 * @drawTable: 画表格
 */
const drawTable = () => {
 
    const titleArr = [10, 100, 290, 360, 430, 500, 600, 690];
    const titleTxtArr = [
        ['货号', '名称及规格', '单位', '数量', '单价', '金额', '备注']
    ]
    const goodsTxtArr = [
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', ''],
        ['', '', '', '', '', '', '']
    ]
    const bottomArr = [10, 100, 690]
    const bottomTxtArr = [
        ['合计大写', ' 拾 万 仟 佰 拾 元 角 分 ¥ ']
    ]
    drawLine(titleArr, titleTxtArr, 120, 690, 1, 16)
    drawLine(titleArr, goodsTxtArr, 151, 690, goodsTxtArr.length, 16, true)
    drawLine(bottomArr, bottomTxtArr, 301, 690, bottomTxtArr.length, 16, true)
}
 
 
/**
 * @drawLine: 画table线
 * @param list {arr} 表格竖线x轴位置
 * @param tlist {arr} 表格需要填写文字 无文字 填 ''
 * @param startHei {num} 开始画线的高度
 * @param lineWidth {num} 横线的长度
 * @param n {num} 行数
 * @param txtHei {num} 文字位置调整
 * @param isTrue {boolean} 是否为物资列表
 */
const drawLine = (list, tlist, startHei, lineWidth, n, txtHei = 14, isTrue = false) => {
    for (let i = 0; i < n; i++) {
        for (let y in list) {
            if (+y !== 0) {
                const poi = list[y] - (list[y] - list[y - 1]) / 2;
                writeTxt(tlist[i][y - 1], '12px', [poi, startHei + txtHei + 30 * i])
            }
 
            context.moveTo(list[y], startHei);
            context.lineTo(list[y], startHei + 30 * (i + 1));
        }
 
        if (isTrue) {
            const mtY = startHei + 30 * n;
 
            if (i == 0) {
                context.moveTo(10, startHei + 30 * i);
                context.lineTo(690, startHei + 30 * i);
            }
 
            context.moveTo(10, mtY);
            context.lineTo(690, mtY);
        }
        context.moveTo(10, startHei + 30 * i);
        context.lineTo(lineWidth, startHei + 30 * i);
    }
 
    if (isTrue) {
        const mtY = startHei + 30 * n;
        context.moveTo(10, mtY);
        context.lineTo(690, mtY);
    }
 
    context.strokeStyle = '#5a5a59';
    context.stroke();
}
 
 
 
/**
 * 添加图片
 * @param imgPath 图片路径 和图片所在位置,图片路径是绝对路径,可以使用path的方法去读取
 * @returns {Promise<void>}
 */
const drawImg = async (imgPath = [{imgUrl: '', position: []}]) => {
    let len = imgPath.length
    for (let i = 0; i < len; i++) {
        const image = await loadImage(imgPath[i].imgUrl)
        context.drawImage(image, ...imgPath[i].position)
    }
 
}
 
 
// 创建画布
const createMyCanvas = () => {
    context.fillStyle = '#a2bcd3'
    context.fillRect(0, 0, width, height)
}
const mime = require('mime-types');
 
router.get('/img', async (ctx) => {
    //创建画布
    createMyCanvas()
    //开始绘制
    context.beginPath()
    writeTxt('送 货 单', 'normal bold 30px', [370, 30])
    writeTxt('No', '20px', [450, 34])
    writeTxt('收货单地址:XXXXX', '14px', [12, 70], 'left')
    writeTxt('地     址:XXXXXXXXXXX', '14px', [12, 100], 'left')
    writeTxt('2022 年 9 月 22 日', '14px', [680, 100], 'right')
    writeTxt('收货单位及经手人(签章):', '14px', [12, 350], 'left')
    writeTxt('送货单位及经手人(签章):', '14px', [400, 350],)
 
    const imgList = [
        {
            // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
            imgUrl: path.join(__dirname + '/reject.png'),
            position: [180, 350, 90, 80]
        },
        {
            imgUrl: path.join(__dirname + '/pass.png'),
            // imgUrl: 'https://profile.csdnimg.cn/4/1/C/0_weixin_41277748',
            position: [500, 350, 90, 80]
        },
    ]
    await drawImg(imgList)
    writeTxt('井底的蜗牛', '24px', [240, 370],)
    writeTxt('井底的蜗牛', '24px', [550, 370],)
    drawTable()
 
    const buffer = canvas.toBuffer("image/png")
    const imgPath = new Date().getTime() + '.png'
    let filPath = path.join(__dirname + '/static/', imgPath)
    //把图片写入static文件夹
    fs.writeFileSync(filPath, buffer)
    let file = fs.readFileSync(filPath)
    let mimeType = mime.lookup(filPath); //读取图片文件类型
    ctx.set('content-type', mimeType); //设置返回类型
    ctx.body = file; //返回图片
    context.clearRect(0, 0, width, height);
});
app.use(router.routes())
app.use(router.allowedMethods())
 
app.listen(3000)

文件中出现的图片

目录格式

启动 node index.js