How can I change color from a PNG image from white to black in Delphi?

3.4k views Asked by At

I am using TPNGList from Gustavo Daud version 1.4 in Delphi XE2

It is holding some PNG 256x256 images that I use as button images.

However there is a need to change the background color and the contrast of the image is not good.

So I have now White images for dark backgrounds.

I need to change them to Black for light backgrounds.

There is transparency and should be kept. There is only white pixels. But, a generic source to target function would be great either.

EDIT: Following sugestion for "go for it" I have tried the following, but only get black or white boxes:

procedure PNGInvertWB(Image: TPngImage; AWhite: Boolean);

  procedure WBInvertRGB(var R, G, B: Byte);
  var
    color: LongInt;
  begin
    if AWhite then
    begin
      if RGB(R, G, B) = clWhite then
      begin
        Color := ColorToRGB(clBlack);
        R := GetRValue(Color);
        G := GetGValue(Color);
        B := GetBValue(Color);
      end;
    end
    else
    begin
      if RGB(R, G, B) = clBlack then
      begin
        Color := ColorToRGB(clWhite);
        R := GetRValue(Color);
        G := GetGValue(Color);
        B := GetBValue(Color);
      end;
    end;
  end;

var
  X, Y, PalCount: Integer;
  Line: PRGBLine;
  PaletteHandle: HPalette;
  Palette: array[Byte] of TPaletteEntry;
begin
  if not (Image.Header.ColorType in [COLOR_GRAYSCALE, COLOR_GRAYSCALEALPHA]) then begin
    if Image.Header.ColorType = COLOR_PALETTE then begin
      PaletteHandle := Image.Palette;
      PalCount := GetPaletteEntries(PaletteHandle, 0, 256, Palette);
      for X := 0 to PalCount - 1 do
        WBInvertRGB(Palette[X].peRed, Palette[X].peGreen, Palette[X].peBlue);
      SetPaletteEntries(PaletteHandle, 0, PalCount, Palette);
      Image.Palette := PaletteHandle;
    end
    else begin
      for Y := 0 to Image.Height - 1 do begin
        Line := Image.Scanline[Y];
        for X := 0 to Image.Width - 1 do
          WBInvertRGB(Line[X].rgbtRed, Line[X].rgbtGreen, Line[X].rgbtBlue);
      end;
    end;
  end;
end;

I am calling this using this code:

procedure TDBNavigator.UpdateColor;
var
  PNGImage: TPngImage;
  HCColor : TColor;

  procedure Invert(AImage: TImage; AWhite: boolean);
  begin
    ConvertToPNG(AImage.Picture.Graphic, PNGImage);
    PNGInvertWB(PNGImage, not AWhite);
    AImage.Picture.Graphic := PNGImage;
  end;

begin
  Color := ThemeManager.CurrentPallete.Color[FThemeColor];

  HCColor := ThemeManager.CurrentPallete.HighContrast(FThemeColor);
  if HCColor <> FCurrentColor then
  begin
    Invert(uiPrevious, HCColor = clWhite);
    Invert(uiNext,     HCColor = clWhite);
    Invert(uiInsert,   HCColor = clWhite);
    Invert(uiPost,     HCColor = clWhite);
    Invert(uiCancel,   HCColor = clWhite);
    Invert(uiDelete,   HCColor = clWhite);
    Invert(uiRefresh,  HCColor = clWhite);
    FCurrentColor := HCColor;
  end;
end;

Not sure which part is wrong. This is part of one component and I am trying to change the image that was assigned on design time. It was a PNG image that I have loaded, 256x256 with transparency.

I need to use that TImage, I know it is not a button. And probably there is components that do that. I need to make by myself because a specific library that I am using.

I got the PNGInvertWB idea from one of the Gustavo functions on PNGFunctions:

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

So, I have no experience with images at all, what is wrong with this code?

This is how it looks like on the component where I have the images:

Original: Original

After: After this funcion above

I have used the following function from the PNGFunctions to try this:

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

  procedure GrayscaleRGB(var R, G, B: Byte);
  var
    X: Byte;
  begin
    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;

Where I have changed the GrayscaleRGB, since it was getting each pixel and changing it to gray scale, so I believed I could change to black or white accordingly.

2

There are 2 answers

1
Livius On BEST ANSWER

will be good to see what you have original a what you get after this code execute

and the question is: Do you really be sure that color of whole background is equ to black or white or it is near black or near white? Becouse you compare exact color RGB(R, G, B) = clWhite

try this

procedure WBInvertRGB(var R, G, B: Byte);
  var
    color: LongInt;
  begin
    if AWhite then
    begin
      if ((R>240) and (G>240) and (B>240)) then
      begin
        Color := ColorToRGB(clBlack);
        R := GetRValue(Color);
        G := GetGValue(Color);
        B := GetBValue(Color);
      end;
    end
    else
    begin
      if ((R<15) and (G<15) and (B<15)) then
      begin
        Color := ColorToRGB(clWhite);
        R := GetRValue(Color);
        G := GetGValue(Color);
        B := GetBValue(Color);
      end;
    end;
  end;

what you get after?

0
Livius On
  1. Then i suppose that this is becouse of transparency (change bottom left corner to some different color)

i do not know details about this png component and do not know if it have pixels property but put this at the end of procedure

Line := Image.Scanline[Image.Height - 1];
Line[0].rgbtRed:= 5;
Line[0].rgbtGreen:= 5;
Line[0].rgbtBlue:= 5;

any change in result?

  1. or you go inside palete code than full colors put breakpoints and you will see where your code break

and add comment here if this is color palete -this make difference becouse you can not change color to white and another to black. You need three steeps one change white to e.g. red black to white and red to black