Wednesday, April 22, 2009

PDF with paging

In my previous post I wrote about how to render the contents of an NSView to a pdf. That method was simple and intuitive. However, it had 1 drawback. It didn't allow me to create paginated PDF's. The way you go about this is much less intuitive and requires you use the NSPrintOperation class. Basically, what you want to do is print to a PDF. You start by creating an NSPrintInfo object using the default printing preferences:


//this will point to our NSPrintInfo object
NSPrintInfo *printInfo;
//this will point to the default printer info object
NSPrintInfo *sharedInfo;
//thi will point to our settings for the NSPrintInfo object
NSMutableDictionary *printInfoDict;
//this will point to the settings for the default NSPrintInfo object
NSMutableDictionary *sharedDict;

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

//below we set the type of printing job to a save job.
[printInfoDict setObject:NSPrintSaveJob 
forKey:NSPrintJobDisposition];

//set the path to the file you want to print to
[printInfoDict setObject:@"/Users/OnCocoa/Desktop/test.pdf" forKey:NSPrintSavePath];

//create our very own NSPrintInfo object with the settings we specified in printInfoDict
printInfo = [[[NSPrintInfo alloc] initWithDictionary: printInfoDict] autorelease];


Once you set up your NSPrintInfo object you have to create an NSPrintOperation object, specifying which view you want to print from and which NSPrintInfo object you want to use.


//create the NSPrintOperation object, specifying docView from the previous post as the NSView to print from.
NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:docView printInfo:printInfo];

//we don't want to show the printing panel
[printOp setShowPanels:NO];

//run the print operation
[printOp runOperation];


Thats all you need to do to print a view to a PDF with paging the vanilla way. If you want to print like Safari does, you have to set up the margins appropriately using the code below:


[printInfo setHorizontalPagination: NSFitPagination];
[printInfo setVerticallyCentered:NO];
[printInfo setHorizontallyCentered:NO];

NSRect imageableBounds = [printInfo imageablePageBounds];
NSSize paperSize = [printInfo paperSize];
if (NSWidth(imageableBounds) > paperSize.width) {
imageableBounds.origin.x = 0;
imageableBounds.size.width = paperSize.width;
}
if (NSHeight(imageableBounds) > paperSize.height) {
imageableBounds.origin.y = 0;
imageableBounds.size.height = paperSize.height;
}

[printInfo setBottomMargin:NSMinY(imageableBounds)];
[printInfo setTopMargin:paperSize.height - NSMinY(imageableBounds) - NSHeight(imageableBounds)];
[printInfo setLeftMargin:NSMinX(imageableBounds)];
[printInfo setRightMargin:paperSize.width - NSMinX(imageableBounds) - NSWidth(imageableBounds)];


One more thing to keep in mind is that if you are generating a PDF from HTML using a WebView (like in my previous post) you have to wait for the contents of the WebView to render. It doesn't matter if you are loading from disk or from the net, you should always set a frame delegate using WebViews -(void)setFrameLoadDelegate:(id)delegate and respond to:


- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame;


This method will get called once the WebView renders the contents of the page you requested. Check out the screenshot below to see what you can accomplish with all this:




Good luck!

Sunday, April 19, 2009

HTML to PDF using WebKit

Creating a PDF with the contents of any NSView is extremely easy. All you have to do is use NSViews:


- (NSData *)dataWithPDFInsideRect:(NSRect)aRect


When trying to create a PDF from HTML the natural way to go is using the method mentioned above from an instance of WebView. Unfortunately, it turns out there is a small inconvenience. When called from an instance of WebView this method WILL NOT draw everything to PDF. It's very easy to test this without writing any code. Try printing a few pages to PDF in Safari. This is how my blog gets rendered:




It's also interesting that google.pl renders without the google logo, while google.us renders properly:




Anyhow, this issue is very easy to solve in code. Let's say you load your PDF as follows:


//get a pointer to the document view so that we render the entire web page, not just the visible portion.
NSView *docView = [[[webview mainFrame] frameView] documentView];

[docView lockFocus];

//create the PDF
NSData *data = [docView dataWithPDFInsideRect:[docView bounds]];

[docView unlockFocus];

//create an instance of a PDFDocument to display in a PDFView.
PDFDocument *doc = [[PDFDocument alloc] initWithData:data];

//display the PDF document in a PDFView
[pdfview setDocument:doc];

[pdfWindow orderFront:nil];


This will produce the standard results. In order to render everything to PDF you have to set WebViews preferences appropriately. WebView has a method - (void)setPreferences:(WebPreferences*)preferences. If you look at the documentation for WebPreferences you will notice it has a method called - (void)setShouldPrintBackgrounds:(BOOL)pb. Setting this to YES solves the minor inconvenience mentioned in this post.


