IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> ESC/POS常用打印指令面向对象封装,PHP处理二维码定位,微信小程序蓝牙打印 -> 正文阅读

[PHP知识库]ESC/POS常用打印指令面向对象封装,PHP处理二维码定位,微信小程序蓝牙打印

热敏小票/标签打印机,使用ESC/POS指令打印,常用指令封装,适用于GBK编码

const PER_MM=8,//每毫米像素数
fontSize=12,//每字符像素数
gbk=require('./gbk'),//兼容中文的字符转换库,文末附链接
/*计算字符串长度(1个中文=2个字符)*/
charLen=str=>{
  let width=0;
  for(let i=0;i<str.length;i++){
    width+=gbk.isAscii(str.charCodeAt(i))?1:2;
  }
  return width;
},
//ESC_POS这段是抄来的,略作调整
ESC_POS={
  ALIGN:{
    C: [0x1b, 0x61, 0x01], // 居中
    L: [0x1b, 0x61, 0x00], // 左对齐
    R: [0x1b, 0x61, 0x02], // 右对齐
  },
  BEEP:[0x1b,0x07], // 蜂鸣器
  COLOR:{
    BLACK:[0x1b,0x72,0x00],
    RED:[0x1b,0x72,0x01]
  },
  /*文本格式*/
  TEXT: {
    NORMAL: [0x1b, 0x21, 0x00], // Normal text
    D_H: [0x1b, 0x21, 0x10], // Double height text
    D_W: [0x1b, 0x21, 0x20], // Double width text
    D_W_H: [0x1b, 0x21, 0x30], // Double width & height text
    UNDERL_OFF: [0x1b, 0x2d, 0x00], // Underline font OFF
    UNDERL_ON: [0x1b, 0x2d, 0x01], // Underline font 1-dot ON
    UNDERL_2: [0x1b, 0x2d, 0x02], // Underline font 2-dot ON
    BOLD_OFF: [0x1b, 0x45, 0x00], // Bold font OFF
    BOLD_ON: [0x1b, 0x45, 0x01], // Bold font ON
    ITALIC_OFF: [0x1b, 0x35], // Italic font ON
    ITALIC_ON: [0x1b, 0x34], // Italic font ON
    FONT_A: [0x1b, 0x4d, 0x00], // Font type A
    FONT_B: [0x1b, 0x4d, 0x01], // Font type B
    FONT_C: [0x1b, 0x4d, 0x02], // Font type C
  },
  LINE_SPACING:{
    LS_DEFAULT:[0x1b,0x32], //默认行高,30点
    LS_SET(size){return [0x1b,0x33,size]} //size点行高
  },
  CUT: {
    FULL: [0x1d, 0x56, 0x00], // 全切
    PART: [0x1d, 0x56, 0x01], // 半切
    FULL_TO: [0x1d, 0x56, 0x40], // 走纸到切纸位置+n/144英寸并全切
    A_TO: [0x1d, 0x56, 0x41], // 走纸到切纸位置+n/144英寸并半切
    B_TO: [0x1d, 0x56, 0x42], // 走纸到切纸位置+n/144英寸并半切
  },
  BARCODE: {
    TXT_OFF: [0x1d, 0x48, 0x00], // HRI barcode chars OFF
    TXT_ABV: [0x1d, 0x48, 0x01], // HRI barcode chars above
    TXT_BLW: [0x1d, 0x48, 0x02], // HRI barcode chars below
    TXT_BTH: [0x1d, 0x48, 0x03], // HRI barcode chars both above and below
    FONT_A: [0x1d, 0x66, 0x00], // Font type A for HRI barcode chars
    FONT_B: [0x1d, 0x66, 0x01], // Font type B for HRI barcode chars
    HEIGHT(h){return [0x1d,0x68,h]},// Barcode Height [1-255]
    WIDTH(w){return [0x1d,0x77,w]},// Barcode Width [2-6]
    HEIGHT_DEFAULT: [0x1d, 0x68, 0x64], // Barcode height default:100
    WIDTH_DEFAULT: [0x1d, 0x77, 0x01], // Barcode width default:1
    UPC_A: [0x1d, 0x6b, 0x00], // 0x41 11,12 48-57
    UPC_E: [0x1d, 0x6b, 0x01], // 0x42 11,12 48-57
    EAN13: [0x1d, 0x6b, 0x02], // 0x43 12,13 48-57
    EAN8: [0x1d, 0x6b, 0x03], // 0x44 7,8 48-57
    CODE39: [0x1d, 0x6b, 0x04], // 0x45 变长 32,36,37,43,45-57,65-90
    I25: [0x1d, 0x6b, 0x05], // 0x46 偶数 48-57 (ITF)
    CODEBAR: [0x1d, 0x6b, 0x06], // 0x47 变长 36,43,45-58,65-68 (NW7)
    CODE93: [0x1d, 0x6b, 0x07], // 0x48 变长 0-127
    CODE128: [0x1d, 0x6b, 0x08], // 0x49 变长 0-127
    CODE11: [0x1d, 0x6b, 0x09], // 0x4a 变长 48-57
    MSI: [0x1d, 0x6b, 0x0a], // 0x4b 变长 48-57
  },
  QRCODE:{
    SIZE(size){return [0x1D,0x28,0x6b,0x03,0x00,0x31,0x43,size]},
    CORRECT_L:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x30], // 可覆盖%7,默认
    CORRECT_M:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x31], // 可覆盖%15
    CORRECT_Q:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x32], // 可覆盖%25
    CORRECT_H:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x33], // 可覆盖%30
  },

  /**
   * [HARDWARE Printer hardware]
   * @type {Object}
   */
  HARDWARE: {
    INIT: [0x1b, 0x40], // Clear data in buffer and reset modes
    HW_SELECT: [0x1b, 0x3d, 0x01], // Printer select
    HW_RESET: [0x1b, 0x3f, 0x0a, 0x00], // Reset printer hardware
  },
  /**
   * [CASH_DRAWER Cash Drawer]
   * @type {Object}
   */
  CASH_DRAWER: {
    CD_KICK_2: [0x1b, 0x70, 0x00], // Sends a pulse to pin 2 []
    CD_KICK_5: [0x1b, 0x70, 0x01], // Sends a pulse to pin 5 []
  },
  /**
   * [MARGINS Margins sizes]
   * @type {Object}
   */
  MARGINS: {
    BOTTOM: [0x1b, 0x4f], // Fix bottom size
    LEFT: [0x1b, 0x6c], // Fix left size
    RIGHT: [0x1b, 0x51], // Fix right size
  },
  /**
   * [IMAGE_FORMAT Image format]
   * @type {Object}
   */
  IMAGE_FORMAT: {
    S_RASTER_N: [0x1d, 0x76, 0x30, 0x00], // Set raster image normal size
    S_RASTER_2W: [0x1d, 0x76, 0x30, 0x01], // Set raster image double width
    S_RASTER_2H: [0x1d, 0x76, 0x30, 0x02], // Set raster image double height
    S_RASTER_Q: [0x1d, 0x76, 0x30, 0x03], // Set raster image quadruple
  },
  /**
   * [BITMAP_FORMAT description]
   * @type {Object}
   */
  BITMAP_FORMAT: {BITMAP_S8:[0x1b,0x2a,0x00],BITMAP_D8:[0x1b,0x2a,0x01],BITMAP_S24:[0x1b,0x2a,0x20],BITMAP_D24:[0x1b,0x2a,0x21]},
  /**
   * [GSV0_FORMAT description]
   * @type {Object}
   */
  GSV0_FORMAT: {
    GSV0_NORMAL: [0x1d, 0x76, 0x30, 0x00],
    GSV0_DW: [0x1d, 0x76, 0x30, 0x01],
    GSV0_DH: [0x1d, 0x76, 0x30, 0x02],
    GSV0_DWDH: [0x1d, 0x76, 0x30, 0x03]
  },
};

