Skip to content

Copying and pasting the contents of a FireMonkey TBitmap

6 October, 2011

One of the various things not yet implemented in FireMonkey is TClipboard. In FMX.Platform you can find routines for getting and setting text on the clipboard (using Variant arguments – I’ve no idea either), but nothing beyond that. On the forums, Mehmed Ali Caliskan posted some code (apparently inspired by those FMX.Platform methods) for saving the contents of a TBitmap to the clipboard on the Mac. Starting with that, I’ve come up with some routines for copying and pasting FMX bitmap contents to the clipboard on both OS X and Windows. These routines have the following signatures:

function CanPasteBitmapFromClipboard: Boolean;
procedure CopyBitmapToClipboard(Bitmap: TBitmap);
function PasteBitmapFromClipboard(Bitmap: TBitmap): Boolean;

Before I present the code, it is worth taking a step back to highlight how TBitmap in FireMonkey covers the ground of both TPicture and TBitmap in the VCL. As a result, instead of having different TGraphic descendants for different sorts of bitmap formats (TBitmap for BMP, TJpegImage for JPEG, TPngImage for PNG, TWicImage for TIFF), there’s just TBitmap which can load from – and save to – a range of formats. Another difference with the VCL is that there’s no FMX.Graphics unit – instead, both TBitmap and TCanvas are declared in FMX.Types.

OS X

The first task is to identify the native clipboard API. On OS X, this means working with the NSPasteboard object for the ‘general pasteboard’ (OS X actually defines more than one system clipboard, but the general one is the one we want). On my admittedly brief inspection, the NSPasteboard class is conceptually pretty similar to the IDataObject interface on Windows – it’s a bit higher level, but not much, and there’s an intertwining of clipboard and drag and drop functionality common to both.

We can get a reference to the general pasteboard like this:

uses
  Macapi.ObjectiveC, Macapi.CoreFoundation, Macapi.CocoaTypes, Macapi.Foundation, Macapi.AppKit;

function GeneralPasteboard: NSPasteboard; inline;
begin
  Result := TNSPasteboard.Wrap(TNSPasteboard.OCClass.generalPasteboard);
end;

To poll for what’s available to paste, call the availableTypeFromArray method. This is passed an NSArray of NSString objects containing the system-defined identifiers for the ‘representation types’ you are interested in – if none is matched, nil is returned, otherwise the first match is.

While you don’t have to break out the needed code into a series of helper functions, I think it makes things clearer if you do. So, first we’ll have a function to construct an NSArray from an arbitrary group of NSObject instances:

function NSObjectArray(const Objs: array of NSObject): NSArray;
var
  Counter: Integer;
  Obj: NSObject;
  Ptrs: array of Pointer;
begin
  SetLength(Ptrs, Length(Objs));
  Counter := 0;
  for Obj in Objs do
    if Obj <> nil then
    begin
      Ptrs[Counter] := (Obj as ILocalObject).GetObjectID;
      Inc(Counter);
    end;
  Result := TNSArray.Wrap(TNSArray.OCClass.arrayWithObjects(Ptrs, Counter));
end;

One of docs I read mentioned the illegality of nil elements, which is why I check for them here.

Using NSObjectArray, we can easily write a function to return an array of the standard bitmap representation types:

function NSBitmapTypesArray: NSArray; inline;
begin
  Result := NSObjectArray([NSPasteboardTypePNG, NSPasteboardTypeTIFF]);
end;

Initially I was just specifying NSPasteboardTypePNG, but then found the Grab utility bundled with Snow Leopard writing only TIFF images to the clipboard general pasteboard. Since TBitmap can read both formats, we should therefore check for both.

Finally, we can write CanPasteBitmapFromClipboard as another one-liner:

function CanPasteBitmapFromClipboard: Boolean;
begin
  Result := (GeneralPasteboard.availableTypeFromArray(NSBitmapTypesArray) <> nil);
end;

For actual pasting, we can do this:

