diff --git a/src/main/java/com/gaotao/modules/base/utils/ZplGenerator.java b/src/main/java/com/gaotao/modules/base/utils/ZplGenerator.java index d5c97c5e..9cd19505 100644 --- a/src/main/java/com/gaotao/modules/base/utils/ZplGenerator.java +++ b/src/main/java/com/gaotao/modules/base/utils/ZplGenerator.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.List; import com.google.zxing.*; @@ -20,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.regex.Pattern; /** * ZPL代码生成器 @@ -28,6 +30,19 @@ import java.util.Map; @Slf4j public class ZplGenerator { private static final String DEFAULT_FONT_FILE = "ARIAL UNICODE MS.TTF"; + /** + * 越南文常用字符覆盖: + * - 基础越南文字母:Ă/ă Â/â Ê/ê Ô/ô Ơ/ơ Ư/ư Đ/đ + * - 扩展重音区:U+1EA0~U+1EF9 + */ + private static final Pattern VIETNAMESE_CHAR_PATTERN = Pattern.compile( + "[\\u0102\\u0103\\u00C2\\u00E2\\u00CA\\u00EA\\u00D4\\u00F4\\u01A0\\u01A1\\u01AF\\u01B0\\u0110\\u0111\\u1EA0-\\u1EF9]" + ); + /** + * 位图文本Y轴微调(dot): + * 越南文位图与普通字体渲染相比存在轻微下移,默认上移1dot做视觉对齐。 + */ + private static final int VIETNAMESE_BITMAP_Y_OFFSET = -5; /** * 兼容既有标签:默认字体仍绑定J槽位。 * 历史模板大量依赖 default -> ^CWJ,E:* 的行为,默认字体改为 ARIAL UNICODE MS 后仍保持 J 槽位, @@ -142,11 +157,9 @@ public class ZplGenerator { private String generateTextZPL(ReportLabelList element, int x, int y) { StringBuilder zpl = new StringBuilder(); - // 设置字体 Integer fontSize = element.getFontSize() != null ? element.getFontSize() : 30; - FontCommand fontCommand = generateFontCommand(element, fontSize); - zpl.append(fontCommand.getSetupCommand()).append("\n"); - String fontSizeCommand = fontCommand.getFontSizeCommand(); + String data = element.getData() != null ? element.getData() : ""; + String alignmentParam = getTextAlignmentParam(element.getTextAlign()); // 如果有复选框 if (Boolean.TRUE.equals(element.getIsChecked())) { @@ -155,10 +168,20 @@ public class ZplGenerator { .append("^AJN,35,35^FD√^FS\n"); } - String data = element.getData() != null ? element.getData() : ""; + // 业务原因:越南文在部分机型的TTF字形渲染存在缺字,改走位图可规避固件字形缺失。 + if (containsVietnameseChars(data)) { + log.debug("检测到越南文文本,切换位图打印: {}", data); + String bitmapText = generateVietnameseTextBitmapZPL(element, x, y, fontSize, data, alignmentParam); + if (!bitmapText.isEmpty()) { + zpl.append(bitmapText); + } + return zpl.toString(); + } - // 处理文本对齐 - String alignmentParam = getTextAlignmentParam(element.getTextAlign()); + // 非越南文维持原来的字体指令路径,避免影响已有中文/英文模板。 + FontCommand fontCommand = generateFontCommand(element, fontSize); + zpl.append(fontCommand.getSetupCommand()).append("\n"); + String fontSizeCommand = fontCommand.getFontSizeCommand(); // 基础文本 if (Boolean.TRUE.equals(element.getNewline())) { @@ -252,8 +275,9 @@ public class ZplGenerator { } } - String setupCommand = "^CI28 ^CW" + resolvedFontLetter + ",E:" + resolvedFontFile - + " ^CF" + resolvedFontLetter + "," + resolvedFontSize; + // 命令间不加空格,避免字体文件名包含空格时把分隔空格误算进^CW参数。 + String setupCommand = "^CI28^CW" + resolvedFontLetter + ",E:" + resolvedFontFile + + "^CF" + resolvedFontLetter + "," + resolvedFontSize; String fontSizeCommand = "^CF" + resolvedFontLetter + "," + resolvedFontSize; log.debug("字体命令解析: fontFamily={}, fontFile={}, fontLetter={}, useDefault={}, chineseFont={}", @@ -261,6 +285,177 @@ public class ZplGenerator { return new FontCommand(setupCommand, fontSizeCommand, resolvedFontFile, resolvedFontLetter, resolvedFontSize); } + private boolean containsVietnameseChars(String text) { + return text != null && !text.isEmpty() && VIETNAMESE_CHAR_PATTERN.matcher(text).find(); + } + + /** + * 越南文文本改为位图输出(^GFA),规避打印机对扩展拉丁字形支持不全的问题。 + */ + private String generateVietnameseTextBitmapZPL(ReportLabelList element, int x, int y, Integer fontSize, + String data, String alignmentParam) { + if (data == null || data.isEmpty()) { + return ""; + } + + int resolvedFontSize = fontSize != null && fontSize > 0 ? fontSize : 30; + String bitmapFontFamily = resolveBitmapFontFamily(element != null ? element.getFontFamily() : null); + int fontStyle = Boolean.TRUE.equals(element.getBold()) ? java.awt.Font.BOLD : java.awt.Font.PLAIN; + java.awt.Font renderFont = new java.awt.Font(bitmapFontFamily, fontStyle, resolvedFontSize); + + BufferedImage measureCanvas = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY); + java.awt.Graphics2D measureGraphics = measureCanvas.createGraphics(); + measureGraphics.setFont(renderFont); + java.awt.FontMetrics metrics = measureGraphics.getFontMetrics(); + + List lines; + if (Boolean.TRUE.equals(element.getNewline())) { + int lineWidth = Math.max(1, Math.round((element.getLineWidth() != null ? element.getLineWidth() : 200) * this.dpi / 25.4f)); + int lineRows = Math.max(1, element.getLineRows() != null ? element.getLineRows() : 2); + lines = wrapTextForBitmap(data, metrics, lineWidth, lineRows); + } else { + lines = Collections.singletonList(data); + } + + int lineHeight = Math.max(1, metrics.getHeight()); + int maxLineWidth = 0; + for (String line : lines) { + maxLineWidth = Math.max(maxLineWidth, metrics.stringWidth(line)); + } + measureGraphics.dispose(); + + // 位图文本与普通^FD文本对齐策略: + // - 上边距设为0,避免^FO(y)后整体下移; + // - 左右/下边距保留,防止抗锯齿和下延笔画被裁切。 + int horizontalPadding = 3; + int topPadding = 0; + int bottomPadding = 2; + int configWidth = Math.max(1, Math.round((element.getLineWidth() != null ? element.getLineWidth() : 200) * this.dpi / 25.4f)); + int imageWidth = Boolean.TRUE.equals(element.getNewline()) + ? Math.max(configWidth, maxLineWidth + horizontalPadding * 2) + : Math.max(1, maxLineWidth + horizontalPadding * 2); + int imageHeight = Math.max(1, lines.size() * lineHeight + topPadding + bottomPadding); + + BufferedImage textImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_BYTE_BINARY); + java.awt.Graphics2D drawGraphics = textImage.createGraphics(); + drawGraphics.setColor(java.awt.Color.WHITE); + drawGraphics.fillRect(0, 0, imageWidth, imageHeight); + drawGraphics.setColor(java.awt.Color.BLACK); + drawGraphics.setFont(renderFont); + drawGraphics.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, + java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + java.awt.FontMetrics drawMetrics = drawGraphics.getFontMetrics(); + int baselineY = topPadding + drawMetrics.getAscent(); + for (int i = 0; i < lines.size(); i++) { + String lineText = lines.get(i); + int linePixelWidth = drawMetrics.stringWidth(lineText); + int drawX = horizontalPadding; + if ("C".equals(alignmentParam)) { + drawX = Math.max(0, (imageWidth - linePixelWidth) / 2); + } else if ("R".equals(alignmentParam)) { + drawX = Math.max(0, imageWidth - linePixelWidth - horizontalPadding); + } + int drawY = baselineY + i * lineHeight; + drawGraphics.drawString(lineText, drawX, drawY); + + if (Boolean.TRUE.equals(element.getFontUnderline())) { + int underlineY = drawY + 1; + drawGraphics.drawLine(drawX, underlineY, drawX + linePixelWidth, underlineY); + } + } + drawGraphics.dispose(); + + return convertBufferedImageToGfa(textImage, x, y + VIETNAMESE_BITMAP_Y_OFFSET); + } + + private List wrapTextForBitmap(String text, java.awt.FontMetrics fontMetrics, int maxWidth, int maxRows) { + List lines = new ArrayList<>(); + if (text == null || text.isEmpty()) { + lines.add(""); + return lines; + } + + StringBuilder currentLine = new StringBuilder(); + for (char c : text.toCharArray()) { + if (c == '\n') { + lines.add(currentLine.toString()); + if (lines.size() >= maxRows) { + return lines; + } + currentLine.setLength(0); + continue; + } + + String candidate = currentLine.toString() + c; + if (fontMetrics.stringWidth(candidate) <= maxWidth || currentLine.length() == 0) { + currentLine.append(c); + } else { + lines.add(currentLine.toString()); + if (lines.size() >= maxRows) { + return lines; + } + currentLine.setLength(0); + currentLine.append(c); + } + } + + if (lines.size() < maxRows && currentLine.length() > 0) { + lines.add(currentLine.toString()); + } + if (lines.isEmpty()) { + lines.add(""); + } + return lines; + } + + private String resolveBitmapFontFamily(String fontFamily) { + if (fontFamily == null || fontFamily.trim().isEmpty() || "default".equalsIgnoreCase(fontFamily.trim())) { + return "Arial Unicode MS"; + } + String normalized = fontFamily.trim(); + if (normalized.toUpperCase(Locale.ROOT).endsWith(".TTF")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + return normalized; + } + + private String convertBufferedImageToGfa(BufferedImage image, int x, int y) { + int width = image.getWidth(); + int height = image.getHeight(); + int widthBytes = (width + 7) / 8; + int totalBytes = widthBytes * height; + StringBuilder imageData = new StringBuilder(totalBytes * 2); + + for (int yPos = 0; yPos < height; yPos++) { + int bit = 0; + int currentByte = 0; + for (int xPos = 0; xPos < width; xPos++) { + int rgb = image.getRGB(xPos, yPos); + int gray = ((rgb >> 16) & 0xff) * 299 + ((rgb >> 8) & 0xff) * 587 + (rgb & 0xff) * 114; + gray = gray / 1000; + currentByte <<= 1; + if (gray < 128) { + currentByte |= 1; + } + bit++; + if (bit == 8) { + imageData.append(String.format("%02X", currentByte)); + bit = 0; + currentByte = 0; + } + } + if (bit > 0) { + currentByte <<= (8 - bit); + imageData.append(String.format("%02X", currentByte)); + } + } + + return "^FO" + x + "," + y + + "^GFA," + totalBytes + "," + totalBytes + "," + widthBytes + "," + imageData + + "^FS"; + } + /** * 统一解析字体文件: * 1) 优先走FontService; @@ -728,11 +923,17 @@ public class ZplGenerator { StringBuilder zpl = new StringBuilder(); Integer fontSize = element.getFontSize() != null ? element.getFontSize() : 30; + String data = element.getData() != null ? element.getData() : "流水号"; + String alignmentParam = getTextAlignmentParam(element.getTextAlign()); + if (containsVietnameseChars(data)) { + log.debug("检测到越南文流水号文本,切换位图打印: {}", data); + return generateVietnameseTextBitmapZPL(element, x, y, fontSize, data, alignmentParam); + } + FontCommand fontCommand = generateFontCommand(element, fontSize); zpl.append(fontCommand.getSetupCommand()).append("\n"); // 基础文本 - String data = element.getData() != null ? element.getData() : "流水号"; zpl.append("^FO").append(x).append(",").append(y).append("^FD").append(data).append("^FS"); return zpl.toString();