class Printer {
  width // 发送打印之前必须先调用setWidth方法
  SPDevs
  pixels
  fontVolume
  constructor(devs,width=48){
    this.SPDevs=devs
    this.setWidth(width)
  }
  setWidth(width){
    this.width=parseInt(width) //如果确定传入的是number型则不需要parseInt
    this.pixels=this.width*PER_MM; //打印内容的宽度,用来控制文本换行,width单位mm
  }
  //data应当是一个对象组成的数组,表示从上到下排列的内容块,一个块内格式统一
  print(data){
    if(this.SPDevs.length===0) return;
    let ESCPOS=this.buildESCPOS(data);
    this.SPDevs.forEach(dev=>{
      dev.port.write(ESCPOS)
    });
  }
  
  //下面的应该是私有方法,因技术条件限制暂未实现
  /*构造ESC/POS指令*/
  buildESCPOS(list){
  	//请注意,初始化时设置了打印区域的宽度,这里预留了8mm的出血以容错,因此设置的宽度是打印内容的像素数+8mm的像素数
    let data=Array.from(ESC_POS.HARDWARE.INIT.concat([29,87,(this.pixels+PER_MM*8)%256,parseInt((this.pixels+PER_MM*8)/256),27,74,10]));
    this.fontVolume=parseInt(this.pixels/fontSize);
    list.forEach(i=>{
      // 对齐方式,align为ESC_POS.ALIGN成员键名
      if(i.align&&ESC_POS.ALIGN[i.align]) data.push(...ESC_POS.ALIGN[i.align]);
      // 颜色(如果支持的话),color为ESC_POS.COLOR成员键名
      if(i.color&&ESC_POS.COLOR[i.color]) data.push(...ESC_POS.COLOR[i.color]);
      // text和fill都是打印的字符,text指文本内容,fill指空白区域填充内容
      if(i.text||i.fill){
        //如果修改了字号,则每行字符容量也要修改
        if(i.size&&ESC_POS.TEXT[i.size]){
          data.push(...ESC_POS.TEXT[i.size])
          if(i.size=='D_W'||i.size=='D_W_H'){
            this.fontVolume=parseInt(this.pixels/fontSize/2);
          }else if(i.size=='NORMAL'||i.size=='D_H'){
            this.fontVolume=parseInt(this.pixels/fontSize);
          }
        }
        //需要注意的是,上面的字号及下面的加粗等文本格式设置在一次打印中是长期有效的,除非后面的内容块中修改
        if(i.blod&&ESC_POS.TEXT['BLOD_'+i.blod]) data.push(...ESC_POS.TEXT['FONT_'+i.blod]);
        if(i.font&&ESC_POS.TEXT['FONT_'+i.font]) data.push(...ESC_POS.TEXT['FONT_'+i.font]);
        if(i.underl&&ESC_POS.TEXT['UNDERL_'+i.underl]) data.push(...ESC_POS.TEXT['FONT_'+i.underl]);

        //如果文本不为空,则打印文本,否则打印填充字符,即以填充字符组成的分割带
        if(i.text){
          // 打印文本时,如果有r(right的意思),则以fill(或空格)填充中间,使r内容在同一行的末尾。这个主要用于商品名称与标价分布在首尾的场景。
          if(i.r) i.text+=(new Array(this.fontVolume-(charLen(i.text)+charLen(i.r))%this.fontVolume).fill(i.fill?i.fill:' ').join(''))+i.r;
          //如果没有r但是有fill,则表示以fill在两侧包围文本,当打印纸比打印口宽度窄很多,又想要同时实现居中对齐和侧边对齐两种需求时,可以用空格包围文本使其看起来居中,整体格式依然采取侧边对齐
          else if(i.fill){
            let count=this.fontVolume-charLen(i.text);
            if(count>0) i.text=(new Array(Math.ceil(count/2)).fill(i.fill).join(''))+i.text+(new Array(parseInt(count/2)).fill(i.fill).join(''));
          }
          data.push(...gbk.U2B(i.text,this.fontVolume));
        }else{
          if(this.fontVolume<charLen(i.fill)) i.fill='-';
          let str=new Array(parseInt(this.fontVolume/charLen(i.fill))).fill(i.fill).join('')
          if(charLen(str)<this.fontVolume){
            for(let i=charLen(str);i<this.fontVolume;i++){
              if(i%2==0) str+=' ';
              else str=' '+str;
            }
          }
          data.push(...gbk.U2B(str,this.fontVolume));
        }
      //10是换行符的十进制编码,line则表示换多少行
      }else if(typeof i.line=='number') data.push(...(new Array(i.line).fill(10)));
      else if(i.beep){
        //不知道是不是我的打印机问题,蜂鸣未生效
        // data.push(0x1b,0x28,0x41,0x04,0x00,0x30,0x00,0x09,0x02)
      }else if(typeof i.barcode=='string'){
        //先过滤掉任何条形码都不能支持的内容
        let str=i.barcode.replace(/[^\x00-\x7F]/g,'');
        if(!str) return true;
        data.push(...ESC_POS.BARCODE.HEIGHT(60));
        let codeLen=str.length;
        //然后根据内容判断选用哪种格式。没错,条形码有多种规范,不同规范可以打印的内容不太一样
        if(/[^\x30-\x39]/.test(str)==false){
          if(codeLen==7||codeLen==8) data.push(...ESC_POS.BARCODE.EAN8);
          else if(codeLen==11) data.push(...ESC_POS.BARCODE.UPC_A);
          else if(codeLen==12||codeLen==13) data.push(...ESC_POS.BARCODE.EAN13);
          else data.push(...ESC_POS.BARCODE[codeLen%2==0?'I25':'CODE11']);
        }else if(/[\x00-\x1F\x21-\x23\x26-\x2A\x2C\x3A-\x40\x5B-\x7F]/.test(str)) data.push(...ESC_POS.BARCODE.CODE93);
        else data.push(...ESC_POS.BARCODE[/[\x20\x25\x45-\x5A]/.test(str)?'CODE39':'CODEBAR']);
        data.push(...str.split('').map(c=>c.charCodeAt(0)),0x00);
      }else if(i.qrcode){
        //目前暂未找到给二维码定位的方法,只能在打印区域内调整对齐方式。如果确实想要实现定位,建议尝试后面的光栅位图方式
        let buffer=gbk.U2B(i.qrcode),len=buffer.length+3
        data.push(27,74,10)
        let p=parseInt(i.size)*PER_MM,poi;
        if(p<80){p=80}else if(p>1000){p=1000}
        if(len<29){poi=19}else if(len<54){poi=23}
        else if(len<85){poi=27}else{poi=len<119?31:35}
        data.push(...ESC_POS.QRCODE.SIZE(p>poi?(p/poi).toFixed():1));
        data.push(...ESC_POS.QRCODE.CORRECT_M);
        data.push(29,40,107,len%256,parseInt(len/256),49,80,48,...buffer) //可能有126字节限制
        data.push(29,40,107,3,0,49,81,48)
        data.push(27,74,10)
      }
      //打印光栅位图,后端将图像处理成点阵数据,主要用于二维码定位
      else if(i.raster) data.push(29,118,48,0,i.x%256,parseInt(i.x/256),i.y%256,parseInt(i.y/256),...i.raster,27,74,10);
      /* // 以光栅格式绘图(Function 112),暂时不可用,可能是设备支持的问题
      data.push(29,40,76,4,0,48,1,51,51)
      data.push(29,40,76,17,0,48,112,48,1,1,49,56,0,1,0,255,255,255,255,255,255,255)
      data.push(29,40,76,2,0,48,2)
      
      //页模式,部分指令目前设备不支持
      data.push(27,76) //进入页模式
      data.push(27,36,0,0) // X归零
      data.push(29,36,0,0) // Y归零,设备不支持Y向位移
      data.push(...ESC_POS.FF) //输出缓冲区并回到标准模式 */
      if(i.cut){
        if(ESC_POS.CUT[i.cut]) data.push(...ESC_POS.CUT[i.cut]);
      }
    });
    if(!list[list.length-1].cut) data.push(...ESC_POS.CUT.FULL);
    return data;
  }
}
module.exports=Printer;

