unit PngFunctions;

interface

uses
  Windows, Graphics, ImgList, Contnrs, pngimage;

{$IF RTLVersion < 20.0 }
  {$IF RTLVersion < 15.0 }
    PngComponents are only compatible with Delphi 7 and higher!
  {$IFEND}
type
  TPngImage = TPNGObject;
{$IFEND}

type
  TPngOption = (pngBlendOnDisabled, pngGrayscaleOnDisabled);
  TPngOptions = set of TPngOption;

procedure MakeDisabledImage(Image: TPngImage; const Options: TPngOptions);
procedure MakeImageBlended(Image: TPngImage; Amount: Byte = 127);
procedure MakeImageGrayscale(Image: TPngImage; Amount: Byte = 255);
procedure DrawPNG(Png: TPngImage; Canvas: TCanvas; const ARect: TRect; const Options: TPngOptions);
procedure ConvertToPNG(Source: TGraphic; Dest: TPngImage);
procedure CreatePNG(Color, Mask: TBitmap; Dest: TPngImage; InverseMask: Boolean = False);
procedure CreatePNGMasked(Bitmap: TBitmap; Mask: TColor; Dest: TPngImage);
procedure SlicePNG(JoinedPNG: TPngImage; Columns, Rows: Integer; out SlicedPNGs: TObjectList);

implementation

uses
  SysUtils;

function ColorToTriple(Color: TColor): TRGBTriple;
var
  ColorRGB: Longint;
begin
  ColorRGB := ColorToRGB(Color);
  Result.rgbtBlue := ColorRGB shr 16 and $FF;
  Result.rgbtGreen := ColorRGB shr 8 and $FF;
  Result.rgbtRed := ColorRGB and $FF;
end;

procedure MakeImageBlended(Image: TPngImage; Amount: Byte = 127);

  procedure ForceAlphachannel(BitTransparency: Boolean; TransparentColor: TColor);
  var
    Assigner: TBitmap;
    Temp: TPngImage;
    X, Y: Integer;
    Line: pngimage.PByteArray;
    Current: TColor;
  begin
    //Not all formats of PNG support an alpha-channel (paletted images for example),
    //so with this function, I simply recreate the PNG as being 32-bits, effectivly
    //forcing an alpha-channel on it.
    Temp := TPngImage.Create;
    try
      Assigner := TBitmap.Create;
      try
        Assigner.Width := Image.Width;
        Assigner.Height := Image.Height;
        Temp.Assign(Assigner);
      finally
        Assigner.Free;
      end;
      Temp.CreateAlpha;
      for Y := 0 to Image.Height - 1 do begin
        Line := Temp.AlphaScanline[Y];
        for X := 0 to Image.Width - 1 do begin
          Current := Image.Pixels[X, Y];
          Temp.Pixels[X, Y] := Current;
          if BitTransparency and (Current = TransparentColor) then
            Line[X] := 0
          else
            Line[X] := Amount;
        end;
      end;
      Image.Assign(Temp);
    finally
      Temp.Free;
    end;
  end;

var
  X, Y: Integer;
  Line: pngimage.PByteArray;
  Forced: Boolean;
  TransparentColor: TColor;
  BitTransparency: Boolean;
begin
  //If the PNG doesn't have an alpha channel, then add one
  BitTransparency := Image.TransparencyMode = ptmBit;
  TransparentColor := Image.TransparentColor;
  Forced := False;
  if not (Image.Header.ColorType in [COLOR_RGBALPHA, COLOR_GRAYSCALEALPHA]) then begin
    Forced := Image.Header.ColorType in [COLOR_GRAYSCALE, COLOR_PALETTE];
    if Forced then
      ForceAlphachannel(BitTransparency, TransparentColor)
    else
      Image.CreateAlpha;
  end;

  //Divide the alpha values by 2
  if not Forced and (Image.Header.ColorType in [COLOR_RGBALPHA, COLOR_GRAYSCALEALPHA]) then begin
    for Y := 0 to Image.Height - 1 do begin
      Line := Image.AlphaScanline[Y];
      for X := 0 to Image.Width - 1 do begin
        if BitTransparency and (Image.Pixels[X, Y] = TransparentColor) then
          Line[X] := 0
        else
          Line[X] := Round(Line[X] / 256 * (Amount + 1));
      end;
    end;
  end;
