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

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

文字打印

先上调用部分的代码,至于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 
#import 
@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 *)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 *)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链接小票打印机(一)

你可能感兴趣的:(蓝牙打印小票排版&&图片打印优化)