package com.mes.print.template; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Printable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; 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 com.mes.print.model.FlowCardData; /** * 流转卡渲染器(支持 PDF 模板 + 二维码) * 复用 OP40 的 LaserCardPrintUtil 渲染逻辑 */ public class FlowCardRenderer { // PDF 模板尺寸(A5 纵向 148x210mm) private static final float PDF_PAGE_W_PT = 419.5f; private static final float PDF_PAGE_H_PT = 595.25f; // 三个动态元素在 PDF 坐标系里的位置(pt,左上原点) private static final float QR_X = 194.5f; private static final float QR_Y = 65.4f; private static final float QR_W = 35.7f; private static final float QR_H = 34.6f; // 文字 baseline(Graphics2D.drawString 的 y 参数) private static final float STEEL_TEXT_X = 291.5f; private static final float STEEL_TEXT_Y = 85.5f; private static final float SN_TEXT_X = 291.5f; private static final float SN_TEXT_Y = 96.4f; private static final int TEXT_FONT_SIZE = 8; // 中文字体候选,按顺序找到系统里第一个可用的 private static final String[] FONT_CANDIDATES = {"SimSun", "宋体", "Microsoft YaHei", "微软雅黑", Font.SANS_SERIF}; // 打印用的底图 DPI;越高越清晰但内存/耗时越大,300 是常规打印标准 private static final float PRINT_TEMPLATE_DPI = 300f; // 模板 PDF 缓存 private static volatile PDDocument templateDoc; private static volatile PDFRenderer templateRenderer; private static volatile BufferedImage templateImage; private static final Object LOCK = new Object(); public static Printable render(final FlowCardData data) { try { BufferedImage bgImage = getTemplateImage(); BufferedImage qr = generateQrCode(data.getSn(), 600); return new Printable() { @Override public int print(Graphics g, PageFormat pf, int idx) { if (idx > 0) return NO_SUCH_PAGE; Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); // 把 PDF 页面尺寸(pt)等比缩放到打印区域,居中 double sx = pf.getImageableWidth() / PDF_PAGE_W_PT; double sy = pf.getImageableHeight() / PDF_PAGE_H_PT; double scale = Math.min(sx, sy); double tx = pf.getImageableX() + (pf.getImageableWidth() - PDF_PAGE_W_PT * scale) / 2.0; double ty = pf.getImageableY() + (pf.getImageableHeight() - PDF_PAGE_H_PT * scale) / 2.0; g2.translate(tx, ty); g2.scale(scale, scale); // 底图 BufferedImage 是 A5 @ 300dpi ≈ 1747x2480 像素,drawImage 到 pt 坐标 419.5x595.25 g2.drawImage(bgImage, 0, 0, (int) Math.ceil(PDF_PAGE_W_PT), (int) Math.ceil(PDF_PAGE_H_PT), null); drawDynamic(g2, qr, data); return PAGE_EXISTS; } }; } catch (Exception e) { throw new RuntimeException("流转卡渲染失败: " + e.getMessage(), e); } } private static void drawDynamic(Graphics2D g2, BufferedImage qr, FlowCardData d) { // 白底盖一下原模板上的二维码占位图 g2.setColor(Color.WHITE); g2.fill(new java.awt.geom.Rectangle2D.Float(QR_X, QR_Y, QR_W, QR_H)); if (qr != null) { g2.drawImage(qr, (int) QR_X, (int) QR_Y, (int) QR_W, (int) QR_H, null); } g2.setColor(Color.BLACK); g2.setFont(pickChineseFont(TEXT_FONT_SIZE)); String steelSn = d.getSteelSn(); String customerSn = d.getSn(); if (steelSn != null) { g2.drawString(steelSn, STEEL_TEXT_X, STEEL_TEXT_Y); } if (customerSn != null) { g2.drawString(customerSn, SN_TEXT_X, SN_TEXT_Y); } } private static BufferedImage getTemplateImage() throws IOException { if (templateImage != null) return templateImage; synchronized (LOCK) { if (templateImage == null) { PDFRenderer r = getRenderer(); templateImage = r.renderImageWithDPI(0, PRINT_TEMPLATE_DPI, ImageType.RGB); System.out.println("[流转卡] 底图已预渲染 " + templateImage.getWidth() + "x" + templateImage.getHeight() + " @" + PRINT_TEMPLATE_DPI + "dpi"); } } return templateImage; } private static PDFRenderer getRenderer() throws IOException { if (templateRenderer != null) return templateRenderer; synchronized (LOCK) { if (templateRenderer == null) { String path = "config/liuzhuanka_template.pdf"; File pdfFile = extractToTemp(path); PDDocument doc = PDDocument.load(pdfFile); templateDoc = doc; templateRenderer = new PDFRenderer(doc); System.out.println("[流转卡] 已加载模板:" + pdfFile.getAbsolutePath()); } } return templateRenderer; } private static File extractToTemp(String cpPath) throws IOException { File direct = new File(cpPath); if (direct.isFile()) { return direct; } try (InputStream is = ClassLoader.getSystemResourceAsStream(cpPath)) { if (is == null) { throw new IOException("找不到流转卡模板:" + cpPath); } Path tmp = Files.createTempFile("liuzhuanka_template_", ".pdf"); tmp.toFile().deleteOnExit(); Files.copy(is, tmp, StandardCopyOption.REPLACE_EXISTING); return tmp.toFile(); } } private static Font pickChineseFont(int size) { String[] available = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); for (String name : FONT_CANDIDATES) { for (String a : available) { if (a.equalsIgnoreCase(name)) { return new Font(name, Font.BOLD, size); } } } return new Font(Font.SANS_SERIF, Font.BOLD, size); } private static BufferedImage generateQrCode(String content, int pixelSize) { if (content == null || content.isEmpty()) return null; try { Map hints = new HashMap<>(); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.MARGIN, 0); BitMatrix bm = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, pixelSize, pixelSize, hints); BufferedImage img = new BufferedImage(pixelSize, pixelSize, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < pixelSize; x++) { for (int y = 0; y < pixelSize; y++) { img.setRGB(x, y, bm.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); } } return img; } catch (WriterException e) { e.printStackTrace(); return null; } } }