end;

procedure MakeImageGrayscale(Image: TPngImage; Amount: Byte = 255);

  procedure GrayscaleRGB(var R, G, B: Byte);
  { Performance optimized version without floating point operations by Christian Budde }
  var
    X: Byte;
  begin
    X := (R * 77 + G * 150 + B * 29) shr 8;
    R := ((R * (255 - Amount)) + (X * Amount) + 128) shr 8;
    G := ((G * (255 - Amount)) + (X * Amount) + 128) shr 8;
    B := ((B * (255 - Amount)) + (X * Amount) + 128) shr 8;
    (* original code
    X := Round(R * 0.30 + G * 0.59 + B * 0.11);
    R := Round(R / 256 * (256 - Amount - 1)) + Round(X / 256 * (Amount + 1));
    G := Round(G / 256 * (256 - Amount - 1)) + Round(X / 256 * (Amount + 1));
    B := Round(B / 256 * (256 - Amount - 1)) + Round(X / 256 * (Amount + 1));
    *)
  end;

var
  X, Y, PalCount: Integer;
  Line: PRGBLine;
  PaletteHandle: HPalette;
  Palette: array[Byte] of TPaletteEntry;
begin
  //Don't do anything if the image is already a grayscaled one
  if not (Image.Header.ColorType in [COLOR_GRAYSCALE, COLOR_GRAYSCALEALPHA]) then begin
    if Image.Header.ColorType = COLOR_PALETTE then begin
      //Grayscale every palette entry
      PaletteHandle := Image.Palette;
      PalCount := GetPaletteEntries(PaletteHandle, 0, 256, Palette);
      for X := 0 to PalCount - 1 do
        GrayscaleRGB(Palette[X].peRed, Palette[X].peGreen, Palette[X].peBlue);
      SetPaletteEntries(PaletteHandle, 0, PalCount, Palette);
      Image.Palette := PaletteHandle;
    end
    else begin
      //Grayscale every pixel
      for Y := 0 to Image.Height - 1 do begin
        Line := Image.Scanline[Y];
        for X := 0 to Image.Width - 1 do
          GrayscaleRGB(Line[X].rgbtRed, Line[X].rgbtGreen, Line[X].rgbtBlue);
      end;
    end;
  end;
end;

procedure DrawPNG(Png: TPngImage; Canvas: TCanvas; const ARect: TRect; const Options: TPngOptions);
var
  PngCopy: TPngImage;
begin
  if Options <> [] then begin
    PngCopy := TPngImage.Create;
    try
      PngCopy.Assign(Png);
      if pngBlendOnDisabled in Options then
        MakeImageBlended(PngCopy);
      if pngGrayscaleOnDisabled in Options then
        MakeImageGrayscale(PngCopy);
      PngCopy.Draw(Canvas, ARect);
    finally
      PngCopy.Free;
    end;
  end
  else begin
    Png.Draw(Canvas, ARect);
  end;
end;

procedure ConvertToPNG(Source: TGraphic; Dest: TPngImage);
type
  TRGBALine = array[Word] of TRGBQuad;
  PRGBALine = ^TRGBALine;
