diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java index 9d5fbe8f2..8d149f3de 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java @@ -75,26 +75,31 @@ public class ThermalReceiptImage { * @return 当前绘制器 */ public ThermalReceiptImage title(String text) { - setFont(Font.BOLD, 34); + setFont(Font.BOLD, 42); drawCentered(text, 16); return this; } /** - * 绘制带黑色边框的大号打印次数提示。 + * 绘制带黑色边框的大号打印次数提示,文字超过可用纸宽时自动换行。 * * @param text 提示文本,例如“第 1 次打印” * @return 当前绘制器 */ public ThermalReceiptImage banner(String text) { - setFont(Font.BOLD, 38); + setFont(Font.BOLD, 44); FontMetrics metrics = graphics.getFontMetrics(); - int height = metrics.getHeight() + 30; + List lines = wrap(text == null ? "" : text, contentWidth() - 32, metrics); + int lineHeight = metrics.getHeight() + 4; + int height = lines.size() * lineHeight + 26; ensureCapacity(height + 20); graphics.setStroke(new BasicStroke(3F)); graphics.drawRect(HORIZONTAL_PADDING, cursorY, contentWidth(), height); - int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(text)) / 2); - graphics.drawString(text, x, cursorY + 15 + metrics.getAscent()); + for (int index = 0; index < lines.size(); index++) { + String line = lines.get(index); + int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(line)) / 2); + graphics.drawString(line, x, cursorY + 13 + index * lineHeight + metrics.getAscent()); + } cursorY += height + 20; return this; } @@ -106,34 +111,38 @@ public class ThermalReceiptImage { * @return 当前绘制器 */ public ThermalReceiptImage verificationCode(String shortCode) { - setFont(Font.BOLD, 52); + setFont(Font.BOLD, 64); drawCentered(shortCode == null || shortCode.isBlank() ? "--" : shortCode, 18); return this; } /** - * 绘制一行左右键值信息,值过长时在下一行继续换行。 + * 绘制一行左右键值信息,值无法完整同行显示时整段从下一行开始并继续换行。 * * @param label 左侧字段名 * @param value 右侧字段值 * @return 当前绘制器 */ public ThermalReceiptImage keyValue(String label, String value) { - setFont(Font.PLAIN, 25); + setFont(Font.PLAIN, 33); String safeLabel = label == null ? "" : label; String safeValue = value == null || value.isBlank() ? "--" : value; FontMetrics metrics = graphics.getFontMetrics(); int labelWidth = metrics.stringWidth(safeLabel + ":"); - int valueWidth = contentWidth() - labelWidth - 12; - List lines = wrap(safeValue, Math.max(120, valueWidth), metrics); + int inlineValueWidth = contentWidth() - labelWidth - 12; + boolean stackValue = metrics.stringWidth(safeValue) > inlineValueWidth; + int valueWidth = stackValue ? contentWidth() : Math.max(120, inlineValueWidth); + List lines = wrap(safeValue, valueWidth, metrics); + int valueLineOffset = stackValue ? 1 : 0; int lineHeight = metrics.getHeight() + 6; - ensureCapacity(lines.size() * lineHeight + 10); + ensureCapacity((lines.size() + valueLineOffset) * lineHeight + 10); graphics.drawString(safeLabel + ":", HORIZONTAL_PADDING, cursorY + metrics.getAscent()); for (int index = 0; index < lines.size(); index++) { - int x = index == 0 ? HORIZONTAL_PADDING + labelWidth + 12 : HORIZONTAL_PADDING + labelWidth + 12; - graphics.drawString(lines.get(index), x, cursorY + index * lineHeight + metrics.getAscent()); + int x = stackValue ? HORIZONTAL_PADDING : HORIZONTAL_PADDING + labelWidth + 12; + graphics.drawString(lines.get(index), x, + cursorY + (index + valueLineOffset) * lineHeight + metrics.getAscent()); } - cursorY += lines.size() * lineHeight + 6; + cursorY += (lines.size() + valueLineOffset) * lineHeight + 6; return this; } @@ -144,8 +153,8 @@ public class ThermalReceiptImage { * @return 当前绘制器 */ public ThermalReceiptImage sectionTitle(String text) { - setFont(Font.BOLD, 28); - ensureCapacity(48); + setFont(Font.BOLD, 36); + ensureCapacity(60); graphics.drawString(text == null ? "" : text, HORIZONTAL_PADDING, cursorY + graphics.getFontMetrics().getAscent()); cursorY += graphics.getFontMetrics().getHeight() + 10; return this; @@ -173,12 +182,12 @@ public class ThermalReceiptImage { String summary = "订餐日期:" + (orderDate == null || orderDate.isBlank() ? "--" : orderDate) + " 餐段:" + (mealName == null || mealName.isBlank() ? "--" : mealName); - setFont(Font.BOLD, 20); + setFont(Font.BOLD, 25); FontMetrics tagMetrics = graphics.getFontMetrics(); int tagWidth = tagMetrics.stringWidth(tag) + 20; int tagHeight = tagMetrics.getHeight() + 6; - setFont(Font.BOLD, 24); + setFont(Font.BOLD, 31); FontMetrics orderMetrics = graphics.getFontMetrics(); int orderX = HORIZONTAL_PADDING + tagWidth + 14; int orderWidth = PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - orderX; @@ -186,7 +195,7 @@ public class ThermalReceiptImage { int orderLineHeight = orderMetrics.getHeight() + 4; int headerHeight = Math.max(tagHeight, orderLines.size() * orderLineHeight); - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); FontMetrics summaryMetrics = graphics.getFontMetrics(); List summaryLines = wrap(summary, contentWidth(), summaryMetrics); int summaryLineHeight = summaryMetrics.getHeight() + 4; @@ -196,20 +205,20 @@ public class ThermalReceiptImage { int blockTop = cursorY; graphics.setColor(Color.BLACK); - setFont(Font.BOLD, 20); + setFont(Font.BOLD, 25); graphics.fillRect(HORIZONTAL_PADDING, blockTop, tagWidth, tagHeight); graphics.setColor(Color.WHITE); graphics.drawString(tag, HORIZONTAL_PADDING + 10, blockTop + 3 + tagMetrics.getAscent()); graphics.setColor(Color.BLACK); - setFont(Font.BOLD, 24); + setFont(Font.BOLD, 31); for (int lineIndex = 0; lineIndex < orderLines.size(); lineIndex++) { graphics.drawString(orderLines.get(lineIndex), orderX, blockTop + lineIndex * orderLineHeight + orderMetrics.getAscent()); } int summaryTop = blockTop + headerHeight + 10; - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); for (int lineIndex = 0; lineIndex < summaryLines.size(); lineIndex++) { graphics.drawString(summaryLines.get(lineIndex), HORIZONTAL_PADDING, summaryTop + lineIndex * summaryLineHeight + summaryMetrics.getAscent()); @@ -273,17 +282,17 @@ public class ThermalReceiptImage { .filter(item -> item != null && !item.isBlank()).toList(); int innerWidth = contentWidth() - PRODUCT_CARD_PADDING * 2; - setFont(Font.BOLD, 21); + setFont(Font.BOLD, 28); FontMetrics tagMetrics = graphics.getFontMetrics(); int tagHeight = tagMetrics.getHeight() + 6; int tagWidth = tagMetrics.stringWidth(typeLabel) + 20; - setFont(Font.BOLD, 26); + setFont(Font.BOLD, 40); FontMetrics nameMetrics = graphics.getFontMetrics(); List nameLines = wrap(safeName, innerWidth, nameMetrics); int nameLineHeight = nameMetrics.getHeight() + 4; - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); FontMetrics priceMetrics = graphics.getFontMetrics(); String priceAndQuantity = "单价 ¥" + safeUnitPrice + " x" + Math.max(0, quantity); String subtotalText = "小计 ¥" + safeSubtotal; @@ -291,7 +300,7 @@ public class ThermalReceiptImage { + priceMetrics.stringWidth(subtotalText) + 24 > innerWidth; int priceLineHeight = priceMetrics.getHeight() + 4; - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); FontMetrics componentMetrics = graphics.getFontMetrics(); int componentLineHeight = componentMetrics.getHeight() + 3; List> componentLines = new ArrayList<>(); @@ -313,7 +322,7 @@ public class ThermalReceiptImage { graphics.setStroke(new BasicStroke(combo ? 4F : 2F)); graphics.drawRect(HORIZONTAL_PADDING, cardTop, contentWidth(), cardHeight); - setFont(Font.BOLD, 21); + setFont(Font.BOLD, 28); if (combo) { graphics.fillRect(contentX, contentY, tagWidth, tagHeight); graphics.setColor(Color.WHITE); @@ -326,14 +335,14 @@ public class ThermalReceiptImage { } contentY += tagHeight + 10; - setFont(Font.BOLD, 26); + setFont(Font.BOLD, 40); for (String line : nameLines) { graphics.drawString(line, contentX, contentY + nameMetrics.getAscent()); contentY += nameLineHeight; } contentY += 8; - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); graphics.drawString(priceAndQuantity, contentX, contentY + priceMetrics.getAscent()); if (splitPriceLine) { contentY += priceLineHeight; @@ -350,10 +359,10 @@ public class ThermalReceiptImage { graphics.drawLine(contentX, contentY, PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - PRODUCT_CARD_PADDING, contentY); contentY += 10; - setFont(Font.BOLD, 21); + setFont(Font.BOLD, 29); graphics.drawString("组合内容", contentX, contentY + componentMetrics.getAscent()); contentY += componentLineHeight; - setFont(Font.PLAIN, 21); + setFont(Font.PLAIN, 29); for (List lines : componentLines) { for (String line : lines) { graphics.drawString(line, contentX + CHILD_INDENT, @@ -376,7 +385,7 @@ public class ThermalReceiptImage { String safeAmount = totalAmount == null || totalAmount.isBlank() ? "--" : totalAmount; String label = "商品合计"; String amount = "¥" + safeAmount; - setFont(Font.BOLD, 30); + setFont(Font.BOLD, 42); FontMetrics metrics = graphics.getFontMetrics(); int blockHeight = metrics.getHeight() + 30; ensureCapacity(blockHeight + 14); @@ -405,7 +414,7 @@ public class ThermalReceiptImage { */ private ThermalReceiptImage item(String name, String trailing, boolean child, boolean emphasized, int trailingWidth) { - setFont(emphasized ? Font.BOLD : Font.PLAIN, child ? 23 : 26); + setFont(emphasized ? Font.BOLD : Font.PLAIN, child ? 30 : 34); FontMetrics metrics = graphics.getFontMetrics(); int indent = child ? CHILD_INDENT : 0; String prefix = child ? "- " : ""; diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImageTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImageTest.java index 07ed1fc75..cce119758 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImageTest.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImageTest.java @@ -45,6 +45,19 @@ class ThermalReceiptImageTest { assertEquals(576, voucher.getWidth()); } + /** 放大后的打印提示必须按纸宽自动换行,不能裁掉多订单数量等关键信息。 */ + @Test + void largeBannerWrapsLongTextAndGrowsCanvas() throws Exception { + BufferedImage shortBanner = ImageIO.read(new ByteArrayInputStream(new ThermalReceiptImage() + .banner("第 1 次打印").toPngBytes())); + BufferedImage longBanner = ImageIO.read(new ByteArrayInputStream(new ThermalReceiptImage() + .banner("第 12 次打印 · 100 笔预约订单合并打印").toPngBytes())); + + assertEquals(ThermalReceiptImage.PAPER_WIDTH_PIXELS, longBanner.getWidth()); + assertTrue(longBanner.getHeight() > shortBanner.getHeight(), + "长提示换行后必须增加票据高度,不能越过右侧纸张边界"); + } + /** 兑换券商品卡片应完整换行长名称和套餐组成,并随内容动态增高。 */ @Test void voucherProductCardsWrapNamesAndComboComponents() throws Exception {