【Delphi】ICON图标文件解析

icon是一种图标格式,用于系统图标、软件图标等,这种图标扩展名为*.icon、*.ico。常见的软件或windows桌面上的那些图标一般都是ICON格式的。

ICON文件格式比较简单,包含文件头段、图像数据头段、图像数据段。

文件头:

文件头为6个字节,定义如下:

type
  ICONDIR = packed record
    idReserved: SmallInt; // Reserved
    idType: SmallInt; // Resource type
    idCount: SmallInt; // Image Count
  end; // 6 bytes

idCount标记了文件中包含的图像数量。

图像数据头段:

紧接着文件头的就是图像数据头段了,它存放着文件中每个图像宽度、高度、颜色数量、数据段的偏移等信息,大小为16 * idCount。它是一个数组,每项数据16字节,定义如下:

type
  ICONDIRENTRY = packed record
    bWidth: Byte; // Width of the image
    bHeight: Byte; // Height of the image (2 * Height)
    bColorCount: Byte; // Number of colors in image (0 when >= 8 bpp)
    bReserved: Byte; // Reserved
    wPlanes: SmallInt; // Color Planes   (-> xHotspot [Cursor])
    wBitCount: SmallInt; // Bits per pixel (-> yHotspot [Cursor])
    dwBytesInRes: Integer; // How many bytes in this resource?
    dwImageOffset: Integer; // Where in the file is this image?
  end; // 16 bytes

读取完图像数据头段后,就可以知道文件中每个图标的大小,颜色位数了,同时直接根据数据段的偏移,读取图像数据段。图像数据的偏移是从文件最开始算起的。

图像数据段:

图像数据为多个图像的DIB数据,根据数据头段的偏移来定位。定位后,读取BIH信息。从BIH信息中,判断颜色位数大于8位的,记取XOR调色盘数据(小于8位的不存在XOR调色盘,只包含有一个MASK调色盘)。读取完XOR调色盘后,初始化图像DIB头,然后在文件中读取DIB数据。

下面来看看实现的加载代码:(注意:本示例代码,只加载ICO文件中颜色位数最高,最大的一个)

相关定义:

type
  dibBPPCts = (bpp_01 = 1, bpp_04 = 4, bpp_08 = 8, bpp_16 = 16, bpp_24 = 24,
    bpp_32 = 32);

  DIBItem = packed record
  private
    function GetBPP: dibBPPCts;
    function GetBytesPerScanline: Integer;
    function GetHeight: Integer;
    function GetWidth: Integer;
    function GetSize: Integer;
  public
    m_uBIH: BITMAPINFOHEADER;
    m_hDC: Integer;
    m_hDIB: Integer;
    m_hOldDIB: Integer;
    m_lpBits: Pointer;
    m_lpBitsSize: Integer;

    function Create(NewWidth, NewHeight: Integer; NewBPP: dibBPPCts): Boolean;
    procedure Free();
    procedure Clone(var toDIB: DIBItem);

    procedure GetPalette(var Palette: TBytes);
    procedure SetPalette(Palette: TBytes);

    function Stretch(dc: HDC; x, y, w, h, xSrc, ySrc, wSrc, hSrc: Integer;
      rop: Cardinal): Integer;
    function Stretch32(dc: HDC; BufferBits: Pointer; BytesPerRow, x, y, w, h, xSrc, ySrc, wSrc, hSrc: Integer): Integer;

    property Width: Integer read GetWidth;
    property Height: Integer read GetHeight;
    property BPP: dibBPPCts read GetBPP;
    property BytesPerScanline: Integer read GetBytesPerScanline;
    property Size: Integer read GetSize;
  end;

  DIBDATA = packed record
    XORDIB: DIBItem; // XOR DIB section
    ANDDIB: DIBItem; // AND DIB section
  end;

type
  BITMAPINFO_001 = packed record
    bmiHeader: BITMAPINFOHEADER;
    bmiColors: array [0 .. 7] of Byte;
  end;

  BITMAPINFO_004 = packed record
    bmiHeader: BITMAPINFOHEADER;
    bmiColors: array [0 .. 63] of Byte;
  end;

  BITMAPINFO_008 = packed record
    bmiHeader: BITMAPINFOHEADER;
    bmiColors: array [0 .. 1023] of Byte;
  end;

  BITMAPINFO_RGB = packed record
    bmiHeader: BITMAPINFOHEADER;
  end;

