Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glitch when resizing TBitmap #30

Closed
Escain opened this issue Sep 4, 2023 · 14 comments
Closed

Glitch when resizing TBitmap #30

Escain opened this issue Sep 4, 2023 · 14 comments

Comments

@Escain
Copy link
Contributor

Escain commented Sep 4, 2023

Hello,

I am trying to use Image32 to scale images in our application. After integrating the library, I am facing some glitches on the resulting TBitmap with transparency.

The full question is described here: https://stackoverflow.com/questions/76818048/resizing-tbitmap-with-alpha-channel-in-delphi

Simplified, the code looks like :

function scaleBitmap( inPict: TPicture; scale: TSize; newRect: TRect ) : TPicture
begin
  var inBitmap: TImage32 := TImage32.Create;
  var outBitmap: TBitmap := TBitmap.Create;

  try
    inBitmap.CopyFromBitmap(inPict.Bitmap);

    inBitmap.Resampler := rBicubicResampler;
    inBitmap.Resize(scale.Width, scale.Height); // New size
    inBitmap.Crop(newRect); // Take only the required part.

    outBitmap.PixelFormat := pf32bit; // I think it's not required
    inBitmap.CopyToBitmap(Result); //Assume already created.
  finally
    FreeAndNil(inBitmap);
    FreeAndNil(outBitmap);
  end;
end

We have to interact with other components, that is why we use TBitmap and not TImage32 as the result.

The resulting image is scaled correctly, but alpha channel seems incorrect:

Original:
2vlDd

Scaled:
Pj9YH

@AngusJohnson
Copy link
Owner

The following looks fine to me, though I don't really understand why you're bothering with both TImage32 and TBitmap.
I suggest you just stick with Image32 😁.

    var
      img: TImage32;
      bmp: TBitmap;
    begin
      
      img := TImage32.Create;
      bmp := TBitmap.Create;
    
      // use img to load the PNG file
      img.LoadFromFile('c:\temp\temp.png');
      // copy the image to a TBitmap
      img.CopyToBitmap(bmp);
      bmp.SaveToFile('c:\temp\temp.bmp');
      // verify that the copied image still looks OK as a saved BMP image
    
      // now clear img so there's no doubt that 
      // the following CopyFromBitmap() step is working
      img.Clear;
      img.CopyFromBitmap(bmp);
    
      // now scale the image using 'img'
      img.Resampler := rBicubicResampler;
      img.Scale(2.0);
    
      // copy scaled image back to the TBitmap
      img.CopyToBitmap(bmp);
      // and save rescaled image to another BMP file
      bmp.SaveToFile('c:\temp\temp2.bmp');
    
      // clean up
      bmp.Free;
      img.Free;

@Escain
Copy link
Contributor Author

Escain commented Sep 4, 2023

Hi Angus,

First of all, thank you for your answer :-)

I tested your code and have the same issue than described. So I created a full example to make it easier to test on your side.

https://github.com/Escain/TestImage32

Please, don't look to closely my code style, it's just a quick-dirty example.

Output:
image

@AngusJohnson
Copy link
Owner

AngusJohnson commented Sep 5, 2023

When you have the sorts of problems you're having now, it's important to simplify the steps as much as possible, so you don't assume the issue is in one particular area. And that's why I posted the code above, since I strongly suspect your problem is elsewhere.

And I'm busy with other things so, rather than me debugging this for you, I suggest you do the following:
Save the bitmap image just before and just after resizing to temporary files (eg bmp1.bmp and bmp2.bmp).
Only then, if (when using an external image viewer) the second saved image still doesn't look accurately resized, that's very considerably narrowed done the issue. And if you're still seeing problems in your scaleBitmap() function, then please test the code I posted above too, because I did that with you PNG image and the resized image using the code I posted above looks fine to me.

@Escain
Copy link
Contributor Author

Escain commented Sep 5, 2023

The images saved by your code also look bad.
I think you used a png without transparency and that is why you don't see the issue.

@AngusJohnson
Copy link
Owner

