springboot集成thymeleaf实战

ops/2024/9/23 11:53:10/

引言

笔者最近接到一个打印标签的需求,由于之前没有做过类似的功能,所以这也是一次学习探索的机会了,打印的效果图如下:
在这里插入图片描述
这个最终的打印是放在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>

二、条形码工具类

java">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 ? '名&nbsp;&nbsp;&nbsp;&nbsp;称:'+ materialName : '名称:未知'}"></p><p th:text="${supplierName != null ? '客&nbsp;&nbsp;&nbsp;&nbsp;户:'+ supplierName : '客户:未知'}"></p><p class="device-code">&nbsp;&nbsp;码:<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>

这个模板里面的变量赋值时比较简单的,主要是有两个图片的变量zlbcImagesequencesNumberImage,一个是智慧停车前面的原型小图标,一个就是条形码是,在赋值时是需要把图片读成BufferedImage,再把BufferedImage使用base64编码一下。

另外一个重要的点是:font-family: ‘SimSun’, sans-serif;
这个属性必须加上,否则后面把html转成PDF时,中文会不显示。

四、字体准备simsun.ttc

这个字体是因为,我要把html转成一个PDF,中间转换需要一些字体,并且支持中文,网上搜索了下,选择了simsun.ttc这个字体,同时我在html上也指定了这个字体。网上下载这个字体资源库后,放在如下位置,以便程序中加载使用。
在这里插入图片描述

五、测试代码

java">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 {@Resourceprivate 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仓库地址


http://www.ppmy.cn/ops/87361.html

相关文章

【Python实战因果推断】69_图因果模型4

目录 Querying a Graph in Python Querying a Graph in Python 在接下来的时刻&#xff0c;你将把这个图输入到一个Python库中&#xff0c;这将使得回答关于它的问题变得相当容易。但在你这样做之前&#xff0c;作为一项练习&#xff0c;为了更好地理解你刚刚学到的概念&#x…

【机器学习】逻辑损失函数的基本概念和探索为什么平方误差损失函数不适用于逻辑回归以及探索逻辑损失函数

引言 在机器学习中&#xff0c;逻辑损失函数&#xff08;Logistic Loss Function&#xff09;是用于训练逻辑回归模型的一种损失函数。逻辑回归是一种广泛用于分类问题的模型&#xff0c;特别是用于二分类问题&#xff0c;它的目标是预测一个实例属于两个类别中的一个的概率 文…

随堂测小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;教师管理&#xff0c;试题信息管理&#xff0c;标签类型管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;考试成绩&#xff0c;试题信息&#xff0…

【Kylin使用心得的介绍】

🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 👻Kylin 👻Kylin是一款Linux发行版,由中国国内的开发者团队…

Http高级interview

1、Http https区别 1、https协议需要到ca申请证书&#xff0c;一般免费证书较少&#xff0c;因而需要一定费用。 2、http是超文本传输协议&#xff0c;信息是明文传输&#xff0c;https则是具有安全性的ssl加密传输协议。 3、http和https使用的是完全不同的连接方式&#xf…

杭州网络安全等级保护测评认证的基本流程

杭州网络安全等级保护测评认证&#xff0c;作为守护数字边界的重大责任&#xff0c;其流程科学而严谨。以下是概述&#xff0c;引领您洞悉杭州地区等级保护的认证之道&#xff1a; 1. 系统识别与定级 通过对系统的识别&#xff0c;根据企业的性质和重要程度&#xff0c;准确地…

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙 文章目录 代码随想录算法训练营Day 63| 图论 part03 | 417.太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙17.太平洋大西洋水流问题一、DFS二、BFS三、本题总结 82…

【python的语法特点,如注释规则、代码缩进、编写规范等】

介绍一下python的语法特点&#xff0c;如注释规则、代码缩进、编写规范等 Python 是一种广泛使用的高级编程语言&#xff0c;以其简洁易读的语法、丰富的标准库和强大的第三方库而闻名。下面我将详细介绍 Python 的一些基本语法特点&#xff0c;包括注释规则、代码缩进、以及编…