如果你是PHP程序员,并且希望实现二维码定位,可以参考我之前发的一篇文章《PHP二维码类库phpqrcode改造面向对象风格》,以及下面这个方法:

public function textWithQR(string $text,string $qr,string $extra='',int $times=1)
    {
        $qr=new QRcode($qr); //这个类见上方链接
        //我用的是76mm打印机,留6mm出血,二维码每个点宽高1mm,points属性是二维码横竖点数,因此70减点数剩下的就是空白区域的宽度,单位mm
        $gdBytes=70-$qr->points;
        $w=$gdBytes*8; //空白区域宽度
        $blank=array_fill(0,$gdBytes,0);
        $h=$qr->points*8; //空白区域高度
        $gd=imagecreate($w,$h);
        imagecolorallocate($gd,255,255,255);
        //这里是用GD库绘图,然后再读取每个像素点并灰度处理,writeOnImg方法是在GD对象上绘制文本,具体参考[《50行带码搞定PHP GD库绘制文本段落》](https://blog.csdn.net/warmbook/article/details/111567238)
        $pos=Image::writeOnImg($gd,$text,9,22,$w,0,intval($h/2)+22,3,30,'./src/font/simhei.ttf');
        $textBytes=intval(ceil($pos[2]/8));
        $textPixel=$textBytes*8;
        $rightBlank=array_fill(0,$gdBytes-$textBytes,0);
        $raster=[];
        for($y=0;$y<$h;$y++){
            if($y%8===0){
                $qrLine=[];
                for($i=0;$i<$qr->points;$i++) $qrLine[]=intval($qr->data[intval($y/8)][$i])*255;
            }
            if($y>=$pos[1]-22&&$y<$pos[1]+$pos[3]-22){
                $bytes=[];
                for($x=0;$x<$textPixel;$x++){
                    $bits[]=imagecolorat($gd,$x,$y)>0;
                    if($x%8===7){
                        $byte=0;
                        foreach($bits AS $k => $v) $byte+=$v*pow(2,7-$k);
                        $bytes[]=$byte;
                        $bits=[];
                    }
                }
                array_push($raster,...$bytes,...$rightBlank);
            }else array_push($raster,...$blank);
            array_push($raster,...$qrLine);
        }
        return [['raster'=>$raster,'x'=>70,'y'=>$h]];
    }

如果碰巧你也在做微信小程序,那么我还封装了一个蓝牙连接的类,并且本文的第一段代码块也有特别的适配,最终使用就像下面这样方便:

const app=getApp(),Printer=require('./path/to/Printer.js')
app.globalData.printer=new Printer(res.data)
app.globalData.printer.print({ESCPOS:[{align:'C',size:25,qrcode:'二维码内容1'},{size:'D_W_H',text:'文本1'},{cut:'FULL'},{align:'C',size:25,qrcode:'二维码内容2'},{size:'D_W_H',text:'文本2'},{cut:'FULL'}]})

链接在这里:《微信小程序蓝牙热敏打印机三件套.zip》

单独的GBK中文转码模块:《gbk.js gb2312编码字符转Uint8Array,解决打印机中文乱码问题》

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 13:46:45  更:2021-08-14 13:50:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/14 2:21:14-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码