Front-end receipt printing, web page printing (uniapp, applet, ESC/POS command)

Since I encountered all kinds of printing during the project, I wanted to write an article to summarize the printing needs and solutions I encountered. In general, the printing requirements I have encountered can be divided into two categories, web page printing and receipt printing. and ideas

Table of contents

 1. Receipt printing

 print instruction package

1. Bluetooth printer

2. Ethernet printer

print receipt

Printing effect (here is only for display, not printing by the above code)

3. USB printer

 2. Web page printing

1.windows.print()

1.1 Using media queries

1.2 Listening to print events

1.3 Page Breaks

1.4 Setting paper


 

 1. Receipt printing

At present, most of the small ticket printers on the market use the printing instruction set as ESC/POS instruction. It can use ASCII code, decimal, and hexadecimal to control printing. We can use it to control font size, print typesetting, and font bold , underline, paper feeding, paper cutting, cash drawer control, etc. The following takes the initialization of the printer as an example:

ASCII码  ESC  @
十进制码  27  64
十六进制  1B  40

The width of small ticket printing paper can generally be divided into 58mm and 80mm. This refers to the width of the printing paper, but in actual printing, the effective printing area is not so wide.

 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符
 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符

The characters mentioned above refer to the content printed on the receipt, in which numbers and letters occupy 1 character, and Chinese characters occupy 2 characters. That is to say, if you use 58mm printing paper, you can print up to 16 Chinese characters or 32 characters per line. numbers.

Of course, this is without changing the font size. If we change the font size, the content of a row will also change.

//控制字符大小

ASCII码   GS  !   n
十进制码  29  33  n
十六进制  1D  21  n

1. Here n is a variable, 0 ≤ n ≤ 255 

2. Expressed in binary, the value range of n is 00000000 to 11111111, where the first four bits of binary are used to control the width, and the last four bits are used to control the height. 0000 means unchanged , 0001 means magnified by 2 times , 0002 means magnified by 3 times , and so on

3. This command is valid for all characters (alphanumeric characters and Chinese characters).

4. Default value: n = 0

 Let's take a look at the different magnifications of characters (1 times here means using the default size):

gain n (binary) n (decimal)
1x width, 1x height 00000000 0
1x width, 2x height 00000001 1
1x width, 3x height 00000002 2
2x width, 1x height 00010000 16
2x width, 2x height 00010001 17
2x width, 3x height 00010002 18
3x width, 1x height 00020000 32
3 times the width, 2 times the height 00020001 33
3 times the width, 3 times the height 00020002 34

PS: It took a long time to print the paper, and the handwriting is a little fuzzy, sorry

 print instruction package

// 打印机纸宽58mm,页的宽度384,字符宽度为1,每行最多盛放32个字符
// 打印机纸宽80mm,页的宽度576,字符宽度为1,每行最多盛放48个字符
const PAGE_WIDTH = 576;
const MAX_CHAR_COUNT_EACH_LINE = 48;

//字符串转字节序列
function stringToByte(str) {
  var bytes = new Array();
  var len, c;
  len = str.length;
  for (var i = 0; i < len; i++) {
    c = str.charCodeAt(i);
    if (c >= 0x010000 && c <= 0x10FFFF) {
      bytes.push(((c >> 18) & 0x07) | 0xF0);
      bytes.push(((c >> 12) & 0x3F) | 0x80);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000800 && c <= 0x00FFFF) {
      bytes.push(((c >> 12) & 0x0F) | 0xE0);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000080 && c <= 0x0007FF) {
      bytes.push(((c >> 6) & 0x1F) | 0xC0);
      bytes.push((c & 0x3F) | 0x80);
    } else {
      bytes.push(c & 0xFF);
    }
  }
  return bytes;
}

