三方又带着他的接口来折磨我了
老板每天都在找新的货代,哪家便宜对接哪家,我们就在后边吭嗤吭嗤改写。写完上线用两天,贵了,算了,不用了。又重新找,重新对接。
这次聊的这位是,某某某货代,对接完了,查询货运面单的时候,其他人都是给个pdf链接,我们打印,贴货上,发走。而他给的是base64的,嘎嘎长一串,好几万字符,而我们还要支持批量打印,又得重新写逻辑了,统一处理的方法到他这里又不适用了,唉。。。
接下来说说处理的过程吧,把我们的业务简化掉。就着重写写使用Java将base64的图片转成pdf并返回给前端的过程吧。这里前边还有一个生成发货单条形码的逻辑和一段根据已有的pdf链接展示pdf的也就一起说了。
具体逻辑是这样的
- 请求api获取base64的图片,其实是个html,外边还包了一层
- 根据物流单号生成一个条码,放在首页
- 解析base64的图片,生成pdf
- 若是调用失败,或者其他原因报错,那么显示一个固定的PDF,上面显示的就是打印面单失败。
好了,开始撸代码
源码
完整的源码在这里ParseBase64ImageAndConvertItToPdf.java,因为我会将代码拆开说。所以先把源码放上来。
base64的图片返回是这样的,img里边是一整个base64的字符,好几万个字符。
<html>
<div style="page-break-after: always;">
<div style="display: relative;">
<div>
<img src="data:image/jpg;base64,/9j/4AAQSkK......KKA" style="max-width:350px;" />
</div>
</div>
</div>
<div style="page-break-after: always;">
<div style="display: relative;">
<div>
<img src="data:image/jpg;base64,/9j/4AAQSk......ZJR" style="max-width:350px;" />
</div>
</div>
</div>
</html>
依赖
依赖是这样的
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>barcodes</artifactId>
<version>7.1.11</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.1.11</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
<scope>compile</scope>
</dependency>
解析
好了,开始解析这个html,读取其中的base64,并生成pdf
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(writer);
Document doc = new Document(pdfDoc);
int errorCount = 0;
这里这四步的内容是
- 创建一个
ByteArrayOutputStream
对象,用于将生成的PDF内容写入内存中的字节数组。 - 使用
PdfWriter
将字节输出流连接到PDF文档。 - 创建
PdfDocument
对象,这是PDF文档的主要表示形式。 - 创建
Document
对象,用于在PDF文档中添加内容。 - 顺带初始化了一个错误次数的计数器
String resultStr = callApi("paramsStr");
然后call了api,来获取结果
针对这个base64的图片,我们是这样处理的。
// 使用Jsoup解析resultStr,将其转换为一个HTML文档对象。
org.jsoup.nodes.Document document = Jsoup.parse(resultStr);
// 选择所有<img>元素。
Elements elements = document.select("img");
// 选择第一个<img>元素。
Element element1 = document.selectFirst("img");
// 初始化一个列表imageBytesList,用于存储解析后的图像字节数据。
List<byte[]> imageBytesList = Lists.newArrayList();
for (Element element : elements) {
// 从每个<img>元素的src属性中提取Base64编码的图像数据。
String base64Image = element.attr("src").split(",")[1];
// 将Base64编码的图像数据解码为字节数组,并添加到imageBytesList中。
// java9以及之后的版本弃用,可以用byte[] imageByte =
// Base64.getDecoder().decode(base64Image);替换
byte[] imageBytes = DatatypeConverter.parseBase64Binary(base64Image);
imageBytesList.add(imageBytes);
}
- 使用Jsoup解析
resultStr
,将其转换为一个HTML文档对象Jsoup
是一个用于解析、操作和清理HTML的Java库。同时也可以用来生成HTML。- 这里的
Document
则是解析之后的整个HTML文档,可以用来查询和修改HTML内容
document.select("img")
方法可以选择所有的img元素document.selectFirst("img")
可以选择第一个img元素DatatypeConverter
则是将Base64编码的图像数据解码为字节数组,这是个用于数据类型转换的 Java 类
但是
DatatypeConverter
的实现依赖于线程本地变量且在数据量大的时候会有性能瓶颈,更重要的是在Java 9 及以后的版本中被标记为已弃用。所以,我们可以使用java.util.Base64
类来解码这个Base64方法是
byte[] imageByte = Base64.getDecoder().decode(base64Image);
至此,我们已经拿到了这个base64文件的字节数组。
条形码
然后我们需要添加一个条码。 使用java生成条码的方法很多,我们就挑其中一个来用吧。
// 使用ImageIO读取字节数组中的图像并获取其宽度和高度。
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageBytesList.get(0)));
float width = bufferedImage.getWidth();
float height = bufferedImage.getHeight();
// 将像素转换为PDF的点(point)单位。
float widthInPoints = width * 72 / 96;
float heightInPoints = height * 72 / 96;
// 创建一个新的页面大小PageSize。
PageSize pageSize = new PageSize(widthInPoints, heightInPoints);
// 向PDF文档添加一个新页面,使用上述页面大小。
pdfDoc.addNewPage(pageSize);
// 创建一个条形码对象并设置条形码数据。
Barcode39 barcode = new Barcode39(pdfDoc);
barcode.setCode("123456");
// 将条形码转换为图像并设置其位置和大小,然后添加到PDF文档中。
Image barcodeImage = new Image(barcode.createFormXObject(null, null, pdfDoc));
barcodeImage.setHorizontalAlignment(HorizontalAlignment.CENTER);
barcodeImage.setMarginTop((heightInPoints - heightInPoints / 8) / 2);
barcodeImage.setWidth(350);
barcodeImage.setHeight(100);
doc.add(barcodeImage);
- 这里首先使用
ImageIO
从上方的字节数组中读取到原始文件的大小,以便于我们创建纸张的时候,创建同样大小的。 - float widthInPoints = width * 72 / 96;这个操作看起来就很奇怪,为什么这样能将像素转换为PDF的点呢?
- 首先是PDF 页面和大多数排版系统都使用点作为单位,进行这种转换确保图像大小在 PDF 中正确显示。
- 每英寸有 96 个像素
- 每英寸有 72 点
- 所以将像素单位直接转换为点单位时可以使用这个操作
- 通过这个转换,确保图像在PDF文档中以正确的尺寸和分辨率呈现。
Barcode39
类是用来生成Code 39条形码
的类。Code39是一种广泛使用的条形码标准,能够编码字母数字字符,包括 A-Z、0-9 和一些特殊字符。- 之前也用过Barcode128的,它能够编码所有 128 个 ASCII 字符,包括控制字符。密度高能够在较小的空间内编码更多的信息
- 相比 Barcode128,Barcode39 的编码密度较低,生成的条形码在相同信息量下更长。
将字符数字转为PDF
之后则是,将上边的字节数组,转为我们需要的pdf了
// 遍历图像字节列表,为每张图像添加一个新的页面。
pdfDoc.addNewPage(pageSize);
// 设置页面边距为0,并将图像添加到页面中。
doc.setMargins(0, 0, 0, 0);
Image img = new Image(ImageDataFactory.create(imageBytes));
img.setHorizontalAlignment(HorizontalAlignment.CENTER);
doc.add(img);
- 这里的
new Image(ImageDataFactory.create(imageBytes))
将一个字节数组(表示图像数据)转换为一个可以添加到 PDF 文档中的图像对象。这是是 PDF 生成过程中关键的一步。
添加链接中的PDF
接下来就再展示一个将链接中的pdf添加进来
// 从指定的URL加载一个外部PDF文档,并将其第一页复制到当前PDF文档中。
String externalPdfUrl = "https://test.luke.com/fail.pdf";
PdfDocument externalPdfDoc = new PdfDocument(new PdfReader(externalPdfUrl));
PdfPage page = externalPdfDoc.getPage(1);
pdfDoc.addPage(page.copyTo(pdfDoc));
externalPdfDoc.close();
结尾
这个没啥字,需要用了贴来用就可以了。 最终需要关一些流之类的:
// 关闭文档和写入器,释放资源。
doc.close();
writer.close();
// 设置HTTP响应的内容类型为PDF,并指定文件名。
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "inline; filename=\"output.pdf\"");
// 设置响应内容长度,并将字节数组输出流中的数据写入响应输出流。
response.setContentLength(baos.size());
baos.writeTo(response.getOutputStream());
至此,这个工作就完成了,对接的三方越多,代码越复杂。复用性还低。真惨哇。回头把这块逻辑改改,提个公共方法出来,再有给我发base64的就不头疼了哇。
转载自:https://juejin.cn/post/7374631918111735846