|
|
@@ -1,54 +1,237 @@
|
|
|
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;
|
|
|
|
|
|
/**
|
|
|
- * 流转卡渲染器(Java2D,A4)
|
|
|
- * 这是最小可用版本,后续按真实模板调整字段位置/表格样式
|
|
|
+ * 流转卡渲染器(支持 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) {
|
|
|
+ e.printStackTrace();
|
|
|
+ // 降级:如果 PDF 模板加载失败,用简单文本打印
|
|
|
+ return renderSimple(data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 降级方案:纯文本流转卡 */
|
|
|
+ private static Printable renderSimple(final FlowCardData data) {
|
|
|
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_TEXT_ANTIALIASING,
|
|
|
- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
+ g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
g2.translate(pf.getImageableX(), pf.getImageableY());
|
|
|
- draw(g2, data);
|
|
|
+ drawSimple(g2, data);
|
|
|
return PAGE_EXISTS;
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- private static void draw(Graphics2D g, FlowCardData d) {
|
|
|
- // 标题
|
|
|
+ private static void drawSimple(Graphics2D g, FlowCardData d) {
|
|
|
g.setFont(new Font("宋体", Font.BOLD, 22));
|
|
|
g.drawString("生产流转卡", 220, 40);
|
|
|
-
|
|
|
- // 分隔线
|
|
|
g.drawLine(30, 60, 550, 60);
|
|
|
-
|
|
|
- // 内容
|
|
|
g.setFont(new Font("宋体", Font.PLAIN, 14));
|
|
|
- int y = 100;
|
|
|
- int step = 32;
|
|
|
+ int y = 100, step = 32;
|
|
|
g.drawString("产品编号: " + safe(d.getProduct()), 50, y); y += step;
|
|
|
- g.drawString("序列号 : " + safe(d.getSn()), 50, y); y += step;
|
|
|
- g.drawString("工序号 : " + safe(d.getOpNo()), 50, y); y += step;
|
|
|
- g.drawString("批次 : " + safe(d.getBatch()), 50, y); y += step;
|
|
|
- g.drawString("日期 : " + safe(d.getDate()), 50, y); y += step;
|
|
|
- g.drawString("操作员 : " + safe(d.getOperator()),50, y); y += step;
|
|
|
- g.drawString("备注 : " + safe(d.getRemark()), 50, y);
|
|
|
+ g.drawString("序列号 : " + safe(d.getSn()), 50, y); y += step;
|
|
|
+ g.drawString("钢印码 : " + safe(d.getSteelSn()), 50, y); y += step;
|
|
|
+ g.drawString("工序号 : " + safe(d.getOpNo()), 50, y); y += step;
|
|
|
+ g.drawString("批次 : " + safe(d.getBatch()), 50, y); y += step;
|
|
|
+ g.drawString("日期 : " + safe(d.getDate()), 50, y); y += step;
|
|
|
+ g.drawString("操作员 : " + safe(d.getOperator()), 50, y); y += step;
|
|
|
+ g.drawString("备注 : " + safe(d.getRemark()), 50, y);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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<EncodeHintType, Object> 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;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static String safe(String s) {
|