//字节序列转ASCII码
//[0x24, 0x26, 0x28, 0x2A] ==> "$&C*"
function byteToString(arr) {
  if (typeof arr === 'string') {
    return arr;
  }
  var str = '',
    _arr = arr;
  for (var i = 0; i < _arr.length; i++) {
    var one = _arr[i].toString(2),
      v = one.match(/^1+?(?=0)/);
    if (v && one.length == 8) {
      var bytesLength = v[0].length;
      var store = _arr[i].toString(2).slice(7 - bytesLength);
      for (var st = 1; st < bytesLength; st++) {
        store += _arr[st + i].toString(2).slice(2);
      }
      str += String.fromCharCode(parseInt(store, 2));
      i += bytesLength - 1;
    } else {
      str += String.fromCharCode(_arr[i]);
    }
  }
  return str;
}
//居中
function Center() {
  var Center = [];
  Center.push(27);
  Center.push(97);
  Center.push(1);
  var strCenter = byteToString(Center);
  return strCenter;
}

//居左
function Left() {
  var Left = [];
  Left.push(27);
  Left.push(97);
  Left.push(0);
  var strLeft = byteToString(Left);
  return strLeft;
}
//居右
function Right() {
  var right = [];
  Left.push(27);
  Left.push(97);
  Left.push(2);
  var strRight = byteToString(right);
  return strRight;
}
//标准字体
function Size1() {
  var Size1 = [];
  Size1.push(29);
  Size1.push(33);
  Size1.push(0);
  var strSize1 = byteToString(Size1);
  return strSize1;
}
//大号字体
/*  放大1倍  n = 0
 *  长宽各放大2倍  n = 17 */
function Size2(n) {
  var Size2 = [];
  Size2.push(29);
  Size2.push(33);
  Size2.push(n);
  var strSize2 = byteToString(Size2);
  return strSize2;
}

// 字体加粗
function boldFontOn() {
  var arr = []
  arr.push(27)
  arr.push(69)
  arr.push(1)
  var cmd = byteToString(arr);
  return cmd
}
// 取消字体加粗
function boldFontOff() {
  var arr = []
  arr.push(27)
  arr.push(69)
  arr.push(0)
  var cmd = byteToString(arr);
  return cmd
}
// 打印并走纸n行
function feedLines(n = 1) {
  var feeds = []
  feeds.push(27)
  feeds.push(100)
  feeds.push(n)
  var printFeedsLines = byteToString(feeds);
  return printFeedsLines
}
// 切纸
function cutPaper() {
  var cut = []
  cut.push(29)
  cut.push(86)
  cut.push(49)
  var cutType = byteToString(cut);
  return cutType
}

// 开钱箱
function open_money_box() {
  var open = []
  open.push(27)
  open.push(112)
  open.push(0)
  open.push(60)
  open.push(255)
  var openType = byteToString(open)
  return openType
}

// 初始化打印机
function init() {
  var arr = []
  arr.push(27)
  arr.push(68)
  arr.push(0)
  var str = byteToString(arr)
  return str
}
/* 
 设置左边距
 len:
 */

function setLeftMargin(len = 1) {
  var arr = []
  arr.push(29)
  arr.push(76)
  arr.push(len)
  var str = byteToString(arr)
  return str
}

// 设置打印区域宽度
function setPrintAreaWidth(width) {
  var arr = []
  arr.push(29)
  arr.push(87)
  arr.push(width)
  var str = byteToString(arr)
  return str
}

/**
 * @param str
 * @returns {boolean} str是否全是中文
 */
function isChinese(str) {
  return /^[\u4e00-\u9fa5]$/.test(str);
}

// str是否全含中文或者中文标点
function isHaveChina(str) {
  if (escape(str).indexOf("%u") < 0) {
    return 0
  } else {
    return 1
  }
}
/**
 * 返回字符串宽度(1个中文=2个英文字符)
 * @param str
 * @returns {number}
 */
function getStringWidth(str) {
  let width = 0;
  for (let i = 0, len = str.length; i < len; i++) {
    width += isHaveChina(str.charAt(i)) ? 2 : 1;
  }
  return width;
}

/**
 * 同一行输出str1, str2,str1居左, str2居右
 * @param {string} str1 内容1
 * @param {string} str2 内容2
 * @param {string} fillWith str1 str2之间的填充字符
 * @param {number} fontWidth 字符宽度 1/2
 *
 */
