问题描述
物流行业需要打印的物流单,我发现它们是通过打印pdf实现的,直接前端浏览器请求后端数据流生成pdf,然后调用操作系统的打印功能实现打印。
难点在于:后端根据物流数据生成pdf快递单;前端根据数据流生成pdf文件。
1.后端根据物流数据生成pdf快递单
- 首先得有一个快递面单模板,每家快递公司都有自己的模板。快递点的老板打印的时候就生成了pdf面单,叫他复制给你一个,或者网上找一个。以下是京东的快递面单PDF.
- 编辑快递面单的内容参数,生成模板pdf
通过文字编辑工具,把上面模板的文字删除,留下它的线条格式和其它不变的地方,然后通过“准备表单”功能,给pdf添加变量参数,这个功能Adobe DC只有付费版的才有,可以上淘宝买个付费版的软件。
给PDF相应的地方“添加文本域”,如上图,然后名称写个变量名,此变量名是和后面的代码中的变量对应的,代码中变量的值会打到这个域中;然后调整下它的区域大小和外观字体大小。我这里一共弄了22个变量,效果如下:
注:两个地方需要用到条形码的,我也给它弄成文本域了,条形码生成看后面的代码。 - 根据物流数据生成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上,具体操作方法,可以搜索查询到,此处不做展示。
- 只需要 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内容。