andrey-ushakov / esc_pos_utils

Basic Flutter/Dart classes for ESC/POS printing
BSD 3-Clause "New" or "Revised" License
140 stars 308 forks source link

Fix CJK String overlap when using Generator.row() method. #62

Open ttyniwa opened 2 years ago

ttyniwa commented 2 years ago

Fix CJK(Chinese, Japanese and Korean) String overlap when using Generator.row() method.

before: IMG_3418

after: IMG_3416

ixre commented 1 year ago

this is work fine for me


// 返回POS协议的打印字节
Future<List<int>> getPosPrintBytes(TicketPrintTask data) async {
  List<int> bytes = [];
  // Xprinter XP-N160I
  final profile = await CapabilityProfile.load();
  // PaperSize.mm80 or PaperSize.mm58
  final generator = Generator(PaperSize.mm80, profile);
  int maxCharsPerLine = 48;
  // 设置行距
  generator.spaceBetweenRows = 5;
  // 设置居中对齐
  generator.setStyles(const PosStyles(align: PosAlign.center));

  // 添加小票标题
  bytes.addAll(generator.emptyLines(2));
  bytes.addAll(generator.text("【${data.name}】",
      containsChinese: true,
      styles: const PosStyles(
        align: PosAlign.center,
        bold: true,
        width: PosTextSize.size2,
        height: PosTextSize.size2,
      )));
  bytes.addAll(generator.emptyLines(1));

  // 打印主要内容
  bytes += getTileBytes(generator, data.body, maxCharsPerLine);
  // 打印商品
  bytes += getItemBytes(generator, data.items, maxCharsPerLine);
  // 打印汇总信息
  bytes += getTileBytes(generator, data.summary, maxCharsPerLine);
  // 打印底脚信息
  bytes += getFooterBytes(generator, data.footer);
  // 推纸
  bytes += generator.feed(5);
  // 切纸
  bytes += generator.cut();
  // bytes += generator.image(thumbnail);
  return bytes;
}

/// 打印小票主体信息
List<int> getTileBytes(
    Generator generator, List<PrintField> body, int maxCharsPerLine) {
  if (body.isEmpty) return [];
  List<int> bytes = [];
  bytes.addAll(generator.emptyLines(1));

  // 打印水平线
  bytes.addAll(generator.hr());

  for (var it in body) {
    it.label = it.label.trim();
    it.value = it.value.trim();

    var txt =
        it.label + _getSpace(maxCharsPerLine, it.label + it.value) + it.value;

    bytes += generator.text(txt,
        containsChinese: true, styles: const PosStyles(align: PosAlign.left));
  }
  return bytes;
}

/// 打印商品信息
List<int> getItemBytes(
    Generator generator, List<TicketDetailsItem> items, int maxCharsPerLine) {
  List<int> bytes = [];
  for (var it in items) {
    if (it.labels.isEmpty || it.data.isEmpty) continue;
    bytes.addAll(generator.emptyLines(1));
    if (it.title.isNotEmpty) {
      bytes.addAll(generator.emptyLines(1));
      bytes.addAll(generator.text(it.title, containsChinese: true));
    }
    // 打印水平线
    bytes.addAll(generator.hr());
    var i = 0;
    for (var data in it.data) {
      var j = 0;
      List<PosColumn> arr = [];
      // 打页列头
      if (i == 0) {
        for (var label in it.labels) {
          arr.add(PosColumn(
              text: label.name.isEmpty ? "-" : label.name,
              width: j == 0 ? 12 - (it.labels.length - 1) * 2 : 2,
              containsChinese: true,
              styles: PosStyles(
                  align: j == it.labels.length - 1
                      ? PosAlign.right
                      : PosAlign.left)));
          j++;
        }
        bytes += _printRow(generator, arr, maxCharsPerLine);
        bytes.addAll(generator.hr());
        arr = [];
        j = 0;
      }

      for (var label in it.labels) {
        arr.add(PosColumn(
            text:
                (data[label.key] ?? "-").isEmpty ? "-" : data[label.key] ?? "-",
            width: j == 0 ? 12 - (it.labels.length - 1) * 2 : 2,
            containsChinese: true,
            styles: PosStyles(
                align: j == it.labels.length - 1
                    ? PosAlign.right
                    : PosAlign.left)));
        j++;
      }
      bytes += _printRow(generator, arr, maxCharsPerLine);
      i++;
    }
  }

  return bytes;
}

