热敏小票/标签打印机,使用ESC/POS指令打印,常用指令封装,适用于GBK编码
const PER_MM=8,
fontSize=12,
gbk=require('./gbk'),
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={
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],
D_H: [0x1b, 0x21, 0x10],
D_W: [0x1b, 0x21, 0x20],
D_W_H: [0x1b, 0x21, 0x30],
UNDERL_OFF: [0x1b, 0x2d, 0x00],
UNDERL_ON: [0x1b, 0x2d, 0x01],
UNDERL_2: [0x1b, 0x2d, 0x02],
BOLD_OFF: [0x1b, 0x45, 0x00],
BOLD_ON: [0x1b, 0x45, 0x01],
ITALIC_OFF: [0x1b, 0x35],
ITALIC_ON: [0x1b, 0x34],
FONT_A: [0x1b, 0x4d, 0x00],
FONT_B: [0x1b, 0x4d, 0x01],
FONT_C: [0x1b, 0x4d, 0x02],
},
LINE_SPACING:{
LS_DEFAULT:[0x1b,0x32],
LS_SET(size){return [0x1b,0x33,size]}
},
CUT: {
FULL: [0x1d, 0x56, 0x00],
PART: [0x1d, 0x56, 0x01],
FULL_TO: [0x1d, 0x56, 0x40],
A_TO: [0x1d, 0x56, 0x41],
B_TO: [0x1d, 0x56, 0x42],
},
BARCODE: {
TXT_OFF: [0x1d, 0x48, 0x00],
TXT_ABV: [0x1d, 0x48, 0x01],
TXT_BLW: [0x1d, 0x48, 0x02],
TXT_BTH: [0x1d, 0x48, 0x03],
FONT_A: [0x1d, 0x66, 0x00],
FONT_B: [0x1d, 0x66, 0x01],
HEIGHT(h){return [0x1d,0x68,h]},
WIDTH(w){return [0x1d,0x77,w]},
HEIGHT_DEFAULT: [0x1d, 0x68, 0x64],
WIDTH_DEFAULT: [0x1d, 0x77, 0x01],
UPC_A: [0x1d, 0x6b, 0x00],
UPC_E: [0x1d, 0x6b, 0x01],
EAN13: [0x1d, 0x6b, 0x02],
EAN8: [0x1d, 0x6b, 0x03],
CODE39: [0x1d, 0x6b, 0x04],
I25: [0x1d, 0x6b, 0x05],
CODEBAR: [0x1d, 0x6b, 0x06],
CODE93: [0x1d, 0x6b, 0x07],
CODE128: [0x1d, 0x6b, 0x08],
CODE11: [0x1d, 0x6b, 0x09],
MSI: [0x1d, 0x6b, 0x0a],
},
QRCODE:{
SIZE(size){return [0x1D,0x28,0x6b,0x03,0x00,0x31,0x43,size]},
CORRECT_L:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x30],
CORRECT_M:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x31],
CORRECT_Q:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x32],
CORRECT_H:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x33],
},
HARDWARE: {
INIT: [0x1b, 0x40],
HW_SELECT: [0x1b, 0x3d, 0x01],
HW_RESET: [0x1b, 0x3f, 0x0a, 0x00],
},
CASH_DRAWER: {
CD_KICK_2: [0x1b, 0x70, 0x00],
CD_KICK_5: [0x1b, 0x70, 0x01],
},
MARGINS: {
BOTTOM: [0x1b, 0x4f],
LEFT: [0x1b, 0x6c],
RIGHT: [0x1b, 0x51],
},
IMAGE_FORMAT: {
S_RASTER_N: [0x1d, 0x76, 0x30, 0x00],
S_RASTER_2W: [0x1d, 0x76, 0x30, 0x01],
S_RASTER_2H: [0x1d, 0x76, 0x30, 0x02],
S_RASTER_Q: [0x1d, 0x76, 0x30, 0x03],
},
BITMAP_FORMAT: {BITMAP_S8:[0x1b,0x2a,0x00],BITMAP_D8:[0x1b,0x2a,0x01],BITMAP_S24:[0x1b,0x2a,0x20],BITMAP_D24:[0x1b,0x2a,0x21]},
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
SPDevs
pixels
fontVolume
constructor(devs,width=48){
this.SPDevs=devs
this.setWidth(width)
}
setWidth(width){
this.width=parseInt(width)
this.pixels=this.width*PER_MM;
}
print(data){
if(this.SPDevs.length===0) return;
let ESCPOS=this.buildESCPOS(data);
this.SPDevs.forEach(dev=>{
dev.port.write(ESCPOS)
});
}
buildESCPOS(list){
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=>{
if(i.align&&ESC_POS.ALIGN[i.align]) data.push(...ESC_POS.ALIGN[i.align]);
if(i.color&&ESC_POS.COLOR[i.color]) data.push(...ESC_POS.COLOR[i.color]);
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){
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;
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));
}
}else if(typeof i.line=='number') data.push(...(new Array(i.line).fill(10)));
else if(i.beep){
}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)
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);
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);
$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);
$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,解决打印机中文乱码问题》
|