D2009 introduces PNG support for Images and Imagelists.
However...
I have an imagelist containing png images with alpha. I want to place one of these on a form using a TImage. How do I do this and get the image nicely composited?
As an example of the problem I'm facing the code below fails to work correctly, and produces the effect shown:
ImageList.GetBitmap(index, Image1.Picture.Bitmap);
(source: clip2net.com)
To explain a bit more:
Drop a Timage on a form, and at design time, load a PNG file with alpha using the Picture property. Note how it is correctly composited with full transparency onto the form.
Now, at design time, add a second empty Timage, add a TImagelist, and add the same PNG to the imagelist. How can I assign the PNG in the TImageList to the second TImage, and have it look identical to the first one?
From my research I found that TImageList stores the images as TBitmaps, so the alpha information is lost on storage, and you can't achieve what you're looking for with the current implementation of TImageList.
Update:
A little more experiments and with the code below i could make transparency work with the code below.
ImageList1.ColorDepth := cd32Bit;
Image2.Transparent := True;
Image2.Canvas.Pen.Style := psClear;
Image2.Canvas.Rectangle(0, 0, Image2.Width+1, Image2.Height+1);
ImageList1.Draw(Image2.Canvas, 0,0,0);
But it didn't look as pretty as a loaded png.
Check the Enable Runtime Themes in the tab at
Project -> Options -> Application tab
This solved my problem for me in RAD Studio 2010.
I just tried a simple test. TImageList contains a PNG image with transparency. I render the image on the second TImage using:
imlImageList.Draw(img2.Canvas, 0, 0, 0);
What made the difference for me was setting img2.Transparent := true (I used the designer, not code).
I stumbled over this discussion-thread:
Tranparent PNGs in D2009 TImageList
#Pekka Nyyssonen: Setting ColorDepth to cd32Bit and DrawingStyle to dsTransparent
worked for me.
I don't have access to delphi 2009 my self so I havn't tried it out, though...
There are several ways to add transparent images to an image list.
With AddMasked or InsertMasked, you add an image and tags a color to be the transparent color:
procedure InsertMasked(Index: Integer; Image: TBitmap; MaskColor: TColor);
function AddMasked(Image: TBitmap; MaskColor: TColor): Integer;
With Insert or Add, you add an image and a mask. The mask if a 2 color (black/white) image where only the white pixels from the image are used, the others are transparent.
function Add(Image, Mask: TBitmap): Integer;
procedure Insert(Index: Integer; Image, Mask: TBitmap);
To the best of my knowledge, this cannot be acheived. None of the suggestions given result in a properly alpha-blended image, which is the primary requirement.
Maybe by defining a class derived from TImageList, which can then access protected methods, something could be got to work. My solution for now is to use a third-party custom ImageList component specifically for this.
Related
After going through several other related questions I couldn't come up with a working code for this, so please spare the "duplicate question" tags.
Given a PNG image with either per-pixel alpha channel or single-color transparency, I need code to draw it onto a TBitmap32 which already contains an image (some drawing goes on before the PNG part). So let's say my TBitmap32 is 200x200, I do some drawing on it, then I want to ~insert a smaller transparent PNG image on top of its current content, transparently according to the PNG's alpha channel data or single-color alpha.
Uses pngimage, GR32;
procedure TForm1.Button1Click(Sender: TObject);
Var b: TBitmap;
b32: TBitmap32;
p: TPngImage;
begin
b := TBitmap.Create;
b32 := TBitmap32.Create;
p := TPngImage.Create;
// 50x50 PNG
p.LoadFromFile('z:\test2.png');
b.Width := 200;
b.Height := 200;
b32.Width := 200;
b32.Height := 200;
// some drawing happens on the b32~
// insert code here to draw the png onto the b32, on top of
// what's already drawn, and at specific coordinates i.e 10,10
/////////////////////////////
b32.DrawTo(b.Canvas.Handle,0,0);
Canvas.Draw(0,0,b);
p.Free;
b32.Free;
b.Free;
end;
Original PNG:
Results so far:
There are two ways of working with transparent PNG files:
Load them into intermediary TBitmap32 bitmaps and then manipulate these TBitmap32 bitmaps.
Use TPngImage.Draw (implemented in Vcl.Imaging.pngimage with Delphi XE2 and later) directly on target Canvas, as you have pointed out.
The second way is preferable when it comes to transparency, because the code that you may find to load a PNG into TBitmap32 may work incorrectly. Here are the two examples of the incorrect code that is used most frequently:
(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, so when you pain, 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, for example, all white pixels become 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).
If you still prefer using TBitmap32, make the following sequence of steps:
Make sure your code correctly converts PNG to TBitmap32.
Do not use TBitmap32 with transparent images to draw directly on HDC, Canvas or TBitmap. Use dmBlend and DrawTo or BlockTransfer() to draw on another TBitmap32. For example, to draw transparently on a TBitmap, create an intermediary cache TBitmap32:
Copy the image from TBitmap to the cache TBitmap32;
Apply your transparent image to the cache TBitmap32 using DrawTo or BlockTransfer(), avoid using Canvas or HDC to mix two images because they lose alpha layer information;
Copy the image back from the cache TBitmap32 to your TBitmap.
The RTF image support is very limited (Windows support, not just in Delphi), other formats than bitmaps and metafiles work but are not displayed correctly by RichEdit control.
One thing I noticed is when Microsoft Word image is copied and pasted into RTF it scales
smoothly as opposed to pasting image manually (as a bitmap).
The reason for that is that Word keeps internally a scaled preview of an image in Metafile format (along with original image) and this scaled version is copied and pasted to RTF and apparently RTF renders MetaFile images smoothly when scaled in RichText editor.
Seems like a good workaround and after implementing embedding BMP in WPF function I noticed one problem I cannot get around: the resulting WMF is double the size of a bitmap. It looks like WMF stores paint buffer or a second copy of the image.
The code:
procedure DoCopyImage(AGraphic: TGraphic; AWidth, AHeight: Integer);
var
mf: TMetafile;
mfc: TMetafileCanvas;
r: Cardinal;
begin
mf := TMetafile.Create;
try
mf.Enhanced := True;
mf.SetSize(AWidth, AHeight);
mfc := TMetafileCanvas.Create(mf, 0);
try
// set clipping region to a whole image
r := CreateRectRgn(0, 0, AWidth, AHeight);
try
SelectClipRgn(mfc.Handle, r)
finally
DeleteObject(r);
end;
if (AGraphic.Width = AWidth) and (AGraphic.Height = AHeight) then
mfc.Draw(0, 0, AGraphic)
else
mfc.StretchDraw(Rect(0, 0, AWidth, AHeight), AGraphic);
finally
mfc.Free;
end;
// Clipboard.Assign(mf);
mf.SaveToFile('C:\4MB_MetaFile_Why.wmf');
finally
mf.Free;
end;
end;
I call it using TBitmap as TGraphic:
pic := TPicture.Create;
pic.LoadFromFile('C:\2MB_24bpp_Bitmap.bmp');
bmp := Graphics.TBitmap.Create;
bmp.Assign(pic.Graphic);
bmp.Dormant; // experimentation
bmp.FreeImage; // experimentation
DoCopyImage(bmp, bmp.Width, bmp.Height);
Can somebody find an explanation for this behaviour? Is WMF storing paint buffer along with bitmap? How to prevent it?
As to why your sizes may be different - once you Draw to the metafile you are turning over the responsibility to the Metafile for saving out the data. Your bit depth could be different in the output. I don't know for sure but I would also wonder if the StretchDraw call ends up saving the original input or if it saves a bitmap with the new number of pixels. If the image is smaller than the size stretch draw call that could explain the difference. You will also have some overhead in the metafile that will not be in the saved bitmap although that should be minimal.
You may want to look at the MetaFile Explorer. It is a tool that will show you the different draw commands that are embedded in the metafile. I tried your code and looked at the resulting image in MetaFileExplorer. The size of the embedded image looked OK to me. Look at the EMR_STRETCHBLT.cbBitsSrc size. However, I do see the size difference you are reporting so something is taking that space.
You stated:
The reason for that is that Word keeps internally a scaled preview of an image in Metafile format (along with original image) and this scaled version is copied and pasted to RTF and apparently RTF renders MetaFile images smoothly when scaled in RichText editor.
I question this assumption a little bit. A bitmap (any raster image) will show different scaling artifacts depending on how the scaling is performed. The Image Scaling Wikipedia page has some good examples of the differences.
When you write a bitmap into a metafile you are then letting the metafile perform the scaling when the image is drawn to the screen. Different algorithms used by the metafile drawing code vs. the RTF drawing code will cause the output to look different. However there is nothing special about a metafile that can't be done with a normal bitmap. In both cases the original pixels need to be resized/resampled.
Metafiles were really intended for something different than just embedding a single bitmap image. Here is a good Windows Metafile FAQ that explains some of the differences. Basically if you define lines, shapes, and text you can get really smooth scaling because you are describing the drawing, not storing individual pixels.
The key part to getting a good looking scaled image is to pick the right scaling routine. I have used the Graphics32 library for this in my application. The standard StretchDraw routines are designed to be fast - not high quality. When getting ready to draw to the screen I pick a Graphics32 resampler that gives me the results I want. This can change depending on the sizes. For example is your final output larger or smaller than the input image. I resize the image to the final output size and then just Draw instead of StretchDraw.
add this code before your mf.SaveToFile('C:\4MB_MetaFile_Why.wmf');
mf.MMHeight := Round(mf.Height / Screen.PixelsPerInch * 2540);
mf.MMWidth := Round(mf.Width / Screen.PixelsPerInch * 2540);
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?
-- 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.
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.
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:
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.
Use a helper routing to transfer transparency information from Vcl.Imaging.pngimage.TPngImage to TBitmap32.
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;
I'm getting the system imagelist (with SHGetFileInfo and SHGFI_LARGEICON), adding two of my own icons and attaching it to a TListView.
The problem is that if the user's icon size isn't set to 32x32 (like it's set to 48x48 for example) the Delphi7 TImageList fails with an "Invalid image size" error.
Does anyone know if a workaround is available? I've tried using TPngImageList but it leads to other issues.
Also, please note that I'd like to preserve the Alpha channel of the icons. Normal 1-bit transparency is not enough, as icons tend to look ugly that way.
Thanks!
I'm not aware of any limitation on the size of images that TImageList can hold. It sounds to me that your problem is that you have icons of different sizes and you can't hold icons of different sizes in the same image list.
If you are working with icons of different sizes then you are going to need to grow the smaller ones in size. You'll have to build it up in code, using a bitmap. You fill the bitmap with pure transparent alpha channel and then blt the smaller icon onto the centre of the bitmap.
Another option would be to maintain two separate image lists but if you need to draw the icons into the same list view then I think that won't get the job done. My guess is that you'll need to grow the small icons.
For alpha, you're going to need to create the image list handle yourself because the ColorDepth property doesn't exist in D7. Because of this, a vanilla D7 TImageList simply cannot support icons with alpha channels.
You work around this limitation by calling ImageList_Create, passing ILC_COLOR32 and assigning the result to ImageList.Handle. Do this before you add any images. You'll have to populate the list at run time rather than design time, but it sounds like you are already doing that.
Here's a screen shot of a 48x48 tool button with a 32bpp icon with alpha transparency:
It's true that I made this in D2010, but my above workaround will work for D7 – I used that mechanism until quite recently with D6. I'm just showing this to prove that the image list can hold 48px icons. Since TImageList is just a wrapper around the system image list component, I believe what you are attempting should be perfectly feasible.
Just when I was about to give up this page led me to the solution:
http://delphihaven.wordpress.com/2010/09/06/custom-drawing-on-glass-2/
Apparently, if you try to add an icon that is bigger than 32x32 to a timagelist in Delphi7, the VCL will give you an "Invalid image size" error while it could simply call the himagelist API - which can easily handle it.
Here is the complete solution:
unit ImageListFix;
interface
uses CommCtrl, Graphics, ImgList;
type
TImageListFixer = class(TCustomImageList)
public
function AddIcon(Image: TIcon): Integer;
end;
implementation
function TImageListFixer.AddIcon(Image: TIcon): Integer;
begin
if Image = nil then
Result := Add(nil, nil)
else
begin
Result := ImageList_AddIcon(Handle, Image.Handle);
Change;
end;
end;
end.
And the code for adding icons to the system imagelist:
DocumentImgList:=TImageListFixer(GetSystemLargeIconsList);
IconToAdd:=TIcon.Create;
try
IconToAdd.Handle := LoadImage(0, 'c:\Ico1.ico', IMAGE_ICON, DocumentImgList.Width, DocumentImgList.Height, LR_LOADFROMFILE);
DocumentImgList.AddIcon(IconToAdd);
IconToAdd.Handle := LoadImage(0, 'c:\Ico2.ico', IMAGE_ICON, DocumentImgList.Width, DocumentImgList.Height, LR_LOADFROMFILE);
DocumentImgList.AddIcon(IconToAdd);
finally
IconToAdd.Free;
end;
TImageList raises an "Invalid image size" error under only 2 conditions:
1) The TImageList's Height or Width property is less than 1, or the Height property is greater than 32768, when the TImageList is initially created via the CreateSize() constructor (there are no such limitations imposed by the Height and Width property setters).
2) you try to add/insert a new TBitmap or TIcon whose dimensions do not fit within TImageList's internal image.
The TImageList of Delphi 2009 has support for PNG images by adding them in the imagelist editor. Is there any way to extract a TPngImage from a TImagelist and preserving the alpha channel?
What I want to do is actually to extract the images from one TImageList, make a disabled version of them and then add them to another TImageList. During this operation I would of course like to preserve the alpha channel of the PNG images.
I did something like this with Delphi 2006.
TImageList contains a protected method GetImages. It can be accessed using the "protected bug"
type
TGetImageImageList = class (TImageList) // Please use a better name!
end;
You can cast the imagelist to the TGetImageImageList to get to the GetImages.
begin
TGetImageList(ImageList).GetImages(index, bitmap, mask);
end;
Bitmap contains the bitmap and mask is a black and white bitmap that determines the transparant sections.
You now can change the bitmap and store it using:
function Add(Image, Mask: TBitmap): Integer;
I hope this gives you enough pointers to explore further.