4

I have problems saving an ImgView32 layer as a TRANSPARENT PNG. I use the code from this question to do the saving. However, the image saves with a white background.

Here is how I initialize my ImgView32, create a layer on it, and then draw a line on it:

procedure TputLine.FormCreate(Sender: TObject);
var
  P: TPoint;
  W, H: Single;
  imwidth: integer;
  imheight: integer;
begin
  imwidth := Iv1.Width;
  imheight := Iv1.Height;
  with iv1 do
  begin
    Selection := nil;
    Layers.Clear;
    Scale := 1;
    Scaled := True;
    Bitmap.DrawMode := dmTransparent;
    Bitmap.SetSize(imwidth, imheight);
    Bitmap.Canvas.Pen.Width := 4;
  end;
  BL := TBitmapLayer.Create(iv1.Layers);
  try
    BL.Bitmap.DrawMode := dmTransparent;
    BL.Bitmap.SetSize(imwidth,imheight);
    BL.Bitmap.Canvas.Pen.Width := penwidth;
    BL.Bitmap.Canvas.Pen.Color := pencolor;
    BL.Location := GR32.FloatRect(0, 0, imwidth, imheight);
    BL.Scaled := False;
  except
    BL.Free;
    raise;
  end;
end;

So iv1 is the name of my ImgView32. Then I draw a line on it using this code:

var 
  bm32:TBitmapLayer;
  ...
begin
  bm32:=(iv1.Layers[0] as TBitmapLayer).Bitmap;
  bm32.canvas.pen.color:=clwhite;
  bm32.canvas.brush.color:=clwhite;
  bm32.canvas.rectangle(0,0,bm32.width-1, bm32.height-1);

  bm32.canvas.Pen.Color:=WinColor(ColorPickerGTK1.SelectedColor);
  bm32.canvas.brush.color:=clWhite;
  bm32.Canvas.Pen.Width:=3;
  bm32.Canvas.MoveTo(0,bm32.Height);
  bm32.Canvas.LineTo(0+150,bm32.Height-250);
end;

If I use the clWhite32 for the above code when drawing the rectangle, then when saving the PNG, the background of the imgView turns black... So I do not understand the problem really.

I do the saving like this:

procedure TputLine.Button2Click(Sender: TObject);
var
  myLay:TBitmapLayer;
begin
  mylay := iv1.Layers.Items[0] as TBitmapLayer;
  SavePNGTransparentX(mylay.Bitmap);
end;

and the actual saving code (from the link described above)

procedure TPutLine.SavePNGTransparentX(bm32:TBitmap32);
var
  Y: Integer;
  X: Integer;
  Png: TPortableNetworkGraphic32;

  function IsWhite(Color32: TColor32): Boolean;
  begin
    Result:= (TColor32Entry(Color32).B = 255) and
             (TColor32Entry(Color32).G = 255) and
             (TColor32Entry(Color32).R = 255);
  end;

begin
    bm32.ResetAlpha;
    for Y := 0 to bm32.Height-1 do
      for X := 0 to bm32.Width-1 do
      begin
        if IsWhite(bm32.Pixel[X, Y]) then
          bm32.Pixel[X,Y]:=Color32(255,255,255,0);
      end;
    Png:= TPortableNetworkGraphic32.Create;
    Png.Assign(bm32);
    Png.SaveToFile('C:\ThisShouldBeTransparent.png');
    Png.Free;
end;

I do not understand why it does not save the layer as transparent PNG. How can I fix it? Any idea is welcome.

You can replicate my problem using the above code. It uses GR32_PNG and GR32_PortableNetworkGraphic. You only need to add a TImgView32 control to your form and add the code listed above.

Community
  • 1
  • 1
user1137313
  • 2,280
  • 7
  • 39
  • 83

1 Answers1

5

The reason to the problem seems to be two-fold.

  • First, when you call Png.Assign(bm32); in unit GR32_PNG, it attempts to find out what the smallest format would be to store the image in. If the image has less than 256 distinct colors, it creates a paletted format, and depending on how many colors it finds, the bit depth can become 1, 2, 4 or 8. As far as my knowledge goes, only images with TrueColor and Alpha can be saved as variable transparency png images.
  • Secondly, you draw with only one color, which triggers the above problem. This is of course not your fault, IMO the above mentioned analysis should be possible to bypass.

