原创

Java-生成二维码带多行文字-支持自动换行-支持tab缩进效果-支持高度自适应

1. 样式1 :

2.1 效果图 :

忽略红色,最外层的红色只是方便展示当前图片的宽高
file

2.2 代码

package cn.jiangjiesheng.utils;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 或者换个思路,写html导出pdf 或者导出png ,不知道能否行得通?参考 eduinp 生产pdf和zip打包的逻辑的逻辑。
 * <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>
 */
public class QRCodeWithTextGenerator {

    private final static Logger log = LoggerFactory.getLogger(QRCodeWithTextGenerator.class);

    private static boolean IS_DEBUG = true;
    private static final String OUTPUT_FILE = "qrcode.png"; // 输出文件名
    private static final String SPACE = " "; // 输出文件名
    private static int QRCODE_WIDTH = 200;
    private static final int FONT_SIZE = 12;
    private static final int EXPANSION_FACTOR = 5; // textInfo 扩大倍数 ,按 \\n 换行(一条属性信息),一条属性信息可能比较长,还会继续换行,最终来裁切图片,此值大致表示每条属性信息最多可能的换行条数
    private static final int SPACE_COUNT = 14; // 换行时左侧的空格占位
    private static Font FONT = new Font("Microsoft YaHei", Font.PLAIN, FONT_SIZE);