type
  icoBPPCts = (Colors_002 = 1, Colors_016 = 4, Colors_256 = 8, Color_True = 24,
    Color_ARGB = 32);

加载代码:

procedure TYxdIcon.LoadFromStream(Stream: TStream);
var
  nImg: Integer;
begin
  // Get icon header
  Fillchar(m_uDir, sizeof(m_uDir), 0);
  Stream.Read(m_uDir, sizeof(m_uDir));

  // Get icon entries
  SetLength(m_uDirEntry, m_uDir.idCount);
  Stream.Read(m_uDirEntry[0], sizeof(ICONDIRENTRY) * m_uDir.idCount);

  // Initialize arrays and monochrome palette
  //SetLength(m_OrderKey, m_uDir.idCount);
  //SetLength(m_uDIBData,  m_uDir.idCount);
  //SetLength(m_DIBData, m_uDir.idCount);
  SetLength(aPalAND, 8);
  FillChar(aPalAND[0], SizeOf(aPalAND), 0);
  aPalAND[4] := $FF;
  aPalAND[5] := $FF;
  aPalAND[6] := $FF;

  // Get images
  nMaxIndex := -1;
  nMaxW := 0;
  for nImg := 0 to m_uDir.idCount - 1 do begin
    if (m_uDirEntry[nImg].bWidth > nMaxW) or
      ((m_uDirEntry[nImg].bWidth = nMaxW) and (m_uDirEntry[nImg].bColorCount = 0)) then begin
      nMaxW := m_uDirEntry[nImg].bWidth;
      nMaxIndex := nImg;
    end;
  end;

  if nMaxIndex > -1 then begin
    // Move to begin of image data
    Stream.Position := m_uDirEntry[nMaxIndex].dwImageOffset;
    // Load BITMAPINFOHEADER
    Stream.Read(uBIH, SizeOf(uBIH));
    // Load XOR palette [?] (<= 8 bpp)
    if uBIH.biBitCount <= 8 then begin
      SetLength(aPalXOR, 4 * Trunc(Power(2, uBIH.biBitCount)));
      Stream.Read(aPalXOR[0], Length(aPalXOR));
    end;

    // Inititalize XOR DIB
    FillChar(m_uDIBData, SizeOf(m_uDIBData), 0);
    m_uDIBData.XORDIB.Create(uBIH.biWidth, uBIH.biHeight div 2, dibBPPCts(ubih.biBitCount));
    if uBIH.biBitCount <= 8 then
      m_uDIBData.XORDIB.SetPalette(aPalXOR);

    // Inititalize AND DIB
    m_uDIBData.ANDDIB.Create(uBIH.biWidth, uBIH.biHeight div 2, bpp_01);
    m_uDIBData.ANDDIB.SetPalette(aPalAND);

    // Read DIB bits
    m_uDIBData.XORDIB.m_lpBits := GetMemory(m_uDIBData.XORDIB.Size);
    m_uDIBData.ANDDIB.m_lpBits := GetMemory(m_uDIBData.ANDDIB.Size);
    Stream.Read(m_uDIBData.XORDIB.m_lpBits^, m_uDIBData.XORDIB.Size);
    Stream.Read(m_uDIBData.ANDDIB.m_lpBits^, m_uDIBData.ANDDIB.Size);

    m_OrderKey := IntToHex(uBIH.biWidth, 3) + IntToHex(uBIH.biHeight div 2, 3) +
      IntToHex(uBIH.biBitCount, 2);
  end;
  Changed(Self);
end;

DIBItem 就是ICON文件中一个图标的图像数据,代码如下:

{ DIBItem }

procedure DIBItem.Clone(var toDIB: DIBItem);
var
  aPal: TBytes;
begin
  if m_hDIB <> 0 then begin
    toDIB.Create(m_uBIH.biWidth, m_uBIH.biHeight, dibBPPCts(m_uBIH.biBitCount));
    if m_lpBits <> nil then begin
      toDIB.m_lpBits := GetMemory(Size);
      CopyMemory(toDIB.m_lpBits, m_lpBits, Size);
    end else
      toDIB.m_lpBits := nil;
    if (m_uBIH.biBitCount <= 8) then begin
        SetLength(aPal, 4 * Trunc(Power(2, m_uBIH.biBitCount)) - 1);
        GetPalette(aPal);
        toDIB.SetPalette(aPal);
    end;
  end;
end;

