热敏小票/标签打印机,使用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,解决打印机中文乱码问题》