    public static void main(String[] args) {
        try {
            //main 方法测试使用 直接使用 static 默认值就行
            //init();

            String QR_CODE_TEXT = "http://code.jiangjiesheng.cn";
            String textInfo = "设备名称: xxxx\n所属机构: 123456789123456789123456789\n所属测点: xxxx\n所属区域: 江苏省南京市江北新区安徽省安庆市\n其他信息: 联系人 xxx \n";

            BufferedImage image = generateQRCodeWithText(QR_CODE_TEXT, QRCODE_WIDTH, textInfo);
            saveImageToFile(image, OUTPUT_FILE);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    @PostConstruct
    public void init() throws IOException, FontFormatException {
        // 加载系统字体
        // FONT = new Font("Microsoft YaHei", Font.PLAIN, FONT_SIZE);

        // 加载自定义字体
        try {
            writeFontFile(this.getClass(), "Microsoft YaHei-normal.ttc", false);
            String fontPath = getFontFilePath4Read("Microsoft YaHei-normal.ttc");

            FONT = Font.createFont(Font.TRUETYPE_FONT, new File(fontPath)).deriveFont(Font.PLAIN, FONT_SIZE);
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            ge.registerFont(FONT);
        } catch (IOException | FontFormatException e) {
            log.error("加载自定义字体出错", e);
        }
    }

    private static BufferedImage generateQRCodeWithText(String codeContent, int qrcodeWidthHeight, String textInfo) throws WriterException {
        // 设置二维码参数
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(EncodeHintType.MARGIN, 1); // 边距

        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(codeContent, BarcodeFormat.QR_CODE, qrcodeWidthHeight, qrcodeWidthHeight, hints);

        int qrWidth = bitMatrix.getWidth();
        int qrHeight = bitMatrix.getHeight();

        // 创建二维码图像
        BufferedImage qrImage = new BufferedImage(qrWidth, qrHeight, BufferedImage.TYPE_INT_ARGB);
        for (int x = 0; x < qrWidth; x++) {
            for (int y = 0; y < qrHeight; y++) {
                qrImage.setRGB(x, y, bitMatrix.get(x, y) ? Color.BLACK.getRGB() : Color.WHITE.getRGB());
            }
        }

        // 创建一个新的图像,包含二维码和文本
        int textPadding = 10; // 文本区域的内边距
        int imageWidth = qrcodeWidthHeight + textPadding; // 文本区域的内边距

        int lineHeight = FONT_SIZE + 10; // 行高

        //默认每一行不需要换行的高度
        int textHeightNoWrap = (int) (lineHeight * (countLines(textInfo))); // +1 最后一行后也保留个空行,模拟padding
        int textMaxHeightWrapped = textHeightNoWrap * EXPANSION_FACTOR;

        // 计算总的宽度和高度
        int totalWidth = qrWidth;
        // int totalHeightNoWrap = qrHeight + textHeightNoWrap + textPadding * 2; // 上下各增加内边距
        int totalMaxHeightWrapped = qrHeight + textMaxHeightWrapped + textPadding * 2; // 上下各增加内边距

        // 创建包含二维码和文本的图像
        BufferedImage fullImage = new BufferedImage(totalWidth, totalMaxHeightWrapped, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = fullImage.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        //设置整个背景白色
        Graphics g = fullImage.getGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, totalWidth, totalMaxHeightWrapped);

        // 绘制二维码
        g2d.drawImage(qrImage, 0, 0, null);

        // 绘制文本区域背景
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, qrHeight + textPadding, totalWidth, textHeightNoWrap + textPadding);

        // 绘制文本
        g2d.setColor(Color.BLACK);
        //Font font = Font.createFont(Font.TRUETYPE_FONT, new File("path/to/font.ttf")); eduinp 有加载字体的逻辑,有关于.的处理
        g2d.setFont(FONT); // 使用支持中文的字体

        String[] lines = textInfo.split("\n");
        int textY = (int) (qrHeight + textPadding * 0); // 文本起始位置
        int textX = textPadding; // 文本起始位置 新增的

        FontRenderContext affineTransform = g2d.getFontRenderContext();

        //因为每一条信息多次换行会带来高度的变化,这里记录下必不换行时新增的行数
        AtomicInteger addedLineCount = new AtomicInteger();

        for (String line : lines) {
            // 只管单行
            //  g2d.drawString(line, textPadding, textY);
            //  textY += lineHeight;

            String currentLine = "";
            for (char word : line.toCharArray()) {
                // 尝试添加单词到当前行
                String newLine = !currentLine.isEmpty() ? (currentLine + word) : String.valueOf(word);

                TextLayout layout = new TextLayout(newLine, FONT, affineTransform);
                double width = layout.getBounds().getWidth();

                if (width > imageWidth - (textPadding * 3)) { //这里 x 3 是为了右侧更宽点
                    // 如果新行超过了图片宽度,则绘制当前行并移到下一行
                    drawText(g2d, currentLine, textX, textY, affineTransform);
                    textY += lineHeight;
                    currentLine = getSpaceString() + word;
                    addedLineCount.addAndGet(1);
                } else {
                    // 否则继续添加单词
                    currentLine = newLine;
                }
            }
            if (!currentLine.isEmpty()) {
                drawText(g2d, currentLine, textX, textY, affineTransform);
                textY += lineHeight;
            }
        }

        g2d.dispose();

        // 添加边框【调试使用,方便debug时显示BufferedImage】
        int borderWidth = 0;
        if (IS_DEBUG) {
            borderWidth = 2;
            Color borderColor = new Color(255, 0, 0); // 浅灰色
            fullImage = addBorder(fullImage, borderWidth, borderColor);
        }
        //裁切

        //整体多出的高度
        int textHeightRealHeight = textHeightNoWrap + lineHeight * addedLineCount.get();
        //这里正常都应该是大于0的,还是多层判断吧
        //- (int)(lineHeight * 0.5) 为了让底部空白小点
        int totalHeightRealHeight = totalMaxHeightWrapped - (textMaxHeightWrapped - textHeightRealHeight) - (int) (lineHeight * 0.5);
        if (totalHeightRealHeight > 0) {
            // 使用 getSubimage 方法裁剪图像
            fullImage = fullImage.getSubimage(0, 0, totalWidth + borderWidth * 2, totalHeightRealHeight);
        }
        //最后画个框
        fullImage = addBorder(fullImage, 1, Color.BLACK);
        return fullImage;

    }

    /**
     * 换行时左侧的空格占位
     *
     * @return
     */
    private static String getSpaceString() {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < SPACE_COUNT; i++) {
            str.append(SPACE);
        }
        return str.toString();
    }

