问题描述

物流行业需要打印的物流单,我发现它们是通过打印pdf实现的,直接前端浏览器请求后端数据流生成pdf,然后调用操作系统的打印功能实现打印。
难点在于:后端根据物流数据生成pdf快递单;前端根据数据流生成pdf文件。

1.后端根据物流数据生成pdf快递单

  1. 首先得有一个快递面单模板,每家快递公司都有自己的模板。快递点的老板打印的时候就生成了pdf面单,叫他复制给你一个,或者网上找一个。以下是京东的快递面单PDF.
  2. springboot 流式输出pdf springboot打印pdf文件_vue.js

  3. 编辑快递面单的内容参数,生成模板pdf
    通过文字编辑工具,把上面模板的文字删除,留下它的线条格式和其它不变的地方,然后通过“准备表单”功能,给pdf添加变量参数,这个功能Adobe DC只有付费版的才有,可以上淘宝买个付费版的软件。

    给PDF相应的地方“添加文本域”,如上图,然后名称写个变量名,此变量名是和后面的代码中的变量对应的,代码中变量的值会打到这个域中;然后调整下它的区域大小和外观字体大小。我这里一共弄了22个变量,效果如下:
    注:两个地方需要用到条形码的,我也给它弄成文本域了,条形码生成看后面的代码。
  4. 根据物流数据生成pdf快递单,以下是代码快。
    pom.xml 中需要导入的依赖包:
<dependency>
		<groupId>com.itextpdf</groupId>
		<artifactId>itextpdf</artifactId>
		<version>5.5.13</version>
	</dependency>

模板类,用于封装数据:

/**
*模板类,字段与上面的变量名一一对应
*/
@Data
public class JdPrintTemplate {

    /**
     * 面单最上面的子条形码
     * */
    private String subQrCode;
    /**
     * 面单下面的父条形码
     * */
    private String pQrCode;

    /**
     * 打印次数
     * */
    private String printTime;
    /**
     * 打印时间
     * */
    private String printDate;

    /**
     * 始发分拣中心
     * */
    private String sourceSortCenterName;

    /**
     *始发道口号
     * */
    private String originalCrossCode;

    private String sourceCrossCode;

    /**
     *始发笼车号
     * */
    private String originalTabletrolleyCode;

    /**
     * 目的分拣中心
     * */
    private String targetSortCenterName;

    private String targetCrossCode;

    /**
     * 目的道口号
     * */
    private String destinationCrossCode;

    /**
     * 目的笼车号
     * */
    private String destinationTabletrolleyCode;

    /**
     * 路区
     * */
    private String road;

    /**
     * 重量
     * */
    private String weight;

    /**
     * 目的站点
     * */
    private String siteName;

    /**
     * 客户名字
     * */
    private String consignee;
    /**
     * 客户电话
     * */
    private String consigneeTel;
    /**
     * 目的地址
     * */
    private String destination;
    /**
     * 发件人名字
     * */
    private String sender;
    /**
     * 发件地址
     * */
    private String senderAddr;

    /**
     * 发件人电话
     * */
    private String senderTel;

    /**
     * 描述
     * */
    private String desc;
    /**
     * 分发码
     * */
    private String distributeCode;
    /**
     * 定单id
     * */
    private String orderId;

    /**
     * 备注
     * */
    private String comment;

    /**
     * 代收金额
     * */
    private String collectMoney;

    /**
     * 应收金额
     * */
    private String totalMoney;

    /**
     * 第几个快递
     * */
    private String serial;

