Jump to content
XylemFlow

Loading and Saving PNG into TBitmap changes the image

Recommended Posts

Posted (edited)

I have some code that loads a png image into a TBitmap and saves it again. However, the output is not the same. The quality of the image seems to deteriorate every time the previous image is loaded and then saved again.

I have attached the PNG image, which contains an alpha channel. The original image has a smooth gradient, but the last one clearly doesn't with clear banding. Does anyone know why this may be? I can perhaps understand why the 2nd image may be different since some aspects of the original PNG may not be supported, but I can't understand why subsequent images would vary to the 2nd image. This would indicate that Delphi FMX cannot correctly load an image that it saved. I'm running on Windows 32 bit and using Delphi 11.2 with default project settings. I'm comparing the images by viewing in Gimp and also by comparing in WinMerge. Even the file sizes vary.

procedure TForm1.Button1Click(Sender: TObject);
var
  bmp : TBitmap;
  i : Integer;
  name : String;
begin
  bmp := TBitmap.Create;
  name := 'whiteglower_8bit';
  for i := 1 to 4 do begin
    bmp.LoadFromFile(name + IntToStr(i) + '.png');
    bmp.SaveToFile(name + IntToStr(i+1) + '.png');
  end;
  bmp.Free;
end;

 

whiteglower_8bit1.png

Edited by XylemFlow
delphi version

Share this post


Link to post
1 hour ago, vfbb said:

@XylemFlow are you using GlobalUseSkia := True;?

No, as I said I'm using Delphi 11.2.

Share this post


Link to post
1 minute ago, JonRobertson said:

Using your code in Delphi 11.3, I get "Bitmap image is not valid" when loading a PNG using TBitmap.

OP is using FMX. You are using VCL...

  • Thanks 1

Share this post


Link to post
1 hour ago, XylemFlow said:

I have attached the PNG image, which contains an alpha channel.

Please also post the image as it is after you have saved it.

 

My guess is that the load+save premultiplies the RGB with the alpha.

Share this post


Link to post
18 hours ago, Anders Melander said:

Please also post the image as it is after you have saved it.

 

My guess is that the load+save premultiplies the RGB with the alpha.

Thanks for the idea. I will look into it. The worst thing is that the issue also happens with TBitmap.LoadFromStream and TBitmap.SaveToStream. This is not surprising since that's what the file load and save functions are using. Note that TBitmap.SaveToStream by default saves as PNG data. Below is code that demonstrates this. I have also attached the resulting first few images, and the last one after 40 load/save cycles showing the issue very clearly.

procedure TForm1.Button2Click(Sender: TObject);
var
  bmp : TBitmap;
  i : Integer;
  name : String;
  strm : TMemoryStream;
begin
  bmp := TBitmap.Create;
  name := 'whiteglower_8bit';
  bmp.LoadFromFile(name + '1.png');
  strm := TMemoryStream.Create;
  for i := 1 to 40 do begin
    strm.Seek(0, 0);
    bmp.SaveToStream(strm);
    strm.Seek(0, 0);
    bmp.LoadFromStream(strm);
    bmp.SaveToFile(name + IntToStr(i+1) + '_stream.png');
  end;
  bmp.Free;
  strm.Free;
end;

This is closer to my use case. My application allows users to save their project in a binary file, which can contain image data like this as well as other data. LoadFromStream and SaveToStream are used to load and save the image data. Each time the user loads a project to work on it and saves it, the quality of the image deteriorates (when alpha channels are used as they often are). This is true whether or not the user modified the image data. This is very bad and I need to find a fix soon if possible. I had assumed incorrectly that saving and loading image data would not change the image data.

whiteglower_8bit2_stream.png

whiteglower_8bit3_stream.png

whiteglower_8bit4_stream.png

whiteglower_8bit5_stream.png

whiteglower_8bit41_stream.png

Share this post


Link to post

I don't use FMX and I haven't looked at the source but as far as I can tell, from your the images and description, the problem is that TBitmap premultiplies on load and unpremultiplies on save. The degradation of pixels with alpha<>255 is caused by the inevitable rounding errors. The VCL TBitmap has the same problem if one messes with the AlphaFormat property. Apparently they didn't learn from their first mistake and instead decided to make it worse.

 

The reason the FMX TBitmap premultiplies the bitmap is probably because it needs that when displaying the bitmap (e.g. when using the Win32 AlphaBlend function).

 

Anyway, the solution to your problem is to not use TBitmap as a storage container. Use TBitmap only for display of the image and use instead use TMemoryStream or something like it for storage.

  • Sad 1

Share this post


Link to post
1 hour ago, Anders Melander said:

I don't use FMX and I haven't looked at the source but as far as I can tell, from your the images and description, the problem is that TBitmap premultiplies on load and unpremultiplies on save. The degradation of pixels with alpha<>255 is caused by the inevitable rounding errors. The VCL TBitmap has the same problem if one messes with the AlphaFormat property. Apparently they didn't learn from their first mistake and instead decided to make it worse.

 

The reason the FMX TBitmap premultiplies the bitmap is probably because it needs that when displaying the bitmap (e.g. when using the Win32 AlphaBlend function).

 

Anyway, the solution to your problem is to not use TBitmap as a storage container. Use TBitmap only for display of the image and use instead use TMemoryStream or something like it for storage.

Thank you. The problem is that storing as TMemoryStream requires reading into a TBitmap each time it needs displaying. That would require too much delay for real time graphics. I could keep a copy of each image as a stream and a bitmap but that would double up the memory requirements. That may be the only option though.

Share this post


Link to post
1 minute ago, XylemFlow said:

The problem is that storing as TMemoryStream requires reading into a TBitmap each time it needs displaying. That would require too much delay for real time graphics.

It does complicate things but yes, as you suggested, you could have a structure that contains both an TStream (for persistence) and a TBitmap (for display). The TBitmap would be created and loaded from the TStream once, on-demand, when the image needs to be displayed. This is also the technique used internally by many VCL image containers (e.g. TGIFImage, TPNGImage, TJPEGImage).

That at least would mean you only "wasted" memory on the bitmaps that were actually displayed.

 

7 minutes ago, XylemFlow said:

I could keep a copy of each image as a stream and a bitmap but that would double up the memory requirements.

Not really. The data in the stream is compressed and probably much smaller than the decompressed image data.

  • Like 1

Share this post


Link to post

Slightly off topic: Reading this site with the standard light layout I thought the images where all broken...until I saw the webp and realized all images are white on transparent background. Silly me:classic_rolleyes:

  • Thanks 2
  • Haha 2

Share this post


Link to post
Posted (edited)
2 hours ago, Anders Melander said:

It does complicate things but yes, as you suggested, you could have a structure that contains both an TStream (for persistence) and a TBitmap (for display). The TBitmap would be created and loaded from the TStream once, on-demand, when the image needs to be displayed. This is also the technique used internally by many VCL image containers (e.g. TGIFImage, TPNGImage, TJPEGImage).

That at least would mean you only "wasted" memory on the bitmaps that were actually displayed.

 

Not really. The data in the stream is compressed and probably much smaller than the decompressed image data.

For my application it's very unlikely that image data would not need to be displayed so I wouldn't save much by doing it on demand. At least I would only need to do it for images containing an alpha channel though.

 

It's true that the stream data is compressed. However, the original image in this case was bit depth 64, which FMX converts to bit depth 32 when loaded. I didn't share that version because it was over 1.2MB, whereas the 32bit version was only 100KB. So if I save it as a stream when it first gets loaded it would still be quite large. I may therefore save it to a stream after it gets loaded into the TBitmap, which would mean that it would still suffer from one level of degradation, but at least it wouldn't continue to lose quality each time the binary was loaded and saved, and would use less memory. 

Edited by XylemFlow

Share this post


Link to post

The Windows D2D implementation in FMX in FMX.Canvas.D2D.pas always assumes a premultiplied alpha. This is probably the source of the problem. Anders Melander can perhaps comment on that with more expertise than I have.

D2D1_ALPHA_MODE (dcommon.h) - Win32 apps | Microsoft Learn

 

One option to solve it would be to write a TCustomBitmapCodec for raw PNG data and load/save your PNGs through this codec exclusively. 

 

If SKIA is an option for you, install SKIA and enable it for your app. SKIA effectively implements its own canvas and its own image codecs, leaving your PNG file as it is. I tested your example image with SKIA enabled and the bitmap stays the same, no matter how often it is loaded and saved. Obviously, the SKIA codecs do a better job.

  • Like 1

Share this post


Link to post
2 hours ago, Alexander Halser said:

The Windows D2D implementation in FMX in FMX.Canvas.D2D.pas always assumes a premultiplied alpha. This is probably the source of the problem.

The Win32 AlphaBlend API also requires alpha premultipled RGB.

But just because the display API requires premultipled pixels doesn't mean that TBitmap should premultiply and then discard the source pixels. What it should have done is to create an internal premultipled copy of the source pixel data for display purpose if and when it was needed.

 

2 hours ago, Alexander Halser said:

One option to solve it would be to write a TCustomBitmapCodec for raw PNG data and load/save your PNGs through this codec exclusively. 

The GR32PNG implementation can probably be adapted for FMX. It's the default PNG format handler in Graphics32:

https://github.com/graphics32/graphics32/blob/master/Source/GR32_PortableNetworkGraphic.pas

https://github.com/graphics32/graphics32/blob/master/Source/GR32_Png.pas

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×