type
  TUserMemoryStream = class(TCustomMemoryStream)
    constructor Create(AMemory: Pointer; const ALength: NativeInt);
    function Write(const Buffer; Count: Longint): Longint; override;
  end;

constructor TUserMemoryStream.Create(AMemory: Pointer; const ALength: NativeInt);
begin
  inherited Create;
  SetPointer(AMemory, ALength);
end;

function TUserMemoryStream.Write(const Buffer; Count: Longint): Longint;
begin
  Result := 0;
end;

function PasteBitmapFromClipboard(Bitmap: TBitmap): Boolean;
var
  Data: NSData;
  DataType: NSString;
  Pasteboard: NSPasteboard;
  Stream: TCustomMemoryStream;
begin
  Pasteboard := GeneralPasteboard;
  DataType := Pasteboard.availableTypeFromArray(NSBitmapTypesArray);
  if DataType <> nil then Data := Pasteboard.dataForType(DataType);
  if Data = nil then Exit(False);
  Stream := TUserMemoryStream.Create(Data.bytes, Data.length);
  try
    Bitmap.Clear(0); //needed due to FMX bug - try pasting the same PNG multiple times
    Bitmap.LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
  Result := True;
end;

Instead of defining and using TUserMemoryStream, you could use a normal TMemoryStream and copy the data into it. I prefer to avoid needless copying of data if I can however.

For going the other way – i.e., assigning a bitmap to the general pasteboard – I’ve come up with the following:

procedure CopyBitmapToClipboard(Bitmap: TBitmap);
var
  Data: CFDataRef;
  Filter: TBitmapCodec;
  Pasteboard: NSPasteboard;
  Stream: TMemoryStream;
begin
  Data := nil;
  Stream := nil;
  Filter := DefaultBitmapCodecClass.Create;
  try
    Stream := TMemoryStream.Create;
    Filter.SaveToStream(Stream, Bitmap, 'png');
    Pasteboard := GeneralPasteboard;
    Pasteboard.declareTypes(NSObjectArray([NSPasteboardTypePNG]), nil);
    Data := CFDataCreateWithBytesNoCopy(nil, Stream.Memory, Stream.Size, kCFAllocatorNull);
    Pasteboard.setData(TNSData.Wrap(Data), NSPasteboardTypePNG);
  finally
    if Data <> nil then CFRelease(Data);
    Filter.Free;
    Stream.Free;
  end;
end;

Once more, I avoid data copying for data copying’s sake, this time by using the CFDataCreateWithBytesNoCopy function to construct a CFDataRef/NSData object that uses a ‘backing store’ provided on the Delphi side (like CFStringRef and NSString, CFDataRef and NSData are ‘toll-free bridged’, i.e. binary compatible).

The bit before the CFDataCreateWithBytesNoCopy call may need some explaining though: while TBitmap has its own SaveToStream method, using our own TBitmapCodec instance explicitly means we can guarantee a PNG image will be outputted – while the present implementation of TBitmap.SaveToStream will output a PNG image if an alpha channel is defined, it will output a JPEG if one isn’t. As an aside, the JPEG format is a poor default in my view given it is a ‘lossy’ one, but that’s the way it is. [Update 16/12/11: this was fixed in update 3 so that TBitmap.SaveToStream always outputs a PNG image.]

Windows

On Windows, implementing CanPasteBitmapFromClipboard is trivial:

uses
  Winapi.Windows;

function CanPasteBitmapFromClipboard: Boolean;
begin
  Result := IsClipboardFormatAvailable(CF_DIB);
end;

While we could check for other formats, that shouldn’t be necessary given DIB is the native display format, and so, essentially universal in a Windows context. In fact, it is so universal, we can assume it will be the pixel format used by TBitmap, avoiding the need to stream out:

procedure CopyBitmapToClipboard(Bitmap: TBitmap);
var
  BitsSize: Integer;
  MemObj: HGLOBAL;
  Ptr: PBitmapInfoHeader;