    /**
     * 获取占位字段
     * */
    public Map<String,String> getColumns(){
        Map<String,String> map = new HashMap();
        map.put("printTime",this.printTime);
        map.put("printDate",this.printDate);
        map.put("serial",this.serial);
        map.put("sourceSortCenterName",this.sourceSortCenterName);
        map.put("originalCrossCode",this.originalCrossCode);
        map.put("collectMoney",this.collectMoney);
        map.put("totalMoney",this.totalMoney);
        map.put("originalTabletrolleyCode",this.originalTabletrolleyCode);
        map.put("targetSortCenterName",this.targetSortCenterName);
        map.put("destinationCrossCode",this.destinationCrossCode);
        map.put("destinationTabletrolleyCode",this.destinationTabletrolleyCode);
        map.put("sourceCrossCode", this.sourceCrossCode);
        map.put("targetCrossCode", this.targetCrossCode);
        map.put("road",this.road);
        map.put("weight",this.weight);
        map.put("siteName",this.siteName);
        map.put("consignee",this.consignee);
        map.put("consigneeTel",this.consigneeTel);
        map.put("destination",this.destination);
        map.put("sender",this.sender);
        map.put("senderAddr",this.senderAddr);
        map.put("senderTel",this.senderTel);
        map.put("desc",this.desc);
        map.put("distributeCode",this.distributeCode);
        map.put("orderId",this.orderId);
        map.put("comment",this.comment);

        return map;
    }

    /**
     * 获取条形码字段
     * */
    public Map<String,String> getQrCodes(){
        Map<String,String> map = new HashMap();
        map.put("subQrCode",this.subQrCode);
        map.put("pQrCode",this.pQrCode);
        return map;
    }

}

工具类,用于生成pdf:

public class PdfUtil {
    private static Logger logger = LoggerFactory.getLogger(PdfUtil.class);

