记录--前端小票打印网页打印

Posted 林恒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录--前端小票打印网页打印相关的知识,希望对你有一定的参考价值。

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

 一、小票打印

目前市面上的小票打印机大多采用的打印指令集为ESC/POS指令,它可以使用ASCII码、十进制、十六进制来控制打印,我们可以使用它来控制字体大小、打印排版、字体加粗、下划线、走纸、切纸、控制钱箱等,下面以初始化打印机为例:

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

小票打印纸的宽度一般可分58mm和80mm,这里指的是打印纸的宽度,但是在实际打印的时候,有效打印区域并没有这么宽。

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

上面说的字符指的是打印到小票上的内容,其中数字和字母占1个字符,中文占2个字符,也就是说,如果使用58mm的打印纸,一行最多可以打印16个汉字或者32个数字 。

当然这是在不改变字体大小的情况下,如果我们改变了字体大小,那么一行盛放的内容也会改变。

//控制字符大小

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

1.这里的n是一个变量, 0 ≤ n ≤ 255 

2.用二进制表示,n的取值范围就是00000000到11111111,其中二进制的前四位用来控制宽度,后四位用来控制高度。0000表示不变0001表示放大2倍0002表示放大3倍,以此类推

3.该命令对所有字符(英数字符和汉字) 有效。

4.缺省值:n = 0

 下面我们来看一下字符的不同放大倍数(这里的1倍,表示使用默认大小):

放大倍数n(二进制)n(十进制)
宽度1倍,高度1倍 00000000 0
宽度1倍,高度2倍 00000001 1
宽度1倍,高度3倍 00000002 2
宽度2倍,高度1倍 00010000 16
宽度2倍,高度2倍 00010001 17
宽度2倍,高度3倍 00010002 18
宽度3倍,高度1倍 00020000 32
宽度3倍,高度2倍 00020001 33
宽度3倍,高度3倍 00020002 34

 

PS:打印纸时间有些长,字迹有些模糊,见谅

 打印指令封装

// 打印机纸宽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);

也就是说,如果我们使用的打印机采用的是ESC/POS指令集(我这里使用过佳博、芯烨、斯普瑞特打印机),只要我们想办法把打印指令发送给打印机,打印机就可以识别到并且进行打印等操作。那么我们该如何发送呢?

1.蓝牙打印机

参考掘金 zgt_不梦的文章 微信小程序连接蓝牙打印机打印图片示例

  • 初始化蓝牙模块 wx.openBluetoothAdapter()
  • 初始化完成后搜寻附近的蓝牙设备 wx.startBluetoothDevicesDiscovery()
  • 监听寻找到新设备的事件 wx.onBluetoothDeviceFound()
  • 在监听寻找到新设备的事件回调中获取所有蓝牙设备列表 wx.getBluetoothDevices()
  • 连接低功耗蓝牙设备 wx.createBLEConnection()
  • 连接成功后获取蓝牙设备服务 wx.getBLEDeviceServices()
  • 在服务中取(notify=true || indicate=true) && write=true 的特征值的 uuid: wx.getBLEDeviceCharacteristics()
  • 完成后停止搜寻 wx.stopBluetoothDevicesDiscovery()
  • 向低功耗蓝牙设备特征值中写入二进制数据 wx.writeBLECharacteristicValue()
  • 离开页面时取消蓝牙连接 wx.closeBLEConnection()
  • 关闭蓝牙模块 wx.closeBluetoothAdapter()

亲测,好使!在uniapp也可以,只需替换对应的API即可

2.网口打印机

