|
|
@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; |
|
|
|
|
|
|
|
|
import java.math.BigDecimal; |
|
|
import java.math.BigDecimal; |
|
|
import java.math.RoundingMode; |
|
|
import java.math.RoundingMode; |
|
|
|
|
|
import java.util.ArrayList; |
|
|
import java.util.List; |
|
|
import java.util.List; |
|
|
|
|
|
|
|
|
import com.google.zxing.*; |
|
|
import com.google.zxing.*; |
|
|
@ -20,6 +21,7 @@ import java.util.Collections; |
|
|
import java.util.HashMap; |
|
|
import java.util.HashMap; |
|
|
import java.util.Locale; |
|
|
import java.util.Locale; |
|
|
import java.util.Map; |
|
|
import java.util.Map; |
|
|
|
|
|
import java.util.regex.Pattern; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* ZPL代码生成器 |
|
|
* ZPL代码生成器 |
|
|
@ -28,6 +30,19 @@ import java.util.Map; |
|
|
@Slf4j |
|
|
@Slf4j |
|
|
public class ZplGenerator { |
|
|
public class ZplGenerator { |
|
|
private static final String DEFAULT_FONT_FILE = "ARIAL UNICODE MS.TTF"; |
|
|
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槽位。 |
|
|
* 兼容既有标签:默认字体仍绑定J槽位。 |
|
|
* 历史模板大量依赖 default -> ^CWJ,E:* 的行为,默认字体改为 ARIAL UNICODE MS 后仍保持 J 槽位, |
|
|
* 历史模板大量依赖 default -> ^CWJ,E:* 的行为,默认字体改为 ARIAL UNICODE MS 后仍保持 J 槽位, |
|
|
@ -142,11 +157,9 @@ public class ZplGenerator { |
|
|
private String generateTextZPL(ReportLabelList element, int x, int y) { |
|
|
private String generateTextZPL(ReportLabelList element, int x, int y) { |
|
|
StringBuilder zpl = new StringBuilder(); |
|
|
StringBuilder zpl = new StringBuilder(); |
|
|
|
|
|
|
|
|
// 设置字体 |
|
|
|
|
|
Integer fontSize = element.getFontSize() != null ? element.getFontSize() : 30; |
|
|
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())) { |
|
|
if (Boolean.TRUE.equals(element.getIsChecked())) { |
|
|
@ -155,10 +168,20 @@ public class ZplGenerator { |
|
|
.append("^AJN,35,35^FD√^FS\n"); |
|
|
.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())) { |
|
|
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; |
|
|
String fontSizeCommand = "^CF" + resolvedFontLetter + "," + resolvedFontSize; |
|
|
|
|
|
|
|
|
log.debug("字体命令解析: fontFamily={}, fontFile={}, fontLetter={}, useDefault={}, chineseFont={}", |
|
|
log.debug("字体命令解析: fontFamily={}, fontFile={}, fontLetter={}, useDefault={}, chineseFont={}", |
|
|
@ -261,6 +285,177 @@ public class ZplGenerator { |
|
|
return new FontCommand(setupCommand, fontSizeCommand, resolvedFontFile, resolvedFontLetter, resolvedFontSize); |
|
|
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<String> 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<String> wrapTextForBitmap(String text, java.awt.FontMetrics fontMetrics, int maxWidth, int maxRows) { |
|
|
|
|
|
List<String> 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; |
|
|
* 1) 优先走FontService; |
|
|
@ -728,11 +923,17 @@ public class ZplGenerator { |
|
|
StringBuilder zpl = new StringBuilder(); |
|
|
StringBuilder zpl = new StringBuilder(); |
|
|
|
|
|
|
|
|
Integer fontSize = element.getFontSize() != null ? element.getFontSize() : 30; |
|
|
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); |
|
|
FontCommand fontCommand = generateFontCommand(element, fontSize); |
|
|
zpl.append(fontCommand.getSetupCommand()).append("\n"); |
|
|
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"); |
|
|
zpl.append("^FO").append(x).append(",").append(y).append("^FD").append(data).append("^FS"); |
|
|
|
|
|
|
|
|
return zpl.toString(); |
|
|
return zpl.toString(); |
|
|
|