How to load TDrawableImage from TStream?

I see that in examples there are many cases:
Image := TDrawableImage.Create('castle-data:/my_image.png');

But I need to load the image from TStream.
For example, from from TStringStream or TMemoryStream.
How can I do it effectively?
I see that TDrawableImage has Load method.
procedure **Load** (const AImage: [TEncodedImage](https://castle-engine.io/apidoc-unstable/html/CastleImages.TEncodedImage.html)); virtual;

Then there is the note:

Do not call this method too often. This methos copies the image contents from TEncodedImage to GPU, which is not something you want to do every frame. Doing this occasionally, or during the loading/preparation stage, is OK. Doing this often, e.g. during every TCastleWindowBase.OnRender or TCastleWindowBase.OnUpdate, will hurt your performance. During the game, it’s most efficient to treat the TDrawableImage instances as “constant” – avoid changing their contents, avoid creating new ones.

Another question:
For example, I have TBitmap. How can I load it effectively to TDrawableImage?

In CastleImages unit I found:
function LoadEncodedImage(Stream: TStream; const MimeType: string;
const AllowedImageClasses: array of TEncodedImageClass;
const Options: TLoadImageOptions = )

Thus probably it should be used somehow?

Note there is Castle Game Engine: CastleImages - an overload that receives a TStream. This way you just have to do something like TDrawableImage.Create(LoadImage(MyStream, StreamMimeType, [TRGBAlphaImage], [])).

I’m not sure about TBitmap. Images like TRGBAlphaImage have FromFpImage procedure, but it’s private, and I’ve never used it before.

Thank you. It’s clear how to use it now.

Indeed the solution is as Eugene described – use LoadImage (or even LoadEncodedImage, both from CastleImages unit) to load image into TCastleImage. This can load from a stream.

The you can initialize TDrawableImage contents from TCastleImage. You can do it using TDrawableImage.Create (to create new TDrawableImage instance) or TDrawableImage.Load (to change an existing TDrawableImage instance).

My example would be:

MyDrawableImage := TDrawableImage.Create(
  LoadEncodedImage(MyStream, StreamMimeType, []), true, true);

It’s mostly equivalent to Eugene example, except

  • I used LoadEncodedImage, which can also load GPU-compressed images. If you don’t need to operate on image pixels from Pascal code, then it’s usually better to use LoadEncodedImage instead of LoadImage. LoadEncodedImage returns TEncodedImage instance, LoadImage returns TCastleImage instance, and TCastleImage is a descendant of TEncodedImage.

  • I didn’t specify format TRGBAlphaImage explicitly. The loading routines can figure out the most efficient format automatically.

The point of the warning at “Load” method (“Do not call this method too often …”) is to avoid changing TDrawableImage contents very often, e.g. at every frame (e.g. in CGE OnRender or OnUpdate events). Doing this very often would mean you lose speed, and it is usually not needed in typical games. You probably don’t need to worry about this warning :slight_smile:

We don’t have a way to easily convert from/to LCL TBitmap and our TCastleImage or TDrawableImage. In normal CGE applications, you would not use TBitmap at all, instead CGE has rich API to load and operate on images. However, I could add conversion routines TBitmap <-> TCastleImage if needed – let me know if you need this.

I wrote the simple code below. The idea is to load image into some format and then save it to database as blob field. Then on demand load it from database.
Thus this is not a simple/traditional game app.
On OnCreate I create TDrawableImage and load test1.bmp to it (because I need to load something to create TDrawableImage). Then when I press btnLoad it will do the main action.

Thus here is the simplified example:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls,
		CastleControl, CastleImages, CastleGLImages;

type

		{ TForm1 }

  TForm1 = class(TForm)
				CastleControlBase1: TCastleControlBase;
				btnLoad: TToggleBox;
				procedure btnLoadChange(Sender: TObject);
				procedure CastleControlBase1Render(Sender: TObject);
				procedure FormCreate(Sender: TObject);
				procedure FormDestroy(Sender: TObject);
  private
    di: TDrawableImage;

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }


procedure TForm1.btnLoadChange(Sender: TObject);
var
   ss: TStringStream;
   MyBitmap: TBitmap;
begin
   MyBitmap := TBitmap.Create;
   MyBitmap.LoadFromFile('test.bmp');
   ss:=TStringStream.Create;
   MyBitmap.SaveToStream(ss);
   // doing some wierd things like saving stream to database as blob and loading it again from database
   di.Load(LoadImage(ss,'image/bmp',[TRGBImage]));
   MyBitmap.Free;
   ss.Free;
end;

procedure TForm1.CastleControlBase1Render(Sender: TObject);
begin
  di.Draw(0,0);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
   di:=TDrawableImage.Create('test1.bmp');
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  di.Free;
end;

end.

I wil cause EReadError in the first line
Stream.ReadBuffer(fhead, SizeOf(fhead));
of
procedure WriteRow(Row: Pointer; bmprow: PByteArray);
located in castleimages_bmp.inc

I am not strictly bound to TBitmap (BMP format). I may use PNG. Thus it’s probably a bad example that I started to do my tests with BMP. I will try to do the same with PNG.

I tried to do the same but with PNG image. It causes FPImageException with “Wrong image format” message.

procedure TForm1.btnLoadPNGClick(Sender: TObject);
var
   ss: TMemoryStream;
   MyPNG: TPortableNetworkGraphic;
begin
   MyPNG := TPortableNetworkGraphic.Create;
   MyPNG.LoadFromFile('test.png');
   ss:=TStringStream.Create;
   MyPNG.SaveToStream(ss);
   // doing some wierd things like saving stream to database as blob and loading it again from database
   di.Load(LoadImage(ss,'image/png',[TRGBAlphaImage]));
   //di.Load(LoadEncodedImage(ss,'image/png',TextureImageClasses));
   MyPNG.Free;
   ss.Free;
end;

Also it’s not clear:
If I use LoadEncodedImage which should be 3rd parameter? I am passing TextureImageClasses to it. But putting [TRGBAlphaImage] instead of TextureImageClasses will cause “Error: ordinal expression expected”.

I wil cause EReadError in the first line
Stream.ReadBuffer(fhead, SizeOf(fhead));
of
procedure WriteRow(Row: Pointer; bmprow: PByteArray);
located in castleimages_bmp.inc
I am not strictly bound to TBitmap (BMP format). I may use PNG. Thus it’s probably a bad example that I started to do my tests with BMP. I will try to do the same with PNG.

An error here usually means that the stream ends unexpectedly.

Make sure to reset the stream position to 0, like ss.Position := 0.

Note: TBitmap is a class in LCL to handle bitmaps. While we don’t have a conversion routines between TBitmap ↔ CGE image formats (at least for now, because they did not seem necessary; but then could be added)…

… but BMP is a file format that CGE should be able to handle, at least the common variants of BMP :slight_smile:

I tried to do the same but with PNG image. It causes FPImageException with “Wrong image format” message.

I am guessing the same reason as above. Try with ss.Position := 0.

If I use LoadEncodedImage which should be 3rd parameter? I am passing TextureImageClasses to it. But putting [TRGBAlphaImage] instead of TextureImageClasses will cause “Error: ordinal expression expected”.

You’re right, and I have no idea why yet. It seems like FPC bug. Created a simple application:

{ Dummy application only to test compilation of Load*Image routines.

  Of course this application will crash at runtime, as S is not initialized.
  And also we don't do anything (not even free) with the Load*Image results.
  But it should compile.
  
  Compile with 
    castle-engine simple-compile a.lpr
}
uses Classes, CastleImages;
const
  ConstantClassesList: array [0..0] of TEncodedImageClass = (
    TRGBAlphaImage
  );
var
  S: TStream;
begin
  LoadImage(S, 'image/png', ConstantClassesList);
  LoadEncodedImage(S, 'image/png', ConstantClassesList);
  LoadImage(S, 'image/png', [TRGBAlphaImage]);
  LoadEncodedImage(S, 'image/png', [TRGBAlphaImage]);
end.

Weirdly, LoadEncodedImage(S, 'image/png', [TRGBAlphaImage]) fails with Error: Ordinal expression expected.

Created a Trello ticket to prepare a testcase independent of CGE, and submit this to FPC: Trello . Any volunteers? :slight_smile:

Reseting the stream position did work. Thus the following example works now:

procedure TForm1.btnLoadPNGClick(Sender: TObject);
var
   ss: TMemoryStream;
   MyPNG: TPortableNetworkGraphic;
begin
   MyPNG := TPortableNetworkGraphic.Create;
   MyPNG.LoadFromFile('test.png');
   ss:=TMemoryStream.Create;
   MyPNG.SaveToStream(ss);
   // doing some wierd things like saving stream to database as blob and loading it again from database
   ss.Position:=0;
   di.Load(LoadImage(ss,'image/png',[TRGBAlphaImage]));
[TRGBAlphaImage]));
   MyPNG.Free;
   ss.Free;
