node+koa+canvas繪制出貨單、收據(jù)票據(jù)的方法
在生成票據(jù)需求中,我們會想到前端生成或者后端生成返回圖片地址訪問兩個方法,前端生成則不需要調(diào)用接口,而后端是在完成整個流程時就進行生成然后把上傳的地址保存數(shù)據(jù)庫
先看效果
下面我們就使用node +koa+canvas后端生成圖片的方法進行生成
使用庫
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
把用到的庫都導(dǎo)入進去,當(dāng)然如何創(chuàng)建node項目我這就不做過多的描述,創(chuàng)建成功后,直接使用 node index.js 就可以啟動服務(wù)了
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)
創(chuàng)建一個api 提供外面可訪問的接口api
在末尾加了一個供外面訪問的接口,啟動服務(wù)后 訪問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 服務(wù)已經(jīng)好了,正片開始,瓜子飲料礦泉水,前面的麻煩讓一讓,
首先我沒得知道,票據(jù)單有哪些內(nèi)容
1、標題:編號,日期,地址;這些都是文字,所以我沒得繪制文字
2、表格:表頭,內(nèi)容,線條;表格就是線條堆積而成的,內(nèi)容就是文字,這里就得繪制線條,文字
3、尾部:文字,印章,簽名;這里需要繪制文字,圖片兩個
總的來說,我們想要繪制出一張票據(jù)單,得要繪制文字,繪制線條,通過線條與文字結(jié)合生成表格,最后添加印章與簽名照片
繪制畫布
1、給畫布設(shè)置長寬
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 寫入文字對齊方式 默認 居中 * @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]); }
畫布繪表格線條函數(shù)
/** * @drawLine: 畫table線 * @param list {arr} 表格豎線x軸位置 * @param tlist {arr} 表格需要填寫文字 無文字 填 '' * @param startHei {num} 開始畫線的高度 * @param lineWidth {num} 橫線的長度 * @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 = [ ['貨號', '名稱及規(guī)格', '單位', '數(shù)量', '單價', '金額', '備注'] ] 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,相關(guān)繪制的函數(shù)已經(jīng)好了,那么接下來就是進行排版了
1、首先的是頭部的標題,單位,位置 編號,和時間
//創(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')(); /*引入是實例化路由 推薦*/ 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 寫入文字對齊方式 默認 居中 * @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 = [ ['貨號', '名稱及規(guī)格', '單位', '數(shù)量', '單價', '金額', '備注'] ] 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} 行數(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 圖片路徑 和圖片所在位置,圖片路徑是絕對路徑,可以使用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)的圖片
目錄格式
啟動 node index.js
到此這篇關(guān)于node+koa+canvas繪制出貨單,收據(jù),票據(jù)的文章就介紹到這了,更多相關(guān)node+koa+canvas繪制出貨單內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
node.js中的fs.readlinkSync方法使用說明
這篇文章主要介紹了node.js中的fs.readlinkSync方法使用說明,本文介紹了fs.readlinkSync方法說明、語法、接收參數(shù)、使用實例和實現(xiàn)源碼,需要的朋友可以參考下2014-12-12nodemon實現(xiàn)Typescript項目熱更新的示例代碼
這篇文章主要介紹了nodemon實現(xiàn)Typescript項目熱更新的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11