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:

uses
  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;
begin
  Result := Stream.Write(NewBytes^, Count);
end;

procedure ReleaseConsumerCallback(Dummy: Pointer); cdecl;
begin
end;

procedure WriteCGImageToStream(const AImage: CGImageRef; AStream: TStream;
  const AType: string = 'public.png'; AOptions: CFDictionaryRef = nil);
var
  Callbacks: CGDataConsumerCallbacks;
  Consumer: CGDataConsumerRef;
  ImageDest: CGImageDestinationRef;
  TypeCF: CFStringRef;
begin
  Callbacks.putBytes := @PutBytesCallback;
  Callbacks.releaseConsumer := ReleaseConsumerCallback;
  ImageDest := nil;
  TypeCF := nil;
  Consumer := CGDataConsumerCreate(AStream, @Callbacks);
  if Consumer = nil then RaiseLastOSError;
  try
    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;
  finally
    if ImageDest <> nil then CFRelease(ImageDest);
    if TypeCF <> nil then CFRelease(TypeCF);
    CGDataConsumerRelease(Consumer);
  end;
end;

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);
var
  Screenshot: CGImageRef;
  Stream: TMemoryStream;
begin
  Stream := nil;
  ScreenShot := CGWindowListCreateImage(CGRectInfinite,
    kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowImageDefault);
  if ScreenShot = nil then RaiseLastOSError;
  try
    Stream := TMemoryStream.Create;
    WriteCGImageToStream(ScreenShot, Stream);
    Stream.Position := 0;
    Dest.LoadFromStream(Stream);
  finally
    CGImageRelease(ScreenShot);
    Stream.Free;
  end;
end;

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)}
const
  CGRectInfinite: CGRect = (origin: (x: -8.98847e+30; y: -8.98847e+307);
    size: (width: 1.79769e+308; height: 1.79769e+308));
{$IFEND}

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

procedure TfrmMacScreenshot.btnTakeScreenshotClick(Sender: TObject);
begin
  TakeScreenshot(imgDest.Bitmap);
end;

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

Leave a comment