React Native 佳博打印机:标签纸 vs 热敏小票纸 — 清晰度调试要点总结

7 阅读25分钟

React Native 佳博打印机:标签纸 vs 热敏小票纸 — 清晰度调试要点总结

核心技术方案:react-native-view-shot 截图 + Base64 传输 + 原生 TSPL 指令打印
技术栈:React Native + ViewShot + 佳博 Gprinter SDK (TSPL)
打印机类型:佳博 GP-3120TU / GP-1324D / GP-2120TF 等兼容 TSPL 指令的机型
读者定位:开发者、运维调试人员,已遇到打印清晰度问题的使用者


目录


一、两种纸张的本质区别

┌────────────────────────────────────────────────────────────────────────────┐
│                           佳博热敏打印机                                     │
│                                                                             │
│  ┌─────────────────────────────┐     ┌─────────────────────────────────┐   │
│  │       标签纸 (Label)        │     │     热敏小票纸 (Receipt)          │   │
│  │                             │     │                                   │   │
│  │  ┌───┐ ┌───┐ ┌───┐ ┌───┐  │     │  ════════════════════════════    │   │
│  │  │ ● │ │ ● │ │ ● │ │ ● │  │     │  ║   连续纸,无间距标记     ║    │   │
│  │  └───┘ └───┘ └───┘ └───┘  │     │  ║   靠 GAP=0 跳过间隙检测 ║    │   │
│  │                             │     │  ════════════════════════════    │   │
│  │  模切纸,每张之间有间隙 (Gap)│     │                                   │   │
│  │  靠间隙传感器定位            │     │  靠固定长度或手动撕纸定位          │   │
│  │                             │     │                                   │   │
│  │  尺寸精确(如 80×60mm)     │     │  宽度固定,高度可变                 │   │
│  └─────────────────────────────┘     └─────────────────────────────────┘   │
│                                                                             │
└────────────────────────────────────────────────────────────────────────────┘
维度标签纸 (Label)热敏小票纸 (Receipt)
物理形态模切片,一张一张分开卷筒连续纸,无分隔
定位方式间隙传感器(Gap Sensor)检测标签间隙无定位,靠指令控制长度
TSPL 指令LabelCommandLabelCommand(同一套 SDK,参数不同)
GAP 参数addGap(2) — 告诉打印机间隙高度 (mm)addGap(0) — 告诉打印机无间隙
SIZE 高度标签的实际高度 (mm),如 60按图片像素反算,heightMm = ceil(bitmapHeight / 8)
打印内容固定尺寸布局(条码 + 文本 + 图片)动态小票(菜品列表、数量、金额)
清晰度关键图片分辨率 + 缩放算法 + 浓度Floyd-Steinberg 抖动二值化 + 图片分辨率 + 浓度
典型尺寸80×60mm, 80×40mm80mm宽 × 可变高度

二、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 参数标签纸小票纸影响
GAPaddGap(2) = 2mmaddGap(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 → BitmapBitmapFactory.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 仍然淡,尝试:

  1. 在 PC 驱动中把浓度也拉到最高
  2. 降低打印速度 (SPEED 1~2)
  3. 更换热敏纸(不同品牌差异巨大,详见第十一章)

八、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.h0  │              │ SIZE.h = 0      │              │
│   └─────────────┘              └─────────────────┘              │
│         │                              │                        │
│         ▼                              ▼                        │
│   重点查:                       重点查:                        │
│   ① 缩放插值 (NEAREST)          ① Floyd-Steinberg 是否生效     │
│   ② 截图分辨率 (≥576px)         ② 截图分辨率 (≥576px)          │
│   ③ 浓度 (DNESITY15)            ③ 缩放插值 (NEAREST)           │
│   ④ GAP 值与实际间隙匹配         ④ 浓度 (DNESITY15 + 降速)      │
│   ⑤ 纸张品牌 (理光/冠豪)         ⑤ GAP=0 确保连续纸              │
│                                 ⑥ 纸张品牌 (理光/冠豪)          │
│                                                                 │
│   共同结论:                                                     │
│   ① 小票纸清晰度的关键是 Floyd-Steinberg 二值化                   │
│   ② 标签纸清晰度的关键是截图分辨率 + NEAREST_NEIGHBOR 缩放        │
│   ③ 所有代码正确但打印仍偏淡 → 99% 是纸张品质问题                 │
│   ④ 同一台机器两种纸效果不同 → 正常,换统一品牌即可               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