Delphi, GR32 + PngObject: converting to Bitmap32 d

2019-02-10 10:49发布

I'm using GR32 for drawing multiple semi-transparent PNG images. So far I've been using the following method:

  png:= TPNGObject.Create;
  png.LoadFromFile(...);
  PaintBox321.Buffer.Canvas.Draw(120, 20, png);

however I wanted to switch to the method proposed on GR32 website (http://graphics32.org/wiki/FAQ/ImageFormatRelated) :

  tmp:= TBitmap32.Create;
  LoadPNGintoBitmap32(tmp, ..., foo);
  tmp.DrawMode:= dmBlend;
  PaintBox321.Buffer.Draw(Rect(20, 20, 20+ tmp.Width, 20+tmp.Height),
   tmp.ClipRect, tmp);

While the first method works perfectly fine, the second - which should give the same result - causes very strange problem with alpha channel, see the image (which also shows comparison to the same image "arranged" in Paint.NET - both background and icon were opened on the layers of the editor). The image depicts that the Bitmap32 is loaded or drawn inproperly. Any tips?

Problem with TBitmap32 alpha channel

-- added 22 Nov

I've found out that it is not about drawing, it's about loading PNG to BMP32. Saving back from BMP32 to PNG generates the incorrect, "whitened" (the one on the left) PNG image.

2条回答
干净又极端
2楼-- · 2019-02-10 11:17

The reason seems to be that the transparency is applied two times to the image when loaded with LoadPNGintoBitmap32, giving it a more transparent and greyish look (more on this later).

First the transparency:

This is code from the original LoadPNGintoBitmap32, the critical parts are marked with comments:

 PNGObject := TPngObject.Create;
 PNGObject.LoadFromStream(srcStream);

 destBitmap.Assign(PNGObject);  // <--- paint to destBitmap's canvas with transparency (!)
 destBitmap.ResetAlpha;         

 case PNGObject.TransparencyMode of  // <--- the following code sets the transparency again for the TBitmap32
 { ... }

The destBitmap.Assign internally does the same as you in your previous approach: it let's the PNG image paint itself to its canvas. This operation respects the alpha channel of the PNG. But this is not necessary, since the alpha channel is assigned to TBitmap32's pixels in a second step!

Now change the code as follows, critical parts are again marked with comments:

 PNGObject := TPngObject.Create;
 PNGObject.LoadFromStream(srcStream);

 PNGObject.RemoveTransparency;  // <--- paint PNG without any transparency...
 destBitmap.Assign(PNGObject);  // <--- ...here
 destBitmap.ResetAlpha;

 srcStream.Position:=0;
 PNGObject.LoadFromStream(srcStream); // <--- read the image again to get the alpha channel back

 case PNGObject.TransparencyMode of   // <--- this is ok now, the alpha channel now only exists in the TBitmap32
 { ... }

The above solution is inefficient because it reads the image twice. But it shows why your second approach produces a more transparent image.

And for the greyishness: There is one more problem in the original code: destBitmap.Assign first fills the background with clWhite32, then paints the image transparently onto it. And then LoadPNGintoBitmap32 comes and adds another layer of transparency on top of it.

查看更多
倾城 Initia
3楼-- · 2019-02-10 11:44

The problem may be that the PNG has incorrectly converted to TBitmap32, losing the transparency information in transit. It is a common case with paletted PNG images. Otherwise, you would not had to use „Bitmap.DrawMode := dmTransparent” and the „OuterColor”. If the transparencry information from PNG would have correctly transferred to TBitmpa32, DrawMode := dmBlend would have worked, without the need to set the OuterColor.

What matters most is how did you load a PNG into the TBitmap32. The TPngImage from Vcl.Imaging.pngimage unit (implemented in Delphi XE2 and later) can draw transparently on bitmaps, preserving what was on that bitmaps, combining colors using the PNG alpha layer, etc, but it doesn’t allow to easily convert various formats of PNG transparency (including paletted) into the alpha component of each pixel of TBitmap32. Once TPngImage have drawn an image, you get the combined RGB for each pixel, but the alpha component is not transferred to the target bitmap.

There are helper routines available that try to load a PNG into a TBitmap32 with transparency, but they have drawbacks:

(1) “LoadPNGintoBitmap32” from http://graphics32.org/wiki/FAQ/ImageFormatRelated - it applies the transparency twice, so the images with alpha values other than 0 or 255 will look differently than in other software (most noticeable on translucent images with glass effects). This code will first apply alpha to RGB and then sets alpha as a separate layer, so when you paint, alpha will be applied again. You can find more information on this issue here: Delphi, GR32 + PngObject: converting to Bitmap32 doesn't work as expected . Besides that, it doesn't convert correctly transparency from paletted images into the alpha layer of TBitmap32. They manually set alpha transparency for the pixels of a certain color of the output bitmap (rendered to RGB) rather doing that before rendering to RGB, so the actual transparency is lost as on your sample image when all white pixels are transparent.

(2) “LoadBitmap32FromPNG” from gr32ex library: https://code.google.com/archive/p/gr32ex/ - a slightly different implementation of the same algorithm as (1), and has the same issues as (1).

So, the solutions are:

  1. Do not use TBitmap32; use Vcl.Imaging.pngimage.TPngImage do draw directly on target bitmap (screen, etc.) – this is the most compatible way that deals correctly with various PNG formats.
  2. Use a helper routing to transfer transparency information from Vcl.Imaging.pngimage.TPngImage to TBitmap32.
  3. Use the GR32 PNG library that can natively load a PNG into TBitmap32 https://sourceforge.net/projects/gr32pnglibrary/ Since you now have all the information on this issue, you may get the right solution for you.

How to load the alpha layer in one pass

Heinrich Ulbricht made a nice suggestion to remove the transparency layer before paining and then to read the image again. To avoid loading the image twice, you can save the alpha layer before calling PNGObject.RemoveTransparency. Here is the code that correctly applies the alpha layer and loads the image only once. Unfortunately, it does not work with paletted images. If you know how to correctly fill the alpha layer of TBitmap32 from any paletted image, without the effects described at Transparent Png to TBitmap32 please let me know.

procedure LoadPNGintoBitmap32(DstBitmap: TBitmap32; SrcStream: TStream; out AlphaChannelUsed: Boolean);
var
  PNGObject: TPngImage;
  PixelPtr: PColor32;
  AlphaPtr: PByte;
  SaveAlpha: PByte;
  I, AlphaSize: Integer;
begin
  AlphaChannelUsed := False;
  PNGObject := TPngImage.Create;
  try
    PNGObject.LoadFromStream(SrcStream);
    AlphaPtr := PByte(PNGObject.AlphaScanline[0]);
    if Assigned(AlphaPtr) then
    begin
      AlphaSize := PNGObject.Width * PNGObject.Height;
      if AlphaSize <= 0 then raise Exception.Create('PNG files with zero dimensions are not supported to be loaded to TBitmap32');
      GetMem(SaveAlpha, AlphaSize);
      try
        Move(AlphaPtr^, SaveAlpha^, AlphaSize);
        PNGObject.RemoveTransparency;
        DstBitmap.Assign(PNGObject);
        DstBitmap.ResetAlpha;
        PixelPtr := PColor32(@DstBitmap.Bits[0]);
        AlphaPtr := SaveAlpha;
        for I := 0 to AlphaSize-1 do
        begin
          PixelPtr^ := (PixelPtr^ and $00FFFFFF) or (TColor32(AlphaPtr^) shl 24);
          Inc(PixelPtr);
          Inc(AlphaPtr);
        end;
      finally
        FreeMem(SaveAlpha, AlphaSize);
      end;
      AlphaChannelUsed := True;
    end else
    if PNGObject.TransparencyMode = ptmNone then
    begin
      DstBitmap.Assign(PNGObject);
    end else
    begin
      raise Exception.Create('Paletted PNG images are not supported in LoadPNGintoBitmap32, transparency cannot be stored to TBitmap32');
    end;
  finally
    FreeAndNil(PNGObject);
  end;
end;
查看更多
登录 后发表回答