iOS SDK - 编程生成PDF文件

79

在我看来,使用CoreGraphics框架进行程序化绘制PDF文件是一项繁琐的工作。

我想要以应用程序中各种视图对象为基础,编程创建一个PDF文件。

我想知道是否有适用于iOS SDK的好的PDF教程或可插入库。

我看过这个教程:PDF Creation Tutorial,但它主要是用C语言编写的。我希望能找到更多Objective-C风格的教程。这似乎也是一种荒谬的编写PDF文件的方法,需要计算线条和其他对象的位置。

void CreatePDFFile (CGRect pageRect, const char *filename) 
{   
    // This code block sets up our PDF Context so that we can draw to it
    CGContextRef pdfContext;
    CFStringRef path;
    CFURLRef url;
    CFMutableDictionaryRef myDictionary = NULL;

    // Create a CFString from the filename we provide to this method when we call it
    path = CFStringCreateWithCString (NULL, filename,
                                      kCFStringEncodingUTF8);

    // Create a CFURL using the CFString we just defined
    url = CFURLCreateWithFileSystemPath (NULL, path,
                                         kCFURLPOSIXPathStyle, 0);
    CFRelease (path);
    // This dictionary contains extra options mostly for 'signing' the PDF
    myDictionary = CFDictionaryCreateMutable(NULL, 0,
                                             &kCFTypeDictionaryKeyCallBacks,
                                             &kCFTypeDictionaryValueCallBacks);

    CFDictionarySetValue(myDictionary, kCGPDFContextTitle, CFSTR("My PDF File"));
    CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR("My Name"));
    // Create our PDF Context with the CFURL, the CGRect we provide, and the above defined dictionary
    pdfContext = CGPDFContextCreateWithURL (url, &pageRect, myDictionary);
    // Cleanup our mess
    CFRelease(myDictionary);
    CFRelease(url);
    // Done creating our PDF Context, now it's time to draw to it

    // Starts our first page
    CGContextBeginPage (pdfContext, &pageRect);

    // Draws a black rectangle around the page inset by 50 on all sides
    CGContextStrokeRect(pdfContext, CGRectMake(50, 50, pageRect.size.width - 100, pageRect.size.height - 100));

    // This code block will create an image that we then draw to the page
    const char *picture = "Picture";
    CGImageRef image;
    CGDataProviderRef provider;
    CFStringRef picturePath;
    CFURLRef pictureURL;

    picturePath = CFStringCreateWithCString (NULL, picture,
                                             kCFStringEncodingUTF8);
    pictureURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), picturePath, CFSTR("png"), NULL);
    CFRelease(picturePath);
    provider = CGDataProviderCreateWithURL (pictureURL);
    CFRelease (pictureURL);
    image = CGImageCreateWithPNGDataProvider (provider, NULL, true, kCGRenderingIntentDefault);
    CGDataProviderRelease (provider);
    CGContextDrawImage (pdfContext, CGRectMake(200, 200, 207, 385),image);
    CGImageRelease (image);
    // End image code

    // Adding some text on top of the image we just added
    CGContextSelectFont (pdfContext, "Helvetica", 16, kCGEncodingMacRoman);
    CGContextSetTextDrawingMode (pdfContext, kCGTextFill);
    CGContextSetRGBFillColor (pdfContext, 0, 0, 0, 1);
    const char *text = "Hello World!";
    CGContextShowTextAtPoint (pdfContext, 260, 390, text, strlen(text));
    // End text

    // We are done drawing to this page, let's end it
    // We could add as many pages as we wanted using CGContextBeginPage/CGContextEndPage
    CGContextEndPage (pdfContext);

    // We are done with our context now, so we release it
    CGContextRelease (pdfContext);
}

编辑:这是一个在iPhone项目中使用libHaru的GitHub示例


1
哈哈,我知道这很老套,但在iPhone上阅读100页的PDF文件并不算什么。这会给服务器带来更大的压力,因为服务器必须将其乘以尝试执行此操作的并发用户数量。 - Armand
4个回答

56

首先,iOS中的CoreGraphics PDF生成存在一个错误,会导致PDF文件损坏。我知道这个问题一直存在,包括iOS 4.1(我没有测试过iOS 4.2)。该问题与字体有关,只有在PDF中包含文本时才会出现。症状是,在生成PDF时,您会在调试控制台中看到类似于以下内容的错误:

<Error>: can't get CIDs for glyphs for 'TimesNewRomanPSMT'

麻烦的是,生成的PDF文件可以在某些PDF阅读器中正常显示,但在其他地方无法正常显示。因此,如果您能控制将用于打开您的PDF文件的软件,则可以忽略此问题(例如,如果您只想在iPhone或Mac桌面上显示PDF,则使用CoreGraphics应该没问题)。但是,如果您需要创建一个可以在任何地方使用的PDF文件,则应详细查看此问题。以下是一些额外信息:

http://www.iphonedevsdk.com/forum/iphone-sdk-development/15505-pdf-font-problem-cant-get-cids-glyphs.html#post97854

