Why does loading of a PNG image format icon cause the "Out of system resources" exception?
Asked Answered
B

1

10

I have a specific icon file, which is composed from PNG compressed images and when I try to load it and add to a TImageList, the Out of system resources exception is raised.

The icon file is here: https://www.dropbox.com/s/toll6jhlwv3cpq0/icon.ico?m

Here is the code, which works with common type of icons, but fails with PNG image icons:

procedure TForm1.Button1Click(Sender: TObject);
var
  Icon: TIcon;
begin
  try
    Icon := TIcon.Create;
    Icon.LoadFromFile('icon.ico');
    ImageList1.AddIcon(Icon);
    Caption := IntToStr(ImageList1.Count);
  finally
    Icon.Free;
  end;
end;

Why does the PNG image icon format fail to load with Out of system resources exception ? How to add this kind of icon to an image list ?

Burcham answered 2/3, 2013 at 2:10 Comment(7)
How many other images are in the image list before adding this icon?Philan
@Ken, you can simulate this even if the image list is empty. I guess it's because the icon which is added is a multi-size icon.Commissariat
@Commissariat - Indeed. The first image can be added with no problem if extracted.Diverticulosis
@TLama: Thanks. I can't access DropBox from where I am right now. It would help if the question actually mentioned both of those things, though (it's a multi-size icon file, and it happens even with an empty ImageList). We shouldn't have to download files from other sites in order to get basic details for the question. :-) You should post that as an answer, since ImageLists have to contain only images that are the same size.Philan
The code is just an example with a Button and an empty ImageList. I should have mentioned that. I didn't know the ico file was a multi-icon (didn't consider TImageList would have a problem with that).Burcham
@Ken, thanks, but I'm afraid the case is not closed yet, since I've tried to extract the first icon from the file this way (as OP actually uses memory stream), but there's the same problem. The problem seems to be in something else than just the multi size icon.Commissariat
That icon consists from PNG images and the internal procedures doesn't count with this format. It expects only bitmap type icons. It will be necessary to read the icon header and check if the ICONDIRENTRY icon entry contains on its dwImageOffset PNG image header. I'll be right back with some code...Commissariat
C
12

Problem source:

The fact, that the icon is a multi-size icon file doesn't matter in this case. The icon's bitmap info header is internally read in a different way than should be. Your icon is the PNG format file icon and those have no bitmap info header structure. The reason, why you are getting Out of system resources exception, is because the internally used procedures expects from icon to have a TBitmapInfoHeader structure and then tries to create a temporary bitmap based on this header information. For your icon it was read like this:

enter image description here

If you take a look closer on the header values, you calculate that the system would try to create a bitmap which would be in size 169478669 * 218103808 pixels at 21060 B per pixel, what would need to have at least 778.5 EB (exabytes) of free memory :-)

Workaround:

That's of course impossible (at this time :-) and happens just because the PNG file format icons doesn't have this bitmap header, but instead contains directly a PNG image on that position. What you can do to workaround this is to check, if there's the PNG signature on the first 8 bytes of the image data, which actually checks if there's a PNG image and if so, treat it as a PNG image, otherwise try to add the icon in a common way through the TIcon object.

In the following code, the ImageListAddIconEx function iterates all the icons in the icon file and when there's one which matches the image list dimensions it is processed. The processing first checks those 8 bytes if there's a PNG image on data offset position and if so, it adds this PNG image to the image list. If not, then the icon is added in a common way through the TIcon object. This function returns index of the added icon in the image list if succeed, -1 otherwise:

uses
  PNGImage;

type
  TIconDirEntry = packed record
    bWidth: Byte;           // image width, in pixels
    bHeight: Byte;          // image height, in pixels
    bColorCount: Byte;      // number of colors in the image (0 if >= 8bpp)
    bReserved: Byte;        // reserved (must be 0)
    wPlanes: Word;          // color planes
    wBitCount: Word;        // bits per pixel
    dwBytesInRes: DWORD;    // image data size
    dwImageOffset: DWORD;   // image data offset
  end;

  TIconDir = packed record
    idReserved: Word;       // reserved (must be 0)
    idType: Word;           // resource type (1 for icons)
    idCount: Word;          // image count
    idEntries: array[0..255] of TIconDirEntry;
  end;
  PIconDir = ^TIconDir;

function ImageListAddIconEx(AImageList: TCustomImageList;
  AIconStream: TMemoryStream): Integer;
var
  I: Integer;
  Data: PByte;
  Icon: TIcon;
  IconHeader: PIconDir;
  Bitmap: TBitmap;
  PNGImage: TPNGImage;
  PNGStream: TMemoryStream;
const
  PNGSignature: array[0..7] of Byte = ($89, $50, $4E, $47, $0D, $0A, $1A, $0A);