    private static void drawText(Graphics2D g2d, String text, int x, int y, FontRenderContext affineTransform) {
        TextLayout layout = new TextLayout(text, g2d.getFont(), affineTransform);
        layout.draw(g2d, x, y + layout.getAscent());
    }

    // 添加边框的方法
    private static BufferedImage addBorder(BufferedImage originalImage, int borderWidthHeight, Color borderColor) {
        // 创建一个带边框的新图像
        BufferedImage borderedImage = new BufferedImage(originalImage.getWidth() + borderWidthHeight * 2,
                originalImage.getHeight() + borderWidthHeight * 2,
                BufferedImage.TYPE_INT_ARGB);

        Graphics2D g2d = borderedImage.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制边框
        g2d.setColor(borderColor);
        g2d.fillRect(0, 0, borderedImage.getWidth(), borderedImage.getHeight());

        // 绘制原始图像到新图像的中心
        g2d.drawImage(originalImage, borderWidthHeight, borderWidthHeight, null);

        g2d.dispose();

        return borderedImage;
    }

    private static int countLines(String text) {
        return text.split("\n").length;
    }

    private static void saveImageToFile(BufferedImage image, String filePath) throws IOException {

        boolean result = ImageIO.write(image, "png", new File(filePath));

        if (!result) {
            System.out.println("Failed to write the image file.");
        } else {
            System.out.println("Image file written successfully.");
        }

    }

    public static void writeFontFile(Class clazz, String fontName, boolean showLog) {
        try {
            String targetFilePath = getFontFilePath(fontName, true);
            File file = new File(targetFilePath);
            if (file.exists()) {
                if (showLog) {
                    log.info("writeFontFile 已存在,不再写入");
                }
                return;
            }

            file.createNewFile();
            file.setWritable(true);
            InputStream inputStream = clazz.getResourceAsStream("/font/" + fontName);
            copyInputStreamToFile(inputStream, file);

            log.info("writeFontFile 已写入到jar包当前文件夹下的./fonts/下");
        } catch (IOException e) {
            log.error("writeFontFile 已写入到jar包当前文件夹下的./fonts/出现异常,异常:{}", e);
        }
    }

    public static void copyInputStreamToFile(InputStream in, File file) throws IOException {
        try (OutputStream out = Files.newOutputStream(file.toPath())) {
            byte[] buffer = new byte[1024];
            int read;

            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } finally {
            // 确保关闭输入流以释放资源
            if (in != null) {
                in.close();
            }
        }
    }

    public static String getFontFilePath4Read(String fontName) {
        return getFontFilePath(fontName, false);
    }

    private static String getFontFilePath(String fontName, boolean is4Write) {
        if (is4Write) {
            return getFontFileHolder() + (fontName);
        }
        // return getFontFileHolder() + (fontName +",0");
        return getFontFileHolder() + (fontName);
    }

    private static String getFontFileHolder() {
        return "." + File.separatorChar;//+ "fonts"+ File.separatorChar; 放一个多级目录创建文件夹失败了
    }
}

2. 样式2

2.1 效果

file

2.2 代码

package cn.jiangjiesheng.utils;

import com.envtoday.ecp.core.utils.QRCodeUtil;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.*;

import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.util.Hashtable;

/**
 * <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>
 */
@Component
@Slf4j
public class QRCodeGenerator {

    private final static int FONT_SIZE = 16;
    private final static String TEXT_SUFFIX = "...";
    private final static String FONT_FILE_NAME = "Microsoft YaHei-normal.ttc";
    private static Font FONT = null;
    private static final int DPI = 300;

    @PostConstruct
    public void init() throws IOException, FontFormatException {
        // 加载当前运行系统已有字体
        // FONT = new Font("Microsoft YaHei", Font.PLAIN, FONT_SIZE);

        // 加载自定义字体
   try {
            writeFontFile(this.getClass(), FONT_FILE_NAME, false);
            String fontPath = getFontFilePath4Read(FONT_FILE_NAME);

            FONT = Font.createFont(Font.TRUETYPE_FONT, new File(fontPath)).deriveFont(Font.PLAIN, FONT_SIZE);
            // 必要,否则不生效
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            ge.registerFont(FONT);
        } catch (IOException | FontFormatException e) {
            log.error("加载自定义字体出错", e);
        }
    }