function inline(str1, str2, fillWith = ' ', fontWidth = 1) {
  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  // 需要填充的字符数量
  let fillCount = lineWidth - (getStringWidth(str1) + getStringWidth(str2)) % lineWidth;
  let fillStr = new Array(fillCount).fill(fillWith.charAt(0)).join('');
  return str1 + fillStr + str2;
}
/**
 * 用字符填充一整行
 * @param {string} fillWith 填充字符
 * @param {number} fontWidth 字符宽度 1/2
 */
function fillLine(fillWith = '-', fontWidth = 1) {
  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  return new Array(lineWidth).fill(fillWith.charAt(0)).join('');
}

/**
 * 文字内容居中,左右用字符填充
 * @param {string} str 文字内容
 * @param {number} fontWidth 字符宽度 1/2
 * @param {string} fillWith str1 str2之间的填充字符
 */
function fillAround(str, fillWith = '-', fontWidth = 1) {
  const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth;
  let strWidth = getStringWidth(str);
  // 内容已经超过一行了,没必要填充
  if (strWidth >= lineWidth) {
    return str;
  }
  // 需要填充的字符数量
  let fillCount = lineWidth - strWidth;
  // 左侧填充的字符数量
  let leftCount = Math.round(fillCount / 2);
  // 两侧的填充字符,需要考虑左边需要填充,右边不需要填充的情况
  let fillStr = new Array(leftCount).fill(fillWith.charAt(0)).join('');
  return fillStr + str + fillStr.substr(0, fillCount - leftCount);
}

That is to say, if the printer we use uses the ESC/POS instruction set (I have used Jiabo, Xinye, and Spirit printers here), as long as we find a way to send the print command to the printer, the printer can recognize it. to and perform operations such as printing. So how do we send it?

1. Bluetooth printer

Refer to Nuggets zgt_ Bumeng 's article  for an example of printing pictures by connecting the WeChat applet to a Bluetooth printer

  • Initialize the Bluetooth module wx.openBluetoothAdapter()
  • Search for nearby Bluetooth devices after initialization wx.startBluetoothDevicesDiscovery()
  • Listen to the event of finding a new device wx.onBluetoothDeviceFound()
  • Obtain a list of all Bluetooth devices in the event callback of listening for new devices wx.getBluetoothDevices()
  • Connect to low energy Bluetooth devices wx.createBLEConnection()
  • After the connection is successful, get the Bluetooth device service wx.getBLEDeviceServices()
  • Get the uuid of the characteristic value of (notify=true || indicate=true) && write=true in the service: wx.getBLEDeviceCharacteristics()
  • Stop hunting when done wx.stopBluetoothDevicesDiscovery()
  • Write binary data to the characteristic value of the Bluetooth low energy device wx.writeBLECharacteristicValue()
  • Cancel the Bluetooth connection wx.closeBLEConnection() when leaving the page
  • Close the Bluetooth module wx.closeBluetoothAdapter()

Personal test, it works! It is also possible in uniapp, just replace the corresponding API

2. Ethernet printer

Here I use the scoket connection. Compared with USB printing, it is necessary to ensure that the printer and the Android device are under the same LAN. The advantage is that the Android device can be far away from the printer (such as printing in the kitchen). Here we take the Sprinter printer as an example: Sprinter official website https://www.sprinter.com.cn/ https://www.sprinter.com.cn/ Before data communication, we need to know where the printer is The IP under this LAN  , the figure below is the tool of "one-key distribution network"

 Through this tool, we can easily and quickly query the IP of the printer, or modify the default assigned IP according to the free network segment. The port of the Spirit POS printer is 9100 .

If it is a printer of another brand, we can also use the arp command to view the IP under the current LAN

How can we test the printer after getting the IP of the printer?

We can use the telnet command (this is generally closed by default in Windows systems, and we need to open it manually)

//telnet + 空格 + ip + 空格 + 端口号
telnet 192.168.5.6 9100

Open the command line window, enter the telnet command, and press Enter

 If the port is closed or cannot be connected, it will show that the link to the host cannot be opened, and the link fails; if the port is open, the link is successful, and it will enter the telnet page (all black), which proves that the port is available.

