干撸一个Node环境的PDF生成服务

本文介绍了如何使用Node.js的Express框架和PDFKit库来创建一个PDF生成服务。内容涉及了利用jimp进行图像处理,以及pdfkit的自动排版和文字处理功能。通过这个服务,公司远程问诊项目实现了PDF处方的生成。文章还提到了其他JAVA的PDF库,并建议根据具体业务需求选择合适的技术方案。

用到的库

  • express:Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。许多流行的开发框架都基于 Express 构建。
  • jimp:是一个使用 JavaScript 编写的用于 Node的图像处理库,具有零依赖的特性。
  • pdfkit:PDFKit是一个用于Node和浏览器的PDF文档生成库,它可以轻松创建复杂的、多页的PDF文档。该API既包括低层函数,也包括高层功能的抽象,PDFKit API被设计得很简单,生成复杂的文档就像调用几个函数一样简单(浏览器端的PDF生成与展示建议使用JSPDF)。

介绍

公司的远程问诊项目需要做一个PDF处方生成的功能,后端是使用JAVA写的,本身JAVA有很多优秀的PDF生成库例如:PDFBoxpdfjetOpenPDFitext-7-core,但是要么收费,要么功能不全,要么版本太低(OpenPDF是一个基于iText4的开源免费的分支),经过比较发现pdfkit的API和文档比较完整,pdfkit有一个很有吸引力的功能是自动排盘和文字自动换行自动隐藏。

代码片段

Http请求处理

const express = require('express');
const app = express()
app.post('/createPdfByJson', (req, res,next) => {
    if (req.method == 'POST') {
        let postData = "";
        req.on('data', function (chuck) {
            postData += chuck;
        });
        req.on('end', function () {
            // 处理参数转换
            let _postData = JSON.parse(postData);
            let _recipe = _postData.recipe;
            _recipe.medicines = _postData.recipeMedicineList;
            _recipe.createTime = new Date(_recipe.createTime);
            // 生成PDF文件
            console.log("生成处方:"+JSON.stringify(_recipe));
            pdf.createPdf(_recipe, function (data) {
                res.statusCode = 200
                res.setHeader('Content-Type', 'application/json')
                res.end(JSON.stringify(data))
            });
        });
    }
});

生成PDF

样式展示

在这里插入图片描述

签名图片处理

/**
 * 下载文件
 * @param {文件地址}} url 
 */
function download(url) {
  return new Promise(function (resolve, reject) {
    https.get(new URL(url).href, (res) => {
      let bufferArray = new Array();
      res.on('data', (d) => {
        bufferArray.push(d);
      });
      res.on('end', () => {
        let imgBuffer = Buffer.concat(bufferArray);
        var ab = new ArrayBuffer(imgBuffer.length);
        var view = new Uint8Array(ab);
        for (var i = 0; i < imgBuffer.length; ++i) {
          view[i] = imgBuffer[i];
        }
        resolve(ab);
      });
      res.on('error', (error) => {
        reject(error);
      });
    });
  });
}

/**
 * 下载图像并旋转图像
 * @param {地址}} url 
 */
function downloadImagRotate(url,rotate) {
  return new Promise(function (resolve, reject) {
  	// URL用来做url的转义,因为传过来的地址不一定是合规的例如带有中文
    https.get(new URL(url).href, (res) => {
      let bufferArray = new Array();
      res.on('data', (d) => {
        bufferArray.push(d);
      });
      res.on('error', (error) => {
        reject(error);
      });
      res.on('end', () => {
        let imgBuffer = Buffer.concat(bufferArray);
        jimp.read(imgBuffer).then(image=>{
          // 图片旋转90度
          image.rotate(90);
          image.getBase64Async(jimp.MIME_PNG).then(data => {
            resolve(data);
          });
        });
      });
    });
  });
}

PDF排版