    public static void main(String[] args) {
        String text = "https://code.jiangjiesheng.cn";
        String filePath = "qrcode.png";

        try {

            int totalImageWH = QRCodeUtil.convertPrintSize2Pixel(4, DPI);
            // int qrCodeWH = convertPrintSize2Pixel(3, 300);
            int qrCodeWH = QRCodeUtil.convertPrintSize2Pixel(3.5f, DPI);
            int rectMargin = QRCodeUtil.convertPrintSize2Pixel(0.5f, DPI);

            //BufferedImage image = createQRCodeWithText(text, "江苏今世缘酒业股份有限公司", "南厂区清下水排口", totalImageWH, qrCodeWH, rectMargin);
            BufferedImage image = createQRCodeWithText(text, "xxx20日下午在xxx接见第33届夏季奥林匹克运动会中国体育代表团全体成员", "南厂区清下水排口", totalImageWH, qrCodeWH, rectMargin);

            image = addBorder(image, 1, Color.BLACK);

            ImageIO.write(image, "png", new File(filePath));
            System.out.println("QR code generated and saved to: " + filePath);
        } catch (WriterException | IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建带文字区域的二维码图片
     *
     * @param qrText       二维码内容
     * @param firstLine    本文区域第一行
     * @param secondLine   本文区域第二行
     * @param totalImageWH 图片整体宽高
     * @param qrCodeWH     二维码宽高
     * @param rectMargin   二维码边框距离图片边缘的距离
     * @return BufferedImage
     * @throws WriterException 异常
     */
    public static BufferedImage createQRCodeWithText(String qrText, String firstLine, String secondLine,
                                                     int totalImageWH, int qrCodeWH, int rectMargin)
            throws WriterException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        Hashtable<EncodeHintType, ErrorCorrectionLevel> hints = new Hashtable<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

        //二维码自身
    BitMatrix bitMatrix = qrCodeWriter.encode(qrText, BarcodeFormat.QR_CODE, qrCodeWH, qrCodeWH, hints);

        int matrixWidth = bitMatrix.getWidth();
        //  int textHeight = 50;
        //  int qrcodeRectWidth = 20;// 10*2
        //整个图像的
        //  int totalImageWidth = matrixWidth + qrcodeRectWidth;
        //  int totalImageHeight = matrixWidth + textHeight;
        int totalImageWidth = totalImageWH;
        int totalImageHeight = totalImageWH;

        BufferedImage image = new BufferedImage(totalImageWidth, totalImageHeight, BufferedImage.TYPE_INT_RGB);
        image.createGraphics();
        //设置整个背景白色
     Graphics2D graphics = (Graphics2D) image.getGraphics();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, totalImageWidth, totalImageHeight);

        // 二维码周围画一个边框
     graphics.setColor(new Color(200, 200, 200)); // Light gray color
        //int rectWH = totalImageWidth - rectMargin * 2 + 2;
        int rectWH = totalImageWidth - rectMargin * 2 + 2;
        // graphics.drawRect(rectMargin, rectMargin, matrixWidth + 2, matrixWidth + 2);
        graphics.drawRect(rectMargin, rectMargin / 2, rectWH, rectWH);

        // Draw the QR code bits
        graphics.setColor(Color.BLACK);
        int xStart = (totalImageWH - qrCodeWH) / 2;
        int yStart = xStart / 2 - 12; // - 12 是动态调整
        for (int i = 0; i < matrixWidth; i++) {
            for (int j = 0; j < matrixWidth; j++) {
                if (bitMatrix.get(i, j)) {
                    graphics.fillRect(i + xStart, j + yStart, 1, 1);
                }
            }
        }

        // 在二维码下方增加文字区域
    graphics.setFont(FONT);
        graphics.setColor(Color.BLACK);

        int yStartText = matrixWidth + 10;

        FontRenderContext affineTransform = graphics.getFontRenderContext();

        //下方显示机构名称+测点名称,各占一行(如果文字字数过多,超二维码宽度,则显示...,不要换行)
        firstLine = getShortText(matrixWidth, firstLine, affineTransform, graphics.getFontMetrics());
        secondLine = getShortText(matrixWidth, secondLine, affineTransform, graphics.getFontMetrics());

        graphics.drawString(firstLine, (totalImageWH - graphics.getFontMetrics().stringWidth(firstLine)) / 2, yStartText);
        graphics.drawString(secondLine, (totalImageWH - graphics.getFontMetrics().stringWidth(secondLine)) / 2, yStartText + 25);

        return image;
    }