After the connection is successful, after we enter any content, press Enter, and the printer will print the content we just entered.

Next we will use scoket to connect Android devices and printers, here I am using uniapp

/**
 * 调用tcp通信进行打印
 * @param {buffer}  buffer       打印数据
 * @param {object}  printerInfo  打印机对象{IP:'',PORT:''}
 */
function tcpWrite(buffer, printerInfo) {
  var Socket = plus.android.importClass("java.net.Socket");
  var PrintWriter = plus.android.importClass("java.io.PrintWriter");
  var BufferedWriter = plus.android.importClass("java.io.BufferedWriter");
  var OutputStreamWriter = plus.android.importClass("java.io.OutputStreamWriter");
  var BufferedReader = plus.android.importClass("java.io.BufferedReader");
  var InputStreamReader = plus.android.importClass("java.io.InputStreamReader");
  var InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");
  //连接  注意:这里的端口一定是数字类型
  var sk = null
  try {
    sk = new Socket(printerInfo.IP, Number(printerInfo.PORT));
    sk.setSoTimeout(5000);
  } catch (e) {
    console.log(e, 'ee')
    uni.showToast({
      icon: 'none',
      title: '打印机连接失败'
    })
  }
  //发送
  try {
    var outputStreamWriter = new OutputStreamWriter(sk.getOutputStream(), "GBK");
    var bufferWriter = new BufferedWriter(outputStreamWriter);
    var out = new PrintWriter(bufferWriter, true);
    out.println(buffer);
    //关闭tcp连接
    out.close();
  } catch (e) {
    console.log(e, 'ee')
    uni.showToast({
      icon: 'none',
      title: '打印机数据传输失败'
    })
  }
}

print receipt

Now we can happily use the printing function, just need to combine the printing commands. It should be noted here that if we set the character size to be enlarged by 2 times in width and height before this, then the characters printed later will be enlarged, so if we want to use the default character size later, we need to set the character size to the default again to overwrite the previous command

//这里的EscPosUtil.js就是上面封装的打印指令

import Esc from './EscPosUtil.js';

// 打印文字格式
let strCenter = Esc.Center(); //文字居中
let strLeft = Esc.Left(); //文字靠左
let strSize1 = Esc.Size1(); //默认文字
let strSize2 = Esc.Size2(17); //文字放大两倍(长宽均为两倍)

let printerInfo = {
  IP:'192.168.5.6',
  PORT: 9100
}


let strCmd = strCenter + Esc.Size2(17) + Esc.boldFontOn() + '测试门店'+ "\n";
  strCmd += strSize1 + Esc.fillLine(' ') + "\n"
  strCmd += strCenter + Esc.Size2(17) + Esc.boldFontOn() + '结账单-堂食'  + "\n";
  strCmd += strSize1 + Esc.fillLine(' ') + "\n"
  strCmd += strLeft + Esc.Size2(17) + "取餐号:" + '62' + "\n";
  strCmd += Esc.inline('桌号:' + '牡丹厅', '人数:' + '6', ' ', 2) + "\n"
  strCmd += Esc.boldFontOff() + strSize1 + Esc.fillLine(' ') + "\n"
  strCmd += strLeft + strSize1 + "订单号:" + '202305171749110001' + "\n";

  // 商品信息
  strCmd += Esc.fillAround('商品') + "\n"
  // 票尾
  strCmd += Esc.fillLine(' ') + "\n"
  strCmd += strCenter + '欢迎下次光临!' + "\n";
  strCmd += Esc.feedLines(4) + "\n"
  // 切纸
  strCmd += Esc.cutPaper()

tcpWrite(strCmd, printerInfo)

Printing effect (here is only for display, not printing by the above code)

3. USB printer

Here I am using a plug-in from the uniapp plug-in market. If you know Android native development, you can also make a native plug-in yourself, or use Native.js to develop it. To use the native plug-in to debug locally, you need to package the "custom debugging base" first, and then package it officially after testing locally.