function createPdf(recipe,callback) {
  new Promise(function(resolve, reject){
    try {
      
      // 创建文档
      let doc = new PDFDocument({
        autoFirstPage: false,
        pdfVersion: "1.7ext3",
        info: {
          Title: "电子处方",
          Author: "Node"
        },
        ownerPassword: '123456',
        permissions: {
          printing: "highResolution",
          modifying: false,
          copying: false,
          annotating: false,
          fillingForms: false,
          contentAccessibility: false,
          documentAssembly: false
        }
      });
      // 创建页面
      doc.addPage({
        size: [498.96, 708.48],
        margins: {
          top: 50,
          bottom: 50,
          left: 72,
          right: 72
        }
      })
      // 处方编码
      .fontSize(12)
      .font(_LanTingXiHei)
      .text('处方单号:'+recipe.recipeId, 20, 30)
      // 角标
      .roundedRect(432, 26, 40, 20, 0)
      .fillAndStroke("#FFFFFF", "#000000")
      .font(_LanTingXiHei)
      .fillColor('#000000')
      .fontSize(12)
      .text('普通', 440, 30)
      .text('', 72, 50)
      // 医生就职机构
      .font(_LanTingHei)
      .fontSize(18)
      .text('', 100, 70)
      .text(recipe.clinicName+' 处方笺', {
        align: 'center',
        bold: 'Courier-Bold'
      })
      .font(_LanTingXiHei)
      // 患者基本信息
      .moveDown()
      .moveTo(20, 105)
      .lineTo(480, 105)
      .fillAndStroke("#000000", "#000000")
      .stroke()
      .moveDown()
      .fontSize(12)
      .text('姓  名 : '+recipe.patientName, 20, 117, {
        width: 285,
        height: 50,
        ellipsis: true
      })
      .text('性 别 : '+(recipe.patientSex==0?'男':'女'), 180, 117, {
        width: 285,
        height: 50,
        ellipsis: true
      })
      .text('年 龄 : '+recipe.patientAge+'岁', 340, 117, {
        width: 285,
        height: 50,
        ellipsis: true
      })
      // 诊断信息
      .moveDown()
      .fontSize(12)
      .text('科  室 : '+recipe.doctorDepartment, 20, 140, {
        width: 285,
        ellipsis: true
      })
      .text('日 期 : '+recipe.createTime.getFullYear()+"年"+(recipe.createTime.getMonth()+1)+"月"+recipe.createTime.getDate()+"日", 180, 140, {
        width: 285,
        ellipsis: true
      })
      .moveDown()
      .text('过敏史 : '+(recipe.allergyHistory?recipe.allergyHistory:''), 20, 163, {
        width: 450,
        height: 20,
        ellipsis: true
      })
      .moveDown()
      .text('诊  断 : ', 20, 186)
      .text(recipe.diagnosis, 72, 186, {
        width: 400,
        height: 40,
        ellipsis: true
      })
      // 药品列表
      .moveDown()
      .moveTo(20, 220)
      .lineTo(480, 220)
      .stroke()
      .moveDown()
      .font('fonts/兰亭黑 GBK.TTF')
      .fontSize(18)
      .text('RP', 20, 230)
      .text('', 20, 259)
      .font(_LanTingXiHei);
      
      for (let i = 0; i < recipe.medicines.length; i++) {
        if (i != 0) {
          doc.moveDown();
        }
        let m = recipe.medicines[i];
        doc
          .fontSize(12)
          .text((i + 1) + '.'+m.medicineName, {
            continued: true,
            align: 'left',
            indent: 5
          })
          .text(m.medicinePec, {
            continued: true,
            align: 'center'
          })
          .text(m.medicineNum + m.packageUnit, {
            align: 'right'
          });
        doc
          .moveDown()
          .fillColor('#333333')
          .text('用法用量:'+m.useInfo, {
            indent: 20
          })
          .fillColor('#000000');
      }
      // 页脚
      doc
      .moveDown()
      .moveTo(20, 620)
      .lineTo(480, 620)
      .stroke()
      .text('', 20, 634)
      .fontSize(12)
      .text('医师:', 20, 644)
      .text('审核/核对:', 180, 644)
      .text('调配:', 350, 644);
      resolve(doc);
    } catch (error) {
      reject(error);
    }
  }).catch(function(erro){
    callback({code:500,data:null,error:erro});
  }).then(function(doc){
    if(!doc){
      return;
    }
    // 处理电子签证
    let eSignature = new Array();
    if(recipe.autograph){
      eSignature.push(download(recipe.autograph));
    }else{
      eSignature.push(false);
    }
    if(recipe.pharmacistAutograph){
      eSignature.push(downloadImagRotate(recipe.pharmacistAutograph,90));
    }else{
      eSignature.push(false);
    }
    if(recipe.dispenserAutograph){
      eSignature.push(downloadImagRotate(recipe.dispenserAutograph,90));
    }else{
      eSignature.push(false);
    }
    if(recipe.clinicOfficialSeal){
      eSignature.push(download(recipe.clinicOfficialSeal));
    }else{
      eSignature.push(false);
    }
    Promise.all(eSignature).catch(function(error){
      callback({code:500,data:null,error:erro});
    }).then(function(imgs){
      if(!imgs){
        return;
      }
      if(imgs[0]){
        doc.image(imgs[0], 40, 624, { width: 100 });
      }
      if(imgs[1]){
        doc.image(imgs[1], 230, 644, { width: 100 });
      }
      if(imgs[2]){
        doc.image(imgs[2], 370, 644, { width: 100 });
      }
      if(imgs[3]){
        doc.image(imgs[3], 345, 510, { width: 130 });
      }
      doc.end();
      return doc;
    }).catch(function(error){
      callback({code:500,data:null,error:error.message});
    }).then(function(_doc){
      if(!_doc){
        return;
      }
      // 上传OSS
      getStream.buffer(_doc).then(function(data){
        let client = new OSS({
          region: '',
          accessKeyId: '',
          accessKeySecret: '',
          bucket: ''
        });
        client.put(recipe.recipeId+"_"+(new Date().getTime())+'.pdf', data).then(function(ossRes){
          callback({code:200,data:ossRes.url,error:null});
        });
      });
    });
  });
}

结语

pdf生成是一个常用的功能,实现方式也有很多种,可以结合自己的业务实际来做技术选型,例如加密、自定义图形、报表等。jimp的优势在于零依赖,node还有另一个优秀的图片处理库sharp它依赖于一个高效的图片处理库libvips.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值