    /**
     * 如果文字字数过多,超二维码宽度,则显示...,不要换行
     * 实时测量计算 + 递归调用
     *
     * @param maxWidth 文本行允许的最大像素宽度
     * @param text 文本内容
     * @param affineTransform affineTransform 测量使用
     * @param fontMetrics fontMetrics 测量使用
     * @return 超长文子后面显示...
     */
    private static String getShortText(int maxWidth, String text, FontRenderContext affineTransform, FontMetrics fontMetrics) {
        double textWidth = measureTextWidth(text, affineTransform, fontMetrics);
        int subCount = text.length() - 1 - TEXT_SUFFIX.length();
        if (textWidth > maxWidth && subCount >= 0) {
            text = text.substring(0, subCount);
            text += TEXT_SUFFIX;
            return getShortText(maxWidth, text, affineTransform, fontMetrics);
        }
        return text;
    }

    /**
     * 测量文字长度
     *
     * @param text 文本
     * @param affineTransform 测量使用
     * @param fontMetrics 测量使用
     * @return 文本占用的像素宽度
     */
    private static double measureTextWidth(String text, FontRenderContext affineTransform, FontMetrics fontMetrics) {
        // 两种方案,结果相差不大
    // 方案1:
        // TextLayout layout = new TextLayout(text, FONT, affineTransform);
        // return layout.getBounds().getWidth();
        // 方案2:
    return fontMetrics.stringWidth(text);
    }

 /**
 * 目标打印尺寸转成像素:
 * 像素 = (3cm / 2.54cm/inch) x 300DPI
 * 1英寸等于2.54厘米。
 *
 * @param length 长度,单位cm
 * @param dpi 打印机dpi
 * @return 像素
 */
    public static int convertPrintSize2Pixel(float length, int dpi) {
        return (int) (length / 2.54 * dpi);
    }

