FlowCardRenderer.java 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package com.mes.print.template;
  2. import java.awt.Color;
  3. import java.awt.Font;
  4. import java.awt.Graphics;
  5. import java.awt.Graphics2D;
  6. import java.awt.GraphicsEnvironment;
  7. import java.awt.RenderingHints;
  8. import java.awt.image.BufferedImage;
  9. import java.awt.print.PageFormat;
  10. import java.awt.print.Printable;
  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.nio.file.Files;
  15. import java.nio.file.Path;
  16. import java.nio.file.StandardCopyOption;
  17. import java.util.HashMap;
  18. import java.util.Map;
  19. import org.apache.pdfbox.pdmodel.PDDocument;
  20. import org.apache.pdfbox.rendering.ImageType;
  21. import org.apache.pdfbox.rendering.PDFRenderer;
  22. import com.google.zxing.BarcodeFormat;
  23. import com.google.zxing.EncodeHintType;
  24. import com.google.zxing.WriterException;
  25. import com.google.zxing.common.BitMatrix;
  26. import com.google.zxing.qrcode.QRCodeWriter;
  27. import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
  28. import com.mes.print.model.FlowCardData;
  29. /**
  30. * 流转卡渲染器(支持 PDF 模板 + 二维码)
  31. * 复用 OP40 的 LaserCardPrintUtil 渲染逻辑
  32. */
  33. public class FlowCardRenderer {
  34. // PDF 模板尺寸(A5 纵向 148x210mm)
  35. private static final float PDF_PAGE_W_PT = 419.5f;
  36. private static final float PDF_PAGE_H_PT = 595.25f;
  37. // 三个动态元素在 PDF 坐标系里的位置(pt,左上原点)
  38. private static final float QR_X = 194.5f;
  39. private static final float QR_Y = 65.4f;
  40. private static final float QR_W = 35.7f;
  41. private static final float QR_H = 34.6f;
  42. // 文字 baseline(Graphics2D.drawString 的 y 参数)
  43. private static final float STEEL_TEXT_X = 291.5f;
  44. private static final float STEEL_TEXT_Y = 85.5f;
  45. private static final float SN_TEXT_X = 291.5f;
  46. private static final float SN_TEXT_Y = 96.4f;
  47. private static final int TEXT_FONT_SIZE = 8;
  48. // 中文字体候选,按顺序找到系统里第一个可用的
  49. private static final String[] FONT_CANDIDATES = {"SimSun", "宋体", "Microsoft YaHei", "微软雅黑", Font.SANS_SERIF};
  50. // 打印用的底图 DPI;越高越清晰但内存/耗时越大,300 是常规打印标准
  51. private static final float PRINT_TEMPLATE_DPI = 300f;
  52. // 模板 PDF 缓存
  53. private static volatile PDDocument templateDoc;
  54. private static volatile PDFRenderer templateRenderer;
  55. private static volatile BufferedImage templateImage;
  56. private static final Object LOCK = new Object();
  57. public static Printable render(final FlowCardData data) {
  58. try {
  59. BufferedImage bgImage = getTemplateImage();
  60. BufferedImage qr = generateQrCode(data.getSn(), 600);
  61. return new Printable() {
  62. @Override
  63. public int print(Graphics g, PageFormat pf, int idx) {
  64. if (idx > 0) return NO_SUCH_PAGE;
  65. Graphics2D g2 = (Graphics2D) g;
  66. g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  67. g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  68. g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
  69. // 把 PDF 页面尺寸(pt)等比缩放到打印区域,居中
  70. double sx = pf.getImageableWidth() / PDF_PAGE_W_PT;
  71. double sy = pf.getImageableHeight() / PDF_PAGE_H_PT;
  72. double scale = Math.min(sx, sy);
  73. double tx = pf.getImageableX() + (pf.getImageableWidth() - PDF_PAGE_W_PT * scale) / 2.0;
  74. double ty = pf.getImageableY() + (pf.getImageableHeight() - PDF_PAGE_H_PT * scale) / 2.0;
  75. g2.translate(tx, ty);
  76. g2.scale(scale, scale);
  77. // 底图 BufferedImage 是 A5 @ 300dpi ≈ 1747x2480 像素,drawImage 到 pt 坐标 419.5x595.25
  78. g2.drawImage(bgImage, 0, 0, (int) Math.ceil(PDF_PAGE_W_PT), (int) Math.ceil(PDF_PAGE_H_PT), null);
  79. drawDynamic(g2, qr, data);
  80. return PAGE_EXISTS;
  81. }
  82. };
  83. } catch (Exception e) {
  84. throw new RuntimeException("流转卡渲染失败: " + e.getMessage(), e);
  85. }
  86. }
  87. private static void drawDynamic(Graphics2D g2, BufferedImage qr, FlowCardData d) {
  88. // 白底盖一下原模板上的二维码占位图
  89. g2.setColor(Color.WHITE);
  90. g2.fill(new java.awt.geom.Rectangle2D.Float(QR_X, QR_Y, QR_W, QR_H));
  91. if (qr != null) {
  92. g2.drawImage(qr, (int) QR_X, (int) QR_Y, (int) QR_W, (int) QR_H, null);
  93. }
  94. g2.setColor(Color.BLACK);
  95. g2.setFont(pickChineseFont(TEXT_FONT_SIZE));
  96. String steelSn = d.getSteelSn();
  97. String customerSn = d.getSn();
  98. if (steelSn != null) {
  99. g2.drawString(steelSn, STEEL_TEXT_X, STEEL_TEXT_Y);
  100. }
  101. if (customerSn != null) {
  102. g2.drawString(customerSn, SN_TEXT_X, SN_TEXT_Y);
  103. }
  104. }
  105. private static BufferedImage getTemplateImage() throws IOException {
  106. if (templateImage != null) return templateImage;
  107. synchronized (LOCK) {
  108. if (templateImage == null) {
  109. PDFRenderer r = getRenderer();
  110. templateImage = r.renderImageWithDPI(0, PRINT_TEMPLATE_DPI, ImageType.RGB);
  111. System.out.println("[流转卡] 底图已预渲染 " + templateImage.getWidth() + "x" + templateImage.getHeight()
  112. + " @" + PRINT_TEMPLATE_DPI + "dpi");
  113. }
  114. }
  115. return templateImage;
  116. }
  117. private static PDFRenderer getRenderer() throws IOException {
  118. if (templateRenderer != null) return templateRenderer;
  119. synchronized (LOCK) {
  120. if (templateRenderer == null) {
  121. String path = "config/liuzhuanka_template.pdf";
  122. File pdfFile = extractToTemp(path);
  123. PDDocument doc = PDDocument.load(pdfFile);
  124. templateDoc = doc;
  125. templateRenderer = new PDFRenderer(doc);
  126. System.out.println("[流转卡] 已加载模板:" + pdfFile.getAbsolutePath());
  127. }
  128. }
  129. return templateRenderer;
  130. }
  131. private static File extractToTemp(String cpPath) throws IOException {
  132. File direct = new File(cpPath);
  133. if (direct.isFile()) {
  134. return direct;
  135. }
  136. try (InputStream is = ClassLoader.getSystemResourceAsStream(cpPath)) {
  137. if (is == null) {
  138. throw new IOException("找不到流转卡模板:" + cpPath);
  139. }
  140. Path tmp = Files.createTempFile("liuzhuanka_template_", ".pdf");
  141. tmp.toFile().deleteOnExit();
  142. Files.copy(is, tmp, StandardCopyOption.REPLACE_EXISTING);
  143. return tmp.toFile();
  144. }
  145. }
  146. private static Font pickChineseFont(int size) {
  147. String[] available = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
  148. for (String name : FONT_CANDIDATES) {
  149. for (String a : available) {
  150. if (a.equalsIgnoreCase(name)) {
  151. return new Font(name, Font.BOLD, size);
  152. }
  153. }
  154. }
  155. return new Font(Font.SANS_SERIF, Font.BOLD, size);
  156. }
  157. private static BufferedImage generateQrCode(String content, int pixelSize) {
  158. if (content == null || content.isEmpty()) return null;
  159. try {
  160. Map<EncodeHintType, Object> hints = new HashMap<>();
  161. hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
  162. hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
  163. hints.put(EncodeHintType.MARGIN, 0);
  164. BitMatrix bm = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, pixelSize, pixelSize, hints);
  165. BufferedImage img = new BufferedImage(pixelSize, pixelSize, BufferedImage.TYPE_INT_RGB);
  166. for (int x = 0; x < pixelSize; x++) {
  167. for (int y = 0; y < pixelSize; y++) {
  168. img.setRGB(x, y, bm.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
  169. }
  170. }
  171. return img;
  172. } catch (WriterException e) {
  173. e.printStackTrace();
  174. return null;
  175. }
  176. }
  177. }