翻译自:How To Create a PDF with Quartz 2D in iOS 5
有时候在app中可能会需要使用app中的数据来创建PDF。例如,你有一个app允许用户签一个合同,你就可能希望用户得到最终的PDF。
但是如何使用程序来生成PDF呢?在iOS中使用Quartz2D会很容易的做到。
在这个教程中,你会亲自体验到,使用 Quartz2D 创建一个简单的 PDF。我们生成的是一个制作发票的PDF APP,如截图所示。
这个教程需要你熟悉iOS5的新特性,例如Storyboards 和 ARC。如果你是刚接触iOS5,可以查看本站点上的iOS5教程。
开始
运行Xcode,使用iOS\Application\Single View Application模板创建一个新工程。在工程名称处输入PDFRenderer,在Device Family处选择iPhone,一定要勾上Use Storyboard 和 Use Automatic Reference Counting,点击finish来创建项目。
这个项目会有两个场景。第一个场景仅仅有一个按钮,点击的时候来展示PDF。第二个场景就是PDF本身。
选择MainStoryboard.storyboard文件。在主窗口中,你将会看到一个控制器。我们需要把这个控制器嵌入到一个导航控制器中,点击Editor菜单,然后选择Embed In/Navigation Controller。
现在视图控制器已和导航控制器联系在一起。
拖动一个UIButton到视图控制器中,命名为”Draw PDF”。
如果你运行程序,你将会看到一个名为“Draw PDF”按钮的简单视图,但当点击的时候并没有任何反应。稍后我们在说这个。
现在来添加第二个视图来显示PDF。
拖动一个新的视图控制器到Storyboard。按住 Ctrl 并拖动“Draw PDF”按钮到新的视图控制器上,你将会看到一个弹出式窗口,类似于下图所示。
选择Push选项,就会在新的视图控制器上创建已segue,当点击按钮时,就会显示新的视图控制器。换句话说,按钮现在正常工作了。
运行app,点击按钮,将会在屏幕上看到一个空的视图控制器。这就是Storyboard。
创建PDF和绘制文本
现在已经有了框架,是时候写代码了啦。
在开始之前,选择File\New\New 在工程中加入新的文件。选在 iOS\Cocoa Touch\UIViewController 子类模板,输入PDFViewController,并确保不选中“With XIB for user interface”,完成创建文件。我们不需要nib是因为使用了storyboard。
在Storyboard中选择最后添加的视图控制器,在identity inspector中把Class改为PDFViewController,这样就把它们联系起来了。
为了在PDF中绘制一些文字,我们将会使用Core Text框架。选择PDFRenderer target,在Build Phases标签栏中选择Link Binaries With Libraries,点击“+”号,选择CoreText框架。
在PDFViewController.h中,导入CoreText:
#import <CoreText/CoreText.h>
现在可以编码了!在PDFViewController.m中添加一个新方法,来创建一个“hello world”的PDF。这个方法很长,但是别担心——稍后我们将会解释它。
-(void)drawText
{
NSString* fileName = @"Invoice.PDF";
NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];
NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);
// Draw the frame.
CTFrameDraw(frameRef, currentContext);
CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}
开始的6行代码,在Documents文件夹中创建一个PDF文件。文件名为Invoice.pdf。
NSString* fileName = @"Invoice.PDF";
NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];
接下来的一段代码我们创建了一个将要绘制到PDF上的字符串“Hello world”。并将字符串转换为对应 CoreGraphics 的CFStringRef。
NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
接下来创建了一个CGRect来定义文本绘制的位置。
CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);
接下来创建一个PDF上下文并标记PDF页面的开始。PDF 文档的每页都从调用 UIGraphicsBeginPDFPageWithInfo开始。
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();
Core Graphics的坐标系从左下角开始绘制,而UIKit的坐标系是从左上角开始的。在开始绘制之前需要将上下文翻转。
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);
然后绘制文本,释放Core Graphics对象,并关闭PDF上下文。
// Draw the frame.
CTFrameDraw(frameRef, currentContext);
CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
如果你有兴趣学习更多关于Core Text是如何工作和你可以用它做的一些更酷的事情,请查阅我们的Core Text教程。
添加UIWebView来展示PDF文件
剩下要做的唯一事情是在屏幕上显示我们 PDF 文件。要做到这一点,请将下面的方法添加到 PDFViewController.m中,并将 UIWebView 添加到视图控制器中来显示我们刚刚创建的 PDF 文件路径。
-(void)showPDFFile
{
NSString* fileName = @"Invoice.PDF";
NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];
UIWebView* webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
NSURL *url = [NSURL fileURLWithPath:pdfFileName];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView setScalesPageToFit:YES];
[webView loadRequest:request];
[self.view addSubview:webView];
}
然后在viewDidLoad中加入如下的方法:
- (void)viewDidLoad
{
[self drawText];
[self showPDFFile];
[super viewDidLoad];
}
现在可以看结果了!编译并运行工程,在放大时,应该看到屏幕上的”Hello World”!
重构
我们的绘图代码并不是真的属于视图控制器,因此,我们新建一个NSObject对象叫做PDFRenderer。使用 iOS\Cocoa Touch\Objective-C 类模板创建一个NSObject的子类,叫做PDFRenderer。
在PDFRenderer.h 中导入Core Text:
#import CoreText/CoreText.h
把绘制文本的方法从PDFViewController.m移到PDFRenderer.m中。
我们把文件名放入新的文本绘制方法中,在PDFViewController.m file中创建一个新方法getPDFFileName。
-(NSString*)getPDFFileName
{
NSString* fileName = @"Invoice.PDF";
NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];
return pdfFileName;
}
然后,在PDFRenderer.m中从绘制文本方法中移除相应的代码,并修改方法把文件名作为一个参数,并使它成为一个类方法。
+(void)drawText:(NSString*)pdfFileName
并在PDFRenderer.h中声明。
然后在PDFViewController.m中导入PDFRenderer:
#import "PDFRenderer.h"
修改viewDidLoad,并调用新的类和方法:
- (void)viewDidLoad
{
NSString* fileName = [self getPDFFileName];
[PDFRenderer drawText:fileName];
[self showPDFFile];
[super viewDidLoad];
}
编译和运行工程。我们的重构并不会使应用成员表现不同,但代码组织的更好。
使用Quartz 2D画线
我们想要的最终的发票由文本、 线条和图像组成。我们有了文本 — — 现在是时候去练习画一条线了。为了做到这一点,我们将使用drawLine 方法 !
在PDFRenderer.m中加入如下的新方法:
+(void)drawLineFromPoint:(CGPoint)from toPoint:(CGPoint)to
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGFloat components[] = {0.2, 0.2, 0.2, 0.3};
CGColorRef color = CGColorCreate(colorspace, components);
CGContextSetStrokeColorWithColor(context, color);
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
CGContextStrokePath(context);
CGColorSpaceRelease(colorspace);
CGColorRelease(color);
}
上面的代码设置我们想要画线的属性,线的宽度和颜色。它在传递到方法中的 CGPoints间 绘制线条。
现在可以从视图控制器中调用此方法。然而,需要注意的是通过调用 UIGraphicsBeginPDFContextToFile,drawText 方法不会创建新的 PDF 图形上下文或新的一页。所以我们需要进行一些修改。
首先,在PDFRenderer中创建一个新方法,叫做drawPDF。
+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
CGPoint from = CGPointMake(0, 0);
CGPoint to = CGPointMake(200, 300);
[PDFRenderer drawLineFromPoint:from toPoint:to];
[self drawText];
// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}
这个方法将会创建我们的图形上下文,绘制文本和画一条线,最后关闭上下文。
注意,drawText方法不再把PDF的文件名作为一个参数。下面是drawText方法:
+(void)drawText
{
NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);
// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);
// Draw the frame.
CTFrameDraw(frameRef, currentContext);
CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
}
在 PDFRenderer.h中声明方法。在View Controller的viewDidLoad的方法中,使用drawText替代drawPDF方法。
好了!编译并运行程序,你将会看到“Hello World”和一条斜线,如下所示:
Where to Go From Here?
在这example project可以上述教程的所有代码。
这个教程结束了,在第二部分,我们将进入更高级的绘图技术,像添加图像和使用 xib 文件使布局过程更为简单。