BSOne.SFC/EM.Lib/PNGComponents/Source/PngFunctions.pas

551 lines
18 KiB
Plaintext

unit PngFunctions;
interface
uses
Windows, Graphics, ImgList, Contnrs, pngimage;
type
TPngOption = (pngBlendOnDisabled, pngGrayscaleOnDisabled);
TPngOptions = set of TPngOption;
TRGBLine = array[Word] of TRGBTriple;
PRGBLine = ^TRGBLine;
TRGBALine = array[Word] of TRGBQuad;
PRGBALine = ^TRGBALine;
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 CopyImageFromImageList(Dest: TPngImage; ImageList: TCustomImageList; Index: Integer);
procedure SlicePNG(JoinedPNG: TPngImage; Columns, Rows: Integer; out SlicedPNGs: TObjectList);
implementation
uses
SysUtils, Classes, PngImageList;
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);
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 CopyImageFromImageList(Dest: TPngImage; ImageList: TCustomImageList; Index: Integer);
var
Icon: TIcon;
IconInfo: TIconInfo;
ColorBitmap, MaskBitmap: TBitmap;
X, Y: Integer;
AlphaLine: pngimage.PByteArray;
Png: TPngImageCollectionItem;
begin
if ImageList is TPngImageList then begin
//This is easy, just copy the PNG object from the imagelist to the PNG object
//from the button
Png := TPNGImageList(ImageList).PngImages[Index];
if Png <> nil then
Dest.Assign(Png.PngImage);
end
else begin
Icon := TIcon.Create;
ColorBitmap := TBitmap.Create;
MaskBitmap := TBitmap.Create;
try
//Try to copy an icon to a PNG object, including transparency
ImageList.GetIcon(Index, Icon);
if GetIconInfo(Icon.Handle, IconInfo) then begin
//First, pump the colors into the PNG object
ColorBitmap.Handle := IconInfo.hbmColor;
ColorBitmap.PixelFormat := pf24bit;
Dest.Assign(ColorBitmap);
//Finally, copy the transparency
Dest.CreateAlpha;
MaskBitmap.Handle := IconInfo.hbmMask;
for Y := 0 to Dest.Height - 1 do begin
AlphaLine := Dest.AlphaScanline[Y];
for X := 0 to Dest.Width - 1 do
AlphaLine^[X] := Integer(GetPixel(MaskBitmap.Canvas.Handle, X, Y) = COLORREF(clBlack)) * $FF;
end;
end;
finally
MaskBitmap.Free;
ColorBitmap.Free;
Icon.Free;
end;
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;
type
TPNGObject = class(TPngImage);
initialization
TPicture.RegisterFileFormat('', '', TPNGObject);
finalization
TPicture.UnregisterGraphicClass(TPNGObject);
end.