From ccab8a9de6cec97ade6eb4304798de5b9ff5210f Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Tue, 22 Sep 2026 09:35:17 +0800 Subject: [PATCH] 11 --- .../hard/api/utils/EscPosReceiptCommand.java | 143 ++++++++++++++---- .../api/utils/EscPosReceiptCommandTest.java | 42 +++-- 2 files changed, 134 insertions(+), 51 deletions(-) diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommand.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommand.java index 0f351a294..c2d5e8479 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommand.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommand.java @@ -15,8 +15,8 @@ import java.util.List; *
排版约定:80mm 纸按 48 个 ANK 字符列计算(中文和制表符占 2 列);中文通过 ESC/POS * 汉字模式(FS & / FS .)以 GBK 编码输出,每行打印后立即关闭汉字模式避免状态泄漏。 * 图片版的矩形边框用 GBK 字库制表符(─ │ ┌ ┐ └ ┘)拼装,相邻字符字模相连成实线; - * 黑底白字标签用 ESC { 反色指令输出。输出内容不含初始化以外的走纸与切刀命令, - * 末尾走纸和切刀由终端按本机配置统一包装。
+ * 黑底白字标签用 GS B 反色指令输出。区块间距按图片版像素值(1px≈1 点)用压缩行距的 + * 空行实现。输出不含切刀和大段走纸命令,末尾走纸和切刀由终端按本机配置统一包装。 */ public class EscPosReceiptCommand implements ReceiptRenderer { @@ -70,27 +70,26 @@ public class EscPosReceiptCommand implements ReceiptRenderer { } /** - * 渲染制表符描边矩形框包围的居中加粗提示,还原图片模式的横幅边框。 + * 渲染倍宽粗横线上下包围的居中倍宽加粗提示,还原图片模式 44px 大字横幅。 + * + *图片版为四边描边框加特大字;指令模式放大到倍宽后文字已占满纸宽,无法再容纳 + * 左右竖线,以两条倍宽粗横线近似横幅上下边,横幅后按图片版留 14 点空隙。
*/ @Override public EscPosReceiptCommand banner(String text) { - List与图片版卡片一一对应:框线对应描边矩形、反色标签对应黑底白字 tag、粗体名称对应大号商品名, - * 组合内容区保留内部短线与缩进组成项;卡片之间输出空行还原图片的卡片间距。
+ *与图片版卡片逐项对应:框线对应描边矩形;组合商品标签为 GS B 反色黑底(对应图片 + * fillRect 黑底白字),单品标签为制表符描边小框(对应图片 drawRect 描边框);卡片内部 + * 压缩行距让左右竖线行间衔接不断线,各区块间距按图片版像素值(1px≈1 点)走纸。
*/ @Override public EscPosReceiptCommand voucherProduct(String name, String quantity, String unitPrice, String subtotal, boolean combo, ListGS ! 与 ESC E 是行内即时指令,竖线输出后再开、名称结束即关,卡片左右边框 + * 不随名称放大变粗,与上下框线粗细一致。
+ * + * @param nameLine 已按倍宽容量折行的名称文本 + * @param padColumns 名称后的倍宽空格填充列数,保证右竖线与上下框线对齐 + */ + private void printScaledNameInBox(String nameLine, int padColumns) { + writeRaw(0x1B, 0x61, ALIGN_LEFT); + writeRaw(0x1C, 0x26); + writeText(BOX_VERTICAL); + writeRaw(0x1D, 0x21, 0x11); + writeRaw(0x1B, 0x45, 1); + writeText(nameLine); + writeRaw(0x1B, 0x45, 0); + writeText(" ".repeat(Math.max(0, padColumns))); + writeRaw(0x1D, 0x21, 0x00); + writeText(BOX_VERTICAL); + writeRaw('\n'); + writeRaw(0x1C, 0x2E); + } + + /** + * 渲染单品类型标签的制表符描边小框(三行),还原图片版单品 tag 的描边矩形黑字。 + * + * @param tag 含前后空格的标签文本,空格保证框内留白 + */ + private void printSingleTagFrame(String tag) { + int frameWidth = displayWidth(tag) / 2; + boxLine(BOX_TOP_LEFT + BOX_HORIZONTAL.repeat(frameWidth) + BOX_TOP_RIGHT, false); + boxLine(BOX_VERTICAL + tag + BOX_VERTICAL, false); + boxLine(BOX_BOTTOM_LEFT + BOX_HORIZONTAL.repeat(frameWidth) + BOX_BOTTOM_RIGHT, false); + } + + /** + * 渲染上下制表符实线包围的商品合计;双倍宽度放不下时自动降为单倍,合计后按图片版留空隙。 */ @Override public EscPosReceiptCommand voucherTotal(String totalAmount) { @@ -235,11 +279,12 @@ public class EscPosReceiptCommand implements ReceiptRenderer { printTwoColumns(label, amount, true); } printLine(BOX_HORIZONTAL.repeat(CHARS_PER_LINE / 2), ALIGN_LEFT, false, 1); + feedGap(14); return this; } /** - * 渲染整行制表符实线分隔。 + * 渲染整行制表符实线分隔;行距自带的行间空隙即视觉间隔,不再额外走纸。 */ @Override public EscPosReceiptCommand divider() { @@ -422,23 +467,24 @@ public class EscPosReceiptCommand implements ReceiptRenderer { writeRaw(0x1B, 0x45, 1); } if (scale > 1) { - // GS ! n:高 4 位为高度倍数、低 4 位为宽度倍数,宽度决定每行容量。 - writeRaw(0x1D, 0x21, (scale << 4) | scale); + // GS ! n:高 4 位为高度倍数、低 4 位为宽度倍数;半字节值 0x01 表示 2 倍,0x00 表示正常大小。 + writeRaw(0x1D, 0x21, ((scale - 1) << 4) | (scale - 1)); } writeRaw(0x1C, 0x26); for (Segment segment : segments) { if (segment.reverse()) { - writeRaw(0x1B, 0x7B, 1); + // GS B n 为黑白反转指令;ESC { 是字符倒置指令,不能用于反色。 + writeRaw(0x1D, 0x42, 1); } writeText(segment.text()); if (segment.reverse()) { - writeRaw(0x1B, 0x7B, 0); + writeRaw(0x1D, 0x42, 0); } } writeRaw('\n'); writeRaw(0x1C, 0x2E); if (scale > 1) { - writeRaw(0x1D, 0x21, 0x01); + writeRaw(0x1D, 0x21, 0x00); } if (bold) { writeRaw(0x1B, 0x45, 0); @@ -461,6 +507,45 @@ public class EscPosReceiptCommand implements ReceiptRenderer { } } + /** + * 输出与图片版像素间距对应的垂直空隙(图片 576px 内容宽与打印 576 点一一对应)。 + * + *不使用 ESC d 点级走纸:实测部分国产打印机对其单位实现不标准,会放大数十倍导致 + * 出纸失控。此处改用所有机型都可靠支持的行距指令:临时把行距压到目标点数、走一个 + * 空行、立即恢复默认行距,单次空隙即目标像素值,不存在被放大的空间。
+ * + * @param pixels 图片版像素间距,0 至 60 + */ + private void feedGap(int pixels) { + if (pixels <= 0) { + return; + } + // 点数换算为 ESC 3 的 1/144 英寸单位(203dpi 下 1 单位约 1.41 点),最小 2 保证肉眼可见。 + int units = Math.max(2, Math.round(pixels * 144f / 203f)); + setLineSpacing(Math.min(255, units)); + writeRaw('\n'); + resetLineSpacing(); + } + + /** + * 设置行间距(ESC 3 n,单位 1/144 英寸,203dpi 下 n≈27 为字高加小缝)。 + * + *默认行距(ESC 2 = 1/6 英寸 ≈ 34 点)会在相邻行间留下约 10 点空隙,导致卡片 + * 左右竖线视觉断线;卡片内部压缩行距后竖线行间衔接。
+ * + * @param units 行间距单位值 + */ + private void setLineSpacing(int units) { + writeRaw(0x1B, 0x33, units); + } + + /** + * 恢复默认行距(ESC 2 = 1/6 英寸)。 + */ + private void resetLineSpacing() { + writeRaw(0x1B, 0x32); + } + /** * 计算文本的显示列数:中文字符按 2 列、其余按 1 列,与打印机汉字模式宽度一致。 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommandTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommandTest.java index ef0fcec18..16047ea5c 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommandTest.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/api/utils/EscPosReceiptCommandTest.java @@ -28,9 +28,9 @@ class EscPosReceiptCommandTest { assertTrue(bytes.length > 2, "指令流应包含初始化之后的内容指令"); } - /** 指令流不得包含切刀和走纸命令:末尾边界由终端按本机配置统一包装。 */ + /** 指令流不得包含切刀和成段走纸:末尾边界由终端按本机配置统一包装,ESC d 点级间距不算走纸命令。 */ @Test - void outputNeverContainsCutOrFeedPaperCommand() { + void outputNeverContainsCutCommand() { String base64 = new EscPosReceiptCommand() .title("预约核销兑换券") .banner("第 1 次打印 · 2 笔预约") @@ -44,7 +44,7 @@ class EscPosReceiptCommandTest { assertFalse(bytes[index] == 0x1D && bytes[index + 1] == 0x56, "指令流不应包含 GS V 切刀命令"); } - assertFalse(containsText(bytes, "\n\n\n"), "指令流不应含成段走纸"); + assertFalse(containsText(bytes, "\n\n\n"), "指令流不应含成段空行走纸"); } /** 每行文本都被汉字模式开启与关闭包裹,截断在任意行都不会让打印机停留在汉字状态。 */ @@ -108,10 +108,10 @@ class EscPosReceiptCommandTest { @Test void voucherTotalScalesShortAmountAndDegradesLongAmount() { byte[] shortAmount = decode(new EscPosReceiptCommand().voucherTotal("28.80")); - assertTrue(contains(shortAmount, 0x1D, 0x21, 0x22), "常规合计应使用双倍宽度 GS ! 0x22"); + assertTrue(contains(shortAmount, 0x1D, 0x21, 0x11), "常规合计应使用倍宽 GS ! 0x11"); byte[] longAmount = decode(new EscPosReceiptCommand().voucherTotal("12345678901234.50")); - assertFalse(contains(longAmount, 0x1D, 0x21, 0x22), "超长合计应降级为单倍宽度"); + assertFalse(contains(longAmount, 0x1D, 0x21, 0x11), "超长合计应降级为单倍宽度"); List