这里我使用的scoket连接,相比于USB打印,这里需要保证打印机和安卓设备在同一局域网下。好处是安卓设备可以和打印机距离较远(比如厨房打印)。这里以斯普瑞特打印机为例:[斯普瑞特官网 www.sprinter.com.cn/在进行数据通信之前,我们需要知道打印机在此局域网下的 IP, 下图为“一键配网”工具

 通过这个工具我们可以方便快捷的查询到打印机的IP,或者可以根据空闲的网段来修改默认分配的IP,斯普瑞特POS打印机的端口是9100。

如果是其他品牌的打印机,我们也可以使用arp命令来查看当前局域网下的IP

拿到打印机的IP之后我们怎么来测试一下打印机呢?

我们可以使用telnet命令(这个在Windows系统一般默认是关闭的,需要我们手动打开)

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

打开命令行窗口输入telnet命令,按下回车

 如果端口关闭或者无法连接,则显示不能打开到主机的链接,链接失败;端口打开的情况下,链接成功,则进入telnet页面(全黑的),证明端口可用。

连接成功后,我们输入任何内容后,按下回车,打印机就会打印我们刚才输入的内容。

接下来我们要使用scoket来连接安卓设备和打印机,这里我使用的是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: \'打印机数据传输失败\'
    )
  

打印小票

目前我们已经可以开心的使用打印功能了,只需要组合一下打印指令即可。这里需要注意的是,如果我们在此之前设置了字符大小宽高均放大2倍,那么后面打印的字符都会被放大,所以如果后面我们想使用默认字符大小,我们还需要再次设置字符大小为默认来覆盖之前的指令

//这里的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)

打印效果(这里仅为展示,非上述代码打印)

3.USB打印机

这里我使用的是uniapp插件市场的插件,如果你了解安卓原生开发,你也可以自己制作一个原生插件,或者使用Native.js开发。使用原生插件在本地调试需要先打包“自定义调试基座”,在本地测试后再打正式包。

uni-app基于nativejs实现USB-OTG通讯 - 简书1,监听USB拔出连接,判断是否含有权限 2,获取权限后,打开设备实现连接 3,读写发送接受数据https://www.jianshu.com/p/7c308ffcd789

uni-app官网uni-app,uniCloud,serverlesshttps://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接口热敏小票打印机插件usbPrinter - DCloud 插件市场本插件提供安卓手机通过USB接口连接热敏小票打印机进行打印的相关功能。通过USB连接相比使用蓝牙连接更稳定。https://ext.dcloud.net.cn/plugin?id=7757

在使用USB插件后,我们可以监听USB设备的插入和拔出,在初始化之后,我们可以进行数据通信,将上面封装的打印指令传给打印机即可

 二、网页打印

由于是网页运行在浏览器中,所以我们只能使用浏览器给我们提供的API

1.windows.print()

这个API在不同的浏览器中会有差异,其作用就是可以把网页中的body元素打印出来,如果我们不想打印整个body元素,则需要将body的innerHTML替换。使用这种方式有时有些页面样式会和打印出来的不一样,那么我们就要使用其他方式来优化。

//使用方法

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

1.1使用媒体查询

@media print 
  //把需要打印时才用到的样式写到这里
  p
    font-size:16px;
  
同理,你也可以直接在CSS文件或者style标签中加上 media="print"
<style media="print">
//CSS代码
</style>

1.2监听打印事件

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



//监听打印之后的事件
window.onafterprint = function() 
   //恢复之前的样式

1.3分页符

  1.3.1 page-break-before  指定元素前插入分页符

  1.3.2 page-break-after  指定元素后插入分页符

描述
auto 默认。如果必要则在元素后插入分页符。
always 在元素后插入分页符。
avoid 避免在元素后插入分页符。
left 在元素之后足够的分页符,一直到一张空白的左页为止。
right 在元素之后足够的分页符,一直到一张空白的右页为止。
inherit 规定应该从父元素继承 page-break-after 属性的设置。

1. 您不能对绝对定位的元素使用此属性。

2. 请尽可能少地使用分页属性,并且避免在表格、浮动元素、带有边框的块元素中使用分页属性。

3. 任何版本的Internet Explorer(包括IE8)支持属性值"left","right",和"inherit"。

4. Firefox,Chrome和Safari不支持属性值"avoid","left"和"right"。.

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

1.3.3 page-break-inside  设置是否在指定元素中插入分页符 