begin
  BitsSize := Bitmap.Width * Bitmap.Height * 4;
  MemObj := GlobalAlloc(GMEM_MOVEABLE and GMEM_DDESHARE, SizeOf(TBitmapInfoHeader) + BitsSize);
  if MemObj = 0 then RaiseLastOSError;
  Ptr := GlobalLock(MemObj);
  if Ptr = nil then
  begin
    GlobalFree(MemObj);
    RaiseLastOSError;
  end;
  //fill out the info header
  FillChar(Ptr^, SizeOf(Ptr^), 0);
  Ptr.biSize := SizeOf(TBitmapInfoHeader);
  Ptr.biPlanes := 1;
  Ptr.biBitCount := 32;
  Ptr.biCompression := BI_RGB;
  Ptr.biWidth := Bitmap.Width;
  if Ptr.biWidth <= 0 then Ptr.biWidth := 1;
  Ptr.biHeight := -Bitmap.Height;
  if Ptr.biHeight >= 0 then Ptr.biHeight := -1;
  //copy over the image bits
  Inc(Ptr);
  if BitsSize <> 0 then
    Move(Bitmap.StartLine^, Ptr^, BitsSize);
  GlobalUnlock(MemObj);
  //assign the completed DIB memory object to the clipboard
  OpenClipboard(0);
  try
    EmptyClipboard;
    if not SetClipboardData(CF_DIB, MemObj) then
    begin
      GlobalFree(MemObj);
      RaiseLastOSError;
    end;
  finally
    CloseClipboard;
  end;
end;

For PasteBitmapFromClipboard though, we do need to stream in since the source may not be a 32 bit colour DIB:

function PasteBitmapFromClipboard(Bitmap: TBitmap): Boolean;
var
  Header: TBitmapFileHeader;
  MemObj: HGLOBAL;
  Ptr: PBitmapInfoHeader;
  Stream: TMemoryStream;
begin
  Ptr := nil;
  Stream := nil;
  OpenClipboard(0);
  try
    MemObj := GetClipboardData(CF_DIB);
    if MemObj = 0 then Exit(False);
    Ptr := GlobalLock(MemObj);
    if Ptr = nil then Exit(False);
    FillChar(Header, SizeOf(Header), 0);
    Header.bfType := $4D42;
    Header.bfSize := SizeOf(Header) + GlobalSize(MemObj);
    Header.bfOffBits := SizeOf(Header) + Ptr.biSize;
    Stream := TMemoryStream.Create;
    Stream.WriteBuffer(Header, SizeOf(Header));
    Stream.WriteBuffer(Ptr^, Header.bfSize - SizeOf(Header));
    Stream.Position := 0;
    Bitmap.LoadFromStream(Stream);
    Result := True;
  finally
    if Ptr <> nil then GlobalUnlock(MemObj);
    CloseClipboard;
    Stream.Free;
  end;
end;

So, there you have it: code to read and write a FireMonkey TBitmap from and to the system clipboard on both OS X and Windows. You can get the complete unit and a small demo project from here.

PS – while I have few complaints about the FMX TBitmap and TCanvas interfaces in general, perhaps inevitably, there was one WTF moment when I was checking them out – the signature for TBitmapCodec.LoadFromStream looks like this:

function LoadFromStream(const AStream: TStream; var Bitmap: TBitmap): Boolean; virtual; abstract;

Was the person who wrote this not quite clear about what const and var actually mean when used on parameters typed to a class, or indeed, any pure reference type…?