var
  MaskLines: array of pngimage.PByteArray;

  function ColorToTriple(const Color: TColor): TRGBTriple;
  begin
    Result.rgbtBlue := Color shr 16 and $FF;
    Result.rgbtGreen := Color shr 8 and $FF;
    Result.rgbtRed := Color and $FF;
  end;

  procedure GetAlphaMask(SourceColor: TBitmap);
  type
    TBitmapInfoV4 = packed record
      bmiHeader: TBitmapV4Header; //Otherwise I may not get per-pixel alpha values.
      bmiColors: array[0..2] of TRGBQuad; // reserve space for color lookup table
    end;
  var
    Bits: PRGBALine;
    { The BitmapInfo parameter to GetDIBits is delared as var parameter. So instead of casting around, we simply use
      the absolute directive to refer to the same memory area. }
    BitmapInfo: TBitmapInfoV4;
    BitmapInfoFake: TBitmapInfo absolute BitmapInfo;
    I, X, Y: Integer;
    HasAlpha: Boolean;
    BitsSize: Integer;
    bmpDC: HDC;
    bmpHandle: HBITMAP;
  begin
    BitsSize := 4 * SourceColor.Width * SourceColor.Height;
    Bits := AllocMem(BitsSize);
    try
      FillChar(BitmapInfo, SizeOf(BitmapInfo), 0);
      BitmapInfo.bmiHeader.bV4Size := SizeOf(BitmapInfo.bmiHeader);
      BitmapInfo.bmiHeader.bV4Width := SourceColor.Width;
      BitmapInfo.bmiHeader.bV4Height := -SourceColor.Height; //Otherwise the image is upside down.
      BitmapInfo.bmiHeader.bV4Planes := 1;
      BitmapInfo.bmiHeader.bV4BitCount := 32;
      BitmapInfo.bmiHeader.bV4V4Compression := BI_BITFIELDS;
      BitmapInfo.bmiHeader.bV4SizeImage := BitsSize;
      BitmapInfo.bmiColors[0].rgbRed := 255;
      BitmapInfo.bmiColors[1].rgbGreen := 255;
      BitmapInfo.bmiColors[2].rgbBlue := 255;

      { Getting the bitmap Handle will invalidate the Canvas.Handle, so it is important to retrieve them in the correct
        order. As parameter evaluation order is undefined and differs between Win32 and Win64, we get invalid values
        for Canvas.Handle when we use those properties directly in the call to GetDIBits. }
      bmpHandle := SourceColor.Handle;
      bmpDC := SourceColor.Canvas.Handle;
      if GetDIBits(bmpDC, bmpHandle, 0, SourceColor.Height, Bits, BitmapInfoFake, DIB_RGB_COLORS) > 0 then begin
        //Because Win32 API is a piece of crap when it comes to icons, I have to check
        //whether an has an alpha-channel the hard way.
        HasAlpha := False;
        for I := 0 to (SourceColor.Height * SourceColor.Width) - 1 do begin
          if Bits[I].rgbReserved <> 0 then begin
            HasAlpha := True;
            Break;
          end;
        end;
        if HasAlpha then begin
          //OK, so not all alpha-values are 0, which indicates the existence of an
          //alpha-channel.
          I := 0;
          for Y := 0 to SourceColor.Height - 1 do
            for X := 0 to SourceColor.Width - 1 do begin
              MaskLines[Y][X] := Bits[I].rgbReserved;
              Inc(I);
            end;
        end;
      end;
    finally
      FreeMem(Bits, BitsSize);
    end;
  end;

  function WinXPOrHigher: Boolean;
  var
    Info: TOSVersionInfo;
  begin
    Info.dwOSVersionInfoSize := SizeOf(Info);
    GetVersionEx(Info);
    Result := (Info.dwPlatformId = VER_PLATFORM_WIN32_NT) and
      ((Info.dwMajorVersion > 5) or
      ((Info.dwMajorVersion = 5) and (Info.dwMinorVersion >= 1)));
  end;

var
  Temp, SourceColor, SourceMask: TBitmap;
  X, Y: Integer;
  Line: PRGBLine;
  MaskLine, AlphaLine: pngimage.PByteArray;
  TransparentColor, CurrentColor: TColor;
  IconInfo: TIconInfo;
  AlphaNeeded: Boolean;