WebPreferences *preferences = [[[WebPreferences alloc] initWithIdentifier:@"testing"] autorelease];
[preferences setShouldPrintBackgrounds:YES];
[webview setPreferences:preferences];


After setting WebViews preferences to print the backgrounds, my blog and all other pages print just fine to PDF.



UPDATE:

Check out my next post where I mention two important things:
1) How to print to a PDF with paging.
2) How to make sure the WebView loaded all it's content before printing (if you're getting a blank page when printing this is what you need!).

Monday, March 30, 2009

Drawing an NSCell from a flipped NSView to a bitmap context


Recently I found myself in need of a custom drawn list view. It's purpose would be to display 1 column and a bunch of rows, similarly to the way NSTableView does, except my view needed more flexibility in terms of the way it shapes and displays items. I thought about subclassing NSTableView, but after careful consideration I've realized that would be an overkill and went with subclassing NSView instead. To make my control efficient, I used the same approach as Apple did with NSTableView - each row is drawn by the same NSCell instance. This way I didn't have to waste memory creating a corresponding NSCell object for each item in the list views content. This was all fairly basic. I added some simple methods, did a little math and presto - custom control a 'la cocoa.


I came across a small issue when implementing drag and drop and I thought the solution might be of interest to some. My custom list views isFlipped method returns YES. This allows the NSScrollView that owns my control to work intuitively - from top to bottom (it's also much more intuitive for me). I do realize there are other ways of accomplishing this, but this just felt like it required the least hassle. All was fine in Cocoa land until I wanted to draw my NSCells to a bitmap and use it as a drag and drop image. There were two goals I wanted to accomplish:

  • Draw the NSCell into a bitmap context without changing anything in it's drawing method - (void)drawInteriorWithFrame:(NSRect)theCellFrame inView:(NSView *)theControlView
  • Draw a few cells and then arrange them into a nice image suitable for drag and drop. This made it unsuitable for me to use any of the NSViews standard methods for drawing to a bitmap context.


The cell I want to draw to the bitmap context is displayed below:


So the standard way to go about drawing to a bitmap context is:

//creating the rectangle that defines the bounds of our bitmap image
NSRect offscreenRect = NSMakeRect(0.0, 0.0, 100, 20);
NSBitmapImageRep* offscreenRep = nil;

//creating the bitmap image
offscreenRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:offscreenRect.size.width
pixelsHigh:offscreenRect.size.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bitmapFormat:0
bytesPerRow:(4 * offscreenRect.size.width)
bitsPerPixel:32];

[NSGraphicsContext saveGraphicsState];

//setting the current context to a bitmap context
[NSGraphicsContext setCurrentContext:[NSGraphicsContext
graphicsContextWithBitmapImageRep:offscreenRep]];

//do some drawing

[NSGraphicsContext restoreGraphicsState];

//creating an NSImage setting whatever we drew above to be it's representation
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


Unfortunately, this left me with:


If you compare that to the original, you will notice that the text views got switched. This was of course, unacceptable.

I started googling and found a bunch of blogs stating that using an NSAffineTransform would solve the problem. So I added:

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];


I really hoped this method would work, because it was the only solution that seemed, at the time, reasonable. It produced the image you see below:


This time the text views were in their proper places, but the text was flipped. Having wasted some time trying to find a solution I decided to give my own wacky idea a go. When using a "ported" graphics context one can set the context as flipped. I decided to create a CGContextRef, use it as a ported and flipped NSGraphicsContext and draw to it like so:

//create a CGContextRef so that we can later port it and make it flipped
CGContextRef context = CGBitmapContextCreate (bitmapData,
offscreenRect.size.width,
offscreenRect.size.height,
8,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast);

[NSGraphicsContext saveGraphicsState];
//here we port the context and make it flipped
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:context flipped:YES]];

NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:offscreenRect.size.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];

//perform drawing

//create a CGImageRef from the CGContextRef
CGImageRef myImage = CGBitmapContextCreateImage (context);

//port the CGImageRef to an NSBitmapImageRep
NSBitmapImageRep* offscreenRep = [[[NSBitmapImageRep alloc] initWithCGImage:myImage] autorelease];

[NSGraphicsContext restoreGraphicsState];

//create the image and set it's representation to what was drawn above
NSImage *image = [[[NSImage alloc] init] autorelease];
[image addRepresentation:offscreenRep];

//this allows us to create an image thats content is transparent (see the 'fraction') parameter below
NSImage *dragImage = [[[NSImage alloc] initWithSize:[image size]] autorelease];
[dragImage lockFocus];
[image compositeToPoint:NSMakePoint(0, 0) operation:NSCompositeSourceOver fraction:0.5];
[dragImage unlockFocus];


As a result I got exactly what I wanted: