iOS 数据导出为PDF(生成PDF文件)

需求:根据App中的数据来生成PDF文件,尽量越小越好,不要爆内存。

找了很多资料,先用第一种方式实现了,然后发现生成的PDF文件过大,又找了新的方案实现,在此记录一下。

两种实现方式说明:
一、自己创建View,按照OC的方式画页面,画完之后将一页页View绘制到PDF文件中

优点:在View中画简单易懂,转成PDF的方式也简单
缺点:由于是将每一页的整个View给当成图片绘制到PDF中,保存的PDF内都是图片,无法修改文字,且!PDF文档非常的大!

二、从头到尾都使用手绘的方式去生成PDF,将各个控件自己画出来

优点:速度很快,生成的PDF文件足够的小,例:测试我的五万条数据,基本都是纯文本的,共计3000多页,需要15s左右,文件大小只有4M,如果使用第一种方式,五千条数据都300M了,差距有点大。
缺点:整个绘制过程比较麻烦,如果是统一样式的列表,还可以用for循环,如果特殊样式太多,全都要自己写。例:一行文本数据显示一行,最后超出的部分省略号表示,这个省略号都要自己写,并且要定义样式与前边的文字相同!
要非常非常注意,在绘制PDF过程中,你创建的对象,都要释放掉。不然几百页的PDF在for循环的过程中会产生非常大的内存占用,点几次生成PDF之后,App直接就因为爆内存崩掉了。
具体如何释放,请看第二种方式中的代码提示

实现过程:
// 首先定义了页面的一些常用数据
static const CGFloat A4Width = 595.f; // PDF页面的宽
static const CGFloat A4Height = 842.f; // PDF页面的高
static const CGFloat topSpace = 40.f; // 页眉和页脚的高度
static const CGFloat bottomSpace = 50.f; // 页眉和页脚的高度 // 下边距需要留出来一定间距,不然会很挤
static const CGFloat leftRightSpace = 20.f; // 左右间距的宽度
static const CGFloat contentHeight = A4Height – topSpace – bottomSpace; // 除去页眉页脚之后的内容高度
static const CGFloat contentWidth = A4Width – leftRightSpace * 2; // 内容宽度
static const CGFloat targetSpace = 10.f; // 每个词条View的间距
static const CGFloat targetHeight = 14.f; // 词条信息每一行的高度
static const CGFloat favoritesHeight = 80.f; // 收藏夹的高度,也是收藏夹图片的高度
第一种实现方式:
/**
 通过在View上画好页面,然后绘制到PDF页面中实现转PDF, 生成的PDF文件因为内部全是图片,文件非常大
 dataInfo:MOJi数据
 pdfName: 保存的PDF名称,需要注意带上.pdf后缀!
 */
+ (void)createPDFViewWithDataInfo:(MOJiPDFDataInfo *)dataInfo PDFName:(NSString *)pdfName {
    
    NSMutableArray *viewArr = [[NSMutableArray alloc] init]; // 存放PDF的页面的数组
    
    // 存放所有词条信息View的数组
    NSMutableArray *targetViewArr = [[NSMutableArray alloc] init];
    NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一个词条的所占高度
    CGFloat allTargetHeight = headerView.height + targetSpace;
    for (int i = 0; i < dataInfo.targetArr.count; i++) {
        MOJiPDFTarget *targetInfo = [dataInfo.targetArr objectAtIndex:i];

        UIView *targetView = [[UIView alloc] initWithFrame:CGRectZero];

        CGFloat height = 100.f; // 这个高度需要自己计算,此处只是示例
        
        targetView.frame = CGRectMake(0, 0, contentWidth, height);
        [targetViewArr addObject:targetView];
        [targetHeightArr addObject:@(height + targetSpace)];
        
        allTargetHeight = allTargetHeight + height + targetSpace;
    }
    
    // 补充说明,其实这里的页码计算方式是不太正确的,你需要根据自己的需求来计算
    // 计算总共需要多少页PDF
    NSInteger allPageCount = ((int)allTargetHeight % (int)contentHeight) > 0 ? (allTargetHeight / contentHeight + 1) : (allTargetHeight / contentHeight);
    
    
    int t = 0; // targetViewArr的计数放这里是为了不在PDF页码循环时重置
    for (int i = 0; i < allPageCount; i++) {
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, A4Width, A4Height)];
        
        // 页眉标题
        
        // 页码
        
        // 页脚
        
        CGFloat topFrom = topSpace;
        
        for (; t < targetViewArr.count; t++) {
            if (t == targetArr.count) break;

            // 剩余距离不够的情况下,翻页
            CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
            if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
            
            UIView *targetView  = [targetViewArr objectAtIndex:t];
            CGFloat targetH     = targetView.height;
            
            targetView.top      = topFrom;
            targetView.left     = leftRightSpace;
            [view addSubview:targetView];
            topFrom = topFrom + targetH + targetSpace;
        }
        
        [viewArr addObject:view];
    }
    
    // 用生成的页面生成PDF
    [MOJiPDF createPDFWithViewArr:[viewArr copy] PDFName:pdfName progress:PDFCreateProgressBlock];
}