The TPortableNetworkGraphic32 class has two properties, BitDepth and ColorType wich control the format of the png image, that would be useful, if they were settable! Attempting to set them in code as:

Png.BitDepth := 8;
Png.ColorType := ctTrueColorAlpha;

leads to exceptions

EPngError with message 'Bit depth may not be specified directly yet!
EPngError with message 'Color Type may not be specified directly yet!

From the wording we can assume some further development in the future.

The cure

To bypass the above described image analysis, you can change line 459 in GR32_PNG.pas.

procedure TPortableNetworkGraphic32.AssignPropertiesFromBitmap32()
var
  ...
begin
  ...
   IsPalette := True; // <--- change to False
  ...

That will take care of the bit depth analysis and prevents palette creation if less than 256 colors. After this hack you can use the SavePNGTransparentX() procedure to save a TBitmap32 to a .png file and preserving transparency.

Then, there's one change more that you may be interested in, regarding the SavePNGTransparentX() procedure. As you have seen, it requires the background of your drawing surface to be white, because it specifically sets the Alpha channel to zero for all white pixels. A TBitmapLayer is however initialized with all pixels (RGBA) as all zeros, so the color components of each pixel makes up to black color (which is not visible because the alpha channel is zero). Therefore you need to fill the drawing layer with white, which makes it opaque, which again covers up all lower layers (beneath the drawing layer).

To make a correction to this you can

  • remove the initialization of the drawing layer to all white
  • change the IsWhite function into an IsBlack function
  • change the assignment of the transparent pixels

Code would become

procedure TForm8.SavePNGTransparentX(bm32:TBitmap32);
var
  Y: Integer;
  X: Integer;
  Png: TPortableNetworkGraphic32;

  function IsBlack(Color32: TColor32): Boolean;
  begin
    Result:= (TColor32Entry(Color32).B = 0) and
             (TColor32Entry(Color32).G = 0) and
             (TColor32Entry(Color32).R = 0);
  end;

  function IsWhite(Color32: TColor32): Boolean;
  begin
    Result:= (TColor32Entry(Color32).B = 255) and
             (TColor32Entry(Color32).G = 255) and
             (TColor32Entry(Color32).R = 255);
  end;

begin
    bm32.ResetAlpha;
    for Y := 0 to bm32.Height-1 do
      for X := 0 to bm32.Width-1 do
      begin
//        if IsWhite(bm32.Pixel[X, Y]) then
//          bm32.Pixel[X,Y]:=Color32(255,255,255,  0);
        if IsBlack(bm32.Pixel[X, Y]) then
          bm32.Pixel[X,Y]:=Color32(  0,  0,  0,  0);
      end;

    Png:= TPortableNetworkGraphic32.Create;
    Png.Assign(bm32);
    Png.SaveToFile('C:\tmp\imgs\ThisShouldBeTransparent3.png');
    Png.Free;
end;

With this change you can see the layers beneath your drawing layer and the resulting file of your drawing layer will have all non-drawn pixels transparent. There is however still one problem in general with the above procedure, and that is that partially transparent pixels loose their transparency, but that will remain for a future excercise.

Here's a few images, First the ImageView with a bitmap loaded on the bottom layer:

enter image description here

Then, I draw a blue cross on the BL layer (ref. your code) using basically the code in your Button1Click() but without the white rectangle.

enter image description here

Then I save the BL layer to a .png file and look at it with Windows Explorer "Preview":

enter image description here

Tom Brunberg
  • 17,724
  • 7
  • 32
  • 46
  • Tom, thank you for such an explicit and complex answer. I will try your suggestions now and come back to you after – user1137313 Apr 13 '15 at 09:59
  • It's beautiful. Thank you very much for an exceptional answer and solution. Also I am glad this question and code will help a lot of other people. As you can see, since the question was asked yesterday, there are already 66 visualisations – user1137313 Apr 13 '15 at 10:09