AngusJohnson commented Sep 5, 2023

I just used the PNG image that you provided.
Nevertheless I've also tested this with transparent PNG images, and it's still OK.

  img := TImage32.Create;
  img.LoadFromFile('c:\temp\test2.png');
  bmp := TBitmap.Create;
  img.CopyToBitmap(bmp);
  bmp.SaveToFile('c:\temp\test2_pre.bmp');
  img.Clear;
  img.CopyFromBitmap(bmp);
  img.Resampler := rBicubicResampler;
  img.Scale(1.25);
  img.CopyToBitmap(bmp);
  bmp.SaveToFile('c:\temp\test2_post.bmp');
  bmp.Free;
  img.Free;

test1

test2

@Escain
Copy link
Contributor Author

Escain commented Sep 5, 2023

Hi Angus,

Thank you for your answer.

The background of your "X"/Close png is white, I suppose that (considering that the glitch is white), the issue is not visible.

The image that I provided is the "Original size" rendering, not the original image with transparency. You can find a fully (non)working example in the test that I provided.

Have a good day.

@AngusJohnson
Copy link
Owner

The background of your "X"/Close png is white

No it's not. I presume whatever app you're using to view this image has a default white background. Change the default transparency background to another color. Alternatively, click on this "X" image above and it'll display the image in a new tab window (which in Chrome at least is usually black).

Anyhow, I've spent more than enough time on this now. Sorry I can't help further.

@Escain
Copy link
Contributor Author

Escain commented Sep 5, 2023

Hi Angus,

I understand that you don't have lot of time, actually I also have little time. But I assume this is the place to report bugs, and the fact that we have little time does not make my research less worthy or this bug to not exist.
Just take your time and have a look when you have time, or leave it for another developer.

  1. How do you get from your sources to the image you post? the test1.bmp and test2.bmp are BMP while the images you show are PNG. Also, they are not resized, they don't even look like it should. It seems like you mixed and sent other non-manipulated images.

So I went with your example and I am unable to use LoadFromFile which returns false. So I used the following example:

var
  png: TPngImage;
  img: TImage32;
  bmp: TBitmap;
 begin
  
  img := TImage32.Create;
  bmp := TBitmap.Create();
  png := TPngImage.Create();
  
  //This does not work to me: 
  //img.LoadFromFile('img2.png');
  //img.CopyToBitmap(bmp);

  //This does
  png.LoadFromFile('img2.png');
  bmp.assign(png);
  
  bmp.SaveToFile('./test1.png');
  img.Clear;
  img.CopyFromBitmap(bmp);
  img.Resampler := rBicubicResampler;
  img.Scale(1.25);
  img.CopyToBitmap(bmp);
  bmp.SaveToFile('./test2.png');
  
  img.Free;
  bmp.Free;
  png.Free;

This returns the following images:

test1
test2

Which indeed have the issue.

Consequently, both images: represented to screen AND saved to file have the same problem, but only after resizing. And only on Delphi (not on Lazarus).

I asked a few colleagues to test my example on their computer in case I have something weird. I will update in case we discover something.

@AngusJohnson
Copy link
Owner

AngusJohnson commented Sep 5, 2023

OK, one last try...

You're evidently doing something wrong with your image rendering (ie display).

This is your PRESCALED image from your post above:
test2
If you look closely you can see that the antialiasing is poorly managed especially top-left.

But using my PNG image above and displaying it on a black background, this is how it should appear:
test2_b

Here's a test application that I'm very confident will render transparent images correctly
Create a new VCL application and just add an OnPaint event:

uses
  img32, img32.Fmt.BMP, img32.Fmt.PNG;

procedure TForm1.FormPaint(Sender: TObject);
var
  img: TImage32;
  rec: TRect;