作为解决方法,我在iPhone上成功地使用了libHaru来替代CoreGraphics PDF生成。最初让libHaru与我的项目一起构建有点棘手,但是一旦我正确地设置了我的项目,它就满足了我的需求。
其次,根据您的PDF格式/布局,您可以考虑使用Interface Builder创建一个视图,作为PDF输出的“模板”。然后,您将编写代码来加载视图,填充任何数据(例如,为UILabel设置文本等),然后将视图的各个元素渲染到PDF中。换句话说,使用IB指定坐标、字体、图像等,并编写代码以通用方式呈现各种元素(例如,UILabel、UIImageView等),以便您不必硬编码所有内容。我使用了这种方法,对我的需求非常适用。同样,这可能或可能不适合您的情况,具体取决于您的PDF格式/布局需要。
编辑:(回应第一个评论)
我的实现是商业产品的一部分,这意味着我不能分享完整的代码,但我可以给出一个大致的概述:
我创建了一个.xib文件,其中包含一个视图,并将该视图大小设置为850 x 1100(我的PDF针对8.5 x 11英寸,因此很容易在设计时坐标之间进行转换)。
在代码中,我加载了该视图:
- (UIView *)loadTemplate
{
    NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ReportTemplate" owner:self options:nil];
    for (id view in nib) {
        if ([view isKindOfClass: [UIView class]]) {
            return view;
        }
    }

    return nil;
}

然后我填充各种元素。我使用标记找到适当的元素,但您也可以使用其他方法。例如:

UILabel *label = (UILabel *)[templateView viewWithTag:TAG_FIRST_NAME];
if (label != nil) {
    label.text = (firstName != nil) ? firstName : @"None";

然后我调用一个函数将视图渲染到PDF文件中。该函数递归遍历视图层次结构并渲染每个子视图。对于我的项目,我只需要支持Label、ImageView和View(用于嵌套视图):

- (void)addObject:(UIView *)view
{
    if (view != nil && !view.hidden) {
        if ([view isKindOfClass:[UILabel class]]) {
            [self addLabel:(UILabel *)view];
        } else if ([view isKindOfClass:[UIImageView class]]) {
            [self addImageView:(UIImageView *)view];
        } else if ([view isKindOfClass:[UIView class]]) {
            [self addContainer:view];
        }
    }
}

作为一个例子,这是我实现的addImageView函数(HPDF_函数来自libHaru):
- (void)addImageView:(UIImageView *)imageView
{
    NSData *pngData = UIImagePNGRepresentation(imageView.image);
    if (pngData != nil) {
        HPDF_Image image = HPDF_LoadPngImageFromMem(_pdf, [pngData bytes], [pngData length]);
        if (image != NULL) {
            CGRect destRect = [self rectToPDF:imageView.frame];

            float x = destRect.origin.x;
            float y = destRect.origin.y - destRect.size.height;
            float width = destRect.size.width;
            float height = destRect.size.height;

            HPDF_Page page = HPDF_GetCurrentPage(_pdf);
            HPDF_Page_DrawImage(page, image, x, y, width, height);
        }
    }
}

希望这能让您有所启发。

你有使用 XIB 作为模板的示例吗? - WrightsCS

12

虽然回复较晚,但由于我在PDF生成方面遇到了许多困难,所以认为值得分享我的看法。你可以使用UIKit方法来生成PDF而非Core Graphics来创建上下文。

苹果已经在绘图和打印指南中进行了详细说明。


4
FWIW - 这个教程的网站链接无法访问,被重定向到了Godaddy(一个域名注册和托管服务提供商)的页面。 - CuriousRabbit

8
iOS上的PDF功能都是基于CoreGraphics的,这很合理,因为iOS中的绘图原语也是基于CoreGraphics的。如果您想直接将2D原语渲染到PDF中,则必须使用CoreGraphics。
还有一些UIKit中存在的对象的快捷方式,如图像。将PNG绘制到PDF上下文仍需要调用CGContextDrawImage,但您可以使用从UIImage获取的CGImage来完成,例如:
UIImage * myPNG = [UIImage imageNamed:@"mypng.png"];
CGContextDrawImage (pdfContext, CGRectMake(200, 200, 207, 385), [myPNG CGImage]);

如果您需要更多有关整体设计方面的指导,请明确说明“应用程序中的各种对象”是什么意思以及您想要实现什么目标。


谢谢您的回复。所谓对象,我只是指我正在获取用户输入的文本,并希望将它们放置在PDF的各个位置。正如您所指出的那样,还有一些图形。就像可以通过电子邮件发送的表格一样。 - WrightsCS
嗨,我有一个问题:用这种方式(CGContextDrawImage),PDF 就不再是矢量格式了。如果我想要在单个 PDF 页面中呈现多个矢量图形,同时确保 PDF 仍然是矢量格式,我该怎么做? - Paradise

2
晚了一点,但可能对其他人有所帮助。听起来你需要的是PDF模板样式操作,而不是在代码中构建PDF。由于你想将其发送电子邮件,因此你已连接到网络,可以使用类似Docmosis云服务这样的服务,它实际上是一个邮件合并服务。向它发送与模板合并的数据/图像。这种方法有很少的代码,并且将大部分处理从iPad应用程序卸载。我曾在一个iPad应用程序中看到过它,效果不错。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接