function DIBItem.Create(NewWidth, NewHeight: Integer;
  NewBPP: dibBPPCts): Boolean;
var
  BI_001: BITMAPINFO_001;
  BI_004: BITMAPINFO_004;
  BI_008: BITMAPINFO_008;
  BI_RGB: BITMAPINFO_RGB;
begin
  Free();

  // Define DIB header
  m_uBIH.biSize := SizeOf(m_uBIH);
  m_uBIH.biPlanes := 1;
  m_uBIH.biBitCount := Integer(NewBPP);
  m_uBIH.biWidth := NewWidth;
  m_uBIH.biHeight := NewHeight;
  m_uBIH.biSizeImage := 4 * ((m_uBIH.biWidth * m_uBIH.biBitCount + 31) div 32) * m_uBIH.biHeight;
  case NewBPP of
    bpp_01: BI_001.bmiHeader := m_uBIH;
    bpp_04: BI_004.bmiHeader := m_uBIH;
    bpp_08: BI_008.bmiHeader := m_uBIH;
  else
    BI_RGB.bmiHeader := m_uBIH
  end;

  // Create DIB and select into a DC
  m_hDC := CreateCompatibleDC(0);
  if m_hDC <> 0 then begin
    case NewBPP of
      bpp_01: m_hDIB := CreateDIBSection(m_hDC, pBitmapInfo(@BI_001)^, DIB_RGB_COLORS, m_lpBits, 0, 0);
      bpp_04: m_hDIB := CreateDIBSection(m_hDC, pBitmapInfo(@BI_004)^, DIB_RGB_COLORS, m_lpBits, 0, 0);
      bpp_08: m_hDIB := CreateDIBSection(m_hDC, pBitmapInfo(@BI_008)^, DIB_RGB_COLORS, m_lpBits, 0, 0);
    else
      m_hDIB := CreateDIBSection(m_hDC, pBitmapInfo(@BI_RGB)^, DIB_RGB_COLORS, m_lpBits, 0, 0);
    end;
    if m_hDIB <> 0 then
      m_hOldDIB := SelectObject(m_hDC, m_hDIB)
    else
      Free;
  end;
  Result := m_hDIB <> 0;
end;

procedure DIBItem.Free;
begin
  if m_hDC <> 0 then begin
    if m_hDIB <> 0 then begin
      SelectObject(m_hDC, m_hOldDIB);
      DeleteObject(m_hDIB);
    end;
    DeleteDC(m_hDC);
  end;
  if m_lpBits <> nil then begin
    FreeMemory(m_lpBits);
    m_lpBits := nil;
  end;
  FillChar(m_uBIH, SizeOf(m_uBIH), 0);
  m_hDC := 0;
  m_hDIB := 0;
  m_hOldDIB := 0;
end;

function DIBItem.GetBPP: dibBPPCts;
begin
  Result := dibBPPCts(m_uBIH.biBitCount);
end;

function DIBItem.GetBytesPerScanline: Integer;
begin
  Result := ((m_uBIH.biWidth * m_uBIH.biBitCount + 31) div 32) * 4;
end;

function DIBItem.GetHeight: Integer;
begin
  Result := m_uBIH.biHeight;
end;

procedure DIBItem.GetPalette(var Palette: TBytes);
begin
  if m_hDIB <> 0 then
    GetDIBColorTable(m_hDC, 0, Trunc(Power(2, m_uBIH.biBitCount)), Palette[Low(Palette)]);
end;

function DIBItem.GetSize: Integer;
begin
  Result := m_uBIH.biSizeImage;
end;

function DIBItem.GetWidth: Integer;
begin
  Result := m_uBIH.biWidth;
end;

procedure DIBItem.SetPalette(Palette: TBytes);
begin
  SetDIBColorTable(m_hDC, 0, (High(Palette) - Low(Palette) + 1) div 4, Palette[Low(Palette)])
end;

function DIBItem.Stretch(dc: HDC; x, y, w, h, xSrc, ySrc, wSrc, hSrc: Integer;
  rop: Cardinal): Integer;
var
  b001: BITMAPINFO_001;
  b004: BITMAPINFO_004;
  b008: BITMAPINFO_008;
  brgb: BITMAPINFO_RGB;
  iLen: Integer;
  lOldMode: Integer;
