node+koa+canvas繪制出貨單、收據(jù)票據(jù)的方法
在生成票據(jù)需求中,我們會(huì)想到前端生成或者后端生成返回圖片地址訪問兩個(gè)方法,前端生成則不需要調(diào)用接口,而后端是在完成整個(gè)流程時(shí)就進(jìn)行生成然后把上傳的地址保存數(shù)據(jù)庫(kù)
先看效果


下面我們就使用node +koa+canvas后端生成圖片的方法進(jìn)行生成
使用庫(kù)
1、node
2、canvas npm install canvas
3、koa npm install koa
4、mime-types npm install mime-types -S
首先創(chuàng)建服務(wù) index.js
把用到的庫(kù)都導(dǎo)入進(jìn)去,當(dāng)然如何創(chuàng)建node項(xiàng)目我這就不做過(guò)多的描述,創(chuàng)建成功后,直接使用 node index.js 就可以啟動(dòng)服務(wù)了
const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是實(shí)例化路由 推薦*/
//....這里需要做很多事
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)創(chuàng)建一個(gè)api 提供外面可訪問的接口api
在末尾加了一個(gè)供外面訪問的接口,啟動(dòng)服務(wù)后 訪問localhost:3000/img 就可以訪問了
const Koa = require('koa')
const app = new Koa()
const {createCanvas, Image} = require('canvas');
const router = require('koa-router')(); /*引入是實(shí)例化路由 推薦*/
//....這里需要做很多事
router.get('/img', async (ctx) => { });
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)ok 服務(wù)已經(jīng)好了,正片開始,瓜子飲料礦泉水,前面的麻煩讓一讓,
首先我沒得知道,票據(jù)單有哪些內(nèi)容
1、標(biāo)題:編號(hào),日期,地址;這些都是文字,所以我沒得繪制文字
2、表格:表頭,內(nèi)容,線條;表格就是線條堆積而成的,內(nèi)容就是文字,這里就得繪制線條,文字
3、尾部:文字,印章,簽名;這里需要繪制文字,圖片兩個(gè)
總的來(lái)說(shuō),我們想要繪制出一張票據(jù)單,得要繪制文字,繪制線條,通過(guò)線條與文字結(jié)合生成表格,最后添加印章與簽名照片
繪制畫布
1、給畫布設(shè)置長(zhǎng)寬
const width = 700
const height = 460
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')2、創(chuàng)建畫布 給畫布添加背景顏色
const createMyCanvas=()=>{
context.fillStyle = '#a2bcd3'
context.fillRect(0, 0, width, height)
}畫布添加文字函數(shù)
/**
* @writeTxt: canvas 寫入字內(nèi)容
* @param {str} t 內(nèi)容
* @param {str} s 字體大小 粗體
* @param {arr} p 寫入文字的位置
* @param {arr} a 寫入文字對(duì)齊方式 默認(rèn) 居中
* @param {obj} c 寫入文字顏色 默認(rèn) #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]);
}畫布繪表格線條函數(shù)
/**
* @drawLine: 畫table線
* @param list {arr} 表格豎線x軸位置
* @param tlist {arr} 表格需要填寫文字 無(wú)文字 填 ''
* @param startHei {num} 開始畫線的高度
* @param lineWidth {num} 橫線的長(zhǎng)度
* @param n {num} 行數(shù)
* @param txtHei {num} 文字位置調(diào)整
* @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 = [
['貨號(hào)', '名稱及規(guī)格', '單位', '數(shù)量', '單價(jià)', '金額', '備注']
]
const goodsTxtArr = [
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', '']
]
const bottomArr=[10,100,690]
const bottomTxtArr=[
['合計(jì)大寫', ' 拾 萬(wàn) 仟 佰 拾 元 角 分 ¥ ']
]
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)
}繪制圖片,這里繪制圖片其實(shí)就是繪制印章
/**
* 添加圖片
* @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,相關(guān)繪制的函數(shù)已經(jīng)好了,那么接下來(lái)就是進(jìn)行排版了
1、首先的是頭部的標(biāo)題,單位,位置 編號(hào),和時(shí)間
//創(chuàng)建畫布
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、表格部分,繪制表頭,表格,表尾
這里直接調(diào)用繪制表格的函數(shù)就可以了
3、票據(jù)尾部,簽章,簽字
writeTxt('收貨單位及經(jīng)手人(簽章):', '14px', [12, 350], 'left')
writeTxt('送貨單位及經(jīng)手人(簽章):', '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],)到這里,完整的票據(jù)就好了
完整代碼
const Koa = require('koa')
const app = new Koa()
const {createCanvas, loadImage, Image} = require('canvas');
const qr = require('qr-image');
const router = require('koa-router')(); /*引入是實(shí)例化路由 推薦*/
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 寫入字內(nèi)容
* @param {str} t 內(nèi)容
* @param {str} s 字體大小 粗體
* @param {arr} p 寫入文字的位置
* @param {arr} a 寫入文字對(duì)齊方式 默認(rèn) 居中
* @param {obj} c 寫入文字顏色 默認(rèn) #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 = [
['貨號(hào)', '名稱及規(guī)格', '單位', '數(shù)量', '單價(jià)', '金額', '備注']
]
const goodsTxtArr = [
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', ''],
['', '', '', '', '', '', '']
]
const bottomArr = [10, 100, 690]
const bottomTxtArr = [
['合計(jì)大寫', ' 拾 萬(wàn) 仟 佰 拾 元 角 分 ¥ ']
]
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} 表格需要填寫文字 無(wú)文字 填 ''
* @param startHei {num} 開始畫線的高度
* @param lineWidth {num} 橫線的長(zhǎng)度
* @param n {num} 行數(shù)
* @param txtHei {num} 文字位置調(diào)整
* @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 圖片路徑 和圖片所在位置,圖片路徑是絕對(duì)路徑,可以使用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)
}
}
// 創(chuàng)建畫布
const createMyCanvas = () => {
context.fillStyle = '#a2bcd3'
context.fillRect(0, 0, width, height)
}
const mime = require('mime-types');
router.get('/img', async (ctx) => {
//創(chuàng)建畫布
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('收貨單位及經(jīng)手人(簽章):', '14px', [12, 350], 'left')
writeTxt('送貨單位及經(jīng)手人(簽章):', '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); //設(shè)置返回類型
ctx.body = file; //返回圖片
context.clearRect(0, 0, width, height);
});
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)文件中出現(xiàn)的圖片


目錄格式

啟動(dòng) node index.js
到此這篇關(guān)于node+koa+canvas繪制出貨單,收據(jù),票據(jù)的文章就介紹到這了,更多相關(guān)node+koa+canvas繪制出貨單內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
package.json的版本號(hào)更新優(yōu)化方法
這篇文章主要為大家介紹了package.json的版本號(hào)更新優(yōu)化方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
NodeJS實(shí)現(xiàn)一個(gè)聊天室功能
這篇文章主要介紹了NodeJS實(shí)現(xiàn)一個(gè)聊天室功能,本文實(shí)例截圖相結(jié)合給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11
node.js微信小程序配置消息推送的實(shí)現(xiàn)
這篇文章主要介紹了node.js微信小程序配置消息推送的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
node.js中的fs.readlinkSync方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.readlinkSync方法使用說(shuō)明,本文介紹了fs.readlinkSync方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
nodemon實(shí)現(xiàn)Typescript項(xiàng)目熱更新的示例代碼
這篇文章主要介紹了nodemon實(shí)現(xiàn)Typescript項(xiàng)目熱更新的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

