Android 藍牙連接 ESC/POS 熱敏打印機打印(ESC/POS指令篇)

上一篇 主要介紹了如何通過藍牙連接到打印機。這一篇,我們就介紹如何向打印機發送打印指令,來打印字符和圖片。

=====================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 文件很像,每一個指令占用多少位,每一位表示什么都是嚴格規定好的,不能超出也不能缺少。
最后希望能幫到需要的人吧,感覺網上這部分資料還是比較少的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容