begin
  Result := 0;
  if m_hDIB = 0 then Exit;
  lOldMode := SetStretchBltMode(dc, COLORONCOLOR);
  iLen := Trunc(Power(2, m_uBIH.biBitCount));
  case dibBPPCts(m_uBIH.biBitCount) of
    bpp_01:
      begin
        b001.bmiHeader := m_uBIH;
        GetDIBColorTable(m_hDC, 0, iLen, b001.bmiColors[0]);
        StretchDIBits(dc, x, y, w, h, xSrc, ySrc, wsrc, hsrc, m_lpBits,
          pBitmapinfo(@b001)^, DIB_RGB_COLORS, rop);
      end;
    bpp_04:
      begin
        b004.bmiHeader := m_uBIH;
        GetDIBColorTable(m_hDC, 0, iLen, b004.bmiColors[0]);
        StretchDIBits(dc, x, y, w, h, xSrc, ySrc, wsrc, hsrc, m_lpBits,
          pBitmapinfo(@b004)^, DIB_RGB_COLORS, rop);
      end;
    bpp_08:
      begin
        b008.bmiHeader := m_uBIH;
        GetDIBColorTable(m_hDC, 0, iLen, b008.bmiColors[0]);
        StretchDIBits(dc, x, y, w, h, xSrc, ySrc, wsrc, hsrc, m_lpBits,
          pBitmapinfo(@b008)^, DIB_RGB_COLORS, rop);
      end;
    else begin
      brgb.bmiHeader := m_uBIH;
      StretchDIBits(dc, x, y, w, h, xSrc, ySrc, wsrc, hsrc, m_lpBits,
        pBitmapinfo(@brgb)^, DIB_RGB_COLORS, rop);
    end;
  end;
  SetStretchBltMode(dc, lOldMode);
  Result := 1;
end;

function DIBItem.Stretch32(dc: HDC; BufferBits: Pointer; BytesPerRow, x, y, w, h, xSrc, ySrc, wSrc, hSrc: Integer): Integer;
var
  i, i2, j, j2: Integer;
  Stretch: Boolean;
  FactorX, FactorY: Double;
  ABytesPerScanline: Integer;
  AlphaSource, ImageData: pPixelLine;
begin
  Result := 0;
  if (m_hDIB = 0) or (m_lpBits = nil) then Exit;

  Stretch := (W <> wSrc) or (H <> hSrc);
  if Stretch then FactorX := W / wSrc else FactorX := 1;
  if Stretch then FactorY := H / hSrc else FactorY := 1;

  AlphaSource := m_lpBits;
  ABytesPerScanline := BytesPerScanline;
  PByte(ImageData) := PByte(Integer(BufferBits) + BytesPerRow * (H - 1));
  BufferBits := ImageData;

  if m_uBIH.biBitCount = 32 then begin

    FOR j := 1 TO H DO begin
      FOR i := 0 TO W - 1 DO begin
        if Stretch then i2 := trunc(i / FactorX) else i2 := i;
        if (AlphaSource[i2].rgbReserved <> 0) then begin
          if (AlphaSource[i2].rgbReserved = 255) then begin
            ImageData[i] := AlphaSource[i2];
          end else
            with ImageData[i] do begin
              rgbRed := ($7F + AlphaSource[i2].rgbRed * AlphaSource[i2].rgbReserved + rgbRed *
                (not AlphaSource[i2].rgbReserved)) div $FF;
              rgbGreen := ($7F + AlphaSource[i2].rgbGreen * AlphaSource[i2].rgbReserved +
                rgbGreen * (not AlphaSource[i2].rgbReserved)) div $FF;
              rgbBlue := ($7F + AlphaSource[i2].rgbBlue * AlphaSource[i2].rgbReserved + rgbBlue *
               (not AlphaSource[i2].rgbReserved)) div $FF;
              rgbReserved := not (($7F + (not rgbReserved) * (not AlphaSource[i2].rgbReserved)) div $FF);
            end;
        end;
      end;

      {Move pointers}
      PByte(ImageData) := PByte(Integer(BufferBits) - BytesPerRow * j);
      if Stretch then j2 := trunc(j / FactorY) else j2 := j;
      PByte(AlphaSource) := PByte(m_lpBits) + ABytesPerScanline * j2;
    end
  end;
  Result := 1;
end;

ICON的显示:

本示例中的TYxdIcon继承自TIcon,本身就是一个Graphic了,所以可以直接重载Draw实现显示。

