需求:将列表数据导出为PDF
实现方式:
一、自己创建View,按照OC的方式画页面,画完之后将一页页View绘制到PDF文件中
优点:在View中画简单易懂,转成PDF的方式也简单
缺点:由于是将整个View给当成图片绘制到PDF中,保存的PDF内都是图片,无法修改文字,且!PDF文档非常的大!
二、从头到尾都使用手绘的方式去生成PDF,将各个控件自己画出来
优点:速度很快,生成的PDF文件足够的小,例:测试我的五万条数据,基本都是纯文本的,共计3000多页,需要15s左右,文件大小只有4M,如果使用第一种方式,五千条数据都300M了,差距有点大。
缺点:整个绘制过程比较麻烦,如果是统一样式的列表,还可以用for循环,如果特殊样式太多,全都要自己写。例:一行文本数据显示一行,最后超出的部分省略号表示,这个省略号都要自己写,并且要定义样式与前边的文字相同!
实现过程:
首先定义了页面的一些常用数据
// 首先定义了页面的一些常用数据
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的页面中,默认坐标原点是在左下角的,你可能需要对页面进行坐标系转换,而且是每个页面都需要先进行坐标系转换。
- 手动绘制时页眉页脚页码这些都需要自己绘制,特别是页码,一定要想好怎么计算总共页面数
- 释放对象,一定要释放你Create的所有对象,不然for循环次数太多,内容太大的时候,内存会崩掉的
- 剩下的就是,文字的字体/颜色/大小/对齐,图片的圆角等等,需要查资料看下怎么设置的问题了
/// 完全手动的画出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);
}
不习惯本页面的,可以查看此页面:https://www.jianshu.com/p/cb88ea5e750a