React Native 佳博打印机:标签纸 vs 热敏小票纸 — 清晰度调试要点总结
核心技术方案:react-native-view-shot 截图 + Base64 传输 + 原生 TSPL 指令打印
技术栈:React Native + ViewShot + 佳博 Gprinter SDK (TSPL)
打印机类型:佳博 GP-3120TU / GP-1324D / GP-2120TF 等兼容 TSPL 指令的机型
读者定位:开发者、运维调试人员,已遇到打印清晰度问题的使用者
目录
- React Native 佳博打印机:标签纸 vs 热敏小票纸 — 清晰度调试要点总结
一、两种纸张的本质区别
┌────────────────────────────────────────────────────────────────────────────┐
│ 佳博热敏打印机 │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ 标签纸 (Label) │ │ 热敏小票纸 (Receipt) │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ ════════════════════════════ │ │
│ │ │ ● │ │ ● │ │ ● │ │ ● │ │ │ ║ 连续纸,无间距标记 ║ │ │
│ │ └───┘ └───┘ └───┘ └───┘ │ │ ║ 靠 GAP=0 跳过间隙检测 ║ │ │
│ │ │ │ ════════════════════════════ │ │
│ │ 模切纸,每张之间有间隙 (Gap)│ │ │ │
│ │ 靠间隙传感器定位 │ │ 靠固定长度或手动撕纸定位 │ │
│ │ │ │ │ │
│ │ 尺寸精确(如 80×60mm) │ │ 宽度固定,高度可变 │ │
│ └─────────────────────────────┘ └─────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
| 维度 | 标签纸 (Label) | 热敏小票纸 (Receipt) |
|---|---|---|
| 物理形态 | 模切片,一张一张分开 | 卷筒连续纸,无分隔 |
| 定位方式 | 间隙传感器(Gap Sensor)检测标签间隙 | 无定位,靠指令控制长度 |
| TSPL 指令 | LabelCommand | LabelCommand(同一套 SDK,参数不同) |
| GAP 参数 | addGap(2) — 告诉打印机间隙高度 (mm) | addGap(0) — 告诉打印机无间隙 |
| SIZE 高度 | 标签的实际高度 (mm),如 60 | 按图片像素反算,heightMm = ceil(bitmapHeight / 8) |
| 打印内容 | 固定尺寸布局(条码 + 文本 + 图片) | 动态小票(菜品列表、数量、金额) |
| 清晰度关键 | 图片分辨率 + 缩放算法 + 浓度 | Floyd-Steinberg 抖动二值化 + 图片分辨率 + 浓度 |
| 典型尺寸 | 80×60mm, 80×40mm | 80mm宽 × 可变高度 |
二、TSPL 指令层面的核心差异
以下是项目中两种纸张使用的 TSPL 指令序列对比(均使用 LabelCommand):
2.1 标签纸 (Label) — printImageBase64 / printImageBase64WithSize
// GprinterModule.java: printImageBase64 / printImageBase64WithSize
LabelCommand tsc = new LabelCommand();
tsc.addUserCommand("\r\n"); // 清空缓冲区
tsc.addSize(widthMm, heightMm); // SIZE: 纸张尺寸 (如 80,60)
tsc.addGap(2); // GAP: 标签间隙 2mm ⚡ 关键区别1
tsc.addDirection(FORWARD, NORMAL); // DIRECTION: 打印方向
tsc.addReference(0, 0); // REFERENCE: 定位偏移
tsc.addDensity(DNESITY15); // DENSITY: 浓度 15 (最浓) ⚡ 关键区别2
tsc.addTear(ON); // TEAR: 打印后撕纸位
tsc.addCls(); // CLS: 清空画布
tsc.drawImage(x, y, width, bitmap); // 绘制位图
tsc.addPrint(1, 1); // PRINT: 1份,每份1张
2.2 热敏小票纸 (Receipt) — printReceiptImageBase64
// GprinterModule.java: printReceiptImageBase64
LabelCommand tsc = new LabelCommand();
tsc.addUserCommand("\r\n"); // 清空缓冲区
tsc.addSize(widthMm, heightMm); // SIZE: 宽80mm, 高按图片算
tsc.addGap(0); // GAP: 0 = 连续纸无间隙 ⚡ 关键区别1
tsc.addDirection(FORWARD, NORMAL); // DIRECTION: 打印方向
tsc.addReference(0, 0); // REFERENCE: 定位偏移
tsc.addDensity(DNESITY15); // DENSITY: 浓度 15 (最浓)
tsc.addTear(ON); // TEAR: 打印后撕纸位
tsc.addCls(); // CLS: 清空画布
// ⚡ 关键区别3:居中 + 已二值化的位图
int totalPaperDots = widthMm * 8;
int xOffset = (totalPaperDots - bitmap.getWidth()) / 2;
tsc.drawImage(xOffset, 0, bitmap.getWidth(), bitmap);
tsc.addPrint(1, 1);
2.3 三个核心差异
| TSPL 参数 | 标签纸 | 小票纸 | 影响 |
|---|---|---|---|
| GAP | addGap(2) = 2mm | addGap(0) = 0 | 设错会导致走纸异常:GAP=2 在小票纸上会在每段之间空走 2mm,造成浪费和定位错乱 |
| SIZE 高度 | 标签实际高度 (如 60mm) | ceil(图片高度/8) mm 按需计算 | 小票纸高度设太大会留白,设太小会截断 |
| 位图处理 | 原始 Bitmap,SDK 内部用 Bayer 4x4 抖动 | 先 Floyd-Steinberg 抖动二值化,再绘制 | 这是清晰度的根本差异来源 |
三、完整打印数据流:ViewShot → Base64 → 原生 Bitmap → TSPL
在深入 TSPL 指令细节之前,必须先理解整个打印数据从哪里来、经过哪些环节到达打印机。本项目的打印方案是:用 react-native-view-shot 将 React Native 视图渲染为 PNG 截图(Base64 编码),再通过原生模块解码→缩放→二值化→发送 TSPL 指令到打印机。
3.1 数据流全景图
┌──────────────────────────────────────────────────────────────────────────────┐
│ JS 层 (React Native) │
│ │
│ ┌──────────────────────┐ │
│ │ <ViewShot> │ ① react-native-view-shot 截图 │
│ │ <View> │ 将包裹的子视图渲染为 PNG 图片 │
│ │ 小票布局内容 │ options: { format: 'png', quality: 1, │
│ │ (菜品、数量、金额) │ result: 'base64' } │
│ │ </View> │ │
│ │ </ViewShot> │ → 输出: "data:image/png;base64,iVBORw0KG..." │
│ └──────────┬───────────┘ │
│ │ │
│ │ receiptRef.current.capture() → base64 URI │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ gprinterManager │ ② JS 层路由分发 │
│ │ .printImageBase64() │ ● 去除 data:image/png;base64, 前缀 │
│ │ │ ● size.height === 0 → printReceiptImageBase64 │
│ │ │ ● size.height > 0 → printImageBase64WithSize │
│ └──────────┬───────────┘ │
│ │ │
│ │ React Native Bridge (序列化字符串传递) │
│ ▼ │
├──────────────────────────────────────────────────────────────────────────────┤
│ 原生层 (Android - GprinterModule.java) │
│ │
│ ┌──────────────────────┐ │
│ │ ③ Base64 解码 │ Base64.decode(base64Image, Base64.DEFAULT) │
│ │ → byte[] │ → 原始 PNG 字节数组 │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ ④ PNG → Bitmap │ BitmapFactory.decodeByteArray(pngBytes, ...) │
│ │ 解码 │ → ARGB_8888 位图(每个像素 4 字节,32位色) │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ ⑤ 缩放到目标宽度 │ Bitmap.createScaledBitmap(src, targetW, targetH, │
│ │ (NEAREST 近邻) │ false) ← NEAREST_NEIGHBOR │
│ │ │ 目标宽度 = widthMm × 8 dots/mm = 80×8 = 640dots │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ ⑥ 二值化 (Halftoning) │ │
│ │ │ │
│ │ 小票通道 (printReceiptImageBase64): │ │
│ │ Floyd-Steinberg 误差扩散抖动 ✅ │ │
│ │ → 视觉效果最佳,无栅格条纹 │ │
│ │ │ │
│ │ 标签通道 (printImageBase64WithSize): │ │
│ │ SDK 内部 Bayer 4x4 抖动 │ │
│ │ → 有 4×4 方格纹理,适合标签/条码 │ │
│ └──────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ ⑦ 组装 TSPL 指令 │ LabelCommand │
│ │ 发送到打印机 │ SIZE → GAP → DENSITY → DIRECTION → │
│ │ │ REFERENCE → CLS → drawImage → PRINT │
│ └──────────┬───────────┘ │
│ │ │
│ │ USB / 网络 (TCP 9100) │
│ ▼ │
├──────────────────────────────────────────────────────────────────────────────┤
│ 佳博热敏打印机 │
│ │
│ 接收 TSPL 指令 → 解析位图 → 加热打印头 → 逐行烧制热敏纸 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
3.2 关键节点说明
| 节点 | 技术细节 | 对清晰度的影响 |
|---|---|---|
| ① ViewShot 截图 | react-native-view-shot 将 RN 视图离屏渲染为 PNG。这是图片质量的源头——如果这里分辨率不够,后面一切优化都白费 | ⭐⭐⭐⭐⭐ 最关键 |
| ② Base64 传输 | Base64 只是编码格式,不会压缩或损失像素数据。PNG 是无损格式,截图 → Base64 → 解码后与原图完全一致 | 无影响 |
| ③ Base64 解码 | Android Base64.decode() 标准解码,无损失 | 无影响 |
| ④ PNG → Bitmap | BitmapFactory.decodeByteArray() 解码为 ARGB_8888(每像素32位),保留完整颜色信息 | 无影响 |
| ⑤ 缩放 | createScaledBitmap(src, w, h, false) — 第三个参数 false = NEAREST_NEIGHBOR 近邻插值。如果用 true (双线性) 会产生灰边,二值化后变成噪点 | ⭐⭐⭐⭐ 很重要 |
| ⑥ 二值化 | 256 级灰度 → 1-bit 黑白。Floyd-Steinberg 误差扩散 vs Bayer 4x4 抖动 是小票纸清晰度的核心差异 | ⭐⭐⭐⭐⭐ 最关键(小票纸) |
| ⑦ TSPL 指令 | GAP/DENSITY/SPEED 等参数影响走纸和着色 | ⭐⭐⭐ 重要 |
3.3 ViewShot 在代码中的位置
// src/pages/food/OnlineOrder.js
// ① ViewShot 组件(放在屏幕外,隐藏截取)
<View style={styles.receiptWrapper}> {/* position: 'absolute', left: -9999 */}
<ViewShot
ref={receiptRef}
options={{
format: 'png', // PNG 无损格式
quality: 1, // 最高质量(1.0)
result: 'base64' // 直接返回 Base64 字符串
}}
style={{
backgroundColor: '#fff',
width: 576, // 截图宽度(像素)
paddingHorizontal: 16
}}
collapsable={false}> // 防止屏幕外视图被 RN 优化回收
{/* 小票布局内容:logo、取餐号、菜品列表、金额等 */}
</ViewShot>
</View>
// ② 打印时调用 capture() 获取 Base64
const uri = await receiptRef.current.capture();
// uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
// ③ 传给打印管理器
await gprinterManager.printImageBase64(uri, 0, 0, labelSize);
3.4 为什么选择 ViewShot + Base64 方案?
| 方案 | 优点 | 缺点 |
|---|---|---|
| ViewShot + Base64 ✅ | ① 小票布局用 React Native 声明式 UI 编写,灵活可维护;② 不依赖后端渲染;③ PNG 无损,清晰度可控 | ① 截图分辨率受限于 RN 视图尺寸;② 屏幕外截图可能被系统回收 |
| Canvas 手动绘制 | 精确控制每个像素 | 工作量大,布局调整困难 |
| 后端生成图片 | 不受前端限制 | 需要网络,离线下单不可用 |
| 原生直接绘制 Bitmap | 最快 | 代码维护成本高,UI 调整需改原生代码 |
四、代码层面的双通道实现
JS 层通过 size.height === 0 判断走哪个打印通道:
// src/utils/Gprinter.js: printImageBase64
async printImageBase64(base64Image, x = 0, y = 0, size) {
// ...
if (size.height === 0 && typeof GprinterPrinter.printReceiptImageBase64 === 'function') {
// → 小票通道:走 printReceiptImageBase64(Floyd-Steinberg 二值化)
return await GprinterPrinter.printReceiptImageBase64(cleanBase64, size.width);
}
// → 标签通道:走 printImageBase64WithSize(原始位图 + SDK 内部抖动)
return await GprinterPrinter.printImageBase64WithSize(
cleanBase64, x, y, size.width, size.height,
);
}
调用方通过 labelSize.height 区分:
// 小票打印:height=0 触发 receipt 通道
const labelSize = { width: 80, height: 0 };
await gprinterManager.printImageBase64(uri, 0, 0, labelSize);
// 标签打印:height 设为实际标签高度(mm)
const labelSize = { width: 80, height: 60 };
await gprinterManager.printImageBase64(uri, 0, 0, labelSize);
五、标签纸(LabelCommand)清晰度调试清单
标签纸不使用 Floyd-Steinberg 预处理(原因见第六章),清晰度依赖以下因素:
5.1 清单
☐ 1. ViewShot 分辨率是否足够?
├── 设置为 format: 'png', quality: 1
└── 截图容器宽度应 ≥ 标签纸可用宽度 × 8 dpi
例如 80mm × 8 dots/mm = 640px(200 DPI 打印机)
建议截图宽度:576px ~ 640px
☐ 2. 缩放算法是否正确?
├── Bitmap.createScaledBitmap(source, w, h, false)
└── 第三个参数 false = NEAREST_NEIGHBOR(近邻插值)
→ 对文字和条形码效果更好,不会产生灰边
如果为 true = 双线性插值 → 文字模糊有灰边
☐ 3. 浓度设置是否合适?
├── tsc.addDensity(DNESITY15) 使用最高浓度 15
└── 如果仍然淡,检查纸张质量(劣质热敏纸打印偏淡,详见第十一章)
☐ 4. 打印速度是否过快?
├── 佳博打印机默认速度可能过快导致着色不足
└── 可尝试通过 TSPL SPEED 指令降低:tsc.addSpeed(3)
(1=最慢最浓,4=最快最淡)
☐ 5. 纸张类型是否匹配?
├── 确认使用标签纸(非连续纸),GAP 设为实际间隙
└── 如果 GAP 设错,打印机会在错误位置打印,导致内容错位
☐ 6. 原始图片是否足够清晰?
└── ViewShot 截图前的 React Native 视图分辨率
小字体文字在 384px 宽度截图后打印会模糊
提升截图宽度即可解决
5.2 标签纸常见问题
| 症状 | 原因 | 解决 |
|---|---|---|
| 打印整体偏淡 | 浓度不够 / 纸张差 | 提高浓度 (DNESITY15) + 降速 (SPEED 2~3),或更换纸张品牌 |
| 文字边缘有灰边 | createScaledBitmap 用了双线性插值 | 第三个参数改为 false (NEAREST_NEIGHBOR) |
| 小字模糊不清 | 截图分辨率不足 | 提升 ViewShot 截图容器宽度 ≥ 576px |
| 条形码扫不出 | 缩放导致条码变形 | 条码用矢量绘制指令 (BARCODE),不要用图片 |
| 打印位置偏移 | GAP/REFERENCE 设置错误 | 校准 GAP 值,检查 addReference(0,0) |
| 标签纸走纸太多 | GAP 值小于实际间隙 | 用量尺测量实际间隙,调整 addGap() |
六、热敏小票纸(含 Floyd-Steinberg)清晰度调试清单
6.1 为什么小票纸必须用 Floyd-Steinberg?
热敏打印机是 1-bit 设备:每个像素只有"烧黑"或"不烧黑"两种状态。PNG 截图通常是 8-bit 灰度图 (0~255),必须做 二值化 才能打印。
原始 PNG 图片(256 级灰度)
│
▼ 二值化(Halftoning)
┌─────────────────────────────┐
│ 方法1: 固定阈值 (Threshold) │ → 中间色调全部丢失,照片变一团黑
│ 方法2: Bayer 4x4 抖动 │ → 有网格纹理,细节粗糙
│ 方法3: Floyd-Steinberg 误差扩散 ✅ → 视觉效果最佳,最接近原图
└─────────────────────────────┘
│
▼
1-bit 黑白位图(打印机能理解的数据)
Gprinter SDK 内部默认使用 Bayer 4x4 抖动(LabelCommand.drawImage()),这导致:
- 图片有明显的 4×4 方格纹理(栅格条纹)
- 文字边缘粗糙不清
- 中间色调(灰色)过渡生硬
本项目已修复:在 printReceiptImageBase64 中增加 floydSteinbergDither() 预处理,再传给 drawImage()。
6.2 清单
☐ 1. 是否走了 Floyd-Steinberg 二值化通道?
├── JS 层确认 size.height === 0
├── Native 层确认调用了 printReceiptImageBase64
└── 可观察 Logcat 日志:"printReceiptImageBase64: Floyd-Steinberg 已启用"
☐ 2. ViewShot 截图参数是否最佳?
├── format: 'png', quality: 1(无损)
├── 截图容器宽度:576px(匹配 80mm × 200DPI = 630dots,保持整数比)
└── collapsable={false} 防止 RN 优化回收屏幕外视图
☐ 3. 截图容器的位置是否会导致"Failed to capture view snapshot"?
├── 容器位于屏幕外(left: -9999 或更大负偏移)
├── 不要用 ScrollView 包裹(会导致 ViewShot 无法正确计算布局)
└── 容器必须有明确的宽高、backgroundColor
☐ 4. 缩放环节是否使用了 NEAREST_NEIGHBOR?
├── Bitmap.createScaledBitmap(bitmap, w, h, false)
└── false = NEAREST_NEIGHBOR,避免灰边
☐ 5. 浓度设置是否足够?
├── tsc.addDensity(DNESITY15) 最高浓度
└── 如果还淡,降低打印速度:tsc.addSpeed(2) 或 tsc.addSpeed(1)
☐ 6. GAP 是否设为 0?
├── 小票纸是连续纸,addGap(0) 必须
└── 如果 addGap(2) 会在每单之间空走 2mm,浪费纸张
☐ 7. 纸张宽度匹配吗?
├── 代码中 widthMm = 80
└── 确认实际装纸是 80mm 宽的热敏卷纸
☐ 8. 打印头是否干净?
├── 热敏打印头长期使用会积累纸屑和碳化残留
└── 用酒精棉清洁打印头(断电操作)
6.3 小票纸常见问题
| 症状 | 原因 | 解决 |
|---|---|---|
| 打印有栅格条纹 (4×4 网格) | 走了标签通道(Bayer 抖动),未走 Floyd-Steinberg 通道 | 确认 size.height === 0 触发 receipt 通道 |
| 整体模糊像蒙了一层雾 | Floyd-Steinberg 未生效 | 检查原生层是否引入了 floydSteinbergDither 方法 |
| 文字边缘有灰边 | 缩放用了双线性插值 | 确认 createScaledBitmap 第三个参数是 false |
| 打印偏淡 | 浓度不够或纸张灵敏度低 | DNESITY15 + SPEED≤2;或更换高灵敏度纸张(详见第十一章) |
| 图片中灰色区域变全黑 | 二值化阈值不当 | Floyd-Steinberg 误差扩散算法不会出现此问题 |
| 打印内容被截断 | SIZE 高度设小了 | heightMm = ceil(bitmapHeight / 8) 确保完整 |
| 打印后有大量空白 | SIZE 高度设大了或 GAP 非零 | 检查 GAP=0,高度仅设为图片实际高度 |
| ViewShot 截图失败 | 屏幕外视图被回收 | collapsable={false} + 足够大的负偏移 |
6.4 Floyd-Steinberg 算法要点(代码参考)
// GprinterModule.java
private Bitmap floydSteinbergDither(Bitmap source) {
int width = source.getWidth();
int height = source.getHeight();
// 提取灰度值
int[] pixels = new int[width * height];
source.getPixels(pixels, 0, width, 0, 0, width, height);
float[] grays = new float[pixels.length];
for (int i = 0; i < pixels.length; i++) {
int r = (pixels[i] >> 16) & 0xFF;
int g = (pixels[i] >> 8) & 0xFF;
int b = pixels[i] & 0xFF;
grays[i] = 0.299f * r + 0.587f * g + 0.114f * b; // 标准亮度公式
}
// Floyd-Steinberg 误差扩散
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = y * width + x;
float oldPixel = grays[idx];
float newPixel = oldPixel < 128 ? 0 : 255; // 阈值 128
grays[idx] = newPixel;
float error = oldPixel - newPixel;
// 将误差分配到相邻未处理像素
// * 7/16
// 3/16 5/16 1/16
if (x + 1 < width) grays[idx + 1] += error * 7f / 16f;
if (y + 1 < height) {
if (x - 1 >= 0) grays[idx + width - 1] += error * 3f / 16f;
grays[idx + width] += error * 5f / 16f;
if (x + 1 < width) grays[idx + width + 1] += error * 1f / 16f;
}
}
}
// 生成 1-bit 位图
int[] bwPixels = new int[pixels.length];
for (int i = 0; i < bwPixels.length; i++) {
int value = grays[i] < 128 ? 0xFF000000 : 0xFFFFFFFF; // 黑 or 白
bwPixels[i] = value;
}
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
result.setPixels(bwPixels, 0, width, 0, 0, width, height);
return result;
}
七、浓度设置详解
7.1 佳博 TSPL DENSITY 指令
// LabelCommand.DENSITY 枚举
DNESITY0 = 0; // 最淡
DNESITY1 = 1;
DNESITY2 = 2;
DNESITY3 = 3;
DNESITY4 = 4;
DNESITY5 = 5;
DNESITY6 = 6;
DNESITY7 = 7;
DNESITY8 = 8;
DNESITY9 = 9;
DNESITY10 = 10; // 默认
DNESITY11 = 11;
DNESITY12 = 12;
DNESITY13 = 13;
DNESITY14 = 14;
DNESITY15 = 15; // 最浓
7.2 浓度与打印速度的平衡
高浓度 (15) + 快速度 (4) = 可能着色不足(打印头接触时间太短)
高浓度 (15) + 慢速度 (1) = 最浓,但打印慢
中浓度 (10) + 慢速度 (2) = 更省打印头,寿命更长
推荐组合:
标签纸:DNESITY15 + SPEED 3(清晰且快速)
小票纸:DNESITY15 + SPEED 2(清晰度优先,小票打印量不大)
7.3 PC 驱动浓度 vs 代码浓度
⚠️ 重要:如果用 PC 的佳博驱动设置过浓度,打印机会记忆该设置。代码中
addDensity()会覆盖。
如果发现代码设了 DNESITY15 仍然淡,尝试:
- 在 PC 驱动中把浓度也拉到最高
- 降低打印速度 (SPEED 1~2)
- 更换热敏纸(不同品牌差异巨大,详见第十一章)
八、ViewShot 截图分辨率的影响
8.1 分辨率计算公式
纸张可打印宽度: 80mm
打印机 DPI: 200 (TSPL 默认 8 dots/mm)
可打印点数: 80 × 8 = 640 dots (但通常留一点边距)
截图宽度建议:
┌──────┬────────────┬─────────────────────────────────────┐
│ 宽度 │ 像素利用率 │ 说明 │
├──────┼────────────┼─────────────────────────────────────┤
│ 384px │ 60% │ ❌ 太模糊,小字糊成一团 │
│ 576px │ 90% │ ✅ 推荐:3:5 整数比,小字清晰 │
│ 640px │ 100% │ ✅ 1:1 映射到打印机点数,理论最佳 │
│ 768px │ 120% │ ⚠️ 需缩放,可能引入额外插值 │
└──────┴────────────┴─────────────────────────────────────┘
8.2 项目中推荐配置
// OnlineOrder.js
<ViewShot
ref={receiptRef}
options={{
format: 'png', // PNG 无损
quality: 1, // 最高质量
result: 'base64' // 直接返回 base64
}}
style={{
backgroundColor: '#fff',
width: 576, // ← 576px 推荐宽度
paddingHorizontal: 16
}}
collapsable={false}> // ← 防止屏幕外视图被回收
{/* 小票布局内容 */}
</ViewShot>
// 外层必须确保不被裁剪
<View style={styles.receiptWrapper}> {/* left: -9999 不够时改为 left: -12000 */}
{lastOrderData && (
<ViewShot ...>
</ViewShot>
)}
</View>
九、常见误区与排查表
| 误区 | 真相 | 影响 |
|---|---|---|
| "浓度设到 15 就一定能看清" | 浓度只影响加热温度,如果图片本身模糊(分辨率不足/双线性插值/未二值化),浓度再高也模糊 | 白费电,打印头容易老化 |
| "截图用 384px 够用了" | 384px 在 640dots 纸上像素利用率仅 60%,每个 1px 的小字特征仅 0.6 个打印点,必然糊 | 小字无法辨认 |
| "标签纸和小票纸代码一样" | GAP=2 vs GAP=0,小票纸还需要 Floyd-Steinberg 二值化 | 小票纸设 GAP=2 会走纸异常 |
| "PC 驱动设置跟打印机无关" | 打印机会记忆 PC 驱动的浓度设定,代码 addDensity 发指令后覆盖 | 如果代码漏发 DENSITY,打印机会用 PC 设定的浓度 |
| "双线性插值更平滑" | 对照片是,但对 1-bit 热敏打印是灾难——边缘变灰,灰色在二值化后变成随机噪点 | 文字边缘"发毛" |
| "Base64 图片压缩导致模糊" | 不会!Base64 只是编码格式,不改变像素数据。截图是 PNG 无损格式 | 模糊的原因在截图分辨率和缩放环节 |
| "所有热敏纸都一样" | 不同品牌热敏涂层配方差异巨大,显色灵敏度、保存寿命天差地别 | 详见第十一章热敏纸选型指南 |
十、完整打印链路检查清单
当打印不清晰时,按此顺序排查:
Step 1: 检查纸张类型
☐ 这是标签纸还是连续小票纸?
☐ size.height === 0 ?(小票):(标签)
Step 2: 检查 ViewShot 截图
☐ quality: 1
☐ format: 'png'
☐ 截图容器宽度 ≥ 576px
☐ collapsable={false}
☐ 容器不在 ScrollView 内
☐ 容器未被屏幕裁剪(负偏移足够大)
Step 3: 检查原生层缩放
☐ Bitmap.createScaledBitmap 第三个参数 = false (NEAREST_NEIGHBOR)
☐ 确认 576px → targetWidthDots 的缩放是正确的
Step 4: 检查二值化(仅小票纸)
☐ 走了 printReceiptImageBase64 通道
☐ Floyd-Steinberg 抖动已触发
☐ 阈值设为 128(标准)
Step 5: 检查 TSPL 指令
☐ addGap(0) — 小票 / addGap(2) — 标签
☐ addSize(widthMm, heightMm) 参数正确
☐ addDensity(DNESITY15) 最高浓度
☐ addSpeed(2) 适当降速(可选)
Step 6: 检查打印头
☐ 打印头是否清洁
☐ 打印头是否有磨损(打印不均有竖线缺失)
Step 7: 检查纸张质量(详见第十一章)
☐ 更换品牌试一下(理光 > 冠豪 > 杂牌)
☐ 热敏纸是否受潮/过期
☐ 用指甲划纸测试灵敏度
十一、热敏纸选型与品质鉴别
同一台打印机、同样的 DENSITY 设置,换上两种不同品牌的纸后一个清晰一个淡——原因 90% 在纸张本身。 本章系统讲解热敏纸的品质差异和选型方法。
11.1 为什么同样打印机会打出不同浓度?
热敏打印的工作原理是:打印头加热 → 热敏涂层发生化学反应 → 变色。
这个过程中有 3 个关键变量:
打印头发热量 ──→ 热敏涂层吸热 ──→ 化学反应显色
(DENSITY) (灵敏度) (色密度)
代码可控 纸张决定 纸张决定
代码中的 DENSITY=15 只是控制打印头加热量,但同样的热量打到不同纸张上,显色密度可以差 2~3 倍。这就是为什么:
- 纸张 A(高灵敏涂层):DENSITY=8 就很黑
- 纸张 B(低灵敏涂层):DENSITY=15 仍然偏淡
11.2 热敏纸的 5 层结构与品质差异
┌──────────────────────────────────────────────────┐
│ 高品质热敏纸 (如理光) │
├──────────────────────────────────────────────────┤
│ ① 顶涂层 (保护层) 厚度 1~3μm │
│ ├── 三防纸有(防水/防油/防酒精) │
│ └── 普通纸没有 │
│ ② 热敏显色层 厚度 3~5μm ← 核心层 │
│ ├── 无色染料 + 显色剂 + 增感剂 │
│ ├── 涂层越厚 → 显色密度越高 → 打印越黑 │
│ └── 增感剂配方 → 决定灵敏度和保存寿命 │
│ ③ 底涂层 (隔离层) 厚度 1~2μm │
│ └── 防止热敏涂料渗透到纸基 │
│ ④ 纸基 厚度 55~65μm │
│ ├── 白度 > 85% → 黑白对比度高 │
│ └── 平滑度越高 → 打印越细腻 │
│ ⑤ 背涂层 (可选) │
│ └── 防止静电,减少打印头磨损 │
└──────────────────────────────────────────────────┘
| 品质维度 | 优质纸 | 劣质纸 | 对清晰度的影响 |
|---|---|---|---|
| 涂层厚度 | ≥4g/m² | ≤3g/m² | 涂层薄→显色物质少→同等热量下颜色浅 |
| 涂层均匀性 | 整卷均匀 | 时厚时薄 | 同一卷纸打印出来一段深一段浅 |
| 白度 | ≥85%(ISO) | ≤75% | 纸基发灰→黑色文字对比度降低→肉眼觉得"淡" |
| 平滑度 | ≥300s(别克) | ≤150s | 粗糙纸面→打印头发热点散布不均→细腻度差 |
| 保存寿命 | 3~5年(常温) | 3~6个月 | 劣质纸打印后很快褪色变黄 |
11.3 显色灵敏度(发色温度)—— 最核心的差异
不同品牌的热敏纸,涂层的发色起始温度不同:
发色温度范围:
高灵敏纸:60~70°C 开始显色 ← 打印深,但怕热(夏天车内可能变黑)
中灵敏纸:70~80°C 开始显色 ← 平衡之选
低灵敏纸:80~90°C 开始显色 ← 打印偏淡,不易意外显色
─────────────────────────────────────────────────
佳博打印头加热曲线:
最佳匹配品 ← 理光 (Ricoh) 热敏纸涂层配方
基本匹配品 ← 冠豪、金华盛
不匹配品 ← 杂牌,发热—显色曲线偏差大
─────────────────────────────────────────────────
你遇到的「同一台打印机两种纸效果不同」就是这个原因:两种纸的发色温度不同,与佳博打印头的加热曲线匹配度不同。
11.4 品牌推荐与采购避坑
| 品牌 | 等级 | 特点 | 适用场景 |
|---|---|---|---|
| 理光 (Ricoh) | ⭐⭐⭐⭐⭐ | 日本涂料,灵敏度高且稳定,保存寿命长,发色黑度行业标杆 | 对清晰度有要求的场景首选 |
| 冠豪 | ⭐⭐⭐⭐ | 国产一线,品质稳定,性价比高 | 日常使用推荐 |
| 金华盛 | ⭐⭐⭐⭐ | 国产一线,涂层均匀性好 | 替代冠豪 |
| SONAO / 盛达 | ⭐⭐⭐ | 中端,批次差异较大 | 预算有限时 |
| 无品牌杂牌 | ⭐~⭐⭐ | 成本极低,但灵敏度、均匀性、保存寿命都无法保证 | 强烈不推荐用于生产环境 |
采购避坑清单:
☐ 不要买无品牌散装纸(淘宝无标白牌卷)
→ 批次间配方可能完全不同,这次清晰不代表下次也清晰
☐ 不要买含双酚A(BPA)的纸用于食品接触场景
→ 食品小票必须要求 BPA-Free(双酚A替代),正规品牌会标注
☐ 不要一次性囤太多
→ 热敏纸保质期通常 2~3 年(密封包装),受潮后 3 个月内报废
☐ 收货后立即做 A/B 对比测试
→ 同一台打印机、同一张图片、同一浓度下对比新旧纸张
→ 打印一张灰度渐变条,肉眼对比发色深度
11.5 储存与防潮要求
| 条件 | 要求 | 超限后果 |
|---|---|---|
| 温度 | 5~30°C(常温) | >40°C 会提前发色变黑 |
| 湿度 | 30%~70% RH | >80% 涂层吸潮失效,打印变淡 |
| 光照 | 避光保存 | 紫外线会破坏染料,打印后也需避光 |
| 开封后 | 1 个月内用完 | 暴露在空气中吸潮速度很快 |
实操建议:
✅ 未开封卷纸存放在阴凉干燥柜中
✅ 已开封的卷纸用塑料袋密封 + 干燥剂
✅ 不要放在厨房、洗碗间等潮湿/高温区域
✅ 打印机内过夜的纸如果第二天打印变淡 → 已受潮,更换新卷
✅ 潮湿地区(南方梅雨季)建议配备电子防潮箱 (RH≤40%)
11.6 快速鉴别方法(无需仪器)
方法一:指甲划痕测试(最实用)
用指甲在热敏纸表面快速划一道,摩擦生热会使涂层显色。 黑痕越深 → 灵敏度越高 → 打印越黑。
划痕颜色参考:
深黑色,反光 → 高灵敏好纸,打印会很黑
深灰到黑色 → 正常可用
浅灰色 → 灵敏度偏低,打印偏淡
几乎无色 → 已失效或极劣质,丢弃
方法二:对光看涂层
将纸对着光,侧着看纸面反光。
- 均匀的轻微光泽 → 涂层涂布均匀
- 斑驳/局部亮局部暗 → 涂层不均匀,打印会有深浅条纹
方法三:荧光灯照射(鉴别涂层厚度)
用紫外灯(验钞灯)照射纸面:
- 强蓝白光反射 → 涂层厚
- 暗弱反射 → 涂层薄
方法四:加热测试(鉴别保存寿命)
将打印好的小票用吹风机热风吹 30 秒(约80°C):
- 不变黑 → 保存寿命长,正规纸
- 整体变黑 → 灵敏度高但保存寿命短(含BPA的廉价纸特征)
11.7 三防纸的额外注意事项
三防纸(防水/防油/防酒精)在热敏涂层上多了一层保护层。这层保护层会:
优点:
✅ 小票遇水不糊、厨房油烟不褪色、酒精消毒不损坏
✅ 保存寿命更长(3~5年)
缺点:
⚠️ 保护层会吸收一部分打印头热量 → 比同品牌普通纸约偏淡 10%~15%
⚠️ 需要更高的 DENSITY 或更慢的 SPEED 来补偿
⚠️ 价格是普通纸的 1.5~2 倍
对策:
三防纸 → DENSITY 15 + SPEED 2(或不高于 3)
同样 DENSITY 下与普通纸对比偏淡是正常现象,不是缺陷
11.8 标签纸选型要点
标签纸除了热敏涂层品质外,还需关注:
| 参数 | 建议值 | 重要程度 |
|---|---|---|
| 底纸厚度 | 60~80μm | 太薄易卷曲卡纸 |
| 胶水类型 | 永久型 / 可移除型按需选 | 冷链标签用专用冷冻胶 |
| 模切精度 | 间隙误差 ≤0.5mm | 影响定位准确性 |
| 排废 | 必须排废干净 | 残留废料会粘在打印头上 |
| 管芯内径 | 标准 25mm / 40mm | 与打印机轴芯匹配 |
十二、总结
打印清晰度 = 分辨率 × 二值化算法 × 浓度 × 纸张质量
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 标签纸 (Label) 小票纸 (Receipt) │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ GAP = 2 │ │ GAP = 0 │ │
│ │ SIZE 固定 │ │ SIZE 按图片算 │ │
│ │ Bayer 抖动 │ │ Floyd-Steinberg │ ⚡ 核心区别 │
│ │ SIZE.h ≠ 0 │ │ SIZE.h = 0 │ │
│ └─────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ 重点查: 重点查: │
│ ① 缩放插值 (NEAREST) ① Floyd-Steinberg 是否生效 │
│ ② 截图分辨率 (≥576px) ② 截图分辨率 (≥576px) │
│ ③ 浓度 (DNESITY15) ③ 缩放插值 (NEAREST) │
│ ④ GAP 值与实际间隙匹配 ④ 浓度 (DNESITY15 + 降速) │
│ ⑤ 纸张品牌 (理光/冠豪) ⑤ GAP=0 确保连续纸 │
│ ⑥ 纸张品牌 (理光/冠豪) │
│ │
│ 共同结论: │
│ ① 小票纸清晰度的关键是 Floyd-Steinberg 二值化 │
│ ② 标签纸清晰度的关键是截图分辨率 + NEAREST_NEIGHBOR 缩放 │
│ ③ 所有代码正确但打印仍偏淡 → 99% 是纸张品质问题 │
│ ④ 同一台机器两种纸效果不同 → 正常,换统一品牌即可 │
│ │
└─────────────────────────────────────────────────────────────────┘