+ (void)createPDFWithViewArr:(NSArray <UIView *>*)viewArr PDFName:(NSString *)pdfName progress:(nullable void(^)(NSString *progress))PDFCreateProgressBlock {
    
    if (viewArr.count == 0 || pdfName.length == 0) return;
    
    NSMutableData *pdfData = [NSMutableData data];
    
    // 文档信息 可设置为nil
    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(pdfData, pageRect, nil);
    
    for (int i = 0; i < viewArr.count; i++) {
        UIView *pageView = [viewArr objectAtIndex:i];
        // PDF文档是分页的,开启一页文档开始绘制
        UIGraphicsBeginPDFPage();
        // 获取当前的上下文
        CGContextRef pdfContext = UIGraphicsGetCurrentContext();
        [pageView.layer renderInContext:pdfContext];
    }
    UIGraphicsEndPDFContext();
    
    NSArray *documentDirectories        = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDirectory         = [documentDirectories objectAtIndex:0];
    NSString *documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:pdfName];
    [pdfData writeToFile:documentDirectoryFilename atomically:YES];
    NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);
}
第二种实现方式:
/// 完全手动的画出PDF
/// @param dataInfo 需要传入的dataInfo
/// @param pdfName PDF名字,且需要带.pdf的后缀
+ (void)toDrawPDFWithDataInfo:(MOJiPDFDataInfo *)dataInfo pdfName:(nullable NSString *)pdfName  {
    
    NSArray *targetArr              = dataInfo.targetArr;
    NSMutableArray *targetHeightArr = [[NSMutableArray alloc] init]; // 存放每一个词条的所占高度

    NSInteger allPageCount = 1;
    for (int i = 0; i < targetArr.count; i++) {
        
        // 在这里写代码,计算出总共需要的页码数,以及每一个词条的高度放入targetHeightArr数组中
         
    }

    // 1.创建media box
    CGFloat myPageWidth     = A4Width;
    CGFloat myPageHeight    = A4Height;
    CGRect mediaBox         = CGRectMake (0, 0, myPageWidth, myPageHeight);

    // 2.设置pdf文档存储的路径
    NSArray *paths               = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = paths[0];
    filePath                     = [documentsDirectory stringByAppendingFormat:@"/%@", pdfName];
    const char *cfilePath        = [filePath UTF8String];
    CFStringRef pathRef          = CFStringCreateWithCString(NULL, cfilePath, kCFStringEncodingUTF8);
//    NSLog(@"filePath = %@", filePath);

    // 3.设置当前pdf页面的属性
    CFStringRef myKeys[3];
    CFTypeRef myValues[3];
    myKeys[0]   = kCGPDFContextMediaBox;
    myValues[0] = (CFTypeRef) CFDataCreate(NULL,(const UInt8 *)&mediaBox, sizeof (CGRect));
    myKeys[1]   = kCGPDFContextTitle;
    myValues[1] = CFSTR("我的PDF");
    myKeys[2]   = kCGPDFContextCreator;
    myValues[2] = CFSTR("PDF作者");

    // 4.获取pdf绘图上下文
    CGContextRef myPDFContext     = MyPDFContextCreate (&mediaBox, pathRef);

    // ————特别注意,字体样式大小和颜色要这样设置,不然无法释放——————
    // 设置字体样式
    CTFontRef ctFontTitleMedium   = CTFontCreateWithName(CFSTR("PingFangSC-Medium"), 12.0, NULL);
    // 设置字体颜色 
    CGFloat cmykValue[] = {0.239, 0.270, 0.298, 1};      
    CGColorRef ctColorBlack = CGColorCreate(CGColorSpaceCreateDeviceRGB(), cmykValue);


    int t = 0; // target的计数放这里是为了不在PDF页码循环时重置
    for (int i = 0; i < allPageCount; i++) {
        if (t == targetArr.count) break;
        
        // 5.开始描绘每一页的页面
        CFDictionaryRef pageDictionary = CFDictionaryCreate(NULL, (const void **) myKeys, (const void **) myValues, 3,
                                                            &kCFTypeDictionaryKeyCallBacks, & kCFTypeDictionaryValueCallBacks);
        CGPDFContextBeginPage(myPDFContext, pageDictionary);
        
        // 默认的原点在左下角,每一页都需要转换坐标系的操作!!!!!

        /* 添加页脚 */
        CGFloat widthFotter = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:@"这是页脚"];
        CGRect rectFooter   = CGRectMake(A4Width - 10.f - widthFotter, 10.f, widthFotter, targetHeight);
        [MOJiPDF drawTextWithText:@"这是页脚" color:ctColorBlack font:ctFontTargetRegular alignMent:kCTTextAlignmentRight rect:rectFooter maxWidth:contentWidth contextRef:myPDFContext];
        
        CGFloat topFrom = topSpace;
        for (; t < targetArr.count; t++) {
            
            // 剩余距离不够的情况下,翻页
            CGFloat th = [[targetHeightArr objectAtIndex:t] floatValue];
            if ((topFrom + th - targetSpace) > (A4Height - bottomSpace)) break;
            
            MOJiPDFTarget *targetInfo   = [targetArr objectAtIndex:t];

            if (i == 0) {
                topFrom = topSpace + favoritesHeight + targetSpace;
            
                UIImage *iconImg    = [MOJiPDF roundCorners:dataInfo.coverImg size:CGSizeMake(favoritesHeight, favoritesHeight) radius:8.f];
                CGRect iconRect     = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, 40, favoritesHeight, favoritesHeight)];
                CGContextDrawImage(myPDFContext, iconRect, iconImg.CGImage);
                iconImg = nil;
            }
            
            CGFloat widthTargetTitle    = [MOJiPDF getStringWidthWithFontSize:[UIFont systemFontOfSize:10.f] height:14.f string:targetInfo.title];
            CGRect rectTargetTitle      = [MOJiPDF getFinallyRectWithOriginalRect:CGRectMake(leftRightSpace, topFrom, widthTargetTitle, targetHeight)];
            [MOJiPDF drawTextWithText:targetInfo.title color:ctColorBlack font:ctFontTargetMedium alignMent:kCTTextAlignmentLeft rect:rectTargetTitle maxWidth:contentWidth contextRef:myPDFContext];
            topFrom = topFrom + targetHeight;
        }
        
        CGPDFContextEndPage(myPDFContext);
        CFRelease(pageDictionary);
    }

    // 6.释放创建的对象
    CFRelease(ctColorBlack);
    CFRelease(ctFontTitleMedium);
    
    CGContextRelease(myPDFContext);
    CFRelease(myValues[0]);
    CFRelease(myValues[1]);
    CFRelease(myValues[2]);
    CFRelease(myKeys[0]);
    CFRelease(myKeys[1]);
    CFRelease(myKeys[2]);
    CFRelease(pathRef);
}
以上是主要的代码,以下是需要用到的几个函数
/*
 * 获取pdf绘图上下文
 * inMediaBox指定pdf页面大小
 * path指定pdf文件保存的路径
 */
