当前位置: 首页 > 后端技术 > Node.js

Node图片处理-Jimp配合node-qrcode生成图片上传摘要

时间:2023-04-03 15:08:35 Node.js

Node图片处理-Jimp配合node-qrcode生成图片上传摘要二维码和文案,生成新图片供用户保存。接到这个需求的时候,我非但没有拒绝的意思,反而有点小激动~因为又可以去探索新的东西了。大致效果如下,原图:效果图:试水canvas一开始我打算在前端用canvas生成图片。我们都知道canvas有合成图片的功能,核心就是drawImage和toDataURL这两个方法。大致思路是:使用drawImage将生成的二维码合并到原图的指定位置,使用fillText方法生成副本,使用toDataURL将图片转为base64,使用atob和Uint8Array转为Buffer进行上传。但是这个方案最终没有成功,因为不同手机的尺寸比例不统一,生成的二维码位置不能准确定位到指定位置,所以采用了另一种方案:节点层生成图片。不需要考虑节点层的适配问题,因为只有一个benchmark,就是原图。生成的二维码和文案的大小和位置可以直接写死。经过研究,最著名的节点图像处理库有两个,分别是:Jimp和Sharp。最终选择了Jimp,因为没有安装Sharp。二维码库有很多,最后决定选择node-qrcode。我们开始做吧!主要步骤是两步,分别是:生成图片,读取图片,上传。下面分两步来讲解生成图片。生成图片是最麻烦的。有很多步骤:使用qrcode生成二维码Buffer和打包二维码Buffer生成Jimp对象的文案并保存。将base64转换为缓冲区。有兴趣的可以参考他们的文档:Jimpnode-qrcodeNodeBuffer生成图片有很多步骤,每一步都依赖于上一步的结果,而且都是异步的。如果你使用回调,你将彻底陷入回调地狱。因此,主要想谈的是代码的组织方式。不怕大家笑话,我的第一版代码是这样的?://生成二维码BufferconstcodeBuffer=yieldnewPromise((resolve,reject)=>{Qrcode.toDataURL(url,{},(err,url)=>{//注意:必须在此处删除“data:image/png;base64,”,以将其转换为正确的缓冲区constres=Buffer.from(url.replace(/.+,/,''),'base64')err?reject(err):resolve(res)})}).catch(()=>{})//生成文本consttextJimp=yieldnewPromise((resolve,reject)=>{newJimp(textBgWidth,config.textBgHeight,+`0xFF${config.textBgColor}`,(err,image)=>{Jimp.loadFont(config.fontPath).then((font)=>{resolve(image.print(font,config.textPadding,10,textContent,10))})})})//将二维码Buffer包装到Jimp对象中constcodeJimp=yieldnewPromise((resolve,reject)=>{Jimp.read(codeBuffer).then((res)=>{if(res){resolve(res.resize(config.codeWidth,config.codeWidth))}else{reject('无法包装缓冲区')}})}).catch(()=>{})产生新的Promise((resolve,reject)=>{Jimp.read(config.originImgPath).then(img=>{img.composite(codeJimp,config.codeLeft,config.codeTop).composite(textJimp,config.textLeft,config.textTop)//由于fs.createReadStream不能接受Buffer作为参数,生成的图片只能暂时保存在本地write(config.tempFilePath,()=>{//resolve()reject('图片保存失败!')})})}).catch((err)=>{console.log('保存图片时出错:',err)})因为我们使用的node前端分离框架grace的版本支持generator语法,所以想到了用yield来同步显示异步操作,但是还是显得太繁琐了?必须重构!承诺在这里!使用promise的链式调用语法,结构会清晰很多。重写后的代码是这样的://CombinemultipleasynchronousI/OconstimgResult=yieldgenerateCode(href)//GenerateQRcodeBuffer.then((res)=>{codeBuffer=res;//包装二维码作为Jimp对象的缓冲区returnwrapCodeBuffer(codeBuffer,imgConfig);}).then((res)=>{codeJimp=res;//生成文本returngenerateText(textBgWidth,textContent,imgConfig);}).then((res)=>{textJimp=res;//组合生成图片returncompositeImg(imgConfig,textJimp,codeJimp);})//成功。then(()=>true)//中间有错误。catch((err)=>{returnfalse;});instantandelegant很多~实现方法也很简单,就是让每一步的方法返回一个promise,以这个方法为例:/***将二维码Buffer包装成Jimp对象*@param{Buffer}codeBuffer[二维codeBuffer对象]*@param{Object}config[配置对象]*@return{Promise}*/functionwrapCodeBuffer(codeBuffer,config){returnnewPromise((resolve,reject)=>{Jimp.read(codeBuffer).then((res)=>{if(res){resolve(res.resize(config.codeWidth,config.codeWidth));}else{reject('封装二维码缓冲区失败');}});});}上传图片接下来就是使用node上传图片了。由于使用的后端接口是基于FormData方法的,所以需要在node层模拟一个FormData上传请求。起初,我完全一头雾水,因为我对http协议的标准只有一知半解。在前端使用FormData上传图片时,我们经常会看到这样的请求体:------WebKitFormBoundarywQMoN5B2ZNAD6uqNContent-Disposition:form-data;名称=“文件”;filename="avatar.jpeg"Content-Type:image/jpeg------WebKitFormBoundarywQMoN5B2ZNAD6uqN--请求头的Content-Type是这样的:Content-Type:multipart/form-data;boundary=----WebKitFormBoundarywQMoN5B2ZNAD6uqN看起来挺复杂的,尤其是这个------WebKitFormBoundarywQMoN5B2ZNAD6uqN--那是什么鬼东西。别急,先说我的上传方法:/***上传图片方法*@param{ClientRequest}request[http.request方法返回的对象]*@param{Object}config[配置对象]*@param{String}cookies[用户请求带来的所有cookies]*@return*/functionuploadImg(request,config,cookies=''){//模拟form-data请求后端接口上传图片constboundaryKey=Math.随机().toString(16);constendData='\r\n----'+boundaryKey+'--';让contentLength=0,content='';content+='\r\n----'+boundaryKey+'\r\n'+'Content-Type:application/octet-stream\r\n'+'Content-Disposition:form-data;名称=“文件”;'+'文件名="bg_invite.png";\r\n'+'内容传输编码:二进制\r\n\r\n';让contentBinary=Buffer.from(content,'utf-8');//获取上传内容总大小contentLength=fs.statSync(config.tempFilePath).size+Buffer.byteLength(contentBinary)+Buffer.byteLength(endData);//设置请求头request.setHeader('Content-Type','multipart/form-data;boundary=--'+boundary钥匙);request.setHeader('Content-Length',contentLength);request.setHeader('Cookie',cookies);request.write(contentBinary);constfileStream=fs.createReadStream(config.tempFilePath,{bufferSize:4*1024});fileStream.on('end',()=>{//发送请求request.end(endData);});fileStream.pipe(request);}可以看到,这个方法其实就是构造了request,拆分出来的东西有:构造请求头,计算上传内容的总大小,将文件写入到http.ClientRequest对象中stream的形式,先说requestheader,FormData形式的RequestContent-Type是multipart/form-data,而且必须提供boundaryfield,但是为什么呢?我们都知道,默认提交表单时,Content-Type是application/x-www-form-urlencoded,在requestbody中以name=John&age=12的形式传递参数,参数为用&隔开。这里的分界线的作用和&一样,用来分隔多个参数,可以自定义。在浏览器中,浏览器自动帮我们生成了,所以我们就知道上面的边界是怎么回事了~我们来看一下每个边界之间的内容,也就是每个字段,包括Content-type和Content-Disposition字段,我们非常陌生。Content-Type与http协议的Content-Type相同,只是在multipart/form-data类型中,我们可以手动指定每个参数的Content-Type。方法中的字段值为application/octet-stream,它告诉服务器这部分内容是字节流,因为我们需要以字节流的形式上传图片。而Content-Disposition是每个参数必选项,取值必须是form-data。header其实还有其他用途,可以参考MDN的官方文档。接下来是计算Content-Length。这里主要用到了node的fs模块和Buffer模块的api,很容易理解,看文档就可以了。最后将图片写入http.ClientRequest对象。该对象由节点的http.request方法返回,是一个可写流。引用官方节点文档:ClientRequest实例是一个可写流。如果需要通过POST请求上传文件,请写入ClientRequest对象。最后调用http.ClientRequest对象的end方法完成request对象的写入,然后发送请求~至此,需要一个Node合成图片并上传就完成了!过程中收获很多!生活还要继续!