描述
auto 默认。如果必要则在元素内部插入分页符。
avoid 避免在元素内部插入分页符。
inherit 规定应该从父元素继承 page-break-inside 属性的设置。
  1. 您不能对绝对定位的元素使用此属性。
  2. 请尽可能少地使用分页属性,并且避免在表格、浮动元素、带有边框的块元素中使用分页属性。
  3. IE8 及更早IE版本不支持 "inherit" 属性。
  4. Firefox, Chrome, 以及 Safari 不支持属性值 "avoid".
//避免在 <pre> 与 <blockquote> 元素中插入分页符:

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

1.4设置纸张

@page:  用来设置页面大小、边距、方向等

//portrait:纵向;  landscape: 横向

@page 
    size: A4 portrait;  //设置纸张及其方向    这里表示使用A4纸张,打印方向为纵向 
    margin: 3.7cm 2.6cm 3.5cm; //设置纸张外边距

 
// 去除页眉
@page  margin-top: 0; 
 
// 去除页脚
@page  margin-bottom: 0; 

 值得注意的是,如果我们使用的打印机是黑白打印的,比如针式打印机,那么我们使用的颜色最好是 #000,如果使用 #999这种灰色,打印效果会很不清晰

本文转载于:

https://juejin.cn/post/7237316724739457061

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

模拟打印小票(支持暂停功能)

public class MainFrame extends JFrame {

    private JButton printButton = new JButton("打印");
    private JButton suspendButton = new JButton("暂停");

    private JLabel printText = new JLabel("模拟小票打印");
    private JTextArea printTextArea = new JTextArea();

    //判断暂停、继续
    private boolean suspend = false;

    //新建一个对象
    Object lock = new Object();

    public MainFrame() {
        initFrame();
        bindEvent();
    }

    private void initFrame() {
        setTitle("模拟小票打印");
        setSize(800, 500);
        setVisible(true);
        setLayout(null);
        setResizable(false);
        setLocationRelativeTo(null);
        Font labelFont = new Font(Font.SERIF, Font.BOLD, 23);

        printText.setBounds(360, 30, 200, 50);
        printText.setFont(labelFont);

        printTextArea.setBounds(200, 100, 420, 160);
        printTextArea.setFont(labelFont);

        printButton.setBounds(new Rectangle(260, 300, 100, 50));
        printButton.setFont(labelFont);

        suspendButton.setBounds(new Rectangle(460, 300, 100, 50));
        suspendButton.setFont(labelFont);

        add(printButton);
        add(suspendButton);
        add(printText);
        add(printTextArea);
    }

    private void bindEvent() {
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        printTextArea.setText("准备打印");
        printButton.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                new Thread(() -> {
                    try {
                        synchronized (lock) {
                            for (int i = 10001; i < 100000; i++) {
                                printTextArea.setText("当前打印单号为:" + i);
                                Thread.sleep(1000);
                                //根据 suspend 来判断 是否让当前线程wait
                                while (suspend) {
                                    int n = i + 1;
                                    printTextArea.setText("打印暂停, 再次打印从" + n + "开始");
                                    lock.wait();
                                }
                            }
                        }
                    } catch (Exception e1) {
                        e1.printStackTrace();
                    }
                }).start();
            }
        });

        //暂停/继续 操作
        suspendButton.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (suspend) {
                    suspend = false;
                    suspendButton.setText("暂停");
                    synchronized (lock) {
                        //唤醒当前线程
                        lock.notifyAll();
                    }
                } else {
                    suspend = true;
                    suspendButton.setText("继续");
                }
            }
        });
    }
}

??exe ??请看??
技术分享图片

技术分享图片

技术分享图片

 

技术分享图片技术分享图片

 





以上是关于记录--前端小票打印网页打印的主要内容,如果未能解决你的问题,请参考以下文章

如何用蓝牙小票打印机打印小票票据?

小白学前端化腐朽为神奇-HTML+CSS3实现永和小票(day01-2)

java票据打印,类似于超市的小票那样的,怎么控制打印的纸张大小啊?

常见小票打印机打印故障及解决方法汇总视频教程

优库蓝牙小票打印机怎么弄

C# Winform打印超市小票的代码,在点击收银按钮时,立马自动打印...跪求帮助,高分!