« Create a PDF

Posted by Brad Miller on April 19, 2003 [Feedback (3) & TrackBack (0)]


// Introduction

In this tutorial we'll look at how to add PDF exportation to an application. It is an easy to add feature that can add a lot of functionality to your program. We'll start off by briefly looking at the commonly shown export method and then implement a method that will export multi-page PDFs.

The test app


// Create the Interface

The sample program will consist of a NSTextView inside of a window with a switch to let the user export a paginated PDF. The "Save as" menu item will also be renamed "Save as PDF". To save time, I've provided a starting project that has the interface already built. You can download it from here.


// The PDF Generation

The first way to create a PDF uses NSView's dataWithPDFInsideRect method and is the manner in which PDF generation is commonly shown. To create a PDF with it, you simply create a NSData object by passing dataWithPDFInsideRect the bounds of the view that you want to save. You then write the data object to a file using NSData's writeToFile:atomically: method.

- (void)didEnd:(NSSavePanel *)sheet
    returnCode:(int)code
    saveFormat:(void *)saveType
{
    if (code == NSOKButton)
    {
        if (pageIt)
        {

        }
        else
        {
            NSRect r = [textView bounds];
            NSData *data = [textView dataWithPDFInsideRect:r];
            
            [data writeToFile:[sheet filename] atomically:YES];
        }
    }
}

This method works great when saving something like a drawing, but it does not work as well when saving a large view. It will produce one large, monolithic page as shown in the left side of Figure 2. What we want instead is a multi page PDF like the one the right side.

Before and After
Figure 2: The left hand side shows the first method's results and the right hand shows the second method's results.

The second method is not too much harder than the first to write, but it's not nearly as intuitive. It uses an NSPrintOperation to generate the PDF. If you've ever added printing support to an application, this will look very familiar to you. All we're doing is printing the document to a file instead of to the printer. The "Save as PDF" button in the print panel generates a PDF the same way.

- (void)didEnd:(NSSavePanel *)sheet
    returnCode:(int)code
    saveFormat:(void *)saveType
{
    if (code == NSOKButton)
    {
        if (pageIt)
        {
            NSPrintInfo *printInfo;
            NSPrintInfo *sharedInfo;
            NSPrintOperation *printOp;
            NSMutableDictionary *printInfoDict;
            NSMutableDictionary *sharedDict;

            sharedInfo = [NSPrintInfo sharedPrintInfo];
            sharedDict = [sharedInfo dictionary];
            printInfoDict = [NSMutableDictionary dictionaryWithDictionary:
                             sharedDict];

The first thing we do create an NSDictionary that contains all the information from NSPrintInfo's sharedPrintInfo. We do this as a convenience shortcut so that we don't have to enter in a value for each of NSPrintInfo's key-value pairs. We now only have to modify the items that we are interested in.

                
            [printInfoDict setObject:NSPrintSaveJob 
                              forKey:NSPrintJobDisposition];
            [printInfoDict setObject:[sheet filename] forKey:NSPrintSavePath];

We now change the two NSPrintInfo attributes that we are interested in. First set the disposition to be an NSPrintSaveJob. It tells the NSPrintOperation that we want to print to a file. We then have to set the NSPrintSavePath constant to hold the path to save the file at. The filename that the save panel returns is the value passed to it.

            printInfo = [[NSPrintInfo alloc] initWithDictionary: printInfoDict];
            [printInfo setHorizontalPagination: NSAutoPagination];
            [printInfo setVerticalPagination: NSAutoPagination];
            [printInfo setVerticallyCentered:NO];

Now that we have our dictionary, we create the NSPrintInfo with it. We then set the pagination type for both the horizontal and vertical to auto paginate. NSAutoPagination uses NSView's built in pagination to break the view into rectangles of equal size. The final attribute of our print info that we set is its vertical centering. By setting it to no, the page's content will appear after the top margin at the top of the page. If we had set it to yes, the text will appear centered on the page.

            printOp = [NSPrintOperation printOperationWithView:textView 
                                                     printInfo:printInfo];
            [printOp setShowPanels:NO];
            [printOp runOperation];
        }
        else
        {
            ...

Finally, we create a NSPrintOperation with our printInfo to generate the PDF. The print panel is set to not be shown since we don't need to interact with it. The print operation is then run to finish the process. You will now have a paginated PDF file at the chosen location.


// Conclusion

PDF's are as simple as that to create in Cocoa. Both methods are good tools to keep in your arsenal. The NSData method works great when you have a fixed sized view that you would want to have as a single page and the printing method is perfect for when you have a view like NSTextView where its contents can easily span multiple pages.


Comments

Hi,
Thank you for the tutorial.
I have some question.... (I'm a newbie)

Where must I put the 2 type code?
Not in main.m i suppose.....
Thank you again

Think different!

Posted by: Yorh on April 24, 2003 07:27 PM

When discussing PDF creation in Mac OS X, it is important to note some of the not-so-well documented portions of NSImage.

When an image is opened by an application, some optimizations are made to increase its drawing and memory performance. A cache is created, and the original representations are discarded. This works very well if you never want to display anything at a higher resolution than a computer screen will allow, but causes numerous problems during printing and PDF export. If you open a PDF in your application, then export to PDF using the above code, you will notice that the image has been rasterized and contains "jaggies". This is because all you are exporting is the cache that was made by NSImage.

To retain all of the original vector data, you should use the below code BEFORE the image is drawn for the first time:

[anNSImageInstance setDataRetained:YES];

This one line of code will save you numerous headaches later on. Feel free to email me with questions regarding NSImage or cocoa programming in general. I'm more than glad to help.

Happy Coding!
- Eliot Simcoe

Posted by: Eliot Simcoe on April 27, 2003 08:28 AM

Hello,

Nice demo. I am trying to achieve something similar, except my custom view is NSSplitView. I can make the Split View fit itself onto a single page and print just one, however I would like to:
(a) use the splitview repeatedly for different pages, and
(b) update the splitview with new data for each successive page.

I am using
- (BOOL)knowsPageRange:(NSRangePointer)range;
- (NSRect)rectForPage:(int)page;
etc. according to apple's suggestion, but when I try to calculate how exactly to fit my splitview onto the page, it doesn't look pretty. Anyone know how to do this?
Thanks!

Posted by: Trevor on September 16, 2003 03:50 PM
Post a comment