CGContextRef MyPDFContextCreate (const CGRect *inMediaBox, CFStringRef path)
{
    CGContextRef myOutContext = NULL;
    CFURLRef url;
    CGDataConsumerRef dataConsumer;
    
    url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, false);
    
    if (url != NULL)
    {
        dataConsumer = CGDataConsumerCreateWithURL(url);
        if (dataConsumer != NULL)
        {
            myOutContext = CGPDFContextCreate (dataConsumer, inMediaBox, NULL);
            CGDataConsumerRelease (dataConsumer);
        }
        CFRelease(url);
    }
    return myOutContext;
}


/**
 绘制文字的方式
 text: 需要绘制的文字
 color:文字颜色
 font:文字字体及大小
 alignment:文字对齐方式 (注:这个参数在原先的写法中没有生效,不知道为什么,暂时不用管它)
 rect:文字所在范围
 maxWidth:最大显示宽度,大于此,先截取然后显示省略
 contextRef:上下文
 */
+ (void)drawTextWithText:(NSString *)text color:(CGColorRef)color font:(CTFontRef)font alignMent:(CTTextAlignment)alignment rect:(CGRect)rect maxWidth:(CGFloat)maxWidth contextRef:(CGContextRef)contextRef {
    
    CFStringRef keys[]      = {kCTFontAttributeName, kCTForegroundColorAttributeName};
    CFTypeRef values[]      = {font, color};
    CFDictionaryRef attr    = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    
    CFAttributedStringRef attrString   = CFAttributedStringCreate(NULL, (__bridge CFStringRef)text, attr);
    CTLineRef line          = CTLineCreateWithAttributedString(attrString);
    
    NSString *dotString     = @"\u2026";
    CFAttributedStringRef dotStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef)dotString, attr);
    CTLineRef token         = CTLineCreateWithAttributedString(dotStringRef);
    
    /** 将现有 CTLineRef 截断并返回一个新的对象
     * width 截断宽度:如果行宽大于截断宽度,则该行将被截断
     * truncationType 截断类型
     * truncationToken 截断用的填充符号,通常是省略号 ... ,为Null时则只截断,不做填充
     *                        该填充符号的宽度必须小于截断宽度,否则该函数返回 NULL;
     */
    CTLineRef newline = CTLineCreateTruncatedLine(line, maxWidth, kCTLineTruncationEnd, token);
    
    CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
    CGContextSetTextPosition(contextRef, rect.origin.x, rect.origin.y);
    CTLineDraw(newline, contextRef);
    
    CFRelease(newline);
    CFRelease(token);
    CFRelease(line);
    CFRelease(dotStringRef);
    CFRelease(attrString);
    CFRelease(attr);
    
    CFRelease(keys[0]);
    CFRelease(keys[1]);
}


// 获取字符串宽度
+ (CGFloat)getStringWidthWithFontSize:(UIFont *)sizeFont height:(CGFloat)height string:(NSString *)string {
    
    CGRect rect = [string boundingRectWithSize:CGSizeMake(MAXFLOAT, height) options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:sizeFont} context:nil];
    return rect.size.width;
}


// 根据正确的坐标系 转换为在PDF画布上的坐标系
+ (CGRect)getFinallyRectWithOriginalRect:(CGRect)originalRect {
    
    CGFloat y = A4Height - originalRect.origin.y - originalRect.size.height;
    return CGRectMake(originalRect.origin.x, y, originalRect.size.width, originalRect.size.height);
}


/**
 给UIImage添加圆角
 img: 需要处理的UIImage
 size:UIImage真实显示时候的size
 radius:UIImage真实显示时候的圆角大小
 */
+ (UIImage *)roundCorners:(UIImage*)img size:(CGSize)size radius:(CGFloat)radius {
    
    int w = img.size.width;
    int h = img.size.height;
    CGFloat modulus = w / size.width; // 本身画图,是根据img的原始尺寸来的,跟要展示的尺寸会不同,需要自己计算在原尺寸上的圆角大小
   
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    /**
     CGContextRef CGBitmapContextCreate (

        void *data,                 指向要渲染的绘制内存的地址。这个内存块的大小至少是(bytesPerRow*height)个字节
        size_t width,               bitmap的宽度,单位为像素
        size_t height,              bitmap的高度,单位为像素
        size_t bitsPerComponent,    内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,你应该将这个值设为8.
        size_t bytesPerRow,         bitmap的每一行在内存所占的比特数
        CGColorSpaceRef colorspace, bitmap上下文使用的颜色空间。
        CGBitmapInfo bitmapInfo     指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符串。
     ); */
    CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, 8 * w, colorSpace, kCGImageAlphaPremultipliedFirst);
   
    CGContextBeginPath(context);
    addRoundedRectToPath(context, CGRectMake(0, 0, w, h), radius * modulus, radius * modulus);
    CGContextClosePath(context);
    CGContextClip(context);
   
    CGContextDrawImage(context, CGRectMake(0, 0, w, h), img.CGImage);
   
    CGImageRef imageMasked = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    
    UIImage * image = [UIImage imageWithCGImage:imageMasked];
    CGImageRelease(imageMasked);
   
    return image;
}


//这是被调用的静态方法,绘制圆角用
static void addRoundedRectToPath(CGContextRef context, CGRect rect,
                                float ovalWidth,float ovalHeight)
{
   float fw, fh;
   if (ovalWidth == 0 || ovalHeight == 0) {
       CGContextAddRect(context, rect);
       return;
   }
   
   CGContextSaveGState(context);
   CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
   CGContextScaleCTM (context, ovalWidth, ovalHeight);
   fw = CGRectGetWidth (rect) / ovalWidth;
   fh = CGRectGetHeight (rect) / ovalHeight;
   CGContextMoveToPoint(context, fw, fh/2);
   CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
   CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
   CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
   CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
   CGContextClosePath(context);
   CGContextRestoreGState(context);
}

上边的代码中可以看到,几乎所有的CFXXXRef和一些CG类型的数据,你创建或者持有的,就必须要释放掉!不然在大量数据的情况下,内存占用非常的严重。

以上基本就是我自己的Demo了,只是去掉了数据部分,你们可以直接复制粘贴然后填充一下数据,差不多就可以运行查看效果了。