    // 利用模板生成pdf
    public static String pdfout(java.util.List<JdPrintTemplate>  tlist) {
        // 模板路径
        String templatePath = System.getProperty("user.dir") + "/downloadPdfPath/pdfTemplate.pdf";
        // 生成的新文件路径
        String fileName = UUID.randomUUID().toString().replace("-","").substring(0,16) +".pdf";
        String newPDFPath = System.getProperty("user.dir") + "/downloadPdfPath/result/"+ fileName;

        FileOutputStream out;

        //每一条数据代表一个pdf表格
        java.util.List<PdfReader> list = new ArrayList();
        try {

            String prefixFont = "";
            String os = System.getProperties().getProperty("os.name");
            if (os.startsWith("win") || os.startsWith("Win")) {
                prefixFont = "C:\\Windows\\Fonts" + File.separator;
            } else {
                prefixFont = "/usr/share/fonts/chinese" + File.separator;
            }

            BaseFont bf = BaseFont.createFont(prefixFont + "simsun.ttc,1", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

            out = new FileOutputStream(newPDFPath);// 输出流

            tlist.forEach(template->{
                try {
                    PdfReader reader = new PdfReader(templatePath);// 读取pdf模板
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();

                    PdfStamper stamper = new PdfStamper(reader, bos);
                    AcroFields form = stamper.getAcroFields();

                    //文字类的内容处理
                    Map<String,String> datemap = template.getColumns();
                    form.addSubstitutionFont(bf);
                    for(String key : datemap.keySet()){
                        String value = datemap.get(key);
                        form.setField(key,value);
                    }
                    //图片类的内容处理
                    Map<String,String> imgmap = template.getQrCodes();
                    for(String key : imgmap.keySet()) {
                        String value = imgmap.get(key);

                        int pageNo = form.getFieldPositions(key).get(0).page;
                        Rectangle signRect = form.getFieldPositions(key).get(0).position;
                        float x = signRect.getLeft();
                        float y = signRect.getBottom();

                        Barcode128 barcode128 = new Barcode128();
                        //条形码字号
                        barcode128.setSize(18);
                        //条形码高度
                        barcode128.setBarHeight(80);
                        //条形码与数字间距
                        barcode128.setBaseline(15);
                        //条形码值
                        barcode128.setCode(value);
                        barcode128.setStartStopText(true);
                        barcode128.setExtended(true);
                        barcode128.setX(2.5f);
                        //绘制条形码在第一页
                        PdfContentByte cb = stamper.getOverContent(pageNo);
                        //生成条形码图片
                        Image image128 = barcode128.createImageWithBarcode(cb, null, null);
                        //图片大小自适应
                        image128.scaleToFit(signRect.getWidth(), signRect.getHeight());
                        //条形码位置
                        image128.setAbsolutePosition(x, y);
                        //加入条形码
                        cb.addImage(image128);
                    }

                    stamper.setFormFlattening(true);// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
                    stamper.close();
                    PdfReader pdfReader = new PdfReader(bos.toByteArray());

                    list.add(pdfReader);
                } catch (DocumentException e) {
                    e.printStackTrace();
                    logger.error("PDF生成出错:{}",e.getMessage());
                } catch (IOException e) {
                    e.printStackTrace();
                    logger.error("PDF生成出错:{}",e.getMessage());
                }
            });

            //上面已经获得了pdf的每一页,这里只需要合并成为一个pdf,然后返回

            Document document = new Document();

            PdfCopy copy = new PdfCopy(document, out);

            document.open();

            for (int k = 0; k < list.size(); k++) {

                PdfReader pdfReader = list.get(k);

                document.newPage();

                copy.addDocument(pdfReader);

            }

            copy.close();

        } catch (IOException e) {
            System.out.println(e);
        } catch (DocumentException e) {
            System.out.println(e);
        }
        return fileName;
    }


    public static void main(String[] args) {

        JdPrintTemplate pTemplate0 = new JdPrintTemplate();
        pTemplate0.setSubQrCode("JDVC11293914838-1-3-");
        pTemplate0.setPQrCode("JDVC11293914838");
        pTemplate0.setPrintTime("1");
        pTemplate0.setPrintDate("2020-11-07 10:52:23");
        pTemplate0.setSerial("第1/3个");
        pTemplate0.setWeight("1.0Kg");
        pTemplate0.setSourceSortCenterName("合肥");
        pTemplate0.setSourceCrossCode("40-济南W");
        pTemplate0.setTargetSortCenterName("烟台");
        pTemplate0.setTargetCrossCode("47-Y6");
        pTemplate0.setSiteName("烟台容大营业部");
        pTemplate0.setRoad("055");
        pTemplate0.setCollectMoney("代收金额:55元");
        pTemplate0.setTotalMoney("应收总计:55元");
        pTemplate0.setConsignee("张三");
        pTemplate0.setConsigneeTel("134567890");
        pTemplate0.setDestination("广州三元里花园酒店");
        pTemplate0.setSender("李四");
        pTemplate0.setSenderTel("189034567");
        pTemplate0.setSenderAddr("深圳宝安");
        pTemplate0.setDesc("防寒衣服");
        pTemplate0.setDistributeCode("143");
        pTemplate0.setOrderId("RX2021110765497664");
        pTemplate0.setComment("客户对我们很重要");

        JdPrintTemplate pTemplate1 = new JdPrintTemplate();
        pTemplate1.setSubQrCode("JDVC456739188890-1-2-");
        pTemplate1.setPQrCode("JDVC456739188890");
        pTemplate1.setPrintTime("1");
        pTemplate1.setPrintDate("2020-11-07 10:52:23");
        pTemplate1.setSerial("第1/2个");
        pTemplate1.setWeight("2.0Kg");
        pTemplate1.setSourceSortCenterName("山东");
        pTemplate1.setSourceCrossCode("40-济南W");
        pTemplate1.setTargetSortCenterName("烟台");
        pTemplate1.setTargetCrossCode("47-Y6");
        pTemplate1.setSiteName("青岛容大营业部");
        pTemplate1.setRoad("056");
        pTemplate1.setCollectMoney("代收金额:65元");
        pTemplate1.setTotalMoney("应收总计:65元");
        pTemplate1.setConsignee("张三1");
        pTemplate1.setConsigneeTel("1888888999");
        pTemplate1.setDestination("青岛三元里花园酒店");
        pTemplate1.setSender("李四1");
        pTemplate1.setSenderTel("189034567");
        pTemplate1.setSenderAddr("深圳宝安1");
        pTemplate1.setDesc("防寒衣服1");
        pTemplate1.setDistributeCode("147");
        pTemplate1.setOrderId("RX20211102345497");
        pTemplate1.setComment("客户对我们很重要11111");

			//每JdPrintTemplate对象数据代表一个pdf表格
        java.util.List<JdPrintTemplate> list = new ArrayList();
        list.add(pTemplate0);
        list.add(pTemplate1);

        pdfout(list);
    }
}

// 模板路径
String templatePath = System.getProperty(“user.dir”) + “/downloadPdfPath/pdfTemplate.pdf”;
// 生成的新文件路径
String fileName = UUID.randomUUID().toString().replace(“-”,“”).substring(0,16) +“.pdf”;
String newPDFPath = System.getProperty(“user.dir”) + “/downloadPdfPath/result/”+ fileName;

把模板PDF命名好,放到相应的位置,就可以生成的打印结果pdf,然后返回生成的文件名,此处就不展示生成的PDF了。

注意:如果是把程序部署上 Linux系统,系统中可能没有对应的字体,会导致生成PDF失败,需要把windows上的字体导出,然后安装到Linux上,具体操作方法,可以搜索查询到,此处不做展示。