Advertisement
9 Comments leave one →
  1. David permalink
    20 October, 2011 4.11 am

    Thank you so much for this blog! Without it, my first FireMonkey project would have come to a screaching halt.
    There is one typo error in the Windows CopyBitmapToClipboard procedure….I found
    “if (SetClipboardData(CF_DIB, MemObj)0) then begin”
    seems to work. Also I had to declare the function arguments as (Bitmap:FMX.Types.TBitmap)
    because otherwise a type tagBitmap was assumed.

    Thanks again, David

  2. Jason Coley permalink
    31 January, 2012 9.29 pm

    Awesome post, but I am getting a weird result in OSX (Lion not sure if this happens on Snow Leopard).

    If I copy an image in the filesystem, I actually get an icon pasted into the bitmap, not the actual data of the image copied, the icon is the generic JPEG, or PNG icon for OSX.
    I am doing the test of CanPasteBitmapFromClipboard before trying to paste the contents to a Bitmap.

    Any ideas?

    • Chris Rolliston permalink*
      1 February, 2012 3.10 am

      ‘Any ideas?’

      Well, I wouldn’t have expected the file *contents*. However, I see TextEdit (though not Word) handles this, so… watch this space – I’ve come up with the code, but I’ll make another blog post out of it.

      • Jason Coley permalink
        1 February, 2012 3.46 am

        Cool, can’t wait!

        I did manage to get the same sort of functionality working with Windows, had to handle CF_HDROP and read the file name using DragQueryFile and then load it depending on the file extension.

      • Chris Rolliston permalink*
        1 February, 2012 11.52 pm

        ‘had to handle CF_HDROP and read the file name using DragQueryFile…’

        It’s as if you have been reading my mind… In terms of concept, a similar thing needs to be done for OS X, though the specifics are quite different obviously.

        • Jason Coley permalink
          2 February, 2012 12.51 am

          this is that I have for windows…

          class function TClipboard.PasteBitmapFromClipboard(Bitmap: TBitmap): Boolean;
          var
          Header : TBitmapFileHeader;
          MemObj : HGLOBAL;
          Ptr : PBitmapInfoHeader;
          Stream : TMemoryStream;
          f : THandle;
          numFiles : Integer;
          FFile : string;
          buffer : array [0..MAX_PATH] of Char;
          begin
          Ptr := nil;
          Stream := nil;

          if Clipboard.HasFormat(CF_HDROP) then
          begin
          Clipboard.Open;
          try
          f := Clipboard.GetAsHandle(CF_HDROP);
          if f 0 then
          begin
          numFiles := DragQueryFile(f, $FFFFFFFF, nil, 0);
          if numFiles = 1 then
          begin
          buffer[0] := #0;
          DragQueryFile(f, 0, buffer, SizeOf(buffer));
          FFile := buffer;
          if ExtractFileExt(FFile) = ‘.jpg’ then
          Bitmap.LoadFromFile(FFile);
          end;
          end;
          finally
          Clipboard.Close;
          end;
          end
          else
          begin
          OpenClipboard(0);
          try
          MemObj := GetClipboardData(CF_DIB);
          if MemObj = 0 then Exit(False);
          Ptr := GlobalLock(MemObj);
          if Ptr = nil then Exit(False);
          FillChar(Header, SizeOf(Header), 0);
          Header.bfType := $4D42;
          Header.bfSize := SizeOf(Header) + GlobalSize(MemObj);
          Header.bfOffBits := SizeOf(Header) + Ptr.biSize;
          Stream := TMemoryStream.Create;
          Stream.WriteBuffer(Header, SizeOf(Header));
          Stream.WriteBuffer(Ptr^, Header.bfSize – SizeOf(Header));
          Stream.Position := 0;
          Bitmap.LoadFromStream(Stream);
          Result := True;
          finally
          if Ptr nil then GlobalUnlock(MemObj);
          CloseClipboard;
          Stream.Free;
          end;
          end;
          end;

          • Chris Rolliston permalink*
            2 February, 2012 1.43 am

            ‘this is that I have for windows…’

            Thanks. See my new post for my version, which also handles the case of getting the icon for a file that isn’t an image file.

Trackbacks

  1. Programmatically taking a screenshot on OS X « Delphi Haven

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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

Follow

Get every new post delivered to your Inbox.