begin
  // initialize result to -1
  Result := -1;
  // point to the icon header
  IconHeader := AIconStream.Memory;
  // iterate all the icons in the icon file
  for I := 0 to IconHeader.idCount - 1 do
  begin
    // if the icon dimensions matches to the image list, then...
    if (IconHeader.idEntries[I].bWidth = AImageList.Width) and
      (IconHeader.idEntries[I].bHeight = AImageList.Height) then
    begin
      // point to the stream beginning
      Data := AIconStream.Memory;
      // point with the Data pointer to the current icon image data
      Inc(Data, IconHeader.idEntries[I].dwImageOffset);
      // check if the first 8 bytes are PNG image signature; if so, then...
      if CompareMem(Data, @PNGSignature[0], 8) then
      begin
        Bitmap := TBitmap.Create;
        try
          PNGImage := TPNGImage.Create;
          try
            PNGStream := TMemoryStream.Create;
            try
              // set the icon stream position to the current icon data offset
              AIconStream.Position := IconHeader.idEntries[I].dwImageOffset;
              // copy the whole PNG image from icon data to a temporary stream
              PNGStream.CopyFrom(AIconStream,
                IconHeader.idEntries[I].dwBytesInRes);
              // reset the temporary stream position to the beginning
              PNGStream.Position := 0;
              // load the temporary stream data to a temporary TPNGImage object
              PNGImage.LoadFromStream(PNGStream);
            finally
              PNGStream.Free;
            end;
            // assign temporary TPNGImage object to a temporary TBitmap object
            Bitmap.Assign(PNGImage);
          finally
            PNGImage.Free;
          end;
          // to properly add the bitmap to the image list set the AlphaFormat
          // to afIgnored, see e.g. https://mcmap.net/q/897789/-add-a-png-image-to-a-imagelist-in-runtime-using-delphi-xe
          // if you don't have TBitmap.AlphaFormat property available, simply
          // comment out the following line
          Bitmap.AlphaFormat := afIgnored;
          // and finally add the temporary TBitmap object to the image list
          Result := AImageList.Add(Bitmap, nil);
        finally
          Bitmap.Free;
        end;
      end
      // the icon is not PNG type icon, so load it to a TIcon object
      else
      begin
        // reset the position of the input stream
        AIconStream.Position := 0;
        // load the icon and add it to the image list in a common way
        Icon := TIcon.Create;
        try
          Icon.LoadFromStream(AIconStream);
          Result := AImageList.AddIcon(Icon);
        finally
          Icon.Free;
        end;
      end;
      // break the loop to exit the function
      Break;
    end;
  end;
end;

And the usage:

procedure TForm1.Button1Click(Sender: TObject);
var
  Index: Integer;
  Stream: TMemoryStream;
begin
  Stream := TMemoryStream.Create;
  try
    Stream.LoadFromFile('d:\Icon.ico');
    Index := ImageListAddIconEx(ImageList1, Stream);
    if (Index <> -1) then
      ImageList1.Draw(Canvas, 8, 8, Index);
  finally
    Stream.Free;
  end;
end;

Conclusion:

I'd say if Microsoft recommends the PNG icon format to use (supported since Windows Vista), it would be fine to update the ReadIcon procedure in Graphics.pas to take this into account.

Something to read:

Commissariat answered 2/3, 2013 at 2:38 Comment(13)
Is there a way to check if a TIcon is a multiicon?Burcham
Is ExtractIconEx() the only possibility to get the small icon? The problem is that unlike in this example my real code has TMemoryStream where the icon data is located, so I can't use ExtractIconEx() without saving the TMemoryStream first, which would be very slow with thousands of icons.Burcham
I've tried your code anyway, and I get Out of System Resources also if Adding IconSmall. It must be something else that is problematic with this icon.Burcham
Well, it got more complicated as you're using streams. There's a way to use CreateIconFromResource, but I couldn't find a way how to stop losing transparency for this kind of icon. If you were using files, the code might be as easy as the first version of this post. I'll try to play with the CreateIconFromResource yet...Commissariat
Thanks TLama. This is some really good hacking you did. I would buy you a beer if it was possible here!Burcham
Glad I could help! Now let's hope that Embarcadero implements something similar into their Graphics unit (so far it's not yet implemented even in Delphi XE3) and that StackOverflow allow questioners buy a beer :-)Commissariat
@TLama: Very nice ! :-) If I hadn't already upvoted your first answer, I'd upvote again for this. <g>Philan
@Ken, thanks! I've been thinking the file is corrupt for a while, since the Icons reference was not updated to mention something so important like this (this must have cause many troubles like this to many applications). I just couldn't fell asleep, why does image editors show them properly, so finally I've opened the file in hex editor (what should I do before claiming something about corrupt file) where I've seen the PNG image signature, and that's the way how I discovered that there's something like PNG icon format :-)Commissariat
"and that StackOverflow allow questioners buy a beer :-)" how to do it?Burcham
There is a small compile problem with Delphi 7. The line if CompareMem(@Data[0], @PNGSignature[0], 8) then // results in Array type required Error; pData^ is $89, so the content is fine. Replacing @Data[0] with @Data compiles fine, but the CompareMem result is False.Burcham
Fixed in post (the Data variable is already a pointer and I should have pass it directly). But anyway, you're using Delphi 7, where I'm not sure how to deal with the missing TBitmap.AlphaFormat property, if will be possible to add the images transparently.Commissariat
You're welcome! You just need to comment out the line with AlphaFormat since this property was added in some later version of Delphi and the result seems to properly preserve transparency.Commissariat
Still not fixed in Delphi. Logged at quality.embarcadero.com/browse/RSP-21318Pallid

© 2022 - 2024 — McMap. All rights reserved.