美文网首页iOS常用
iOS 数据导出为PDF(生成PDF文件)

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

作者: 这位网友 | 来源:发表于2020-10-23 18:10 被阅读0次

    需求:根据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了,只是去掉了数据部分,你们可以直接复制粘贴然后填充一下数据,差不多就可以运行查看效果了。

    相关文章

      网友评论

        本文标题:iOS 数据导出为PDF(生成PDF文件)

        本文链接:https://www.haomeiwen.com/subject/lqtbmktx.html