begin
  rec := self.ClientRect;
  img := TImage32.Create(rec.Width, rec.Height);
  img.Clear(clBlack32);
  // draw the black background
  img.CopyToDc(canvas.Handle, 0,0);
  img.LoadFromFile('c:\temp\test2.png');
  // draw the unchanged image
  img.CopyToDc(canvas.Handle, 100,100);
  img.Resampler := rBicubicResampler;
  img.Scale(2.5);
  // draw the scaled image
  img.CopyToDc(canvas.Handle, 100,200);
  img.Free;
end;

test2_app

//This does not work to me:
//img.LoadFromFile('img2.png');
//img.CopyToBitmap(bmp);

I suspect you've forgotten to add the necessary units in your uses clause.
For FMX apps, make sure you add Img32.Fmt.FMX
For VCL apps, make sure you add Img32.Fmt.BMP and Img32.Fmt.PNG

@Escain
Copy link
Contributor Author

Escain commented Sep 6, 2023

Hi Angus,

So I could effectively reproduce your code and I made this following modification to show BOTH: working and not-working cases:

procedure TForm1.FormPaint(Sender: TObject);
var
  img: TImage32;
  png: TPngImage;
  rec: TRect;
  bmp: TBitmap;
begin
  rec := self.ClientRect;

  // Some background on the frame, NOT in the image, otherwise we don't have transparency.
  Canvas.Brush.Color := TColor($8F8F6A);
  Canvas.RoundRect(rec.Left, rec.Top, rec.Right, rec.Bottom, 10, 10);

  // Few objects creation
  png := TPngImage.Create();
  img := TImage32.Create(rec.Width, rec.Height);
  bmp := TBitmap.Create();

  //This works without artifacts, but CopyTo/FromBitmap is not used
  img.Clear();
  img.LoadFromFile('img2.png');
  img.Scale(2.7);
  img.CopyToDc(canvas.Handle, 0,0);

  //This also works, but there is no resizing
  png.LoadFromFile('img2.png');
  bmp.assign(png);
  Canvas.Draw(200, 0, bmp);

  //This also works, but CopyToBitmap is not used
  bmp.assign(png);
  img.Clear();
  img.CopyFromBitmap(bmp);
  img.Scale(2.7);
  img.CopyToDc(canvas.Handle, 0,200);

  //This don't work.
  bmp.assign(png);
  img.Clear();
  img.CopyFromBitmap(bmp);
  img.Scale(4);
  img.CopyToBitmap(bmp); // Probable faulty function?
  Canvas.Draw(200, 200, bmp);
  

  // Clean
  img.Free;
  bmp.Free;
  png.Free;
end;

Note: the bottom-left case, using CopyFromBitmap also seems to have some black circle, which is not too evident (probably due to the scaling), but I believe that the same artifact is in both functions.

image

@AngusJohnson
Copy link
Owner

AngusJohnson commented Sep 6, 2023

img.CopyToBitmap(bmp); // Probable faulty function?

You're right, in a sense it is faulty! 😱
However in my defence, I'd argue that this is really due to a TBitmap (or ??Windows) bug rather than a bug in Image32 😁.
In essence, the problem is that whenever a fully transparent pixel (alpha = 0) also has non-zero color channel values, then TBitmap will fully render that pixel (as if the alpha channel = 255).
So the workaround is to draw to the bitmap canvas rather than using a straight bit copy.

procedure TImage32.CopyToBitmap(bmp: TBitmap);
begin
  if not Assigned(bmp) then Exit;
  bmp.PixelFormat := pf32bit;
  bmp.AlphaFormat := afDefined;
  bmp.Width := Width;
  bmp.Height := Height;
  bmp.Canvas.Brush.Color := 0;
  bmp.Canvas.FillRect(types.Rect(0, 0, Width, Height));
  CopyToDc(bmp.Canvas.Handle, 0,0, true);
end;

So mostly mea culpa, and thank you for your patience and persistence.

