Friday, January 9, 2009

PDF to JPEG conversion

Various PDFs collected from around the net would be better off as individual image files. You'd think there'd be a standard tool to convert them but I couldn't find any at a price point I was interested in. Fortunate OSX Python has access to CoreGraphics which can do the heavy lifting.

import sys,re,os,os.path
from CoreGraphics import *

def doit(pdfname):
  if not".pdf$",pdfname): return
  print pdfname
  dirname = re.sub(".pdf$","",pdfname)
     print "Can't create directory '%s'"%(dirname)
  pdf = CGPDFDocumentCreateWithProvider(CGDataProviderCreateWithFilename(pdfname))
  cs = CGColorSpaceCreateDeviceRGB()
  bg = CGFloatArray(5)       # create's an array of 5 0's which is good enough for me
  for i in range(1, pdf.getNumberOfPages() + 1):
     page = pdf.getPage(i)
     r = page.getBoxRect(kCGPDFMediaBox)
     h = r.getHeight()
     w = r.getWidth()
     del page

     #c = CGBitmapContextCreateWithColor(int(w), int(h), cs, (0,0,0,0))
     c = CGBitmapContextCreateWithColor(int(w), int(h), cs, bg)
     c.writeToFile(os.path.join(dirname, "page%04d.jpg"%i),kCGImageFormatJPEG)
     del c
  del cs
  del pdf

if __name__=='__main__':
  for a in sys.argv[1:]: doit(a)
The original version of this script was broken by Snow Leopard (which upgraded Python to 2.6.1). The call to CGBitmapContextCreateWithColor() failed with an error message about the 4th argument which it seems to think shouldn't be a 'const float[5]'.
The solution is to pass in a CGFloatArray() object instead. I haven't been able to modify one of those, but the default thats produced when you use 'bg = CGFloatArray(5)' appears to be good enough. Those objects still look leaky as hell but what are ya gonna do?
Squirrel:~ jeff$ python
Python 2.6.1 (r261:67515, Jul  7 2009, 23:51:51)
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from CoreGraphics import CGFloatArray
>>> a = CGFloatArray(5)
>>> print repr(a)
<CoreGraphics.CGFloatArray; proxy of <Swig Object of type 'CGFloatArray *' at 0x2287a0> >
>>> print repr(a[0])
swig/python detected a memory leak of type 'CGFloat *', no destructor found.
<Swig Object of type 'CGFloat *' at 0x224d10>


  1. There's an unmatched bracket on line 25. I've tried the script, but it does nothing but create a folder. If I comment out the return statement, it works.
    I'd like to include a modified version of this in a suite of python scripts that leverage Quartz to do stuff. Would that be ok? I've recently posted to the Quartz Dev mailing list, if that helps. Ben BW

    1. Thanks for that - I'm surprised at it since I'm pretty sure I cut/paste that in from my working script. I must have been messing around with the try/catch stuff.

      Feel free to include this in anything you like - any code I publish here can be considered to be in the public domain.

    2. Ah, no, I realise what's happened. Its a cut/paste indentation error. The return should only have happened in the except.

  2. I'm enjoying learning python and using Quartz, but I'm also infuriated by it! Making sure the objects and methods have all the right bits is a nightmare.
    The other problem is that Apple keep deprecating stuff, so most of the code examples contain things that will break or don't work.
    The problem with the FloatArray set to 0 is that it produces TIFFs with a transparent background. I've tried adding a white rectangle using:
    CGContextSetFillColorWithColor (c, kCGColorWhite)
    CGContextFillRect (c, CGRectMake (0, 0, w, y ))
    But get a variety of errors. (Also, it seems I have to import Quartz AND CoreGraphics!! Various things remain undefined if I only import one or other.)
    Also writeToFile is deprecated, and you're supposed to use CGImageDestination. I'm trying to create 300dpi TIFFs, which is difficult.
    The scripts that I've produced so far (admittedly, mostly a garland of other people's flowers) can be found here:

    1. To be honest, I have not used this script in years, I really only needed it way back when, so I can't guarantee it will continue to work into the future.

      The problem with using CoreGraphics via Python is that you are perilously close to the metal with these methods, talking to swig-generated wrappers rather than hand-crafted code, hence the weird leak message I described above.

      Any time I'm tempted to do stuff like this these days, I knock up a app using my custom embedded framework that allows the app complete control over *how* it exposes the internal APIs to Python.