begin
  Assert(Dest <> nil, 'Dest is nil!');
  //A PNG does not have to be converted
  if Source is TPngImage then begin
    Dest.Assign(Source);
    Exit;
  end;

  AlphaNeeded := False;
  Temp := TBitmap.Create;
  SetLength(MaskLines, Source.Height);
  for Y := 0 to Source.Height - 1 do begin
    MaskLines[Y] := AllocMem(Source.Width);
    FillMemory(MaskLines[Y], Source.Width, 255);
  end;
  try
    //Initialize intermediate color bitmap
    Temp.Width := Source.Width;
    Temp.Height := Source.Height;
    Temp.PixelFormat := pf24bit;

    //Now figure out the transparency
    if Source is TBitmap then begin
      if Source.Transparent then begin
        //TBitmap is just about comparing the drawn colors against the TransparentColor
        if TBitmap(Source).TransparentMode = tmFixed then
          TransparentColor := TBitmap(Source).TransparentColor
        else
          TransparentColor := TBitmap(Source).Canvas.Pixels[0, Source.Height - 1];

        for Y := 0 to Temp.Height - 1 do begin
          Line := Temp.ScanLine[Y];
          MaskLine := MaskLines[Y];
          for X := 0 to Temp.Width - 1 do begin
            CurrentColor := GetPixel(TBitmap(Source).Canvas.Handle, X, Y);
            if CurrentColor = TransparentColor then begin
              MaskLine^[X] := 0;
              AlphaNeeded := True;
            end;
            Line[X] := ColorToTriple(CurrentColor);
          end;
        end;
      end
      else begin
        Temp.Canvas.Draw(0, 0, Source);
      end;
    end
    else if Source is TIcon then begin
      //TIcon is more complicated, because there are bitmasked (classic) icons and
      //alphablended (modern) icons. Not to forget about the "inverse" color.
      GetIconInfo(TIcon(Source).Handle, IconInfo);
      SourceColor := TBitmap.Create;
      SourceMask := TBitmap.Create;
      try
        SourceColor.Handle := IconInfo.hbmColor;
        SourceMask.Handle := IconInfo.hbmMask;
        Temp.Canvas.Draw(0, 0, SourceColor);
        for Y := 0 to Temp.Height - 1 do begin
          MaskLine := MaskLines[Y];
          for X := 0 to Temp.Width - 1 do begin
            if GetPixel(SourceMask.Canvas.Handle, X, Y) <> 0 then begin
              MaskLine^[X] := 0;
              AlphaNeeded := True;
            end;
          end;
        end;
        if (GetDeviceCaps(SourceColor.Canvas.Handle, BITSPIXEL) = 32) and WinXPOrHigher then begin
          //This doesn't neccesarily mean we actually have 32bpp in the icon, because the
          //bpp of an icon is always the same as the display settings, regardless of the
          //actual color depth of the icon :(
          AlphaNeeded := True;
          GetAlphaMask(SourceColor);
        end;
        //This still doesn't work for alphablended icons...
      finally
        SourceColor.Free;
        SourceMask.Free
      end;
    end;

    //And finally, assign the destination PNG image
    Dest.Assign(Temp);
    if AlphaNeeded then begin
      Dest.CreateAlpha;
      for Y := 0 to Dest.Height - 1 do begin
        AlphaLine := Dest.AlphaScanline[Y];
        CopyMemory(AlphaLine, MaskLines[Y], Temp.Width);
      end;
    end;

  finally
    for Y := 0 to Source.Height - 1 do
      FreeMem(MaskLines[Y], Source.Width);
    Temp.Free;
  end;
end;

procedure CreatePNG(Color, Mask: TBitmap; Dest: TPngImage; InverseMask: Boolean = False);
var
  Temp: TBitmap;
  Line: pngimage.PByteArray;
  X, Y: Integer;
begin
  Assert(Dest <> nil, 'Dest is nil!');
  //Create a PNG from two separate color and mask bitmaps. InverseMask should be
  //True if white means transparent, and black means opaque.
  if not (Color.PixelFormat in [pf24bit, pf32bit]) then begin
    Temp := TBitmap.Create;
    try
      Temp.Assign(Color);
      Temp.PixelFormat := pf24bit;
      Dest.Assign(Temp);
    finally
      Temp.Free;
    end;
  end
  else begin
    Dest.Assign(Color);
  end;

  //Copy the alpha channel.
  Dest.CreateAlpha;
  for Y := 0 to Dest.Height - 1 do begin
    Line := Dest.AlphaScanline[Y];
    for X := 0 to Dest.Width - 1 do begin
      if InverseMask then
        Line[X] := 255 - (GetPixel(Mask.Canvas.Handle, X, Y) and $FF)
      else
        Line[X] := GetPixel(Mask.Canvas.Handle, X, Y) and $FF;
    end;
  end;
end;

procedure CreatePNGMasked(Bitmap: TBitmap; Mask: TColor; Dest: TPngImage);
var
  Temp: TBitmap;
  Line: pngimage.PByteArray;
  X, Y: Integer;
begin
  Assert(Dest <> nil, 'Dest is nil!');
  //Create a PNG from two separate color and mask bitmaps. InverseMask should be
  //True if white means transparent, and black means opaque.
  if not (Bitmap.PixelFormat in [pf24bit, pf32bit]) then begin
    Temp := TBitmap.Create;
    try
      Temp.Assign(Bitmap);
      Temp.PixelFormat := pf24bit;
      Dest.Assign(Temp);
    finally
      Temp.Free;
    end;
  end
  else begin
    Dest.Assign(Bitmap);
  end;

  //Copy the alpha channel.
  Dest.CreateAlpha;
  for Y := 0 to Dest.Height - 1 do begin
    Line := Dest.AlphaScanline[Y];
    for X := 0 to Dest.Width - 1 do
      Line[X] := Integer(TColor(GetPixel(Bitmap.Canvas.Handle, X, Y)) <> Mask) * $FF;
  end;
end;

procedure SlicePNG(JoinedPNG: TPngImage; Columns, Rows: Integer; out SlicedPNGs: TObjectList);
var
  X, Y, ImageX, ImageY, OffsetX, OffsetY: Integer;
  Width, Height: Integer;
  Bitmap: TBitmap;
  BitmapLine: PRGBLine;
  AlphaLineA, AlphaLineB: pngimage.PByteArray;
  PNG: TPngImage;
begin
  //This function slices a large PNG file (e.g. an image with all images for a
  //toolbar) into smaller, equally-sized pictures.
  SlicedPNGs := TObjectList.Create(False);
  Width := JoinedPNG.Width div Columns;
  Height := JoinedPNG.Height div Rows;

  //Loop through the columns and rows to create each individual image
  for ImageY := 0 to Rows - 1 do begin
    for ImageX := 0 to Columns - 1 do begin
      OffsetX := ImageX * Width;
      OffsetY := ImageY * Height;
      Bitmap := TBitmap.Create;
      try
        Bitmap.Width := Width;
        Bitmap.Height := Height;
        Bitmap.PixelFormat := pf24bit;

        //Copy the color information into a temporary bitmap. We can't use TPngImage.Draw
        //here, because that would combine the color and alpha values.
        for Y := 0 to Bitmap.Height - 1 do begin
          BitmapLine := Bitmap.Scanline[Y];
          for X := 0 to Bitmap.Width - 1 do
            BitmapLine[X] := ColorToTriple(JoinedPNG.Pixels[X + OffsetX, Y + OffsetY]);
        end;

        PNG := TPngImage.Create;
        PNG.Assign(Bitmap);

        if JoinedPNG.Header.ColorType in [COLOR_GRAYSCALEALPHA, COLOR_RGBALPHA] then begin
          //Copy the alpha channel
          PNG.CreateAlpha;
          for Y := 0 to PNG.Height - 1 do begin
            AlphaLineA := JoinedPNG.AlphaScanline[Y + OffsetY];
            AlphaLineB := PNG.AlphaScanline[Y];
            for X := 0 to PNG.Width - 1 do
              AlphaLineB[X] := AlphaLineA[X + OffsetX];
          end;
        end;

        SlicedPNGs.Add(PNG);
      finally
        Bitmap.Free;
      end;
    end;
  end;
end;

procedure MakeDisabledImage(Image: TPngImage; const Options: TPngOptions);
begin
  if pngBlendOnDisabled in Options then
    MakeImageBlended(Image);
  if pngGrayscaleOnDisabled in Options then
    MakeImageGrayscale(Image);
end;

{$IF RTLVersion >= 20.0 }
{$IFDEF RegisterOldPngFormat}
type
  TPNGObject = class(TPngImage);
initialization
  TPicture.RegisterFileFormat('', '', TPNGObject);
finalization
  TPicture.UnregisterGraphicClass(TPNGObject);
{$ENDIF}
{$IFEND}
end.
