[NB: this post was written in ignorance of the interface changes involved in the soon-to-be-released XE3. Unfortunately, these changes broke the hack outlined below. However, the demo linked to has been updated to work in both XE2 and XE3 RTM. I shan’t say more than that, since who knows what any XE3 update will bring, let alone XE4…]
In a previous post I walked through how to create a simple PDF viewer in Delphi XE2 using the OS X API. Recently, I got a follow up question about using the API to create a PDF, and more particularly, create a PDF by drawing to a FMX TCanvas.
In principle this shouldn’t be too hard to do, since OS X allows you to write a PDF using exactly the same drawing functions used to draw to the screen or a printer. In the jargon of the API, those functions work against a refrence to an arbirary ‘graphics context’ (CGContextRef), just like how the drawing functions of the Windows API work against a handle to an arbitrary ‘device context’ (HDC). And, just as a VCL TCanvas is essentially a wrapper round a native HDC, so a FMX/Mac TCanvas is essentially a wrapper round a native CGContextRef. All we need do, then, is create a standalone TCanvas and assign its Handle property appropriately. Simples!
Alas, but regular readers will know what comes next – the FMX TCanvas does not actually have a Handle property! As I’ve said many times previously on this blog, while the VCL is very friendly towards native interop, the flaming ape takes great offence at the very thought. (*) As before then, we need to find an
ugly hack workaround.
If you look at the FMX source, you’ll find that a TCanvas can be initialised with either a TCustomCommonForm descendant, a TBitmap or a TPrinter. Unfortunately, the code in the last two cases could do with a bit of spring cleaning – it would seem bitmap and printer canvases need some special handling, but instead of TCanvas providing suitable callback opportunities, it performs that special handling itself. However, when a canvas is told to get its CGContextRef from a form, the Mac code at least makes no special assumptions. Even better, the mechanism by which a TCanvas gets the CGContextRef for a form is eminently hijackable, since the value is simply presented to the canvas by the form’s ContextHandle property… and this property is read/write with no setter method potentially getting in the way.
Given this, it would appear we could do the following:
- Create a PDF graphics context (the API function to call for this is either CGPDFContextCreateWithURL or CGPDFContextCreate).
- Create a dummy form, and assign its ContextHandle property to the value just returned by CGPDFContextCreateXXX.
- Instantiate a standalone TCanvas, passing its constructor a reference to the dummy form.
- Draw to the canvas as normal.
- Release the PDF context (CGContextRelease).
And indeed, this procedure does actually work.
Since the Core XXX API code on OS X can a be a little bit fiddly (is there any iFan out there who will defend CFRetain deliberately crashing when passed a nil pointer?), I’ve wrapped all the necessary code in a small unit, CCR.MacPDFWriter, that you can find on Google Code here. The unit exposes a TPDFWriter base class and two concrete descendants, TPDFFileWriter for writing straight to a new PDF file, and TInMemoryPDFWriter for writing to an internal memory block that you can subsquently read off either to a stream, a file, or a TBytes dynamic array. The two descendant classes wrap CGPDFContextCreateWithURL and CGPDFContextCreate respectively, though most of the code is in the base class.
Alongside the unit itself I’ve also written a small demo project. This is composed of a single form with a tab control and a ‘Create PDF File’ button. The tab control itself has two tabs, one for a cover page and one for a page of text:
The cover page is a TImage and a couple of TText controls all rooted to a base TLayout. When the PDF is being created, outputting page one is therefore just a matter of asking the TLayout to draw itself onto the TPDFFileWriter’s canvas. In the case of the second page, in contrast, text is written out explicitly using the canvas’ FillText method, with the page margins being determined by a mixture of TRectangle and TSplitter controls on the tab. Here’s what the complete code for the button’s OnClick handler looks like:
procedure TfrmPDFWriter.btnCreatePDFClick(Sender: TObject); var R: TRectF; Writer: TPDFFileWriter; begin Writer := TPDFFileWriter.Create; try Writer.FileName := ExpandFileName('~/Documents/Test.pdf'); //assign some metadata Writer.Author := txtAuthor.Text; Writer.Creator := ExtractFileName(GetModuleName(0)); Writer.Keywords.Add('test'); Writer.Keywords.Add('demo'); Writer.Subject := 'My first PDF file'; Writer.Title := txtTitle.Text; Writer.OwnerPassword := 'SuperSecretPassword'; Writer.AllowPrinting := False; //set the page size Writer.Width := lyoCoverPage.Width; Writer.Height := lyoCoverPage.Height; //begin writing the document, and output the cover page Writer.BeginDoc; lyoCoverPage.PaintTo(Writer.Canvas, lyoCoverPage.ClipRect); Writer.NewPage; //after calculating the margins, write the text R := TMemoAccess(memContent).ContentRect; R.Offset(memContent.Position.X, memContent.Position.Y); Writer.Canvas.Fill.Assign(memContent.FontFill); Writer.Canvas.Font.Assign(memContent.Font); Writer.Canvas.FillText(R, memContent.Text, True, 1, , memContent.TextAlign, TTextAlign.taLeading); //finish up Writer.EndDoc; finally Writer.Free; end; end;
As you can see, I’ve written the TPDFWriter interface to broadly follow the model set by TPrinter. Thus, first you assign properties to set metadata and the page size, secondly call BeginDoc, thirdly issue drawing commands to the canvas interspersed with NewPage calls, before finally finishing things off with a call to EndDoc.
Once run, the code just presented creates a PDF like the following (you can’t see it, but the keywords are listed a click away, and Preview respects the printing ‘restriction’ too):
I’ve included all the code amongst the source for the FMX TClipboard and Mac Preferences API wrapper I blogged about recently – check it all out on Google Code here, or enter this for the SVN URL to download.
(*) PS: when I complain about FMX’s poor native interop, I’m not calling for ‘native controls’ as some people have, since native controls would mean radically transforming FMX into something quite different to what it is in XE2. Rather, I’m asking for hooks to be added for the native functionality that is already being wrapped by the framework. Put bluntly, the fact you can do things like paint a FMX control hierarchy onto a PDF graphics context via a single TCanvas call is pretty neat, and not something the framework designers should be looking to forestall – yet forestalling is exactly what they are doing when they hide away almost all platform-specific things inside their units’ implementation sections.