type
  TYxdIcon = class(TIcon)
  private
    m_uDir: ICONDIR; // Icon file header
    m_uDirEntry: array of ICONDIRENTRY; // Icon image headers
    m_OrderKey: string; // Image format key
    aPalXOR: TBytes;
    aPalAND: TBytes;
    nMaxW: Byte;
    nMaxIndex: Integer;
    uBIH: BITMAPINFOHEADER;

    BufferDC: HDC;
    OldBufferBitmap, BufferBitmap: HBitmap;
    LastW, LastH: Integer;
    FUseBuffer: Boolean;
  protected
    m_uDIBData: DIBDATA; // Icon data (DIBs)
    function GetEmpty: Boolean; override;
    function GetHeight: Integer; override;
    function GetWidth: Integer; override;
    procedure Draw(ACanvas: TCanvas; const Rect: TRect); override;
    function GetSupportsPartialTransparency: Boolean; override;
  public
    constructor Create; override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure LoadFromStream(Stream: TStream); override;
    procedure SaveToStream(Stream: TStream); override;
    property UseBuffer: Boolean read FUseBuffer write FUseBuffer;
  end;
procedure TYxdIcon.Draw(ACanvas: TCanvas; const Rect: TRect);
var
  BitmapInfo: TBitmapInfo;
  BufferBits, Buf: Pointer;
  AlphaSource, ImageData: pPixelLine;
  W, H: Integer;
  BytesPerRow: Integer;
  I, J: Integer;
begin
  W := Rect.Right - Rect.Left;
  H := Rect.Bottom - Rect.Top;

  BitmapInfo := GetBitmapInfoHeader(W, H);
  BufferDC := CreateCompatibleDC(0);
  if (BufferDC = 0) then Exit;

  BytesPerRow := (((BitmapInfo.bmiHeader.biBitCount * W) + 31) and
    not 31) div 8;

  BufferBitmap := CreateDIBSection(BufferDC, pBitmapInfo(@BitmapInfo)^,
    DIB_RGB_COLORS, BufferBits, 0, 0);
  OldBufferBitmap := SelectObject(BufferDC, BufferBitmap);
  BitBlt(BufferDC, 0, 0, W, H, ACanvas.Handle, Rect.Left, Rect.Top, SRCCOPY);

  if m_uDIBData.XORDIB.BPP = bpp_32 then begin
    m_uDIBData.XORDIB.Stretch32(BufferDC, BufferBits, BytesPerRow, Rect.Left, Rect.Top,
      W, H, 0, 0, m_uDIBData.XORDIB.Width, m_uDIBData.XORDIB.Height);
  end else begin
    // 画 Mask 层
    m_uDIBData.ANDDIB.Stretch(BufferDC, Rect.Left, Rect.Top, W, H, 0, 0,
      m_uDIBData.XORDIB.Width, m_uDIBData.XORDIB.Height, SRCCOPY);

    // 保存透明区域信息
    Buf := GetMemory(BytesPerRow*H);
    CopyMemory(Buf, BufferBits, BytesPerRow*H);

    // 画实际图像
    m_uDIBData.XORDIB.Stretch(BufferDC, Rect.Left, Rect.Top, W, H, 0, 0,
      m_uDIBData.XORDIB.Width, m_uDIBData.XORDIB.Height, SRCCOPY);

    // 应用 Mask
    PByte(ImageData) := PByte(BufferBits);
    AlphaSource := Buf;
    FOR j := 1 TO H DO begin
      FOR i := 0 TO W - 1 DO begin
        if (AlphaSource[i].rgbBlue = 0) and (AlphaSource[i].rgbGreen = 0) and (AlphaSource[i].rgbRed = 0) then begin
          ImageData[i].rgbReserved := $ff;
        end else
          ImageData[i].rgbReserved := 0;
      end;
      {Move pointers}
      inc(PByte(AlphaSource), BytesPerRow);
      inc(PByte(ImageData), BytesPerRow);
    end;
    FreeMemory(Buf);
  end;

  TYxdCanvas(ACanvas).RequiredState([csHandleValid]);
  BitBlt(ACanvas.Handle, Rect.Left, Rect.Top, W, H, BufferDC, 0, 0, SRCCOPY);

  SelectObject(BufferDC, OldBufferBitmap);
  DeleteObject(BufferBitmap);
  DeleteDC(BufferDC);
  BufferBitmap := 0;
end;

显示中主要特别处理的地方就是区分是否为32位的带透明通道图标。不带透明通道的,使用MASK层进行透明处理。