  1. 只需要 controller 接口调用 PdfUtil 的生成pdf方法把文件名返回给前端,前端根据文件名来请求生成文件流接口,下面展示生成文件流接口:
@Api(tags = "资源服务接口")
@RestController
@RequestMapping("/downloadResource")
public class ResourceController {
    private Logger logger = LoggerFactory.getLogger(ResourceController.class);

    @Value("${wl.pdfDownloadPath:/downloadPdfPath/result/}")
    private String pdfDownloadPath;

    /**
     * 获取资源
     * */
    @GetMapping(
            value = "/{fileName:.+}",
            produces = {MediaType.MULTIPART_FORM_DATA_VALUE}
    )
    public byte[] getFileWithMediaType(@PathVariable("fileName") String fileName) throws IOException {

        File file = new File(System.getProperty("user.dir") +this.pdfDownloadPath + fileName);

        InputStream in = new FileInputStream(file);

        return IOUtils.toByteArray(in);
    }
}

2.前端vue根据文件流生成pdf

部分关键代码如下:

//打印快递面单
    printExpressSheetButton() {
      console.log(this.deliveryIds);
      //请求生成打印京东面单pdf,返回pdf文件名
      printExpressSheet(this.deliveryIds).then((res) => {
        console.log(res);
          let strs=res.msg.split(":");
          this.$message({
            message: strs[1],
            type: strs[0]=="success"?"success":"warning"
          })
          //res.data就是文件名
         this.getExpressSheetResource(res.data);
      });
    },

	// :获取打印面单数据
    getExpressSheetResource(fileName) {
      getExpressSheetResource(fileName).then((res) => {
      //res返回的就是文件流数据,调用的接口就是"获取资源"接口
        console.log("获取打印面单数据");
        //下面几步是关键
          const binaryData = [];
          binaryData.push(res);
          //获取blob链接,此处是关键
          this.pdfUrl = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));
          window.open(this.pdfUrl);//浏览器会打开新窗口展示pdf文件
      });
    },

上面的javascript代码会打开新窗口展示pdf,就和我们用浏览器打开本地pdf文件一样,然后就可以找页面上的打印按钮,调用操作系统的打印功能开始打印了。

以上代码还有可以优化的地方:pdf如果有100页,从后端到前端传输是需要时间的,而上面的vue代码是等文件传输完后再展示。可以优化成传输了多少就展示多少,用预览的方式展示pdf内容。