    /**
     * 压缩分辨率
     * 入参改成File,图片内容就是固化的,
     * BufferedImage在修改尺寸或dpi后,图片内容也会发生变动(由大图变成小图时)
     *
     * @param width 目标宽高
     * @param dpi 目标dpi,打印常见推荐至少 >= 300
     * @return BufferedImage
     */
    public static BufferedImage compressImage(BufferedImage bufferedImage, int width, int dpi) {
        int newWidth = convertPrintSize2Pixel(width, dpi);

        Image scaledImage = bufferedImage.getScaledInstance(newWidth, newWidth, Image.SCALE_SMOOTH);
        bufferedImage = new BufferedImage(newWidth, newWidth, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = bufferedImage.createGraphics();
        graphics2D.drawImage(scaledImage, 0, 0, null);
        graphics2D.dispose();
        return bufferedImage;
    }

    /**
     * 添加边框的方法
     * @param originalImage BufferedImage
     * @param borderWidthHeight 边框的宽高
     * @param borderColor 边框的颜色
     * @return BufferedImage
     */
    private static BufferedImage addBorder(BufferedImage originalImage, int borderWidthHeight, Color borderColor) {
        // 创建一个带边框的新图像
        BufferedImage borderedImage = new BufferedImage(originalImage.getWidth() + borderWidthHeight * 2,
                originalImage.getHeight() + borderWidthHeight * 2,
                BufferedImage.TYPE_INT_ARGB);

        Graphics2D g2d = borderedImage.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制边框
        g2d.setColor(borderColor);
        g2d.fillRect(0, 0, borderedImage.getWidth(), borderedImage.getHeight());

        // 绘制原始图像到新图像的中心
        g2d.drawImage(originalImage, borderWidthHeight, borderWidthHeight, null);

        g2d.dispose();

        return borderedImage;
    }

    /**
     * BufferedImage存入本地File文件
     *
     * @param image BufferedImage
     * @param filePath 文件路径
     * @throws IOException IO异常
     */
    private static void saveImageToFile(BufferedImage image, String filePath) throws IOException {
        boolean result = ImageIO.write(image, "png", new File(filePath));
        if (!result) {
            log.error("图片写入本地失败,file path :{} ", filePath);
        } else {
            log.info("图片写入本地成功,file path :{} ", filePath);
        }
    }

    /**
     * 将 BufferedImage 转换为 InputStream
     *
     * @param image BufferedImage
     * @param format 格式
     * @return 输入流
     * @throws Exception 异常
     */
    public static InputStream bufferedImageToInputStream(BufferedImage image, String format) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, format, baos);
        baos.flush();
        byte[] imageBytes = baos.toByteArray();
        ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
        baos.close();
        return bais;
    }

    /**
     * 写入字体到本地
     *
     * @param clazz  clazz
     * @param fontName 自定义字体名称
     * @param showLog 是否显示日日志
     */
    public static void writeFontFile(Class<?> clazz, String fontName, boolean showLog) {
        try {
            String targetFilePath = getFontFilePath(fontName, true);
            File file = new File(targetFilePath);
            if (file.exists()) {
                if (showLog) {
                    log.info("writeFontFile 已存在,不再写入");
                }
                return;
            }

            boolean newFile = file.createNewFile();
            boolean writable = file.setWritable(true);

            InputStream inputStream = clazz.getResourceAsStream("/font/" + fontName);
            if (null == inputStream) {
                throw new FileNotFoundException("字体文件未找到");
            }
            copyInputStreamToFile(inputStream, file);
            log.info("writeFontFile 已写入到jar包当前文件夹下的./fonts/下");
        } catch (IOException e) {
            log.error("writeFontFile 已写入到jar包当前文件夹下的./fonts/出现异常,异常:", e);
        }
    }

    /**
     * 输入流写入到文件
     *
     * @param in 输入流
     * @param file 文件
     * @throws IOException io异常
     */
    public static void copyInputStreamToFile(InputStream in, File file) throws IOException {
        try (OutputStream out = Files.newOutputStream(file.toPath())) {
            byte[] buffer = new byte[1024];
            int read;

            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
        } finally {
            // 确保关闭输入流以释放资源
            if (in != null) {
                in.close();
            }
        }
    }

    /**
     * 获取自定义字体的路径
     *
     * @param fontName 自定义字体名称
     * @return 自定义字体路径
     */
    public static String getFontFilePath4Read(String fontName) {
        return getFontFilePath(fontName, false);
    }

    private static String getFontFilePath(String fontName, boolean is4Write) {
        if (is4Write) {
            return getFontFileHolder() + (fontName);
        }
        // 如果是itextpdf服务,需要这样写
        // return getFontFileHolder() + (fontName +",0");
        return getFontFileHolder() + (fontName);
    }

    /**
     * 获取自定义字体文件夹
     *
     * @return 自定义字体文件夹
     */
    private static String getFontFileHolder() {
        // 放一个多级目录创建文件夹失败了
        // return "." + File.separatorChar + "fonts"+ File.separatorChar;
        return "." + File.separatorChar;
    }

}

3. 样式3

长方形二维码,文本区域左下侧,代码来自zxx

3.1 代码

package cn.jiangjiesheng.common;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.extern.slf4j.Slf4j;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;


