美文网首页app开发ios初级篇Android技术知识
Android 蓝牙连接 ESC/POS 热敏打印机打印(ESC

Android 蓝牙连接 ESC/POS 热敏打印机打印(ESC

作者: VitaminChen | 来源:发表于2016-05-22 17:31 被阅读18211次

    上一篇 主要介绍了如何通过蓝牙连接到打印机。这一篇,我们就介绍如何向打印机发送打印指令,来打印字符和图片。

    =====================2017.05.09 更新====================

    终于抽时间整了一个可以运行的demo出来,实现了以下功能:

    • 检测蓝牙开启状态
    • 显示已配对设备
    • 连接打印机
    • 打印测试,包括打印标题,打印两列三列文字,打印图片等

    最终demo及打印的小票示例:


    Demo界面 打印小票示例
    ========================以下是原文=======================

    1. 构造输出流

    首先要明确一点,就是蓝牙连接打印机这种场景下,手机是 Client 端,打印机是 Server 端。

    在上一篇的最后,我们从 BluetoothSocket 得到了一个OutputStream。这里我们做一层包装,得到一个OutputStreamWriter 对象:

    OutputStreamWriter writer = new OutputStreamWriter(outputStream, "GBK");
    

    这样做主要是为了后面可以直接输出字符串,不然只能输出 int 或 byte 数据;

    2. 常用打印指令

    手机通过蓝牙向打印机发送的都是纯字节流,那么打印机如何知道该打印的是一个文本,还是条形码,还是图片数据呢?这里就要介绍 ESC/POS 打印控制命令

    • 初始化打印机
      初始化打印机指令

    在每次打印开始之前要调用该指令对打印机进行初始化。向打印机发送这条指令对应的代码就是:

      protected void initPrinter() throws IOException {  
            writer.write(0x1B);  
            writer.write(0x40);  
            writer.flush();  
      }
    
    • 打印文本
      没有对应指令,直接输出
    protected void printText(String text) throws IOException {  
            writer.write(text);
            writer.flush();
        } 
    
    • 设置文本对齐方式
    文本对齐方式指令

    对应的发送指令的代码:

        /* 设置文本对齐方式
         * @param align 打印位置  0:居左(默认) 1:居中 2:居右 
         * @throws IOException 
         */  
        protected void setAlignPosition(int align) throws IOException {  
            writer.write(0x1B);  
            writer.write(0x61);  
            writer.write(align);  
            writer.flush();  
        }
    

    与初始化指令不同的是,这条指令带有一个参数n。

    • 换行和制表符
      直接输出对应的字符:
        protected void nextLine() throws IOException {  
            writer.write("\n");  
            writer.flush();  
        }
    
        protected void printTab(int length) throws IOException {  
            for (int i = 0; i < length; i++) {  
                writer.write("\t");  
            }  
            writer.flush();  
        }  
    

    这两个指令在打印订单详情的时候使用最多。尤其是制表符,可以让每一列的文字对齐。

    • 设置行间距
    设置行间距指令

    n表示行间距为n个像素点,最大值256

    protected void setLineGap(int gap) throws IOException {  
            writer.write(0x1B);  
            writer.write(0x33);  
            writer.write(gap);  
            writer.flush();  
    }
    

    这个指令在后面打印图片的时候会用到。

    3. 打印图片

    很多小票上面都会附上一个二维码,用户扫描之后,可以获得更多的信息。因为热敏打印机只能打印黑白两色,所以首先把图片转成纯黑白的,再调用图片打印指令进行打印。

    3.1 打印图片指令

    打印图片指令

    这个指令的参数很多,一个一个来说:

    • m:取值十进制 0、1、32、33。设置打印精度,0、1对应每行8个点,32、33对应每行24个点,对应最高的打印精度(其实这里也没太搞清楚取值0、1或者取值32、33的区别,只要记住取值33,对应每行24个点,后面还有用)
    • n1, n2 : 表示图片的宽度,为什么有两个?其实只是分成了高位和低位两部分,因为每部分只有8bit,最大表示256。所以 n1 = 图片宽度 % 256,n2 = 图片宽度 / 256。假设图片宽300,那么n1=1,n2=44
    • d1 d2 ... dk 这部分就是转换成字节流的图像数据了

    3.2 图片分辨率调整

    如果分辨率过大,超过了打印机可打印的最大宽度,那么超出的部分将无法打印。我试验的这台最大宽度是 384 个像素点,超过这个宽度的数据无法被打印出来。所以在开始打印之前,我们需要调整图片的分辨率。代码如下:

        /**
         * 对图片进行压缩(去除透明度)
         *
         * @param bitmapOrg
         */
        public static Bitmap compressPic(Bitmap bitmap) {
            // 获取这个图片的宽和高
            int width = bitmap.getWidth();
            int height = bitmap.getHeight();
            // 指定调整后的宽度和高度
            int newWidth = 240;
            int newHeight = 240;
            Bitmap targetBmp = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
            Canvas targetCanvas = new Canvas(targetBmp);
            targetCanvas.drawColor(0xffffffff);
            targetCanvas.drawBitmap(bitmap, new Rect(0, 0, width, height), new Rect(0, 0, newWidth, newHeight), null);
            return targetBmp;
        }
    

    3.2 图片黑白化处理

    因为能够打印的图像只有黑白两色,所以需要先做黑白化的处理。这一部分其实又细分为彩色图片->灰度图片,灰度图片->黑白图片两步。直接上代码:

        /**
         * 灰度图片黑白化,黑色是1,白色是0
         *
         * @param x   横坐标
         * @param y   纵坐标
         * @param bit 位图
         * @return
         */
        public static byte px2Byte(int x, int y, Bitmap bit) {
            if (x < bit.getWidth() && y < bit.getHeight()) {
                byte b;
                int pixel = bit.getPixel(x, y);
                int red = (pixel & 0x00ff0000) >> 16; // 取高两位
                int green = (pixel & 0x0000ff00) >> 8; // 取中两位
                int blue = pixel & 0x000000ff; // 取低两位
                int gray = RGB2Gray(red, green, blue);
                if (gray < 128) {
                    b = 1;
                } else {
                    b = 0;
                }
                return b;
            }
            return 0;
        }
    
        /**
         * 图片灰度的转化
         */
        private static int RGB2Gray(int r, int g, int b) {
            int gray = (int) (0.29900 * r + 0.58700 * g + 0.11400 * b);  //灰度转化公式
            return gray;
        }
    

    其中的灰度化转换公式是一个广为流传的公式,具体原理不明。我们直接看灰度转化为黑白的函数 px2Byte(int x, int y, Bitmap bit)。对于一个 Bitmap 中的任意一个坐标点,取出其 RGB 三色信息后做灰度化处理,然后对于灰度小于128的,用黑色表示,灰度大于128的,用白色表示。

    3.3 逐行打印图片

    其实打印图片和打印文本是一样的,也是一行一行的打印。直接上代码吧,注释已经尽量详细了。

        /*************************************************************************
         * 假设一个240*240的图片,分辨率设为24, 共分10行打印
         * 每一行,是一个 240*24 的点阵, 每一列有24个点,存储在3个byte里面。
         * 每个byte存储8个像素点信息。因为只有黑白两色,所以对应为1的位是黑色,对应为0的位是白色
         **************************************************************************/
        /**
         * 把一张Bitmap图片转化为打印机可以打印的字节流
         *
         * @param bmp
         * @return
         */
        public static byte[] draw2PxPoint(Bitmap bmp) {
            //用来存储转换后的 bitmap 数据。为什么要再加1000,这是为了应对当图片高度无法      
            //整除24时的情况。比如bitmap 分辨率为 240 * 250,占用 7500 byte,
            //但是实际上要存储11行数据,每一行需要 24 * 240 / 8 =720byte 的空间。再加上一些指令存储的开销,
            //所以多申请 1000byte 的空间是稳妥的,不然运行时会抛出数组访问越界的异常。
            int size = bmp.getWidth() * bmp.getHeight() / 8 + 1000;
            byte[] data = new byte[size];
            int k = 0;
            //设置行距为0的指令
            data[k++] = 0x1B;
            data[k++] = 0x33;
            data[k++] = 0x00;
            // 逐行打印
            for (int j = 0; j < bmp.getHeight() / 24f; j++) {
                //打印图片的指令
                data[k++] = 0x1B;
                data[k++] = 0x2A;
                data[k++] = 33; 
                data[k++] = (byte) (bmp.getWidth() % 256); //nL
                data[k++] = (byte) (bmp.getWidth() / 256); //nH
                //对于每一行,逐列打印
                for (int i = 0; i < bmp.getWidth(); i++) {
                    //每一列24个像素点,分为3个字节存储
                    for (int m = 0; m < 3; m++) {
                        //每个字节表示8个像素点,0表示白色,1表示黑色
                        for (int n = 0; n < 8; n++) {
                            byte b = px2Byte(i, j * 24 + m * 8 + n, bmp);
                            data[k] += data[k] + b;
                        }
                        k++;
                    }
                }
                data[k++] = 10;//换行
            }
            return data;
        }
    

    4. 总结

    用两篇介绍了一个比较冷门的应用,纯粹是因为自己花了很多时间去搞懂原理,所以希望记录下来。尤其是图片打印部分,废了好多纸啊哈哈哈,一个字节操作错误,打印出来就是一堆乱码。感觉和 java 的 .class 文件很像,每一个指令占用多少位,每一位表示什么都是严格规定好的,不能超出也不能缺少。
    最后希望能帮到需要的人吧,感觉网上这部分资料还是比较少的。

    相关文章

      网友评论

      • Alren不吃盒饭:楼主,我下载直接打印 中文部分为啥都是乱码
      • 39c19244ea8d:楼主,请问为什么只能打印240高度的图片?更长的后面都是乱码。
        39c19244ea8d:usb 传数据出问题。
      • 苦农海码:博主,请问下图片只能打印黑白两色的吗,想打印出灰度图有没有思路和建议,我看到你的算法中是把图片黑白化了(灰度小于128的直接判断为1否则为0),使用这个方法打印出的只能是黑白图
      • 营小光的盒子:想知道断开重连这种功能怎么写?
      • 453ef8cce18e:您好,我想请问一下,二维码打印出来的了,但是扫描没反应,您知道这是什么原因吗?
      • Juny_1089:正好用到,感谢
      • 0c37836b4524:大神,传不过去数据啊!我打印的,数据还没过去就打印了!请问怎么解决?
      • 请叫我甄汇编:打印机打印一般卡住了
      • 圈子里的圈子:楼主你好,查看了网上大量的打印二维码的,发现都是针对24点的打印机,按照代码操作,发现打印出来的全是黑色,如果是像我这种8点的打印机怎么写。
      • 7be867e2fa28:博主,请教一个不明问题,就是打印的时候,打印机上蓝牙图标闪烁了,说明有数据传输了,但是显示"等待数据...",一会就没有了,纸也不动,这是什么情况,怎么走纸的?
      • be757fadea9d:博主您好,在demo里试验了一下zxing打印二维码并成功了。
        想保持打印机得连接状态 自己新建一个activity然后点击按钮打印
        请问一下大概思路?
      • 1298e8fc73f7:最近项目要用到指令集,大神可以分享一下demo吗? zwf520xyq@163.com.感激
      • b6b8a8c72db7:博主,我问下,同时连接多个小票机,状态都是连接上的,但是打印的时候支配的小票机只有一个?这个问题该如何解决?
      • violentV:博主你好,有个问题:打印机的模式是RECEIPT模式时demo是正常可以打印的,但是在LABEL模式下就打印不了,查看了POS/ESC打印控制命令,没找到切换模式的,我怀疑demo的默认模式是RECEIPT,和LABEL模式不匹配,所以当打印机是LABEL模式时打印不了,博主有遇到这个问题吗?盼复!
      • 编程之夜:博主,没有用Android,用的是jdk没有Bitmap怎么办啊?
      • 小孩程序员:博主,我打印图片高度不一样,如何设置自适应???
      • 叨码:如何通过指令获取打印机的状态 比如缺纸 脱机之类的?
      • 实心棒棒糖: public final static int WIDTH_PIXEL = 384;博主,你这个纸质宽度是怎么计算的
      • tmyzh:请问一下有没有设置过字符集,打印一些其他国家的特殊字符,我现在打印出来会乱码
        tmyzh:@VitaminChen 这个我看到了 因为我是用的GprintSDk这个完成的功能,他的连接包括流的创建都是封装的,我之前有处理另一种打印机也是同样的问题,后来通过设置的字符集成功的,现在这个同样的指令却不行
        VitaminChen:@tmyzh 有呀,看第一部分,构造输出流的时候,设置了GBK,如果是其他国家,应该要用UTF-8吧
      • 拾取灬回忆:如果打印实线怎么做呢?目前是...可是想改成实线用————————这个打印出来有断点
        VitaminChen:@拾取灬回忆 属于特殊字符,在输入法的特殊符号里可以找到 ─
        拾取灬回忆:@VitaminChen 怎么打出制表符? 我用减号加shift打出来的————这种有断点
        VitaminChen:@拾取灬回忆 使用制表符试试 ────────
      • fb8298800ea8:楼主有demo吗?能不能发一份呀396628224@qq.com谢谢啦~
      • 恰恰在理工:博主你的 bmp.getHight()/24f 这句是否写错了啊,在某些宽高的情况下依然会报下标越界,是否应该是24而不是24f啊?
      • fbb542c7a9ed:Demo 在打印 图片 部分有点问题,58mm 热敏打印机出来 的是 乱码;为什么 缩小 图片尺寸 ,依然 不能解决?
      • 落马洲:请问有没有iOS的版本?
      • 虚通磨忍:大牛,左边打印logo图片,右边打印一维码,这个怎么调
        虚通磨忍:@VitaminChen 目前是这么处理的,某些打印机打印出来有白色空隙
        VitaminChen:@刚刚好1016 这个我暂时想不到好的办法。因为这种打印机都是一行一行的打印的。所以除非先把logo和一维码处理成一张图片
      • 简简单单一家子:博主的com.github.promeg:tinypinyin:1.0.0在哪儿
      • 爆发小宇:你好,我最近有需求,是用热敏打印机,打印快递单,是不是用打印协议,可以适配大部分打印机啊
        VitaminChen:@爆发小宇 这个不敢说绝对兼容,有一些可能还是会有问题,最好测试几个主流品牌
      • 那就走吧玉二:博主写的很仔细,之前看了博主的分析讲解自己写了一个demo,在公司配的打印机上打印出来的一堆乱码,今天下载了博主的demo,运行了一下还是一堆乱码,不排除是打印机机型的问题,问下博主的打印机型号是什么,感激不尽
        VitaminChen:@e10640117 芯烨 XP-58
      • 风舞杰克:博主,请问下你的小票案例中,一行得文字是怎么设置居中,靠右和靠左的,我试了,在一行里面调用setAlignPosition(0)靠左打印“时间”,然后设置setAlignPosition(2)靠右打印具体的日期无效,但是换一行后在打印就会靠右了???
        VitaminChen:@风舞杰克 一行里面只能设置一种对齐模式,不能一部分靠左,一部分靠右的。你所说的这种格式需要自己计算中间的空格来实现,我的demo里有对应的方法 printTwoColumn
      • 837b3ac9c2a9:博主,请问蓝牙打印机,怎么设置纸张的高度?
        VitaminChen:@追逐_61af 这是一卷的那种打印纸,所以是没有高度这一说的,内容有多长就打多长
      • f51d703b93ad:楼主能给个demo吗 ,383256860@qq.com
      • 叨码:大神 有二维码和条码打印的案例么?
      • 小鬼鬼9:源码有吗?1062082817@qq.com 跪求
      • 85e257fa2919:很赞的一篇文章,目前正在做相关项目,大神有木有源码发来参考下,不甚感激:330727483@qq.com
      • cs_double:Socket socket = new Socket("192.168.52.10", 9100);

        OutputStream os = socket.getOutputStream();

        byte [] buffer = draw2PxPoint(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));

        os.write(buffer);
        os.flush();

        os.close();
        socket.close();
        楼主,我打印是这样写的,方法复制的你上面的,为啥打印出来的是乱码,没有把图片打印出来啊?
        cs_double:@VitaminChen 我刚也试了,打印sdcard下一张png图片,也是72*72的分辨率的,结果也是乱码
        cs_double:@VitaminChen 72*72
        VitaminChen:图片分辨率是多少?
      • cs_double:楼主,这个只能打印黑白的吗?如果是彩色打印机,能把图片颜色打出来吗?
        圈子里的圈子:楼主你好,查看了网上大量的打印二维码的,发现都是针对24点的打印机,按照代码操作,发现打印出来的全是黑色,如果是像我这种8点的打印机怎么写。
        cs_double:@VitaminChen 好的吧,那如果要手机连接打印机,打印彩色图片,这能做到吗?
        VitaminChen:@cs_double 只有黑白的,热敏打印机也没有彩色吧?
      • superoidlau:博主,按行打印图片时,最后返回的是byte[],最后是不是需要将byte[] data转化成String,然后传入到writer.write(Arrays.toString(data)),对否??
        VitaminChen:@ForeCheng 可以的,实质上还是一个输出流,OutputStreamWriter 只是一层包装而已
        superoidlau:@VitaminChen嗯, 那我既要打印文字,又要打印图片,这样有两个输出管道OutputStream不可行吧?
        VitaminChen:@ForeCheng 看文章第1节,从 BluetoothSocket 得到的OutputStream 可以直接打印字节流的,包装后的 OutputStreamWriter 是打印字符串的,所以打印图片的时候用 OutputStream.write(byte[] bytes)
      • 跑步与开车:谢谢楼主
      • 20e38ca6066f:这个指令集是否不同打印机厂商的会有不同
        superoidlau:@20e38ca6066f 不会吧,每个打印机厂商支持这个ESC/POS指令集的话,那都是一样的吧,也就是通用的,按理说不存在不一致的情况。
      • Somnus123:楼主,为什么我用你的代码打印图片就是一大堆乱码,急
      • 低吟浅唱1990:楼主有没有一个完整的demo 看看。 我要把它转成OC的
        温柔的温神:我也需要,兄弟
      • 低吟浅唱1990:有没有对应的OC的代码
      • xueximj:非常感谢,网上好多都是360*360,一直没看懂怎么改,这个正是需要的,谢谢啦
      • fd612b7c8ed9:这种打印方法每行之间会有白色间隙吧?我是遇到了如此问题,暂时还没有解决。但目前估计需要page mode或者下载到打印机缓存再一起打印就可以消除白色间隙。你打出来会有么?
        VitaminChen:@熊猫蛋 是的,每行打印前都要设置一遍
        fd612b7c8ed9: @VitaminChen 是加在每次发送esc *这个行数据包前面,每发一次都加么还是,总得打印前发一次就行?我试过几次。你说的是路通模式下吧?不是page模式吧?我这就回家试试!
        VitaminChen:@熊猫蛋 开始也遇到这个问题的,但是有这几行代码就解决了
        //设置行距为0的指令
        data[k++] = 0x1B;
        data[k++] = 0x33;
        data[k++] = 0x00;
      • cgrass:博主的 打印图片开的1000字节空间有时候真的是害死人
        多1000的 空间 导致 的问题是,图片和 文字连着打印,有些打印机会识别不出剩下的字节数,导致图片打印完,卡顿几秒再进行打印文字,
        :解决方案就是:精确的算出需要的size,可能是我的能力不够算了半天都是错的,那索性不进行计算用一个函数就搞定了,函数如下:
        long a=System.currentTimeMillis();
        //开辟精确的 byte[k]
        byte[] data1=new byte[k];
        //然后复制到新的数组中
        System.arraycopy(data,0,data1,0,k);
        long b=System.currentTimeMillis();
        //计算是不是耗时间,几乎不耗时间
        System.out.println("结束字节:"+k+"---"+data.length+"耗时:"+(b-a));
        return data1;
        出去就不会出现卡顿的现象
        cgrass:@Sanisy k就是上面作者的 k 初始是0,一点都没变,我只不过 是对后面的结果进行处理
        Sanisy:@半仙也迷茫了 请问一下K是什么?是bitmap.getByteCount()么?
        VitaminChen:@青半仙 谢谢反馈,可能我测的两台打印机正好没什么问题,所以没有深入研究这个问题。
      • yunhen:不错,确实能打印图片,不过我想做到断开后从新打印,不是断点续传,但是遇到问题 ,第一条打印不出自己设置的title,只能打印出图片,up知道是什么原因吗,怎么解决呢
        Sanisy:@yunhen 请问一下能分享一下代码么,还有打印机怎么设置蓝牙打印啊
      • 大lan猫:不错,解决了我的问题
      • 7504bbfe3fd2:写的十分不错。

      本文标题:Android 蓝牙连接 ESC/POS 热敏打印机打印(ESC

      本文链接:https://www.haomeiwen.com/subject/ssynrttx.html