end;

And resetting stream position works fine for BMP now. The following example works:

procedure TForm1.btnLoadChange(Sender: TObject);
var
   ss: TStringStream;
   MyBitmap: TBitmap;
begin
   MyBitmap := TBitmap.Create;
   MyBitmap.LoadFromFile('test.bmp');
   ss:=TStringStream.Create;
   MyBitmap.SaveToStream(ss);
   // doing some wierd things like saving stream to database as blob and loading it again from database
   ss.Position:=0;
   di.Load(LoadImage(ss,'image/bmp',[TRGBImage])); 
   MyBitmap.Free;
   ss.Free;
end;

Michalis, yes, this is the really FPC bug. I found out more about it.
The definitions of functions:

function LoadImage(Stream: TStream; const MimeType: string;
  const AllowedImageClasses: array of TEncodedImageClass)
  :TCastleImage; overload; 
  
function LoadEncodedImage(Stream: TStream; const MimeType: string;
  const AllowedImageClasses: array of TEncodedImageClass;
  const Options: TLoadImageOptions = [])
  :TEncodedImage; overload;

So first 3 parameters are defined exactly the same,
I was able to handle this situation and it works now by adding 4th parameter.
For example, the following example works now:

procedure TForm1.btnLoadChange(Sender: TObject);
var
   ss: TStringStream;
   MyBitmap: TBitmap;
begin
   MyBitmap := TBitmap.Create;
   MyBitmap.LoadFromFile('test.bmp');
   ss:=TStringStream.Create;
   MyBitmap.SaveToStream(ss);
   // doing some wierd things like saving stream to database as blob and loading it again from database
   ss.Position:=0;
   //di.Load(LoadImage(ss,'image/bmp',[TRGBImage]));
   di.Load(LoadEncodedImage(ss,'image/bmp',[TRGBImage],[]));
   MyBitmap.Free;
   ss.Free;
end; 

Thus it works now if we explicitely set value for 4th parameter.
While it should work without 4th parameter this way:

di.Load(LoadEncodedImage(ss,‘image/bmp’,[TRGBImage]));

But it does not work (Error: Ordinal expression expected).
Hence I confirm: it’s FPC bug independent of CGE.