/**
 * @Description:
 * @Date 2024/8/21 13:36
 * @Author martin
 */
@Slf4j
public class QRCodeWithText {

    private static boolean IS_DEBUG = false;
    private static final String OUTPUT_FILE = "qrcode.png"; // 输出文件名
    private static final String SPACE = " "; // 输出文件名
    private static int DEFAULT_QRCODE_WIDTH = 200;
    private static final int FONT_SIZE = 14;
    // textInfo 扩大倍数 ,按 \\n 换行(一条属性信息),一条属性信息可能比较长,还会继续换行,最终来裁切图片,此值大致表示每条属性信息最多可能的换行条数
    private static final int EXPANSION_FACTOR = 5;
    // 换行时左侧的空格占位
    // textInfo = "设备名称: xxxx\n所属机构: 123456789123456789123456789\n所属测点: xxxx\n所属区域: 江苏省南京市江北新区安徽省安庆市\n其他信息: 联系人 xxx \n";
    private static final int SPACE_COUNT = 0;

    private static Font FONT = null;

    public static void main(String[] args) {
        try {
            initFont();
            // 调用方法并传入动态参数
            generateQRCodeWithTable(
                    "Sample QR Code Data",
                    "南京江北新区",
                    "江苏联通监测点",
                    "123.png"
            );
            System.out.println("打印图片结束");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void initFont() throws IOException, FontFormatException {
        // writeFontFile(this.getClass(),"Microsoft YaHei-normal.ttc",false);
        String fontPath = getFontFilePath4Read("Microsoft YaHei-normal.ttc");
        // 加载系统字体
        FONT = new Font("Microsoft YaHei", Font.PLAIN, FONT_SIZE);

        // 加载自定义字体
        try {
            FONT = Font.createFont(Font.TRUETYPE_FONT, new File(fontPath)).deriveFont(Font.PLAIN, FONT_SIZE);
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            ge.registerFont(FONT);
        } catch (IOException | FontFormatException e) {
            log.error("加载自定义字体出错", e);
        }
    }

    public static String getFontFilePath4Read(String fontName) {
        return getFontFilePath(fontName, false);
    }

    private static String getFontFilePath(String fontName, boolean is4Write) {
        if (is4Write) {
            return getFontFileHolder() + (fontName);
        }
        // return getFontFileHolder() + (fontName +",0");
        return getFontFileHolder() + (fontName);
    }

    private static String getFontFileHolder() {
        return "." + File.separatorChar;//+ "fonts"+ File.separatorChar; 放一个多级目录创建文件夹失败了
    }

    public static void generateQRCodeWithTable(String data, String orgName, String monitorName, String outputFilePath) throws WriterException, IOException {
        int qrCodeWidth = 300; // 二维码的宽度
        int qrCodeHeight = 300; // 二维码的高度
        int tableWidth = 200; // 表格宽度
        int tableHeight = 180; // 表格高度
        int fixedColWidth = 60; // 第一列固定列宽度
        int imageWidth = qrCodeWidth + tableWidth; // 图像宽度:二维码宽度 + 表格宽度
        int imageHeight = Math.max(qrCodeHeight, tableHeight); // 图像高度:二维码高度和表格高度的最大值
        int titleHeight = qrCodeHeight - tableHeight - 60; // 标题高度

        // 创建二维码
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        BitMatrix bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, qrCodeWidth, qrCodeHeight);

        BufferedImage qrCodeImage = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < qrCodeWidth; x++) {
            for (int y = 0; y < qrCodeHeight; y++) {
                qrCodeImage.setRGB(x, y, bitMatrix.get(x, y) ? Color.BLACK.getRGB() : Color.WHITE.getRGB());
            }
        }

        // 创建最终图像
        BufferedImage finalImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = finalImage.createGraphics();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, imageWidth, imageHeight);


        // 绘制表格
        int tableX = 5; // 表格X坐标,图像左边缘
        int tableY = imageHeight - tableHeight - 40; // 表格Y坐标,与底部对齐

