弁言
笔者最近接到一个打印标签的需求,由于之前没有做过类似的功能,以是这也是一次学习探索的机会了,打印的结果图如下:
这个最终的打印是放在58mm*58mm的小标签纸上,条形码就是下面的35165165qweqweqe序列号天生的,也是图片形式。序列号应该放在条形码的正下方居中位置的,但是由于笔者前端技术有点拉跨,遇到样式啥的就头疼,这也是尽力后的结果了。下面看集成过程吧。
一、引入pom相关依靠包
笔者的情况是JDK17,pom相关版本如下,详细用什么版本不固定,不报错就行。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <dependency>
- <groupId>com.google.zxing</groupId>
- <artifactId>core</artifactId>
- <version>3.4.1</version>
- </dependency>
- <dependency>
- <groupId>com.google.zxing</groupId>
- <artifactId>javase</artifactId>
- <version>3.4.1</version>
- </dependency>
- <dependency>
- <groupId>ognl</groupId>
- <artifactId>ognl</artifactId>
- <version>3.4.3</version>
- </dependency>
- <!-- Flying Saucer -->
- <dependency>
- <groupId>org.xhtmlrenderer</groupId>
- <artifactId>flying-saucer-pdf</artifactId>
- <version>9.1.20</version>
- </dependency>
- <!--itext-->
- <dependency>
- <groupId>com.lowagie</groupId>
- <artifactId>itext</artifactId>
- <version>2.1.7</version>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- <version>3.12.0</version>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <scope>provided</scope>
- </dependency>
复制代码 二、条形码工具类
- package com.hulei.thymeleafproject;
- import com.google.zxing.BarcodeFormat;
- import com.google.zxing.EncodeHintType;
- import com.google.zxing.client.j2se.MatrixToImageWriter;
- import com.google.zxing.common.BitMatrix;
- import com.google.zxing.oned.Code128Writer;
- import org.apache.commons.lang3.StringUtils;
- import javax.imageio.ImageIO;
- import java.awt.*;
- import java.awt.image.BufferedImage;
- import java.io.ByteArrayOutputStream;
- import java.io.IOException;
- import java.util.HashMap;
- import java.util.Map;
- /**
- * @author hulei
- * @Date 2024/7/26 14:15
- * @Description: 条形码工具类
- *
- */
- public class BarCodeUtils {
- /**
- * 默认图片宽度
- */
- private static final int DEFAULT_PICTURE_WIDTH = 400;
- /**
- * 默认图片高度
- */
- private static final int DEFAULT_PICTURE_HEIGHT = 200;
- /**
- * 默认条形码宽度
- */
- private static final int DEFAULT_BAR_CODE_WIDTH = 300;
- /**
- * 默认条形码高度
- */
- private static final int DEFAULT_BAR_CODE_HEIGHT = 30;
- /**
- * 默认字体大小
- */
- private static final int DEFAULT_FONT_SIZE = 15;
- /**
- * 图片格式
- */
- private static final String FORMAT = "png";
- /**
- * 字符集
- */
- private static final String CHARSET = "utf-8";
- /**
- * 设置 条形码参数
- */
- private static final Map<EncodeHintType, Object> hints = new HashMap<>();
- static {
- hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
- }
- /**
- * 获取条形码图片
- *
- * @param codeValue 条形码内容
- * @return 条形码图片
- */
- public static BufferedImage getBarCodeImage(String codeValue) {
- return getBarCodeImage(codeValue, DEFAULT_BAR_CODE_WIDTH, DEFAULT_BAR_CODE_HEIGHT);
- }
- /**
- * 获取条形码图片
- *
- * @param codeValue 条形码内容
- * @param width 宽度
- * @param height 高度
- * @return 条形码图片
- */
- public static BufferedImage getBarCodeImage(String codeValue, int width, int height) {
- // CODE_128是最常用的条形码格式
- return getBarCodeImage(codeValue, width, height, BarcodeFormat.CODE_128);
- }
- /**
- * 获取条形码图片
- *
- * @param codeValue 条形码内容
- * @param width 宽度
- * @param height 高度
- * @param barcodeFormat 条形码编码格式
- * @return 条形码图片
- */
- public static BufferedImage getBarCodeImage(String codeValue, int width, int height, BarcodeFormat barcodeFormat) {
- Code128Writer writer = switch (barcodeFormat) {
- case CODE_128 ->
- // 最常见的条形码,但是不支持中文
- new Code128Writer();
- case PDF_417 ->
- // 支持中文的条形码格式
- new Code128Writer();
- // 如果使用到其他格式,可以在这里添加
- default -> new Code128Writer();
- };
- // 编码内容, 编码类型, 宽度, 高度, 设置参数
- BitMatrix bitMatrix;
- bitMatrix = writer.encode(codeValue, barcodeFormat, width, height, hints);
- return MatrixToImageWriter.toBufferedImage(bitMatrix);
- }
- /**
- * 获取条形码
- *
- * @param codeValue 条形码内容
- * @param bottomStr 底部文字
- */
- public static BufferedImage getBarCodeWithWords(String codeValue, String bottomStr) {
- return getBarCodeWithWords(codeValue, bottomStr, "", "", "");
- }
- /**
- * 获取条形码
- * @param codeValue 条形码内容
- * @param bottomStr 底部文字
- * @param topLeftStr 左上角文字
- * @param topRightStr 右上角文字
- */
- public static BufferedImage getBarCodeWithWords(String codeValue,
- String bottomStr,
- String bottomStr2,
- String topLeftStr,
- String topRightStr) {
- return getCodeWithWords(getBarCodeImage(codeValue),
- bottomStr,
- bottomStr2,
- topLeftStr,
- topRightStr,
- DEFAULT_PICTURE_WIDTH,
- DEFAULT_PICTURE_HEIGHT,
- 0,
- -20,
- 0,
- 0,
- 0,
- 0,
- DEFAULT_FONT_SIZE);
- }
- /**
- * 获取条形码
- *
- * @param codeImage 条形码图片
- * @param firstBottomStr 底部文字首行
- * @param secondBottomStr 底部文字次行
- * @param topLeftStr 左上角文字
- * @param topRightStr 右上角文字
- * @param pictureWidth 图片宽度
- * @param pictureHeight 图片高度
- * @param codeOffsetX 条形码宽度
- * @param codeOffsetY 条形码高度
- * @param topLeftOffsetX 左上角文字X轴偏移量
- * @param topLeftOffsetY 左上角文字Y轴偏移量
- * @param topRightOffsetX 右上角文字X轴偏移量
- * @param topRightOffsetY 右上角文字Y轴偏移量
- * @param fontSize 字体大小
- * @return 条形码图片
- */
- public static BufferedImage getCodeWithWords(BufferedImage codeImage,
- String firstBottomStr,
- String secondBottomStr,
- String topLeftStr,
- String topRightStr,
- int pictureWidth,
- int pictureHeight,
- int codeOffsetX,
- int codeOffsetY,
- int topLeftOffsetX,
- int topLeftOffsetY,
- int topRightOffsetX,
- int topRightOffsetY,
- int fontSize) {
- BufferedImage picImage = new BufferedImage(pictureWidth, pictureHeight, BufferedImage.TYPE_INT_RGB);
- Graphics2D g2d = picImage.createGraphics();
- // 抗锯齿
- setGraphics2D(g2d);
- // 设置白色
- setColorWhite(g2d, picImage.getWidth(), picImage.getHeight());
- // 条形码默认居中显示
- int codeStartX = (pictureWidth - codeImage.getWidth()) / 2 + codeOffsetX;
- int codeStartY = (pictureHeight - codeImage.getHeight()) / 2 + codeOffsetY;
- // 画条形码到新的面板
- g2d.drawImage(codeImage, codeStartX, codeStartY, codeImage.getWidth(), codeImage.getHeight(), null);
- // 画文字到新的面板
- g2d.setColor(Color.BLACK);
- // 字体、字型、字号
- g2d.setFont(new Font("微软雅黑", Font.PLAIN, fontSize));
- // 文字与条形码之间的间隔
- int wordAndCodeSpacing1 = 0;
- if (StringUtils.isNotEmpty(firstBottomStr)) {
- // 文字长度
- int strWidth = g2d.getFontMetrics().stringWidth(firstBottomStr);
- // 文字X轴开始坐标,这里是居中
- int strStartX = codeStartX + (codeImage.getWidth() - strWidth) / 2;
- // 文字Y轴开始坐标
- int strStartY = codeStartY + codeImage.getHeight() + fontSize + wordAndCodeSpacing1;
- // 画文字
- g2d.drawString(firstBottomStr, strStartX, strStartY);
- }
- // 文字与条形码之间的间隔
- int wordAndCodeSpacing2 = 30;
- if (StringUtils.isNotEmpty(secondBottomStr)) {
- // 文字长度
- int strWidth = g2d.getFontMetrics().stringWidth(secondBottomStr);
- // 文字X轴开始坐标,这里是居中
- int strStartX = codeStartX + (codeImage.getWidth() - strWidth) / 2;
- // 文字Y轴开始坐标
- int strStartY = codeStartY + codeImage.getHeight() + fontSize + wordAndCodeSpacing2;
- // 画文字
- g2d.drawString(secondBottomStr, strStartX, strStartY);
- }
- if (StringUtils.isNotEmpty(topLeftStr)) {
- // 文字长度
- int strWidth = g2d.getFontMetrics().stringWidth(topLeftStr);
- // 文字X轴开始坐标
- int strStartX = codeStartX + topLeftOffsetX;
- // 文字Y轴开始坐标
- int strStartY = codeStartY + topLeftOffsetY - wordAndCodeSpacing1;
- // 画文字
- g2d.drawString(topLeftStr, strStartX, strStartY);
- }
- if (StringUtils.isNotEmpty(topRightStr)) {
- // 文字长度
- int strWidth = g2d.getFontMetrics().stringWidth(topRightStr);
- // 文字X轴开始坐标,这里是居中
- int strStartX = codeStartX + codeImage.getWidth() - strWidth + topRightOffsetX;
- // 文字Y轴开始坐标
- int strStartY = codeStartY + topRightOffsetY - wordAndCodeSpacing1;
- // 画文字
- g2d.drawString(topRightStr, strStartX, strStartY);
- }
- g2d.dispose();
- picImage.flush();
- return picImage;
- }
- /**
- * 设置 Graphics2D 属性 (抗锯齿)
- *
- * @param g2d Graphics2D提供对几何形状、坐标转换、颜色管理和文本布局更为复杂的控制
- */
- private static void setGraphics2D(Graphics2D g2d) {
- g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT);
- Stroke s = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
- g2d.setStroke(s);
- }
- /**
- * 设置背景为白色
- *
- * @param g2d Graphics2D提供对几何形状、坐标转换、颜色管理和文本布局更为复杂的控制
- */
- private static void setColorWhite(Graphics2D g2d, int width, int height) {
- g2d.setColor(Color.WHITE);
- //填充整个屏幕
- g2d.fillRect(0, 0, width, height);
- //设置笔刷
- g2d.setColor(Color.BLACK);
- }
- /**
- * 将 BufferedImage 转为 base64
- */
- public static String bufferedImage2Base64(BufferedImage image) throws IOException {
- // 输出流
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- ImageIO.write(image, FORMAT, stream);
- java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
- String imgBase64 = new String(encoder.encode(stream.toByteArray()), CHARSET);
- imgBase64 = "data:image/" + FORMAT + ";base64," + imgBase64;
- return imgBase64;
- }
- }
复制代码 这个工具类中,默认天生的条形码图片格式是png,固然可以自己修改格式。
三、thymeleaf画模板
这个就是打印模板了,thymeleaf和freemarker一样都是模板引擎,freemarker模板语法更简单些。如果需要简单的变量替换和循环,FreeMarker大概是更好的选择。如果需要更丰富的模板功能和动态内容处理,Thymeleaf大概更得当。笔者这里选择的是thymeleaf。
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <title>维修库商品打印标签模板</title>
- <meta charset="UTF-8"></meta>
- <style> body, html {
- margin: 0;
- padding: 0;
- width: 70mm;
- height: 70mm;
- font-family: 'SimSun', sans-serif; /* 防止生成的PDF中文不显示 */
- }
- h1 {
- text-align: center;
- font-size: 12px;
- line-height: 1.5;
- }
- p {
- font-size: 12px;
- margin: 3px 0;
- }
- .device-code {
- display: flex; /* 使用Flexbox布局 */
- align-items: center; /* 垂直居中对齐 */
- }
- .sn-container {
- display: inline-flex; /* 内联Flexbox容器 */
- align-items: center; /* 垂直居中对齐 */
- margin-left: 2px; /* 与“设备码:”之间的间距 */
- }
- .sn-image {
- width: auto; /* 图片宽度自适应 */
- }
- .sn-text {
- margin-top: 5px; /* 文本与图片之间的间距 */
- text-align: center; /* 文字居中 */
- }
- img {
- vertical-align: middle;
- display: inline-block;
- }
- </style>
- </head>
- <body>
- <div>
- <h1>
- <img th:src="${zlbcImage}" alt="Image" style="height:30px;"></img>智链泊车
- </h1>
- <p th:text="${createTime != null ? '入库日期:'+ createTime : '入库日期:未知'}"></p>
- <p th:text="${materialName != null ? '名 称:'+ materialName : '名称:未知'}"></p>
- <p th:text="${supplierName != null ? '客 户:'+ supplierName : '客户:未知'}"></p>
- <p class="device-code">
- 设 备 码:
- <span class="sn-container">
- <img class="sn-image" th:src="${sequencesNumberImage}" alt="Image"/>
- <div class="sn-text" th:text="${sequencesNumber}">${sequencesNumber}</div>
- </span>
- </p>
- </div>
- </body>
- </html>
复制代码 这个模板里面的变量赋值时比较简单的,主要是有两个图片的变量zlbcImage和sequencesNumberImage,一个是聪明停车前面的原型小图标,一个就是条形码是,在赋值时是需要把图片读成BufferedImage,再把BufferedImage使用base64编码一下。
另外一个紧张的点是:font-family: ‘SimSun’, sans-serif;
这个属性必须加上,否则背面把html转成PDF时,中文会不表现。
四、字体准备simsun.ttc
这个字体是因为,我要把html转成一个PDF,中心转换需要一些字体,并且支持中文,网上搜索了下,选择了simsun.ttc这个字体,同时我在html上也指定了这个字体。网上下载这个字体资源库后,放在如下位置,以便程序中加载使用。
五、测试代码
- package com.hulei.thymeleafproject;
- import com.lowagie.text.pdf.BaseFont;
- import jakarta.annotation.Resource;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RestController;
- import org.thymeleaf.TemplateEngine;
- import org.thymeleaf.context.Context;
- import org.xhtmlrenderer.pdf.ITextFontResolver;
- import org.xhtmlrenderer.pdf.ITextRenderer;
- import javax.imageio.ImageIO;
- import java.awt.image.BufferedImage;
- import java.io.*;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.text.SimpleDateFormat;
- import java.util.*;
- import java.util.List;
- /**
- * @author hulei
- * @date 2024/7/27 9:26
- */
- @RestController
- public class TestController {
- @Resource
- private TemplateEngine templateEngineBySelf;
- @PostMapping("/printSNLabel")
- public void test(@RequestBody List<PrintSNLabelReqDTO> list) {
- list.forEach(loop -> {
- Map<String, Object> map = new HashMap<>();
- map.put("createTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
- map.put("materialName", loop.getMaterialName());
- map.put("supplierName", loop.getSupplierName());
- //设备码图片二进制字节流
- BufferedImage sequencesNumberImage = BarCodeUtils.getBarCodeImage(loop.getSequencesNumber(), 100, 50);
- this.storeImage(sequencesNumberImage, "E:/111.png");
- try {
- String base64Image = BarCodeUtils.bufferedImage2Base64(sequencesNumberImage);
- System.out.println("base64Image: " + base64Image);
- map.put("sequencesNumberImage", base64Image);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- map.put("sequencesNumber", loop.getSequencesNumber());
- try {
- ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
- String symbolImagePath = "images/zlbcImage.png";
- InputStream inputStream = classLoader.getResourceAsStream(symbolImagePath);
- assert inputStream != null;
- BufferedImage zlbcImageBufferedImage = ImageIO.read(inputStream);
- this.storeImage(zlbcImageBufferedImage, "E:/222.png");
- String zlbcImage = BarCodeUtils.bufferedImage2Base64(zlbcImageBufferedImage);
- System.out.println("zlbcImage: " + zlbcImage);
- map.put("zlbcImage", zlbcImage);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- try {
- generateSNPicture(map);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- }
- private void generateSNPicture(Map<String,Object> map) throws IOException {
- // 填充模板数据
- Context context = new Context();
- context.setVariable("createTime", map.get("createTime"));
- context.setVariable("materialName", map.get("materialName"));
- context.setVariable("supplierName", map.get("supplierName"));
- context.setVariable("sequencesNumberImage", map.get("sequencesNumberImage"));
- context.setVariable("sequencesNumber", map.get("sequencesNumber"));
- context.setVariable("zlbcImage", map.get("zlbcImage"));
- String htmlContent = templateEngineBySelf.process("printTemplate", context);
- System.out.println(htmlContent);
- htmlToPdf(htmlContent);
- }
- private void htmlToPdf(String htmlContent){
- try {
- //创建PDf文件
- ITextRenderer renderer = new ITextRenderer();
- //获取使用的字体数据(由于对中文字体显示可能会不支持,所以需要主动添加字体数据设置。)
- ITextFontResolver fontResolver = renderer.getFontResolver();
- fontResolver.addFont("templates/fonts/simsun.ttc",BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
- //设置文件名称
- String sDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
- String sTime = new SimpleDateFormat("HHmmssSSS").format(new Date());
- // 生成临时文件
- Path tempPdfPath = Files.createTempFile("temp_pdf_"+sDate+sTime, ".pdf");
- String pdfFilePath = tempPdfPath.toAbsolutePath().toString();
- // 将html生成文档
- renderer.setDocumentFromString(htmlContent);
- renderer.layout();
- OutputStream os = new FileOutputStream(pdfFilePath);
- // 将文档写入到输出流中
- renderer.createPDF(os);
- // 关闭流
- os.close();
- //把临时生成的文件转移到E盘,这里可以根据个人需求选在把临时文件上传到文件服务器
- System.out.println("pdfFilePath: "+pdfFilePath);
- File tempPdfFile = tempPdfPath.toFile();
- System.out.println("tempPdfFileName: "+tempPdfFile.getName());
- // 复制文件到E盘
- try {
- Path targetPath = Paths.get("E:", tempPdfFile.getName()); // 目标路径
- Files.copy(tempPdfPath, targetPath);
- System.out.println("文件已复制到 E 盘");
- } catch (Exception e) {
- System.err.println("复制文件时发生错误: " + e.getMessage());
- }
- //删除临时生成的本地PDF文件
- Files.delete(tempPdfPath);
- } catch (Exception e) {
- System.out.println("生成pdf文件失败");
- throw new RuntimeException(e);
- }
- }
- private void storeImage(BufferedImage image, String filePath){
- try {
- // 指定输出文件路径和格式
- File outputFile = new File(filePath);
- // 使用 ImageIO.write 方法将图片写入磁盘
- boolean isWritten = ImageIO.write(image, "png", outputFile);
- if (isWritten) {
- System.out.println("图片已成功保存到磁盘.");
- } else {
- System.out.println("图片保存失败.");
- }
- } catch (IOException e) {
- System.err.println("保存图片时发生错误: " + e.getMessage());
- }
- }
- }
复制代码 这里为了展示代码,没有分层了,全都放在了controller层。主要分为三块:加载html模板,变量赋值,html转pdf。
转成pdf后的结果如下:
Apifox测试工具,测试数据如下,留意json是数组形式,因为后端controller接收的是List
整个代码我已上传到gitee:gitee堆栈地址
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |