iOS-PDF文件生成与导出

979 阅读4分钟

最近项目需求,需要将用户写的本地内容写成生成PDF,然后导出功能.经过多方资料的查阅.了解了一些相关知识,特此记录一下开发中遇到的问题和解决思路.

用户写的内容包含:图片(最多12张),内容(分页),标题(固定一行)

一:生成PDF的思路.

第一种是自己画,但是布局,文字支持很繁琐.然后该用本人采用的是第二种方法.第二种网上说爆内存,但是后面也解决啦.

第二种:利用View布局,然后将View生成PDF.直观方便.(目前我使用也是这个方法).

  • 1.第一步就是固定好pdf的尺寸,和页眉页脚的高度,和各个控件之间的高度间隔:
CGFloat A4Width = 595.f; // PDF页面的宽

CGFloat A4Height = 842.f; // PDF页面的高

CGFloat topSpace = 40.f; // 页眉和页脚的高度

CGFloat bottomSpace = 50.f; // 页眉和页脚的高度 // 下边距需要留出来一定间距,不然会很挤
CGFloat leftRightSpace = 20.f; // 左右间距的宽度

CGFloat contentHeight = A4Height-topSpace-bottomSpace; // 除去页眉页脚之后的内容高度

CGFloat contentWidth = A4Width-leftRightSpace * 2; // 内容宽度

NSInteger imagesNumberInLine = 3; // 每行显示图片的个数

CGFloat imageInsertSpace = 20.f; // 左右间距的宽度

CGFloat contentWidthHeightForImage = (A4Width - leftRightSpace *2 - imageInsertSpace*2)/imagesNumberInLine; // 图片宽度和高度

CGFloat targetSpace = 10.f; // 每个词条View的间距

  • 2.然后就是根据每篇文章内容生成view,图片的九宫格布局,文本的换行,和pdf的分页.
  • pdf页码
allTargetHeigh:总高度 子控件高度+控件之见的间隔 (不包含页眉页脚)
contentHeight:出去页眉页脚之后的高度

NSInteger allPageCount = allTargetHeigh / contentHeight > 0 ? (allTargetHeigh / contentHeight + 1) : allTargetHeigh / contentHeight;
    1. 文本分页,开发过程中最头疼就是这个文本写到下一页的问题.一种方法时直接计算,根据pdf宽度和字号大小,计算出每行大概多少字,然后计算多少行,然后对内容进行截取.但是文本里面有自定义表情,和emoji表情,而且截取操作,容易发生奔溃,就果断舍弃.后来查资料发现可以通过lable指定顶宽度获取当前行数,还可以获取每行的文本.这就有解决了这个头疼的问题. 获取文本行数(ps:浏览器编辑器总是莫名的加星号-_-||)
- (NSArray *)getLinesArrayOfStringWidth:(CGFloat)width {

    NSString *text = [**self** text];

    UIFont *font = [**self** font];

    **if** (text == **nil**) {

        **return** **nil**;

    }

    CTFontRef myFont = CTFontCreateWithName(( CFStringRef)([font fontName]), [font pointSize], **NULL**);

    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];

    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];

    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

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

    [attStr addAttribute:(NSString *)kCTFontAttributeName

                   value:( **__bridge**  **id**)myFont

                   range:NSMakeRange(0, attStr.length)];

    CFRelease(myFont);

    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(( CFAttributedStringRef)attStr);

    CGMutablePathRef path = CGPathCreateMutable();

    CGPathAddRect(path, **NULL**, CGRectMake(0,0,width,100000));

    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, **NULL**);

    NSArray *lines = ( NSArray *)CTFrameGetLines(frame);

    NSMutableArray *linesArray = [[NSMutableArray alloc]init];

    **for** (**id** line **in** lines) {

        CTLineRef lineRef = ( **__bridge**  CTLineRef )line;

        CFRange lineRange = CTLineGetStringRange(lineRef);

        NSRange range = NSMakeRange(lineRange.location, lineRange.length);

        NSString *lineString = [text substringWithRange:range];

        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr,

                                       lineRange,

                                       kCTKernAttributeName,

                                       (CFTypeRef)([NSNumber numberWithFloat:0.0]));

        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attStr,

                                       lineRange,

                                       kCTKernAttributeName,

                                       (CFTypeRef)([NSNumber numberWithInt:0.0]));

        [linesArray addObject:lineString];

    }

    CGPathRelease(path);

    CFRelease(frame);

    CFRelease(frameSetter);

    **return** (NSArray *)linesArray;

}

然后就直接把每行文本都用一个Label 来显示,这样直接布局lable即可,没有显示的label 显示到下一页上去.完美解决了文本分页的问题.

  • 4.就是最后一步生成PDF.点击开始,内存直接爆掉,文件数量2篇.生成->导出->完美.当内容增加到100篇的时候开始内存暴增,而且导出的PDf很大.这..........
    开始发现不对劲.排查问题有两点:
    1).for循环产生的大量label ,view,image没有及时释放.
    2).所有产生的要生成PDF的每一页的View都在数组里面放着,只有PDF生成代码结束才释放.
    这不爆内存真是见鬼了-_-||.
    第一次解决:
    1>.撤掉数组,每次布局好一页View,马上生成pdf放到本地文件夹,然后把当前View 释放掉. (所有页面生成结束,统一合成PDF,然后删除临时文件)
    2>.添加autoreleasepool,保证临时变量使用后及时释放.
    3>.压缩图片.
for (NSString *str in lines) {
                    @autoreleasepool {
                        CGFloat height = 22;
                        UILabel *targetV = [UILabel new];
                        targetV.numberOfLines = 0;
                        targetV.text = str;
                        targetV.textColor = UIColor.blackColor;
                        targetV.font = [UIFont systemFontOfSize:18];
                        targetV.lineBreakMode = NSLineBreakByWordWrapping;
                        [targetArr addObject:contontLayer];
                        [targetHeightArr addObject:@(height + targetSpace)];
                        allTargetHeigh = allTargetHeigh + height + targetSpace;
                }

但是发现内容增加到500篇时,还是爆掉. 第二次解决:把所有控件换成Calayer.这次内存直接没超过50M.还是很可以的,而且pdf文件导出也只有不到10M.
总体上已经可以接受啦.

PDF生成代码

- (**void**)createPDFWithViewArr:(CALayer*)viewArr PDFName:(NSString *)pdfName progress:(**nullable** **void**(^)(NSString *progress))PDFCreateProgressBlock {

//    if (viewArr.count == 0 || pdfName.length == 0) return;

    // 文档信息 可设置为nil

    

    **self**.pdfData = [NSMutableData data];

    **@autoreleasepool** {

        CFMutableDictionaryRef myDictionary = CFDictionaryCreateMutable(**nil**, 0,

                                                 &kCFTypeDictionaryKeyCallBacks,

                                                 &kCFTypeDictionaryValueCallBacks);

        CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("PDF Content Title"));

        CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("PDF Author"));

\


        // 设置PDF文件每页的尺寸

        CGRect pageRect =  CGRectMake(0, 0, A4Width, A4Height);

        // PDF绘制尺寸,设置为CGRectZero则使用默认值612*912

        UIGraphicsBeginPDFContextToData(**self**.pdfData, pageRect, **nil**);

        // PDF文档是分页的,开启一页文档开始绘制

        UIGraphicsBeginPDFPage();

        // 获取当前的上下文

        CGContextRef pdfContext = UIGraphicsGetCurrentContext();

        [viewArr renderInContext:pdfContext];

        UIGraphicsEndPDFContext();

        viewArr = **nil**;

    }

    NSArray *documentDirectories        = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, **YES**);

    NSString *documentDirectory         = [documentDirectories objectAtIndex:0];

    NSString *documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:pdfName];

    [**self**.pdfData writeToFile:documentDirectoryFilename atomically:**YES**];

    NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);

    [**self**.viewArr removeAllObjects];

    [**self**.pathArr addObject:documentDirectoryFilename];
}

好啦,今天先到这吧.如有问题欢迎指出.