        int numRows = 3; // 表格行数
        int numCols = 2; // 表格列数
        int cellWidth = (tableWidth - fixedColWidth) / (numCols - 1); // 单元格宽度(排除第一列自定义的宽度)
        int cellHeight = tableHeight / numRows; // 单元格高度


        // 绘制表格网格
        graphics.setColor(Color.BLACK);
        for (int i = 0; i <= numCols; i++) {
            int colWidth = i == 1 ? fixedColWidth : cellWidth; // 第一列固定宽度,第二列开始自适应宽度
            graphics.drawLine(tableX + i * colWidth, tableY, tableX + i * colWidth, tableY + tableHeight);
        }
        for (int i = 0; i <= numRows; i++) {
            graphics.drawLine(tableX, tableY + i * cellHeight, tableX + tableWidth, tableY + i * cellHeight);
        }

        // 绘制转置后的表格内容
        graphics.setFont(new Font("Microsoft YaHei", Font.PLAIN, 12)); // 使用系统自带的中文字体
        graphics.setColor(Color.BLACK);
        String[][] tableData = {
                {"测点名称", "机构名称", "所属区域"},
                {orgName, monitorName, "江苏省南京市浦口区"}
        };

        // 转置表格数据
        String[][] transposedData = transposeData(tableData);

        // 绘制转置后的数据
        FontMetrics metrics = graphics.getFontMetrics();
        for (int row = 0; row < numRows; row++) {
            for (int col = 0; col < numCols; col++) {
                int colWidth = col == 0 ? fixedColWidth : cellWidth; // 第一列固定宽度,第二列开始自适应宽度
                String cellText = transposedData[row][col]; // 转置后的数据
                java.util.List<String> lines = wrapText(cellText, metrics, colWidth);
                int textY = tableY + row * cellHeight + (cellHeight - lines.size() * metrics.getHeight()) / 2 + metrics.getAscent();

                // 绘制每一行文本
                for (String lineText : lines) {
                    int textX = tableX + col * colWidth + (colWidth - metrics.stringWidth(lineText)) / 2;
                    graphics.drawString(lineText, textX, textY);
                    textY += metrics.getHeight();
                }
            }
        }

        // 绘制二维码
        // graphics.drawImage(qrCodeImage, tableWidth, 0, null);

        // 绘制“监测点”文本在表格中间上方
        String titleText = "监测点位";
        Font titleFont = new Font("Microsoft YaHei", Font.BOLD, 24); // 设置标题字体和大小
        graphics.setFont(titleFont);
        FontMetrics fontMetrics = graphics.getFontMetrics();
        int textWidth = fontMetrics.stringWidth(titleText);
        int textX1 = tableX + (tableWidth - textWidth) / 2; // 中心对齐X坐标
        int textY1 = tableY + 15; // 上方的Y坐标(可以调整)
        graphics.drawString(titleText, textX1, titleHeight);


        graphics.dispose();

        // 保存图像
        ImageIO.write(finalImage, "png", new File(outputFilePath));
    }

    // 转置二维数组
    private static String[][] transposeData(String[][] data) {
        int rows = data.length;
        int cols = data[0].length;
        String[][] transposed = new String[cols][rows];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                transposed[j][i] = data[i][j];
            }
        }
        return transposed;
    }

    // 将文本分行以适应单元格宽度
    private static java.util.List<String> wrapText(String text, FontMetrics metrics, int cellWidth) {
        java.util.List<String> lines = new ArrayList<>();
        StringBuilder line = new StringBuilder();
        int lineWidth = 0;

        for (char c : text.toCharArray()) {
            int charWidth = metrics.charWidth(c);
            if (lineWidth + charWidth > cellWidth) {
                lines.add(line.toString());
                line.setLength(0);
                lineWidth = 0;
            }
            line.append(c);
            lineWidth += charWidth;
        }

        if (line.length() > 0) {
            lines.add(line.toString());
        }

        return lines;
    }
}
正文到此结束
本文目录