uni-app implements USB-OTG communication based on nativejs - short book 1, monitor the USB connection, judge whether it has permission 2, after obtaining the permission, open the device to realize the connection 3, read, write, send and receive data https://www.jianshu. com/p/7c308ffcd789

uni-app官网uni-app,uniCloud,serverless https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92% E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6

Android USB interface thermal receipt printer plug-in usbPrinter - DCloud plug-in market This plug-in provides related functions for Android mobile phones to connect to thermal receipt printers through USB interface for printing. Connecting via USB is more stable than using Bluetooth. https://ext.dcloud.net.cn/plugin?id=7757 After using the USB plug-in, we can monitor the plug-in and pull-out of the USB device. After initialization, we can perform data communication and transmit the print command encapsulated above to to the printer

 2. Web page printing

Since the webpage runs in the browser, we can only use the API provided by the browser

1.windows.print()

This API will be different in different browsers. Its function is to print out the body element in the web page. If we don't want to print the entire body element, we need to replace the innerHTML of the body. Using this method sometimes some page styles will be different from the printed ones, so we have to use other methods to optimize.

//使用方法

document.body.innerHTML = newstr;  // 把需要打印的指定内容赋给body
window.print();

1.1 Using media queries


@media print {
  //把需要打印时才用到的样式写到这里
  p{
    font-size:16px;
  }
}

Similarly, you can also directly add media="print" to the CSS file or style tag

<style media="print">
//CSS代码
</style>

1.2 Listening to print events

//监听打印之前的事件
window.onbeforeprint = function() {
  //可以修改元素样式
}
//监听打印之后的事件
window.onafterprint = function() {
   //恢复之前的样式
}

1.3 Page Breaks

  1.3.1 page-break-before inserts a page break before the specified element

  1.3.2 page-break-after Insert a page break after the specified element

page-break-before, page-break-after page break properties
value describe
auto default. Inserts a page break after the element if necessary.
always Inserts a page break after an element.
avoid Avoid inserting page breaks after elements.
left Enough page breaks after the element, up to a blank left page.
right Enough page breaks after the element, up to a blank right page.
you inherit Specifies that the setting of the page-break-after attribute should be inherited from the parent element.

1. You cannot use this property on absolutely positioned elements.

2. Please use paging attributes as little as possible, and avoid using paging attributes in tables, floating elements, and block elements with borders.

3. Any version of Internet Explorer (including IE8) supports attribute values ​​"left", "right", and "inherit".

4. Firefox, Chrome and Safari do not support the attribute values ​​"avoid", "left" and "right". .

@media print {
    footer {page-break-after: always;}
}

1.3.3 page-break-inside Set whether to insert a page break in the specified element 

page-break-inside page break property
value describe
auto default. Inserts a page break inside the element if necessary.
avoid Avoid inserting page breaks inside elements.
you inherit Specifies that the setting of the page-break-inside attribute should be inherited from the parent element.
  1. You cannot use this attribute on absolutely positioned elements.
  2. Please use the pagination attribute as little as possible, and avoid using the paging attribute in tables, floating elements, and block elements with borders.
  3. IE8 and earlier IE versions do not support the "inherit" attribute.
  4. Firefox, Chrome, and Safari do not support the attribute value "avoid".
//避免在 <pre> 与 <blockquote> 元素中插入分页符:

@media print {
    pre, blockquote {page-break-inside: avoid;}
}

1.4 Setting paper

@page: Used to set the page size, margins, orientation, etc.

//portrait:纵向;  landscape: 横向

@page {
    size: A4 portrait;  //设置纸张及其方向    这里表示使用A4纸张,打印方向为纵向 
    margin: 3.7cm 2.6cm 3.5cm; //设置纸张外边距
}
 
// 去除页眉
@page { margin-top: 0; }
 
// 去除页脚
@page { margin-bottom: 0; }

 It is worth noting that if the printer we use prints in black and white, such as a dot matrix printer, then the color we use should be #000 . If you use gray like #999 , the printing effect will be very unclear

 3. End

If you have any good suggestions and ideas, welcome to discuss

 

Guess you like

Origin blog.csdn.net/Linxi_001/article/details/130867268