/// 打印底部信息
List<int> getFooterBytes(Generator generator, PrintFooter footer) {
  List<int> bytes = [];
  const boldStyle = PosStyles(bold: true);
  const centerStyle = PosStyles(align: PosAlign.center);
  bytes.addAll(generator.emptyLines(1));

  bytes.addAll(generator.hr());
  if (footer.text.isNotEmpty) {
    bytes.addAll(
        generator.text(footer.text, containsChinese: true, styles: boldStyle));
    bytes.add(generator.spaceBetweenRows * 2);
  }
  if (footer.text.isNotEmpty) {
    bytes.addAll(generator.text(footer.instructions,
        containsChinese: true, styles: boldStyle));
    bytes.add(generator.spaceBetweenRows * 2);
  }
  if (footer.phone.isNotEmpty) {
    bytes.addAll(generator.text(footer.phone,
        containsChinese: true, styles: centerStyle));
    bytes.add(generator.spaceBetweenRows * 1);
  }
  if (footer.suggest.isNotEmpty) {
    bytes.addAll(generator.hr());
    bytes.addAll(generator.text(footer.suggest,
        containsChinese: true, styles: centerStyle));
  }
  return bytes;
}

/// 打印行信息,因为直接打印行会导致中英文重叠
List<int> _printRow(Generator geneartor, List<PosColumn> arr, int max) {
  int unitChars = max * 2 ~/ 12; // 每个单元格应显示的长度
  int firstColChars = max - (arr.length - 1) * unitChars; // 第一行应显示的长度
  int firstMaxChars = firstColChars ~/ 2;
  // 第一行没有显示完的字符
  String firstCutChars = arr[0].text.length > firstMaxChars
      ? arr[0].text.substring(firstMaxChars)
      : "";
  String firstColTxt = firstCutChars.isEmpty
      ? arr[0].text
      : arr[0].text.substring(0, firstMaxChars);
  String firstLine = firstColTxt + _getSpace(firstColChars, firstColTxt);
  for (int i = 1; i < arr.length; i++) {
    if (i == arr.length - 1) {
      // 右对齐
      firstLine += _getSpace(unitChars, arr[i].text) + arr[i].text;
    } else {
      // 左对齐
      firstLine += arr[i].text + _getSpace(unitChars, arr[i].text);
    }
  }
  var bytes = geneartor.text(firstLine, containsChinese: true);
  while (firstCutChars.isNotEmpty) {
    bytes.add(geneartor.spaceBetweenRows);
    var isOver = firstCutChars.length <= firstMaxChars;
    var firstColTxt =
        isOver ? firstCutChars : firstCutChars.substring(firstMaxChars);
    bytes.addAll(geneartor.text(firstColTxt + _getSpace(max, firstColTxt),
        containsChinese: true));
    firstCutChars = isOver ? "" : firstCutChars.substring(firstMaxChars);
  }
  return bytes;
}

int _getStringLen(String word) {
  RegExp chineseRegexp = RegExp("[\u4e00-\u9fa5]");
  int chLen = chineseRegexp.allMatches(word).length;
  var t = (word.length + chLen);
  if (word.contains("¥") || word.contains("(") || word.contains(")")) {
    t += 1;
  }
  return t;
}

// 计算同一行用省略号代替的长度
String _getSpace(int max, String word) {
  var t = max - _getStringLen(word);
  List<String> bytes = [];
  for (var i = 0; i < t; i++) {
    bytes.add(" ");
  }
  return bytes.join("");
}