2006/02/25

Quartz Musings (II): PDF Explorations

In the previous post, I explored various ways of drawing simple shapes with CoreGraphics in a window. I was mainly talking about what is possible to do with a CoreGraphics context. But how do we get such a context ?

My application is a small AppKit-based application, so I just have to defined my custom view, something inheriting from the generic NSView class, and define my own "draw" method:

 NSGraphicsContext* nsctx = [NSGraphicsContext currentContext];
 CGContextRef context = [nsctx graphicsPort];
     myCustomDrawingInContext(context);

AppKit maintains a notion of what is the current context, so I just have to get my raw CoreGraphics CGContextRef from that. Easy. But CoreGraphics offers more: an OpenGL context, a bitmap context for offscreen drawing, or a PDF context.

PDF generation for free

Let's play with the PDF context. It's a bit more complicated to setup than a simple onscreen context, but actually very straightforward.

 CGContextRef context;
 CGRect bounds = CGRectMake(0,0,500,500);
 context = CGPDFContextCreateWithURL(myFilePathURL, &bounds , NULL);
 CGContextBeginPage (context, &bounds);

 myCustomDrawingInContext(context);
 CGContextEndPage (context);
 CGContextRelease (context);
CGPDFContext actually defines additional PDF-specific functions to handle pages, but also to setup a hyperlink for a given zone, or include author, title, output intent metadata in the PDF file. What is really interesting here is that I don't have to change a single line to my drawing code, my PDF context is a specialized instance of a generic CoreGraphics context, so the various drawing techniques should still work. And it's fully vector-based, look at the closeup.

Let's look at the generated PDF for the 'naive' drawing code (omitting the grid drawing for concision, just keeping the ellipse part):

%PDF-1.3
2 0 obj
<< /Length 4 0 R >>
stream
q Q q /Cs1 cs 0 0.5 0 sc 25 62.5 m 25 62.5 l 25 69.403557 19.403561
75 12.5 75 c 5.5964403 75 0 69.403557 0 62.5 c 0 55.596439 
5.5964403 50 12.5 50 c 19.403561 50 25 55.596439 25 62.5 c 25 
62.5 25 62.5 25 62.500004 c h 25 112.5 m 25 112.5 l 25 119.40356 
19.403561 125 12.5 125 c 5.5964403 125 0 119.40356 0 112.5 c 0 
105.59644 5.5964403 100 12.5 100 c 19.403561 100 25 105.59644
[...]
What do we have here ? 2 0 obj tells that we're going to define an object whose identify is 2 0. Within the stream (it is actually a bit more complicated as the stream is Zlib encoded, but I show here the decoded stream), we found a big list of PDF instructions. For instance /Cs1 cs specifies the colorspace (referenced by /Cs1), sc is the stroking color. m is a "move to" instruction and l is a "line to". c defines a Bezier curve, and this continues for lines and lines. So apparently this defines drawing for all our ellipses. Neat.

Remember the CGLayer technique ? We were constructing the ellipse in a 'layer' context and then stamping the layer in the drawing context for better performance, to avoid drawing the ellipse again and again. Let's give a look to the CGLayer-based PDF.

%PDF-1.3
2 0 obj
<< /Length 4 0 R >>
stream
q Q q Q q 0 50 25 25 re W n q 1 0 0 1 0 50 cm /Fm1 Do Q Q q Q q 
0 100 25 25 re W n q 1 0 0 1 0 100 cm /Fm1 Do Q Q q Q q 0 175 
25 25 re W n q 1 0 0 1 0 175 cm /Fm1 Do Q Q q Q q 0 200 25 25 
re W n q 1 0 0 1 0 200 cm /Fm1 Do Q Q q Q q 0 225 25 25 re W n 
q 1 0 0 1 0 225 cm /Fm1 Do Q Q q Q q 0 275 25 25 re W n q 1 0 
0 1 0 275 cm /Fm1 Do Q Q q Q q 0 300 25 25 re W n q 1 0 0 1 0
300 cm /Fm1 Do Q Q q Q q 0 375 25 25 re W n q 1 0 0 1 0 375 
cm /Fm1 Do Q Q
[...]
What's different here ? cm is a coordinate transformation operator, re defines a rectangle, Q and q pop and push the graphics state, and we have this strange /Fm1 Do.

/Fm1 is actually an external object - XObject in PDF speak - something defined outside of the current stream. If we look further into this PDF file, we'll found:

3 0 obj
<< /ProcSet [ /PDF ] /XObject << /Fm1 5 0 R >> >>
endobj
5 0 obj
<< /Length 416 0 R /Type /XObject /Subtype /Form /FormType 1 /BBox
[0 0 25 25] /Resources 6 0 R >>
stream
q Q q /Cs1 cs 0 0.5 0 sc 25 12.5 m 25 12.5 l 25 19.403561 19.403561 
25 12.5 25 c 5.5964403 25 0 19.403561 0 12.5 c 0 5.5964403 
5.5964403 0 12.5 0 c 19.403561 0 25 5.5964403 25 12.5 c 25 
12.500001 25 12.500002 25 12.500002 c h f* Q
endstream
endobj
This snippet is in two parts: the first one defines the name /Fm1 and tells us that it is actually the object 5 0 (defined immediately after). This object 5 0 is quite interesting. It's defined as a "form" (more about that later) XObject, a bounding box for this object is present (0 0 - 25 25), and what do we found in the definition (the stream section): a suite of m, l, c, the same move-to, line-to, Bezier curve operator we discovered in the previous file. But this time, the stream is really short. So apparently, Quartz is doing the right thing here: defining the ellipse into this Fm1 object, and reusing it all over the place with the /Fm1 Do which will cause the redrawing of the Fm1 definition in place.

Let's check the PDF specification for confirmation.

A form XObject is a self-contained description of any sequence of graphics objects (including path objects, text objects, and sampled images), defined as a PDF content stream. It may be painted multiple times—either on several pages or at several locations on the same page—and will produce the same output each time, subject only to the graphics state at the time it is invoked. Not only is this shared definition economical to represent in the PDF file, but under suitable circumstances, the PDF viewer can optimize execution by caching the results of rendering the form XObject for repeated reuse.

Conclusion ? Quartz does the right thing. When I use a CGLayer in my CoreGraphics code to use a bit of caching, if I'm using a window graphic context, CoreGraphics will cache the drawing and paste it where needed, but if I'm using a PDF graphic context, the (vector-based) drawing definition is cached and the correct PDF abstraction is used to store and reference it: a XObject. And for instance, an OpenGL-based context could upload the CGLayer rendering to a texture in VRAM and reuse it directly from there on the GPU.

Rendering PDF

Just for fun, I adapted the PDF context code to actually use it as a renderer onscreen. Three steps:

Here is the code (slightly edited for lisibility):

  CGContextRef context;
  CGDataConsumerRef datacon;
  NSMutableData* data;
  
  data = [[[NSMutableData alloc] init] autorelease];

  datacon = CGDataConsumerCreateWithCFData((CFMutableDataRef)data);
  context = CGPDFContextCreate(datacon,&bounds,NULL);
    
  CGContextBeginPage (context, &bounds);

  /* My drawing code, using 'context' */

  CGContextEndPage (context);
  
  CGContextRelease (context);
  CGDataConsumerRelease(datacon);

  [self setDocument:[[[PDFDocument alloc] initWithData:data] autorelease]]; 
I was surprised to obtain almost 20fps, with this highly inefficient way to animate.

Comments: Post a Comment

<< Home

This page is powered by Blogger. Isn't yours?