Programmatically taking a screenshot on OS X

Browsing Embarcadero’s FireMonkey forum, I noticed an unanswered question about programmatically taking a screenshot on OS X. Having previously figured out how to paste a graphic from the OS X clipboard to a FireMonkey TBitmap (see here), I thought doing a screenshot couldn’t be any harder. As it turned out, it was perhaps a bit more convoluted, and certainly quite different to doing such a thing on Windows. Nonetheless, the process is fairly straightforward once you have all the various pieces to fit together.

In essence, there are two problems to solve: taking the actual screenshot, and getting the image taken into a TBitmap. With respect to the second step, the general approach must be to write the image data to a memory stream, which can then be read by a TBitmap. This was the method I used in the clipboard case, though the specifics in the screenshot one are quite different.

To actually take a screenshot in the first place, the CGWindowListCreateImage API function can be used. This is part of the Core Graphics API layer, which like Core Foundation has a straight C interface which tries to be as object oriented as is reasonably possible, to the extent a purely procedural interface can be. As a result, what CGWindowListCreateImage returns is an opaque pointer (CGImageRef) to an object that needs releasing after we’ve done with it. Unfortunately, there is no CFImage ‘method’ to directly extract the image bytes however: instead, we must create a CGDataConsumer object, to pass to CGImageDestination object, which should then have the CFImage ‘added’ to it before being ‘finalised’. The last step will cause the bitmap bytes to be written to the data consumer, which we connect to a TMemoryStream via a procedural callback. Small mercies there’s no need to write a Cocoa-style delegate class too, eh?

To see this all in practice, create a new FireMonkey HD application, and add to the form a TButton and a suitable sized TImageControl. Caption the button ‘Take Screenshot’, then head for the form’s PAS file. Here, add the following as the implementation section’s uses clause:

  Macapi.CoreFoundation, Macapi.CocoaTypes, Macapi.CoreGraphics, Macapi.ImageIO;

First thing we’ll implement is the code to output the image data held by a CGImageRef to a TStream:

function PutBytesCallback(Stream: TStream; NewBytes: Pointer;
  Count: LongInt): LongInt; cdecl;
  Result := Stream.Write(NewBytes^, Count);

procedure ReleaseConsumerCallback(Dummy: Pointer); cdecl;

procedure WriteCGImageToStream(const AImage: CGImageRef; AStream: TStream;
  const AType: string = 'public.png'; AOptions: CFDictionaryRef = nil);
  Callbacks: CGDataConsumerCallbacks;
  Consumer: CGDataConsumerRef;
  ImageDest: CGImageDestinationRef;
  TypeCF: CFStringRef;
  Callbacks.putBytes := @PutBytesCallback;
  Callbacks.releaseConsumer := ReleaseConsumerCallback;
  ImageDest := nil;
  TypeCF := nil;
  Consumer := CGDataConsumerCreate(AStream, @Callbacks);
  if Consumer = nil then RaiseLastOSError;
    TypeCF := CFStringCreateWithCharactersNoCopy(nil, PChar(AType), Length(AType),
      kCFAllocatorNull); //wrap the Delphi string in a CFString shell
    ImageDest := CGImageDestinationCreateWithDataConsumer(Consumer, TypeCF, 1, AOptions);
    if ImageDest = nil then RaiseLastOSError;
    CGImageDestinationAddImage(ImageDest, AImage, nil);
    if CGImageDestinationFinalize(ImageDest) = 0 then RaiseLastOSError;
    if ImageDest <> nil then CFRelease(ImageDest);
    if TypeCF <> nil then CFRelease(TypeCF);

When extracting image data, we need to request a specific graphic type via an appropriate ‘Uniform Type Identifier’. PNG is an appropriate default since it will definitely be available, and is lossless.

Now we need to implement the routine to actually take the screenshot:

procedure TakeScreenshot(Dest: TBitmap);
  Screenshot: CGImageRef;
  Stream: TMemoryStream;
  Stream := nil;
  ScreenShot := CGWindowListCreateImage(CGRectInfinite,
    kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowImageDefault);
  if ScreenShot = nil then RaiseLastOSError;
    Stream := TMemoryStream.Create;
    WriteCGImageToStream(ScreenShot, Stream);
    Stream.Position := 0;

As the number of arguments passed to CGWindowListCreateImage indicates, the API is quite flexible. We’ll just take a straight screenshot however.

If you now try to compile the code, you’ve find it barfs at CGRectInfinite – while all the functions and types we require are translated by the stock Macapi.* units, the CGRectInfinite constant is missing for some reason. So, add it like this:

{$IF NOT DECLARED(CGRectInfinite)}
  CGRectInfinite: CGRect = (origin: (x: -8.98847e+30; y: -8.98847e+307);
    size: (width: 1.79769e+308; height: 1.79769e+308));

Lastly, handle the button’s OnClick event as the following, where imgDest is the name of the image control:

procedure TfrmMacScreenshot.btnTakeScreenshotClick(Sender: TObject);

Run the application, and clicking the button should output a screenshot to the control:

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s