Messing with PDF files

by jegeblad

A couple of weeks ago I messed about a bit with creating PDF files on the iPhone. Although the iPhone OS includes Apple's excellent drawing kit, Quartz which is built to generate PDF files, I stumbled on several issues, so I thought I would share some of my experiences here.

PDF creation
PDF creation is essentially quite easy. You create a PDF document and CGContext for it. Here PDF output will appear in a CFMutableData, but you can output to a file quite easily as well:

double const pointToMM = 0.35277777778;
double targetW = 145.0/pointToMM, targetH = 90.0/pointToMM;
double rescale = min( targetW/(cDimensionX), targetH/(cDimensionY));
CGRect pageRect = CGRectMake(0,0,targetW,targetH);
CFMutableDataRef data = CFDataCreateMutable(NULL, 0);
CGDataConsumerRef consumer = CGDataConsumerCreateWithCFData(data);
CGContextRef pdfContext = CGPDFContextCreate (consumer, &pageRect, NULL);
// Create PDF Page
CGContextBeginPage (pdfContext, &pageRect);
// Draw PDF into pdfContext
// [INSERT CODE HERE]
// end draw PDF
CGContextEndPage (pdfContext);
// Close PDF
CGPDFContextClose(pdfContext);
CGContextRelease (pdfContext);
// Use data
CFRelease(data);

I stumbled on three problems on the iPhone during the drawing phase:

  1. Drawing images
  2. Drawing text
  3. Drawing from other PDF files

Drawing images
It took a while for me to realize it but when I used an UIImage from e.g. a JPEG file it seems like the JPEG source is embedded in the PDF file. That is a good thing because it allows us to control compression of images embedded in PDF files. It is also important if you create your own images. E.g. If a user uses a filter in Lifecards I create my own UIImage with code that looks like this:

CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceUserRGB);
CGContextRef offscreenContext = CGBitmapContextCreate(filterImageBuffer, width, height, 8, rowBytes, rgbColorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
// Begin drawing code
// [INSERT DRAWING CODE]
// End drawing code
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, filterImageBuffer, rowBytes * height, NULL);
CGImageRef cgImage = CGImageCreate(width, height, 8, 32, rowBytes, rgbColorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big, dataProvider, NULL, false, kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
CGColorSpaceRelease(rgbColorSpace);
UIImage * newUIImage = [[UIImage alloc] initWithCGImage:cgImage];
CGImageRelease(cgImage);

This code creates a new UIImage newUIImage which is uncompressed. We can create a UIImage with jpeg compression as source as follows:

// Create compressed
// Note: data here is auto-released
NSData * data = UIImageJPEGRepresentation(newUIImage, 0.95);
UIImage * compressedImage = [[UIImage alloc] initWithData:data];

Then whenever we wish to draw the image in the PDF file we simply draw the compressed image instead of the uncompressed one:

[compressedImage drawInRect:CGRectMake(0,0,width,height)]

This way the jpeg compressed version is used instead and it will greatly reduce the size of your PDF files. Of course, newImage could just be your source image if that is also a UIImage.

Note: One thing which is very important is not to release the compressed image until you finish the PDF page. Quartz needs the images when it finalizes the current PDF page for some reason. It doesn't seem like the image is expanded in memory at that time, but I could be wrong since I used the Leaks application and activity monitor to tell.

I should note that I never did find any documentation on this subject so some of this is speculation but it looks like it works.

Drawing text

The other problem I had was drawing text. I use the well-known NSString extension in UIKit:

drawInRect:withFont:

Whenever I used that I got some errors on the console of the form

can't get CIDs for glyphs for 'ArialMT'.
CGFont/Freetype: The function 'get_subset_format' is currently unimplemented

As far as I can tell those errors are related to the fact that the iPhone is unable to embed fonts in the PDF-files.

At one point I thought that I could get the MacRoman parts (non unicode characters) of fonts embedded with a couple of tricks, but when I returned to look at the code after a couple of days I was unable to reproduce those results. So there may or may not be a way to do it.

Anyway, being a man of the world I was more interested in being able to include unicode text. It works directly using the NSString extension, but it returns those ugly error messages, and the fonts used must be installed in the operating system that you use to view the PDF files. Since most Windows installations does not include Georgia, Marker Felt, Zapfino, etc., you pretty much exclude the use of those fonts.

There is another way though, which requires the use of an undocumented function:

extern "C" bool CGFontGetGlyphsForUnichars(CGFontRef, unichar[], CGGlyph[], size_t);

When I first tried to use it from my C++ code I forgot the extern "C" bit, so remember that. The function is somewhere in the public API and while it is available on the desktop versions of Mac OS X, it seems like Apple forgot to include it in the header files for the iPhone.
Basically this function can give you glyphs that you need to draw the text directly. Glyphs are not directly characters. For instance a glyph can contain two characters if the font designer allows for a bit of overlap. It often happens with letters "f" and "i".

Before we can draw anything we need to create a CGFont of course:

CGFont cgfont = CGFontCreateWithFontName ((CFStringRef)fontName);
CGContextSetFont(cg, cgfont);
CGContextSetFontSize(cg, fontSize);

(here fontName is an NSString)

Now, if we wish to draw e.g. an NSString called word we begin by getting some info about the glyphs as follows:

int count = [word.s length];
unichar * buffer = new unichar[count+1];
CGGlyph * glyphs = new CGGlyph[count+1];
int * adv = new int[count+1];
CGRect * rects = new CGRect[count+1];
[word.s getCharacters:buffer];
CGFontGetGlyphsForUnichars(cgfont, buffer, glyphs, [word.s length]);
CGFontGetGlyphAdvances(cgfont,glyphs,count,adv);
CGFontGetGlyphBBoxes(cgfont, glyphs, count, rects);

In other words, we get the glyphs for the letters of the word and then we get the dimensions and advances of the individual glyphs. Note: I could not find any way to determine the number of glyphs for a word.

Now we are ready to draw the glyphs. We can do it by:

CGContextShowGlyphsAtPoint(outputCG, x, y, glyphs,count);

That's neat but it doesn't solve the whole font-embedding problem. However, we can solve that problem if get quartz to draw the text as vector elements instead of ... well... text. We simply change the way text is drawn using the function:


CGContextSetTextDrawingMode (outputCG, kCGTextClip);

This will use the text as a clipping path instead of simply drawing it. If we first call CGContextShowGlyphsAtPoint and then draw a filled rectangle around the area where the glyphs are shown, that rectangle will be clipped to the glyphs and essentially we are drawing the letters of the word. However, since we are drawing a rectangle and not text, the resulting PDF file will not include the text but instead a set of vector elements that look like text. Therefore Quartz no longer needs to embed the fonts in the PDF file. One way to do this is by the following loop:

double scale = ffontSize / double( CGFontGetUnitsPerEm(cgfont) );
for (size_t i = 0; i < count; ++i) {
CGContextSaveGState(outputCG);
CGContextShowGlyphsAtPoint(outputCG, x, p.y + spaceH, &glyphs[i], 1 );
CGContextBeginPath(outputCG);
CGContextAddRect(outputCG,
CGRectMake(x+scale* rects[i].origin.x , p.y + spaceH + scale*(rects[i].origin.y),
scale*(rects[i].size.width), scale*(rects[i].size.height)) );
CGContextFillPath(outputCG);
CGContextRestoreGState(outputCG);
x += adv[i] * scale;
}

Here I draw one glyph at a time by setting the clipping path to the individual glyph and drawing a filled rectangle behind it. I have of course set the current fill color to the color I want the text in. The beauty of this is that you could also have gradient fills and other neat effects that I hope to explore in the near future.

Note: Remeber to release the font when you are done:


CGFontRelease(cgfont);

PDF embedding

One of the cool things about quartz is its ability to draw from other PDF files. The code looks like this:

CGPDFDocumentRef document = GetPDFDocumentRef (filename);
CGPDFPageRef page = CGPDFDocumentGetPage (document, 1);
CGRect box = CGPDFPageGetBoxRect(page,kCGPDFArtBox);
CGRectMake(bounds.origin.x,bounds.origin.y,bounds.dimension.x,bounds.dimension.y),0,false);
CGContextSaveGState(context);
CGContextTranslateCTM(context, bounds.origin.x, bounds.origin.y);
CGContextScaleCTM(context, bounds.dimension.x/box.size.width, bounds.dimension.y/box.size.height);
CGContextTranslateCTM(context, -box.origin.x, -box.origin.y);
CGContextDrawPDFPage (context, page);
CGPDFDocumentRelease (document);
CGContextRestoreGState(context);

If you do this directly on the iPhone you'll experience a crash. From what I can tell, it comes from the fact that quartz doesn't retain the PDF document when you draw it, so you have to postpone
CGPDFDocumentRelease (document);
until you have called CGContextEndPage (pdfContext).

Epilogue

That's it for now. It may seem pretty straight-forward but realizing how to embed images and dealing with the whole font-embedding issues took me a couple of days of experimenting, so I hope I have made life easier for someone else out there!