Saturday, June 14, 2008

Vectored PICT to PDF conversion in your code

You should not be creating any PICT files, but longtime Mac users might have a large number of .pict or .pct files lying around, and some (all?) of the tools Apple provides to open such files and convert them to PDF do a horrid job of it. This applies only to vectored PICTs, as bitmapped PICTS will look bad regardless.


First I refer you to this screen capture of a simple AppleWorks drawing:

You will notice that it looks a little archaic with it's lack of anti-aliasing, but that's what people had in the day.

I copied and pasted this image into TextEdit.app, saved the document as RTFD and then used the "View Contents" item in the Finder contextual menu to get to an actual old fashioned .pict file. This is how hard it is these days to generate a PICT file.


Just to see how not to do this, open the .pict file in OS X Preview.app, zoom in a bit, and you will see that however Preview is rendering PICTs it is doing a pretty poor job of it. I'm guessing it is using QuickTime import to create a bitmapped version.

Tell Preview to save as a PDF and you get this mess:


The basic problem here is that Apple provides an API for rendering a PICT into a Quartz context (and thus into PDF) which preserves the vectored nature of the original and some applications do not use it. If you use this API, your onscreen representation of PICT files will be as good as they can be, and you will be able to export them to comparatively nice PDF files. This does not mean they will look as good as PDFs which had been created from the ground up as such; PICTs lack of support for transparency, Bezier curves, fractional coordinate systems and rotated text make that impossible. But it will look a lot nicer.


QDPictRef LoadPICTFromURL(const CFURLRef& pictFileLocation)
{// warning, I did not actually compile or test this code
QDPictRef result = 0;
CGDataProviderRef dataProvider = CGDataProviderCreateWithURL(pictFileLocation);
result = QDPictCreateWithProvider(dataProvider);
CFRelease(dataProvider);

return result;
}

Look around the header files for the QDPictToCGContext.h header and you will find:

QDPictDrawToCGContext( CGContextRef ctx, CGRect rect, QDPictRef pictRef);


Then you can use the resulting QDPictRef to draw into a Quartz context, and if that CGContextRef was created via a call to CGPDFContextCreate, then you have created as nice a copy of the original PICT as is possible as seen by this screen shot at 200% zoom:

and this PDF result:


Note that rotated text still looks horrible, as the only way to make QuickDraw draw rotated text onscreen was to draw into an offscreen bitmap and then rotate the pixels in the bitmap. It was possible, however, to insert a series of PicComments which inserted rotated text into a PICT. I've checked this out with PICTs created by a separate application, and the Apple PICT to PDF converter honors these comments. I guess AppleWorks just didn't bother to put them in. [Update: Paragraph rewritten to add extra info, and to hide temporary idiocy on my part.]

Also check out the little bump in the arrow heads, probably a glitch that went unnoticed in the non-antialiased original caused by drawing the shaft too long and not quite on center. Otherwise, the new antialiased version looks much nicer. And you can even select and copy the (non-rotated) text right here in the browser.

It's not unusual, but unfortunate that Apple is inconsistent in using its own API. Preview.app obviously does not, nor does the PICT CoverFlow plugin, but TextEdit.app appears to, resulting in the oddity of a PICT in a CoverFlowed RTFd document looking much better than the CoverFlow of the PICT file itself.

Regardless, I recommend that anybody with a large collection of vectored PICTs make PDF copies of them as there may come a version of Mac OS X which will not have any support for PICTs at all. For instance, I doubt you can view PICTs on an iPhone. Warning: see my other posts about how PDFs do not contain the extra data which allows the originating application to recreate the original document. So keep the originals around too.

[Update: I've corresponded with Thorsten Lemke, proprietor of LemkeSoft and creator of the well known Graphics Converter application. He immediately saw the value in incorporating improved Quickdraw picture processing and conversion in his product and version 6.11 and higher (including this morning's beta) will feature it. I envy the nimbleness of independent software vendors. I can't tell you how long my day job company takes getting a minor release out to our customers.]

[Update: here is the source for a simple automator plugin I threw together to make the conversion.

#import "QuickDrawPictureToPDFExporter.h"

QDPictRef LoadPICTFromURL(const CFURLRef pictFileLocation)
{
QDPictRef result = 0;
CGDataProviderRef dataProvider = CGDataProviderCreateWithURL(pictFileLocation);
result = QDPictCreateWithProvider(dataProvider);
CFRelease(dataProvider);
return result;
}

@implementation QuickDrawPictureToPDFExporter

CGContextRef CreatePDFContext(const CGRect mediaRect, CFMutableDataRef theData)
{
CGContextRef result = 0;
if(theData != 0)
{
CGDataConsumerRef theConsumer =CGDataConsumerCreateWithCFData(theData);
if(theConsumer != 0)
{
result = CGPDFContextCreate(theConsumer, &mediaRect, NULL);
CGDataConsumerRelease(theConsumer);
}
}
return result;
}

- (id)runWithInput:(id)input fromAction:(AMAction *)anAction error:(NSDictionary **)errorInfo
{
NSMutableArray* output = [NSMutableArray array];

if (![input isKindOfClass:[NSArray class]])
{
input = [NSArray arrayWithObject:input];
}

NSEnumerator *enumerator = [input objectEnumerator];
NSString* aPath =nil;

while (aPath = [enumerator nextObject])
{
NSURL *inputURL = [NSURL fileURLWithPath:aPath];
QDPictRef aPictRef = LoadPICTFromURL((CFURLRef) inputURL);
BOOL drawn = NO;
CFMutableDataRef theData = 0;
if(aPictRef)
{
CGRect mediaRect = QDPictGetBounds(aPictRef);

theData =CFDataCreateMutable(NULL, 0);
CGContextRef cgContext = CreatePDFContext(mediaRect, theData);
if(cgContext != 0)
{
CGContextBeginPage(cgContext, &mediaRect);
CGContextSaveGState(cgContext);
QDPictDrawToCGContext(cgContext,mediaRect,aPictRef);

CGContextEndPage(cgContext);
CGContextRestoreGState(cgContext);
CGContextFlush(cgContext);
drawn = YES;
CGContextRelease(cgContext);
}
QDPictRelease(aPictRef);
}
else
{
NSString *errorString = NSLocalizedString(@"Picture to PDF could not read the input file as a PICT.",
@"Could not read input file");
*errorInfo = [NSDictionary dictionaryWithObjectsAndKeys: [errorString autorelease],
NSAppleScriptErrorMessage, nil];
}
if(drawn && theData)
{
NSString* outputPath = [[aPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"pdf"];

if (![(NSData*)theData writeToFile:outputPath atomically:YES])
{
NSString *errorString = NSLocalizedString(@"Picture to PDF could not could not create output file.",@"Couldn't write data to file");
*errorInfo = [NSDictionary dictionaryWithObjectsAndKeys: [errorString autorelease], NSAppleScriptErrorMessage, nil];
}
else
{
[output addObject:outputPath];
}
}
else
{
NSString *errorString = NSLocalizedString(@"Picture to PDF could not draw the File.",
@"Could not draw output file");
*errorInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[errorString autorelease], NSAppleScriptErrorMessage, nil];

}
if(theData)
{
CFRelease(theData);
theData = 0;
}
}
return output;
}

@end

]