Edit 1:
TBitmap really doesn't handle 32bit alpha transparency well.
And that's because most Windows GDI functions were designed before transparency was even considered.
For example in the following code, if you uncomment the bmp.SaveToFile() line, it will break the bmp image's transparency.
But with that line left commented, that bmp is drawn properly.

  img := TImage32.Create;
  bmp := TBitmap.Create;
  try
    img.LoadFromFile('c:\temp\white1.png');
    //draw 'img' directly onto the form's canvas
    img.CopyToDc(canvas.Handle, 100, 100);

    //scale 'img' and then copy it to 'bmp' ...
    img.Scale(3.0);
    img.CopyToBitmap(bmp);

    //note: the following line erases alpha channel info
    //bmp.SaveToFile('c:\temp\white2.bmp');

    //finally draw 'bmp' onto the form's canvas
    canvas.Draw(100, 200, bmp);
  finally
    bmp.Free;
    img.Free;
  end;

test3_app

white1.png:
https://github.com/AngusJohnson/Image32/assets/5280692/95101c57-836e-4b94-b8ae-756e267fe738

Edit 2:
And just to prove a point (that this problem is mostly unrelated to Image32):

  png := TPngImage.Create;
  bmp := TBitmap.Create;
  try
    png.LoadFromFile('c:\temp\white1.png');
    bmp.Assign(png);
    canvas.Draw(100, 100, bmp); // this will display properly
    bmp.SaveToFile('c:\temp\white2.bmp');
    canvas.Draw(100, 200, bmp); // but this won't display properly
  finally
    bmp.Free;
    png.Free;
  end;

@AngusJohnson
Copy link
Owner

I've just updated TImage32.CopyToBitmap() again in the repository so that it will now blend copy TImage32 onto the bitmap (instead of replacing the bitmap's image). Now the user will have complete control over the bitmap's size and background.

Image32/source/Img32.pas

Lines 259 to 260 in 9fc9caa

//CopyToBitmap: blend copies self over the bitmap's existing image
procedure CopyToBitmap(bmp: TBitmap; dstLeft: integer = 0; dstTop: integer = 0);

@Escain
Copy link
Contributor Author

Escain commented Sep 7, 2023

Hi Angus,

That is fantastic that you already got one of the two functions solved. Images now looks good in our GUI :-)

image

I totally agree: when making software, we usually have to deal with workaround for bugs that are in third-party libraries that we include, like the TBitmap that are not fully compatible with transparency. In our case, we are using Image32 exactly due to that: TBitmap resizing (StretchDraw) is not compatible with transparency.

If you want to keep the previous functionality, here is something that seems to work (Not heavily tested). However your version seems more versatile.

class procedure TImage32.ClearBitmapToTransparent(ABitmap: TBitmap);
var
  x, y: Integer;
  p: PRGBQuad;
begin
  if not Assigned(ABitmap) then Exit;

  // Ensure the bitmap format supports transparency
  ABitmap.PixelFormat := pf32bit;

  // Access the raw bitmap data
  ABitmap.Handle;

  // Loop through all pixels
  for y := 0 to ABitmap.Height - 1 do
  begin
    p := ABitmap.ScanLine[y];
    for x := 0 to ABitmap.Width - 1 do
    begin
      p^.rgbReserved := 0;
      p^.rgbRed := 0;
      p^.rgbGreen := 0;
      p^.rgbBlue := 0;
      Inc(p);
    end;
  end;

  ABitmap.Modified := True;
end;

procedure TImage32.CopyToBitmap(bmp: TBitmap);
begin
  if Assigned(bmp) then
  begin
    bmp.SetSize(Width, Height);
    bmp.PixelFormat := pf32bit;
    bmp.AlphaFormat := afDefined;
    ClearBitmapToTransparent(bmp);
    CopyToDc(bmp.Canvas.Handle, 0, 0, HasTransparency);
  end;
end;
````````

@AngusJohnson
Copy link
Owner

AngusJohnson commented May 7, 2024

I'm commenting on this rather old thread to acknowledge that there was also a bug in TImage32.CopyToBitmap (which I've just fixed). When saving images with no transparency, they were being saved to stream as 24bpp images though incorrectly using bitfields. Apparently only 16bpp and 32bpp should use BI_BITFIELDS, though most software just ignores bitfields when loading 24bpp images.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants