springboot集成thymeleaf实战

引言

笔者最近接到一个打印标签的需求,由于之前没有做过类似的功能,所以这也是一次学习探索的机会了,打印的效果图如下:
在这里插入图片描述
这个最终的打印是放在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 ? '名&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上也指定了这个字体。网上下载这个字体资源库后,放在如下位置,以便程序中加载使用。
在这里插入图片描述

五、测试代码

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仓库地址

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值