XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

蓝牙打印小票排版&&图片打印优化

96 
走着走着就会敲代码了 
 0.1 2019.02.19 14:41* 字数 1473 阅读 222评论 0

去年七八月份做的一个小功能,过了有点久,现在抽个时间整理下以备后用。本文主要是针对蓝牙打印小票做的处理以及优化打印,连接蓝牙那些就不说,这边就讲一些针对文字打印排版,图片打印排版以及优化打印速度的处理。
蓝牙打印小票,分两种打印方式:文字打印、图片打印,当然图文混合的也是有的哈,图文混合就不多说了自己结合处理就行。

文字打印

先上调用部分的代码,至于HLPrinter,可以直接通过这个Github上获取得到,这边就不在赘述了。

- (NSData *)getBluetoothPrintWith {
    HLPrinter *printer = [[HLPrinter alloc] init];
    // 账单名称
    [printer appendText:_printEnvelop.billName alignment:HLTextAlignmentCenter fontSize:HLFontSizeTitleSmalle];
    // 收费金额
    [printer appendText:_printEnvelop.amount alignment:HLTextAlignmentCenter fontSize:HLFontSizeTitleMiddle];
    // 缴费状态
    [printer appendText:_printEnvelop.payStatus alignment:HLTextAlignmentCenter fontSize:HLFontSizeTitleSmalle];
    // 收费项目
    for (ZTGResponseTuitionBillFeaturesEnvelop *envelop in _printEnvelop.features) {
        if (envelop.value.length >= 15) {
            [printer appendTitle:envelop.name value:envelop.value valueOffset:145];
        } else [printer appendTitle:envelop.name value:envelop.value];
    }
    // 空行方便用户查看
    [printer appendTitle:@" " value:@" " fontSize:HLFontSizeTitleBig];

    NSData *mainData = [printer getFinalData];
    return mainData;
}

文字打印的数据还是非常快的,但是一些打印机的问题会导致国际通用编码kCFStringEncodingGB_18030_2000打印出来的一些比较不常见的文字出现乱码,本文总结的时候小票已经丢光了没法上图请见谅。只能考虑通过数据请求获取到数据然后排版,排版完成后转为图片...

图片打印

文字转为图片

百度了半天只有一些参考的,最后只能自己造轮子上代码,SZYPrinterManager.h文件

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class ZTGResponseTuitionBillFeaturesEnvelop;

typedef NS_ENUM(NSInteger, SZYPrinterFontSize) {
    SZYFontSizeTitleSmalle = 12,
    SZYFontSizeTitleMiddle = 24,
    SZYFontSizeTitleBig = 40
};

typedef NS_ENUM(NSInteger, SZYPrinterLogoAligment) {
    SZYPrinterLogoAligmentTop,
    SZYPrinterLogoAligmentCenter,
    SZYPrinterLogoAligmentBottom,
    SZYPrinterLogoAligmentTopLeft,
    SZYPrinterLogoAligmentTopRight,
};


@interface SZYPrinterManager : NSObject

- (UIImage *)convertToImage;

- (UIImage *)convertToImageAppendLogo:(UIImage *)logoImage agliment:(SZYPrinterLogoAligment)agliment;

- (void)appendTitle:(NSString *)title fontSize:(SZYPrinterFontSize)fontSize;

- (void)appendTitle:(NSString *)title textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize;

- (void)appendTitle:(NSString *)title value:(NSString *)value;

- (void)appendTitleWithArray:(NSArray<ZTGResponseTuitionBillFeaturesEnvelop *> *)array textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize;

- (void)appendTitle:(NSString *)title value:(NSString *)value textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize;

- (void)appendSpacingLine;

@end

这边的ZTGResponseTuitionBillFeaturesEnvelop,是服务器返回的数据模型,键值对哈,如下所示:

@interface ZTGResponseTuitionBillFeaturesEnvelop : SZYJsonModel
/**
 金额(分)
 */
@property (nonatomic, copy) NSString *name;
/**
 收费项目
 */
@property (nonatomic, copy) NSString *value;

@end

下面的是实现SZYPrinterManager.m文件

#import "SZYPrinterManager.h"
#import "ZTGResponseTuitionBillPrintEnvelop.h"

static const CGFloat kPrinterMaxWidth      = 384.0f;
static const CGFloat kPrinterTextSpecing   = 30.0f;// 一个汉字可以当20pt来计算,一个字母可以当10pt计算
static const CGFloat kPrinterLineSpacing   = 5.0f;
static const CGFloat kPrinterLogoWidth     = 125.0f;
static const CGFloat kPrinterLogoMargin    = 0.0f;
static const CGFloat kPrinterPrintMargin   = 80.0f;
static const CGFloat kPrinterSpacingMargin = 15.0f;
static const CGFloat kPrinterCompress      = 0.1f;

@interface SZYPrinterManager ()
@property (nonatomic, strong) NSMutableAttributedString *attributed;
@property (nonatomic, assign) CGFloat textHeight;

@end


@implementation SZYPrinterManager

- (instancetype)init {
    self = [super init];
    if (self) {
        _attributed = [[NSMutableAttributedString alloc] init];
        _textHeight = 0;
    }
    return self;
}

- (void)appendTitle:(NSString *)title fontSize:(SZYPrinterFontSize)fontSize {
    [self appendTitle:title value:nil textAlignment:NSTextAlignmentLeft fontSize:fontSize];
}


- (void)appendTitle:(NSString *)title value:(NSString *)value {
    [self appendTitle:title value:value textAlignment:NSTextAlignmentLeft fontSize:SZYFontSizeTitleMiddle];
}

- (void)appendTitleWithArray:(NSArray<ZTGResponseTuitionBillFeaturesEnvelop *> *)array textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize {
    [array enumerateObjectsUsingBlock:^(ZTGResponseTuitionBillFeaturesEnvelop * envelop, NSUInteger idx, BOOL * _Nonnull stop) {
        [self appendTitle:envelop.name value:envelop.value textAlignment:textAlignment fontSize:fontSize];
    }];
}

- (void)appendTitle:(NSString *)title textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize {
    if (![title isNotBlank]) {
        return;
    }
    // 段落
    NSMutableAttributedString *attribute = [[NSMutableAttributedString alloc] initWithString:title];
    [attribute addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:fontSize] range:NSMakeRange(0, title.length)];

    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
    paragraphStyle.alignment = textAlignment;
    [paragraphStyle setLineSpacing:kPrinterLineSpacing];

    [attribute addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, title.length)];

    [_attributed appendAttributedString:attribute];
    // 高度
    _textHeight += [self HeightForString:attribute fontSize:fontSize];
    // 换行
    [_attributed appendAttributedString:[self newLine]];
    // 换行高度
    _textHeight += kPrinterLineSpacing;
}

- (void)appendTitle:(NSString *)title value:(NSString *)value textAlignment:(NSTextAlignment)textAlignment fontSize:(SZYPrinterFontSize)fontSize {
    if (![title isNotBlank] && ![value isNotBlank]) {
        return;
    }
    title = [title isNotBlank] ? title : @" ";
    value = [value isNotBlank] ? value : @"";

    NSString *string = [title stringByAppendingString:value];
    // 段落
    NSMutableAttributedString *attribute = [[NSMutableAttributedString alloc] initWithString:string];
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
    paragraphStyle.alignment = textAlignment;
    [paragraphStyle setLineSpacing:kPrinterLineSpacing];
    [attribute addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, string.length)];
    // 获取真实间距
    CGFloat spacing = [self getSpacingWithTitle:title content:value fontSize:fontSize];
    [attribute addAttribute:NSKernAttributeName value:@(spacing) range:NSMakeRange(title.length - 1, 1)];
    [attribute addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:fontSize] range:NSMakeRange(0, string.length)];
    [_attributed appendAttributedString:attribute];
    // 高度
    _textHeight += [self HeightForString:attribute fontSize:fontSize];
    // 换行
    [_attributed appendAttributedString:[self newLine]];
    // 换行高度
    _textHeight += kPrinterLineSpacing;
}

- (UIImage *)convertToImage {
    UIImage *image = [self convertToImageAppendLogo:nil agliment:0];

    return image;
}

- (UIImage *)convertToImageAppendLogo:(UIImage *)logoImage agliment:(SZYPrinterLogoAligment)agliment {
    // 比原本高度多出 80提高用户体验
    CGSize size = CGSizeMake(kPrinterMaxWidth, _textHeight + kPrinterPrintMargin);

    UIGraphicsBeginImageContext(size);
    CGContextRef context = UIGraphicsGetCurrentContext();

    [[UIColor whiteColor] set];

    CGRect rect = CGRectMake(0, 0, size.width + 1, size.height + 1);

    CGContextFillRect(context, rect);
    [_attributed drawInRect:rect];
    if (!logoImage) {
        UIImage *resultImg = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        NSData *tempData = UIImageJPEGRepresentation(resultImg, kPrinterCompress);
        resultImg = [[UIImage alloc] initWithData:tempData];
        return resultImg;
    }
    // 水印
    CGFloat logoHeight = (logoImage.size.height / logoImage.size.width) * kPrinterLogoWidth;

    CGFloat logoLeft = (kPrinterMaxWidth - kPrinterLogoWidth) / 2;
    CGFloat logoTop = 0;
    switch (agliment) {
        case SZYPrinterLogoAligmentTop:
            logoTop = 0;
            break;
        case SZYPrinterLogoAligmentCenter:
            logoTop = (_textHeight - logoHeight) / 2;
            break;
        case SZYPrinterLogoAligmentBottom:
            logoTop = _textHeight - logoHeight;
            break;
        case SZYPrinterLogoAligmentTopLeft:
        {
            logoLeft = kPrinterLogoMargin;
            logoTop = kPrinterLogoMargin;
        }
            break;
        case SZYPrinterLogoAligmentTopRight:
        {
            logoLeft = kPrinterMaxWidth - kPrinterLogoWidth - kPrinterLogoMargin;
            logoTop = kPrinterLogoMargin;
        }
            break;
        default:
            break;
    }
    [logoImage drawInRect:CGRectMake(logoLeft, logoTop, kPrinterLogoWidth, logoHeight)];
    UIImage *resultImg = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();//关闭上下文
    // 压缩图片
    NSData *tempData = UIImageJPEGRepresentation(resultImg, kPrinterCompress);
    resultImg = [[UIImage alloc] initWithData:tempData];

    return resultImg;
}

- (void)appendSpacingLine {
    NSString *string = @"   \n";
    NSAttributedString *newline = [[NSAttributedString alloc] initWithString:string];
    NSMutableAttributedString *attribute = [[NSMutableAttributedString alloc] initWithString:string];
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByCharWrapping;
    [paragraphStyle setLineSpacing:kPrinterSpacingMargin];
    [attribute addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, string.length)];

    [_attributed appendAttributedString:attribute];
    // 高度
    _textHeight += kPrinterSpacingMargin * 2;
}

#pragma mark private
// 换行
- (NSAttributedString *)newLine {
    NSAttributedString *newline = [[NSAttributedString alloc] initWithString:@"\n"];
    return newline;
}

// 获取间距
- (CGFloat)getSpacingWithTitle:(NSString *)title content:(NSString *)content fontSize:(SZYPrinterFontSize)fontSize {
    CGFloat spacing = 0;
    NSString *string = [title stringByAppendingString:content];
    UIFont *font = [UIFont systemFontOfSize:fontSize];
    CGFloat allWidth = [self widthForString:string fontSize:font] + kPrinterTextSpecing;
    if (allWidth > kPrinterMaxWidth) {
        // 超过部分,默认间距
        spacing = kPrinterTextSpecing;
    } else {
        // 未超过部分,用间距补足
        CGFloat titleWidth = [self widthForString:title fontSize:font];
        CGFloat contentWidth = [self widthForString:content fontSize:font];
        spacing = kPrinterMaxWidth - titleWidth - contentWidth;
    }
    return spacing;
}

// 获取文本宽度
- (CGFloat)widthForString:(NSString *)string fontSize:(UIFont *)font {
    NSDictionary * detailDic = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName,nil];
    CGFloat width = [string boundingRectWithSize:CGSizeMake(MAXFLOAT, 12) options:NSStringDrawingUsesLineFragmentOrigin attributes:detailDic context:nil].size.width;
    return width;
}

// 获取文本高度
- (CGFloat)HeightForString:(NSAttributedString *)attribute fontSize:(SZYPrinterFontSize)fontSize {
    NSDictionary * detailDic = [NSDictionary dictionaryWithObjectsAndKeys:[UIFont systemFontOfSize:fontSize], NSFontAttributeName,nil];
    CGFloat height = [attribute.string boundingRectWithSize:CGSizeMake(kPrinterMaxWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:detailDic context:nil].size.height;
    return height;
}

@end

自己造的轮子基本就以上代码,这边简单的说明一下,字号、logo或者说水印所在的位置这些都可以根据自己所需要扩展枚举,以及调整相对应的属性哈。
.m中常量
kPrinterMaxWidth:打印纸的宽度市面上的基本是384,这个数值是跟打印机的厂商确认过的,超过这个值的后果就是乱码...
kPrinterTextSpecing:打印小票对应的是:

收费项            金额
午餐费            10元
课本费            100元

类似以上的格式,这个值对应的是中间最小间距
kPrinterLineSpacing:文字排版时的行间距
kPrinterLogoWidth:水印(比如盖章)的宽度
kPrinterLogoMargin:水印边距,这边设置是默认距离顶部、左边或者右边有着相同的边距
kPrinterPrintMargin:文字的边距
kPrinterSpacingMargin:文字底部的边距,主要是用于优化用户体验,不会出现打印的小票文字刚刚好就在撕纸的位置,会有空白行,所以这边是设置空白行的高度
kPrinterCompress:生成图片后的压缩系数

轮子造好了,下面就是调用

- (void)getImage {
    SZYPrinterManager *printer = [[SZYPrinterManager alloc] init];
    // 空行
    [printer appendSpacingLine];
    // 账单名称
    [printer appendTitle:_printEnvelop.billName textAlignment:NSTextAlignmentCenter fontSize:SZYFontSizeTitleMiddle];
    // 收费金额
    [printer appendTitle:_printEnvelop.amount textAlignment:NSTextAlignmentCenter fontSize:SZYFontSizeTitleBig];
    // 空行
    [printer appendSpacingLine];
    // 收费项目
    [printer appendTitleWithArray:_printEnvelop.features textAlignment:NSTextAlignmentRight fontSize:SZYFontSizeTitleMiddle];
    // 添加水印
    UIImage *tempImage = [image blackAndWhiteImage];
_printImage = [printer convertToImageAppendLogo:tempImage agliment:SZYPrinterLogoAligmentTopRight];
  // 不需要水印
  // _printImage = [printer convertToImage];
}

后面就是根据HLPrinter里面提供的类UIImage+Bitmap.h将图片转为蓝牙打印的NSData.

优化

通过上面或者别的方式打印的时候,能明显的感觉到打印速度跟龟速差别不大,然后再更安卓的对比一下,硬伤啊,开始着手优化打印速度。

打印速度慢的原因

通常我们使用蓝牙打印是CBCharacteristicWriteWithResponse用这种方式给蓝牙传输数据,需要等响应保证数据传输的安全,但是这种方式是每隔80毫秒传输一次的,然而数据又是那么大,每次传输还有上限,比如我这边每次传输的长度上限设置的是128,因为需要适配不同的机型,没法设置得比较大的值,如果能设置大的值就可以减少传输次数也能提高速度,现实不允许。

优化

那就考虑是不是可以使用CBCharacteristicWriteWithoutResponse,这种不安全的方式来传输,自己来设置时间间隔不需要等待那么长的时间.
传输如下,代码如下:

int maxLengthForOnce = 128;
CBCharacteristicWriteType writeType = kBillPrinterWithoutResponse ? CBCharacteristicWriteWithoutResponse : CBCharacteristicWriteWithResponse;

if (printerData.length <= maxLengthForOnce) {
    [self writeToCBPeripheral:peripheral value:printerData forCharacteristic:characteristic type:writeType];

} else {

    NSInteger index = 0;

    for (index = 0; index < printerData.length - maxLengthForOnce; index += maxLengthForOnce) {
        if (kBillPrinterWithoutResponse) {
          [NSThread sleepForTimeInterval:0.03];
        }
        NSData *subData = [printerData subdataWithRange:NSMakeRange(index, maxLengthForOnce)];
        [self writeToCBPeripheral:peripheral value:subData forCharacteristic:characteristic type:writeType];

    }
    NSData *leftData = [printerData subdataWithRange:NSMakeRange(index, printerData.length - index)];
    if (leftData) {
        [self writeToCBPeripheral:peripheral value:leftData forCharacteristic:characteristic type:writeType];
    }
}
});

测试一下,速度有所提升,能愉快的打印了。

进一步优化

经过多次测试之后,发现传输后如果数据量太大的话,后续可能会出现乱码,前面打印正常后面出现乱码?what????一样的时间间隔一样的数据量,怎么会乱码?
思考:是不是传输给蓝牙后,蓝牙数据会堆叠????
那是不是可以考虑前期间隔短一点,让蓝牙打印机数据差不多堆叠满了,然后间隔传输时间长点让数据给打印出来,消化掉???来一波骚操作,多次测试之后发现是可行的。
代码如下:

int maxLengthForOnce = 128;
CBCharacteristicWriteType writeType = kBillPrinterWithoutResponse ? CBCharacteristicWriteWithoutResponse : CBCharacteristicWriteWithResponse;

if (printerData.length <= maxLengthForOnce) {
    [self writeToCBPeripheral:peripheral value:printerData forCharacteristic:characteristic type:writeType];

} else {

    NSInteger index = 0;

    for (index = 0; index < printerData.length - maxLengthForOnce; index += maxLengthForOnce) {
        if (kBillPrinterWithoutResponse) {
            if (index * maxLengthForOnce > 30000) {
                [NSThread sleepForTimeInterval:0.04];
            } else if (index * maxLengthForOnce > 60000) {
                [NSThread sleepForTimeInterval:0.05];
            } else {
                [NSThread sleepForTimeInterval:0.03];
            }
        }
        NSData *subData = [printerData subdataWithRange:NSMakeRange(index, maxLengthForOnce)];
        [self writeToCBPeripheral:peripheral value:subData forCharacteristic:characteristic type:writeType];

    }
    NSData *leftData = [printerData subdataWithRange:NSMakeRange(index, printerData.length - index)];
    if (leftData) {
        [self writeToCBPeripheral:peripheral value:leftData forCharacteristic:characteristic type:writeType];
    }
}
});

不同的打印机存在差别,具体的临界值需要自行测试哈,上面的是我这边打印机的测试值,如果有错误可自行测试调整哈。

结语

这边就不提供demo了哈,耍个流氓😄。好几个月的代码了,一直没时间整理总结,现在抽个时间讲了下大概思路。这边就讲排版的处理、优化打印速度,如果觉得没必要刻意参考下别人的帖子哈。 至于蓝牙参数的设置、以及打印图片的时候是使用点阵图还是光栅图,可以自行测试哈,当然笔者测试过了,光栅图打印出来跟你设置的参数可以让图片比较小一些,点阵图主要是提现这清晰上,公司的打印机对光栅图打印出来的小票表示太模糊了只能使用点阵图处理,这边就简单说一下。

附上,参考的链接:
iOS开发之蓝牙/Socket链接小票打印机(一)


iOS开发之蓝牙/Socket链接小票打印机(一)

96 
赵梦楠 
 1.3 2017.12.19 15:27* 字数 2639 阅读 2883评论 24
 

前言

之前公司有个面向商户的项目,需要连接商户打印机打印小票的功能。于是对这方面进行了学习研究,最后“顺利”的完成了项目需求。这里主要是对项目中用到的知识点进行一些总结。这篇文章主要包含的相关知识有:Socket、CoreBluetooth、网口小票打印机、蓝牙小票打印机、ESC/POS打印命令集、图片打印等。

概述

整个打印流程大致分可以为三个步骤,①链接打印机;②编辑排版打印内容;③发送数据给打印机;

①和③根据不同的打印机类型,我们要采取不同的链接方式。网口打印机通过Socket进行链接(需在同一局域网下),蓝牙打印机自然是通过蓝牙进行链接。
②编辑排版打印内容,需要通过ESC/POS打印命令集来做,以下会进行相关的介绍。

其实,步骤②编辑排版打印内容,放到后台做是更加合理的,这样Android和iOS两端就避免了都要写编辑排版的代码,而且也能避免排版上的差异。我们公司也是这样做的,所以步骤②就可以改为从后台获取要打印的数据。

ESC/POS打印命令集

简介

WPSON StandardCode for Printer 是EPSON公司自己制定的针式打印机的标准化指令集,现在已成为针式打印机控制语言事实上的工业标准。ESC/POS打印命令集是ESC打印控制命令的简化版本,现在大多数票据打印都采用ESC/POS指令集。其显著特征是:其中很大一部分指令都是以ESC控制符开始的一串代码。

打印机的型号种类有很多,不同的厂家也对其产品做了相应的定制。但是,ESC/POS指令集基本都会支持。关于指令的详细内容,网上有很多文档,另外每个品牌的官网,也会有对应的打印机指令文档提供下载。我们可以下载下来研究。这里简单介绍几种常用的指令:

指令介绍

说明:一般打印机接受指令都支持三种格式:ASCII、十进制、十六进制。

1、初始化打印机

ASCII十进制十六进制
ESC @ 27 64 1B 40

说明:清除打印缓冲区,删除用户自定义字符,打印模式被设为上电时的默认值模式。

代码:

//重置打印机
- (void)resetPrinter {
   Byte reset[] = {0x1B,0x40};
   [self.printData appendBytes:reset length:1];
}

注意:经笔者测试发现,使用初始化命令,之后的一条命令可能会失效,目前未找到原因,可能是打印机问题。另外,由于此命令会清除缓冲区,频繁调用可能会导致数据丢失,因此尽量少用此命令。

2、打印并换行

ASCII十进制十六进制
LF 10 0A

说明:将打印缓冲区中的数据打印出来,并且按照当前行间距,把打印纸向前推进一行。

代码:

//打印机并换行
- (void)printAndNewline {
   Byte next[] = {0x0A};
   [self.printData appendBytes:next length:1];
}

3、打印并走n点行纸

ASCII十进制十六进制
LESC J n 27 74 n 1B 4A n

说明:打印缓冲区数据并走纸[ n × 纵向或横向移动单位] 英寸。0 ≤n ≤ 255。最大走纸距离是956 mm(不同品牌打印机数值不同)。如果超出这个距离,取最大距离。

代码:

//打印缓冲区数据,并往前走纸n点行
- (void)printAndGoNPointLine:(int)n {
   Byte line[] = {0x1B, 0x4A, n};
   [self.printData appendBytes:line length:3];
}

注意:这里是走纸点行数,要与字符行数区分

4、打印并走n行纸

ASCII十进制十六进制
ESC d n 27 100 n 1B 64 n

说明:打印缓冲区里的数据并向前走纸n行(字符行)。0 ≤n ≤ 255。该命令不影响由ESC 2 或ESC 3设置的行间距。 最大走纸距离为1016 mm,当所设的值大于1016 mm时,取最大值。

代码:

//打印缓冲区数据,并往前走纸n行
- (void)printAndGoNLine:(int)n {
   Byte line[] = {0x1B, 0x64, n};
   [self.printData appendBytes:line length:3];
}

注意:这里是走纸字符行数,要与点行数区分。只有设置了行距后,此命令才有效。使用此命令前,要先使用换行指令,否则设置无效

5、设置默认行距(1/6英寸)

ASCII十进制十六进制
ESC 2 27 50 1B 32

说明:选择约3.75mm 行间距。约34个点。

代码:

//设置默认行间距
- (void)printDefaultLineSpace {
   Byte defaultLineSpace[] = {0x1B,0x32};
   [self.printData appendBytes:defaultLineSpace length:2];
}

6、设置行间距为n 点行

ASCII十进制十六进制
ESC 3 n 27 51 n 1B 33 n

说明:设置行间距为[ n × 纵向或横向移动单位] 英寸。

代码:

//设置行间距为n个点
- (void)printLineSpace:(int)n {
   Byte lineSpace[] = {0x1B,0x33,n};
   [self.printData appendBytes:lineSpace length:3];
}

注意:使用此命令前,要先使用换行指令,否则设置无效

7、设置字符右间距

ASCII十进制十六进制
ESC SP n 27 32 n 1B 20 n

说明:设置字符的右间距为[n×横向移动单位或纵向移动单位]英寸。0 ≤ n ≤255。最大右间距是31.91毫米(255/203 英寸)。任何超过这个值的设置都自动转换为最大右间距。

代码:

//字符右间距
- (void)printCharRightSpace:(int)n {
   Byte line[] = {0x1B, 0x20, n};
   [self.printData appendBytes:line length:3];
}

注意:此命令对汉字无效

8、设置输出对齐方式

ASCII十进制十六进制
ESC a n 27 97 n 1B 61 n

说明:n = 0或48 为左对齐;n = 1或49为中间对齐;n = 2或50位右对齐。

代码:

//设置对齐方式
- (void)setAlignment:(MNAlignmentType)alignmentType {
   Byte align[] = {0x1B,0x61,alignmentType};
   [self.printData appendBytes:align length:3];
}

9、设置字体大小

ASCII十进制十六进制
GS ! n 29 33 n 1D 21 n

 

 

说明:用0 到2 位选择字符高度,4 到7 位选择字符宽度。
 

代码:

//字符放大倍数
typedef enum: UInt8 {
   MNPrintFont_1 = 0x00,
   MNPrintFont_2 = 0x11,
   MNPrintFont_3 = 0x22,
   MNPrintFont_4 = 0x33,
   MNPrintFont_5 = 0x44,
   MNPrintFont_6 = 0x55,
   MNPrintFont_7 = 0x66,
   MNPrintFont_8 = 0x77,
} MNPrintFont;


//设置字体大小
-(void)printCharSize:(MNPrintFont)printFont {
   Byte font[] = {0x1D,0x21,printFont};
   [self.printData appendBytes:font length:3];
};

10、选择切纸模式和切纸

ASCII十进制十六进制
GS V m 29 86 m 1D 56 m
ASCII十进制十六进制
GS V m n 29 86 m n 1D 56 m n

说明:

  • m=0,1,49 ;0 表示全切, 1表示半切,当打印机没有半切功能时,全切;
  • m=66, 0≤n≤255 ;当m=66时, n表示走纸到(约18mm)+[n*0.125mm] 位置切纸

代码:

//切纸模式
typedef enum :UInt8 {
   MNCutPaperModelFull = 0x00,
   MNCutPaperModelHalf = 0x01,
   MNCutPaperModelFeedPaperHalf = 0x66
}MNCutPaperModel;

- (void)printCutPaper:(MNCutPaperModel)model Num:(int)n {
   if (model == MNCutPaperModelFull) {
       Byte cut[] = {0x1D, 0x56, model, n};
       [self.printData appendBytes:cut length:4];
   } else {
       Byte cut[] = {0x1D, 0x56, model};
       [self.printData appendBytes:cut length:3];
   }
}

注意这条指令需要打印机支持切纸

10、产生钱箱脉冲(开钱箱)

ASCII十进制十六进制
ESC p m t1 t2 27 112 m t1 t2 1B 70 m t1 t2

说明:

  • m = 0, 1, 48, 49 ; 0 ≤ t1 ≤ 255, 0 ≤ t2 ≤ 255 ;
  • 输出由t1和t2设定的钱箱开启脉冲到由m指定的引脚:
M十进制
0, 48 钱箱插座的引脚 2
1, 49 钱箱插座的引脚 5

代码:

//产生钱箱控制脉冲,一般一个打印机连接一个钱箱,这里默认写死了
-(void)printOpenCashDrawer {
    Byte open[] = {0x1B, 0x70, 0x00, 0x80, 0xFF};
    [self.printData appendBytes:open length:5];
}

注意这条指令需要打印机连接钱箱

打印内容

说明:这里只要将打印内容通过kCFStringEncodingGB_18030_2000编码,然后发送给打印机

代码:

- (void)printWithContent:(NSString *)content {
    NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
    NSData *data = [content dataUsingEncoding:enc];
    [self.printData appendData:data];
}

以上只是部分指令,可根据需求,参考指令集文档再做相应的添加。这里要提一下的是,小票打印多用于订单详情类信息,为了是排版更美观,这里用的比较多的是制表符/t来使每一列对齐,可以直接这样使用[self.printManager printWithContent:@"\t"];

图片打印

关于图片打印,这里介绍两种打印指令:

 
位图模式.png
 
光栅位图.png

原理

因为小票打印机多为热敏打印机,或针式打印机,且颜色只有黑白两色。因此,要打印图片,首先要获取图片的像素数据,然后将图片进行黑白二值化处理,之后拼接打印数据,黑色为打印的点,白色为不打印的点。如此逐行打印图片数据。

调整分辨率

-(UIImage*)scaleImageWithImage:(UIImage*)image width:(NSInteger)width height:(NSInteger)height
{
    CGSize size;
    size.width = width;
    size.height = height;
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0, 0, width, height)];
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}

获取像素数据

-(CGContextRef)CreateARGBBitmapContextWithCGImageRef:(CGImageRef)inImage
{
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;
    
    // Get image width, height. We'll use the entire image.
    size_t pixelsWide = CGImageGetWidth(inImage);
    size_t pixelsHigh = CGImageGetHeight(inImage);
    
    // Declare the number of bytes per row. Each pixel in the bitmap in this
    // example is represented by 4 bytes; 8 bits each of red, green, blue, and
    // alpha.
    bitmapBytesPerRow   = (int)(pixelsWide * 4);
    bitmapByteCount     = (int)(bitmapBytesPerRow * pixelsHigh);
    
    // Use the generic RGB color space.
    colorSpace =CGColorSpaceCreateDeviceRGB();
    if (colorSpace == NULL)
    {
        return NULL;
    }
    
    // Allocate memory for image data. This is the destination in memory
    // where any drawing to the bitmap context will be rendered.
    bitmapData = malloc( bitmapByteCount );
    if (bitmapData == NULL)
    {
        CGColorSpaceRelease( colorSpace );
        return NULL;
    }
    
    // Create the bitmap context. We want pre-multiplied ARGB, 8-bits
    // per component. Regardless of what the source image format is
    // (CMYK, Grayscale, and so on) it will be converted over to the format
    // specified here by CGBitmapContextCreate.
    context = CGBitmapContextCreate (bitmapData,
                                     pixelsWide,
                                     pixelsHigh,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedFirst);
    if (context == NULL)
    {
        free (bitmapData);
    }
    
    // Make sure and release colorspace before returning
    CGColorSpaceRelease( colorSpace );
    
    return context;
}

位图模式指令

  • 根据像素信息将图片进行黑白化处理,并逐行拼接打印信息
typedef enum {
    ALPHA = 0,
    BLUE = 1,
    GREEN = 2,
    RED = 3
} PIXELS;

- (NSData *) imageToThermalData:(UIImage*)image {
    CGImageRef imageRef = image.CGImage;
    // Create a bitmap context to draw the uiimage into
    CGContextRef context = [self CreateARGBBitmapContextWithCGImageRef:imageRef];
    if(!context) {
        return NULL;
    }
    
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    CGRect rect = CGRectMake(0, 0, width, height);
    
    // Draw image into the context to get the raw image data
    CGContextDrawImage(context, rect, imageRef);
    
    // Get a pointer to the data
    uint32_t *bitmapData = (uint32_t *)CGBitmapContextGetData(context);
    
    if(bitmapData) {
        
        uint8_t *m_imageData = (uint8_t *) malloc(width * height/8 + 8*height/8);
        memset(m_imageData, 0, width * height/8 + 8*height/8);
        int result_index = 0;
        
        for(int y = 0; (y + 24) < height;) {
            m_imageData[result_index++] = 27;
            m_imageData[result_index++] = 51;
            m_imageData[result_index++] = 0;
            
            m_imageData[result_index++] = 27;
            m_imageData[result_index++] = 42;
            m_imageData[result_index++] = 33;
            
            m_imageData[result_index++] = width%256;
            m_imageData[result_index++] = width/256;
            for(int x = 0; x < width; x++) {
                int value = 0;
                for (int temp_y = 0 ; temp_y < 8; ++temp_y)
                {
                    uint8_t *rgbaPixel = (uint8_t *) &bitmapData[(y+temp_y) * width + x];
                    uint32_t gray = 0.3 * rgbaPixel[RED] + 0.59 * rgbaPixel[GREEN] + 0.11 * rgbaPixel[BLUE];
                    
                    if (gray < 127)
                    {
                        value += 1<<(7-temp_y)&255;
                    }
                }
                m_imageData[result_index++] = value;
                
                value = 0;
                for (int temp_y = 8 ; temp_y < 16; ++temp_y)
                {
                    uint8_t *rgbaPixel = (uint8_t *) &bitmapData[(y+temp_y) * width + x];
                    uint32_t gray = 0.3 * rgbaPixel[RED] + 0.59 * rgbaPixel[GREEN] + 0.11 * rgbaPixel[BLUE];
                    
                    if (gray < 127)
                    {
                        value += 1<<(7-temp_y%8)&255;
                    }
                    
                }
                m_imageData[result_index++] = value;
                
                value = 0;
                for (int temp_y = 16 ; temp_y < 24; ++temp_y)
                {
                    uint8_t *rgbaPixel = (uint8_t *) &bitmapData[(y+temp_y) * width + x];
                    uint32_t gray = 0.3 * rgbaPixel[RED] + 0.59 * rgbaPixel[GREEN] + 0.11 * rgbaPixel[BLUE];
                    
                    if (gray < 127)
                    {
                        value += 1<<(7-temp_y%8)&255;
                    }
                    
                }
                m_imageData[result_index++] = value;
            }
            m_imageData[result_index++] = 13;
            m_imageData[result_index++] = 10;
            y += 24;
        }
        NSMutableData *data = [[NSMutableData alloc] initWithCapacity:0];
        [data appendBytes:m_imageData length:result_index];
        free(bitmapData);
        return data;
        
    } else {
        NSLog(@"Error getting bitmap pixel data\n");
    }
    
    CGContextRelease(context);
    
    return nil ;
}

光栅位图指令

#pragma mark ********************另一种打印图片的方式****************************
typedef struct ARGBPixel {
    
    u_int8_t             red;
    u_int8_t             green;
    u_int8_t             blue;
    u_int8_t             alpha;
    
} ARGBPixel ;

#pragma mark 获取打印图片数据
-(NSData *)getDataForPrintWith:(UIImage *)image{
    
    CGImageRef cgImage = [image CGImage];
    
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    
    
    NSData* bitmapData = [self getBitmapImageDataWith:cgImage];
    
    const char * bytes = bitmapData.bytes;
    
    NSMutableData * data = [[NSMutableData alloc] init];
    
    //横向点数计算需要除以8
    NSInteger w8 = width / 8;
    //如果有余数,点数+1
    NSInteger remain8 = width % 8;
    if (remain8 > 0) {
        w8 = w8 + 1;
    }
    /**
     根据公式计算出 打印指令需要的参数
     指令:十六进制码 1D 76 30 m xL xH yL yH d1...dk
     m为模式,如果是58毫秒打印机,m=1即可
     xL 为宽度/256的余数,由于横向点数计算为像素数/8,因此需要 xL = width/(8*256)
     xH 为宽度/256的整数
     yL 为高度/256的余数
     yH 为高度/256的整数
     **/
    NSInteger xL = w8 % 256;
    NSInteger xH = width / (88 * 256);
    NSInteger yL = height % 256;
    NSInteger yH = height / 256;
    
    Byte cmd[] = {0x1d,0x76,0x30,0,xL,xH,yL,yH};
    
    
    [data appendBytes:cmd length:8];
    
    for (int h = 0; h < height; h++) {
        for (int w = 0; w < w8; w++) {
            u_int8_t n = 0;
            for (int i=0; i<8; i++) {
                int x = i + w * 8;
                u_int8_t ch;
                if (x < width) {
                    int pindex = h * (int)width + x;
                    ch = bytes[pindex];
                }
                else{
                    ch = 0x00;
                }
                n = n << 1;
                n = n | ch;
            }
            [data appendBytes:&n length:1];
        }
    }
    return data;
}
#pragma mark 获取图片点阵图数据
-(NSData *)getBitmapImageDataWith:(CGImageRef)cgImage{
    
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    
    NSInteger psize = sizeof(ARGBPixel);
    
    ARGBPixel * pixels = malloc(width * height * psize);
    
    NSMutableData* data = [[NSMutableData alloc] init];
    
    [self ManipulateImagePixelDataWithCGImageRef:cgImage imageData:pixels];
    
    for (int h = 0; h < height; h++) {
        for (int w = 0; w < width; w++) {
            
            int pIndex = (w + (h * (u_int32_t)width));
            ARGBPixel pixel = pixels[pIndex];
            
            if ((0.3*pixel.red + 0.59*pixel.green + 0.11*pixel.blue) <= 127) {
                //打印黑
                u_int8_t ch = 0x01;
                [data appendBytes:&ch length:1];
            }
            else{
                //打印白
                u_int8_t ch = 0x00;
                [data appendBytes:&ch length:1];
            }
        }
    }
    
    return data;
}

// 获取像素信息
-(void)ManipulateImagePixelDataWithCGImageRef:(CGImageRef)inImage imageData:(void*)oimageData
{
    // Create the bitmap context
    CGContextRef cgctx = [self CreateARGBBitmapContextWithCGImageRef:inImage];
    if (cgctx == NULL)
    {
        // error creating context
        return;
    }
    
    // Get image width, height. We'll use the entire image.
    size_t w = CGImageGetWidth(inImage);
    size_t h = CGImageGetHeight(inImage);
    CGRect rect = {{0,0},{w,h}};
    
    // Draw the image to the bitmap context. Once we draw, the memory
    // allocated for the context for rendering will then contain the
    // raw image data in the specified color space.
    CGContextDrawImage(cgctx, rect, inImage);
    
    // Now we can get a pointer to the image data associated with the bitmap
    // context.
    void *data = CGBitmapContextGetData(cgctx);
    if (data != NULL)
    {
        CGContextRelease(cgctx);
        memcpy(oimageData, data, w * h * sizeof(u_int8_t) * 4);
        free(data);
        return;
    }
    
    // When finished, release the context
    CGContextRelease(cgctx);
    // Free image data memory for the context
    if (data)
    {
        free(data);
    }
    
    return;
}

拼接图片数据,准备发给打印机

//打印图片
- (void)printWithImage:(UIImage *)image width:(float)width height:(float)height {
    UIImage * printImage = [self scaleImageWithImage:image width:width height:height];
    NSData *data = [self imageToThermalData:printImage];
    [self.printData appendData:data];
}

提高图片打印速度

由于打印图片是根据像素点来逐行打印,因此数据量会远高于普通文字,这就造成了打印图片的速度回比文字慢,尤其是蓝牙打印机。解决方法可以从两个方面入手,1、增加每次发送的数据量(主要针对蓝牙打印机);2、减少图片的数据量。

增加每次发送的数据量(主要针对蓝牙打印机);

关于这一点,在下一篇讲到蓝牙时也会说到,由于蓝牙硬件限制,每次给打印机发送的数据量是有限制的,因此要将打印数据拆分,循环发送,代码如下:

- (void)printLongData:(NSData *)printContent{
    NSUInteger cellMin;
    NSUInteger cellLen;
    //数据长度
    NSUInteger strLength = [printContent length];
    if (strLength < 1) {
        return;
    }
    //MAX_CHARACTERISTIC_VALUE_SIZE = 120
    NSUInteger cellCount = (strLength % MAX_CHARACTERISTIC_VALUE_SIZE) ? (strLength/MAX_CHARACTERISTIC_VALUE_SIZE + 1):(strLength/MAX_CHARACTERISTIC_VALUE_SIZE);
    for (int i = 0; i < cellCount; i++) {
        cellMin = i*MAX_CHARACTERISTIC_VALUE_SIZE;
        if (cellMin + MAX_CHARACTERISTIC_VALUE_SIZE > strLength) {
            cellLen = strLength-cellMin;
        }
        else {
            cellLen = MAX_CHARACTERISTIC_VALUE_SIZE;
        }
        NSRange rang = NSMakeRange(cellMin, cellLen);
        //        截取打印数据
        NSData *subData = [printContent subdataWithRange:rang];
        //循环写入数据
        [self.peripheral writeValue:subData forCharacteristic:self.characteristicInfo type:CBCharacteristicWriteWithResponse];
    }
}

这里的MAX_CHARACTERISTIC_VALUE_SIZE是个宏定义,表示每次发送的数据长度,经笔者测试,当MAX_CHARACTERISTIC_VALUE_SIZE = 20时,打印文字是正常速度。但打印图片的速度非常慢,应该在硬件允许的范围内,每次发尽量多的数据。不同品牌型号的打印机,这个参数是不同的,笔者的蓝牙打印机该值最多到140。超出后会出现无法打印问题。最后笔者将该值定为MAX_CHARACTERISTIC_VALUE_SIZE = 120,测试了公司几台打印机都没有问题。

另外iOS9以后增加了方法maximumWriteValueLengthForType:可以获取写入特诊的最大写入数据量,但经笔者测试,对于部分打印机(比如我们公司的)是不准确的,因此,不要太依赖此方法,最好还是自己取一个合适的值。

减少图片的数据量

要减少图片的数据量,我们可以降低分辨率。通过研究指令集笔者发现,光栅位图的倍宽,横向分辨率降低了一倍。倍高,纵向分辨率降低了一倍。因此,笔者尝试选择倍宽、倍高模式,即m=3;此时发现打印出的图片尺寸比图片要大一倍。这样我们只要将图片的宽、高分别除以2。

比如我们要打印宽、高为250的图片。m = 3 时,打印命令改为:

Byte cmd[] = {0x1d,0x76,0x30,3,xL,xH,yL,yH};

调用时:

[self.printManager printWithImage:[UIImage imageNamed:@"1513654780"] width:250/2 height:250/2];

经笔者测试,倍宽、倍高模式打印机图片的速度,和打印文字速度相差无几。但图片的清晰度会有所下降。究竟使用哪种,可自行权衡。

使用举例

示例代码

这里只是简单的讲解举例,代码并没有很好的封装,我们可以根据自己的需求,封装一个适合自己的模板类。

    self.printManager.printData.length = 0;
//    [self.printManager resetPrinter];
    
//    [self.printManager printLineSpace:50];
    [self.printManager printCharSize:MNPrintFont_2];
    [self.printManager setAlignment:MNAlignmentTypeCenter];
    [self.printManager printCharRightSpace:1];
    [self.printManager printWithContent:@"这是标题"];
    
    [self.printManager printAndNewline];
//    [self.printManager printAndGoNLine:1];
    [self.printManager printAndGoNPointLine:60];
    
    [self.printManager setAlignment:MNAlignmentTypeLeft];
    [self.printManager printCharSize:MNPrintFont_1];
    [self.printManager printWithContent:@"商品名称"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"数量"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"价格"];
    
    [self.printManager printAndNewline];
//    [self.printManager printAndGoNLine:1];
    [self.printManager printAndGoNPointLine:34];
    
    [self.printManager printWithContent:@"商品1"];
    [self.printManager printWithContent:@"\t\t"];
    [self.printManager printWithContent:@"2"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"1999"];
    
    [self.printManager printAndNewline];
//    [self.printManager printAndGoNLine:1];
    [self.printManager printAndGoNPointLine:25];
    
    [self.printManager printWithContent:@"商品2"];
    [self.printManager printWithContent:@"\t\t"];
    [self.printManager printWithContent:@"200"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"19"];
    
    [self.printManager printAndNewline];
//    [self.printManager printAndGoNLine:1];
    [self.printManager printAndGoNPointLine:25];
    
    [self.printManager printWithContent:@"商品3"];
    [self.printManager printWithContent:@"\t\t"];
    [self.printManager printWithContent:@"200"];
    [self.printManager printWithContent:@"\t"];
    [self.printManager printWithContent:@"19"];
    
    [self.printManager printAndNewline];
//    [self.printManager printAndGoNLine:2];
    [self.printManager printAndGoNPointLine:25];
    
    [self.printManager setAlignment:MNAlignmentTypeCenter];
    [self.printManager printWithContent:@"-----------------------------"];
    
    [self.printManager printAndNewline];
    //    [self.printManager printAndGoNLine:2];
    [self.printManager printAndGoNPointLine:25];
    
    [self.printManager setAlignment:MNAlignmentTypeRight];
    [self.printManager printWithContent:@"总计:11598元"];
    
    [self.printManager printAndNewline];
    [self.printManager printAndGoNPointLine:100];
    
    [self.printManager setAlignment:MNAlignmentTypeCenter];
    [self.printManager printWithImage:[UIImage imageNamed:@"1513654780"] width:200 height:200];
    
    [self.printManager printAndNewline];
    [self.printManager printAndGoNPointLine:150];
    

效果图

 
小票效果图.jpeg

总结

篇幅所限,这一篇先介绍通过ESC/POS打印命令集,拼接打印指令,排版打印格式。接下来的文章会介绍如何通过蓝牙或Socket将我们编辑的打印数据发送给打印机。

posted on 2019-08-20 10:31  不及格的程序员-八神  阅读(247)  评论(0编辑  收藏  举报