{* UltraStar Deluxe - Karaoke Game * * UltraStar Deluxe is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * $URL$ * $Id$ *} unit UImage; interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses SDL, UPath; {$DEFINE HavePNG} {$DEFINE HaveBMP} {$DEFINE HaveJPG} const PixelFmt_RGBA: TSDL_Pixelformat = ( palette: nil; BitsPerPixel: 32; BytesPerPixel: 4; Rloss: 0; Gloss: 0; Bloss: 0; Aloss: 0; Rshift: 0; Gshift: 8; Bshift: 16; Ashift: 24; Rmask: $000000ff; Gmask: $0000ff00; Bmask: $00ff0000; Amask: $ff000000; ColorKey: 0; Alpha: 255 ); PixelFmt_RGB: TSDL_Pixelformat = ( palette: nil; BitsPerPixel: 24; BytesPerPixel: 3; Rloss: 0; Gloss: 0; Bloss: 0; Aloss: 0; Rshift: 0; Gshift: 8; Bshift: 16; Ashift: 0; Rmask: $000000ff; Gmask: $0000ff00; Bmask: $00ff0000; Amask: $00000000; ColorKey: 0; Alpha: 255 ); PixelFmt_BGRA: TSDL_Pixelformat = ( palette: nil; BitsPerPixel: 32; BytesPerPixel: 4; Rloss: 0; Gloss: 0; Bloss: 0; Aloss: 0; Rshift: 16; Gshift: 8; Bshift: 0; Ashift: 24; Rmask: $00ff0000; Gmask: $0000ff00; Bmask: $000000ff; Amask: $ff000000; ColorKey: 0; Alpha: 255 ); PixelFmt_BGR: TSDL_Pixelformat = ( palette: nil; BitsPerPixel: 24; BytesPerPixel: 3; Rloss: 0; Gloss: 0; Bloss: 0; Aloss: 0; Rshift: 16; Gshift: 8; Bshift: 0; Ashift: 0; Rmask: $00ff0000; Gmask: $0000ff00; Bmask: $000000ff; Amask: $00000000; ColorKey: 0; Alpha: 255 ); type TImagePixelFmt = ( ipfRGBA, ipfRGB, ipfBGRA, ipfBGR ); (******************************************************* * Image saving *******************************************************) {$IFDEF HavePNG} function WritePNGImage(const FileName: IPath; Surface: PSDL_Surface): boolean; {$ENDIF} {$IFDEF HaveBMP} function WriteBMPImage(const FileName: IPath; Surface: PSDL_Surface): boolean; {$ENDIF} {$IFDEF HaveJPG} function WriteJPGImage(const FileName: IPath; Surface: PSDL_Surface; Quality: integer): boolean; {$ENDIF} (******************************************************* * Image loading *******************************************************) function LoadImage(const Filename: IPath): PSDL_Surface; (******************************************************* * Image manipulation *******************************************************) function PixelFormatEquals(fmt1, fmt2: PSDL_PixelFormat): boolean; procedure ScaleImage(var ImgSurface: PSDL_Surface; Width, Height: cardinal); procedure FitImage(var ImgSurface: PSDL_Surface; Width, Height: cardinal); procedure ColorizeImage(ImgSurface: PSDL_Surface; NewColor: cardinal); implementation uses SysUtils, Classes, Math, {$IFDEF MSWINDOWS} Windows, {$ENDIF} {$IFDEF HaveJPG} {$IFDEF Delphi} Graphics, jpeg, {$ELSE} jpeglib, jerror, jcparam, jdatadst, jcapimin, jcapistd, {$ENDIF} {$ENDIF} {$IFDEF HavePNG} png, {$ENDIF} zlib, sdl_image, sdlutils, sdlstreams, UCommon, ULog; function IsRGBSurface(pixelFmt: PSDL_PixelFormat): boolean; begin Result := (pixelFmt.BitsPerPixel = 24) and (pixelFmt.RMask = $0000FF) and (pixelFmt.GMask = $00FF00) and (pixelFmt.BMask = $FF0000); end; function IsRGBASurface(pixelFmt: PSDL_PixelFormat): boolean; begin Result := (pixelFmt.BitsPerPixel = 32) and (pixelFmt.RMask = $000000FF) and (pixelFmt.GMask = $0000FF00) and (pixelFmt.BMask = $00FF0000) and (pixelFmt.AMask = $FF000000); end; function IsBGRSurface(pixelFmt: PSDL_PixelFormat): boolean; begin Result := (pixelFmt.BitsPerPixel = 24) and (pixelFmt.BMask = $0000FF) and (pixelFmt.GMask = $00FF00) and (pixelFmt.RMask = $FF0000); end; function IsBGRASurface(pixelFmt: PSDL_PixelFormat): boolean; begin Result := (pixelFmt.BitsPerPixel = 32) and (pixelFmt.BMask = $000000FF) and (pixelFmt.GMask = $0000FF00) and (pixelFmt.RMask = $00FF0000) and (pixelFmt.AMask = $FF000000); end; // Converts alpha-formats to BGRA, non-alpha to BGR, and leaves BGR(A) as is // sets converted to true if the surface needed to be converted function ConvertToBGR_BGRASurface(Surface: PSDL_Surface; out Converted: boolean): PSDL_Surface; var pixelFmt: PSDL_PixelFormat; begin pixelFmt := Surface.format; if (IsBGRSurface(pixelFmt) or IsBGRASurface(pixelFmt)) then begin Converted := false; Result := Surface; end else begin // invalid format -> needs conversion if (pixelFmt.AMask <> 0) then Result := SDL_ConvertSurface(Surface, @PixelFmt_BGRA, SDL_SWSURFACE) else Result := SDL_ConvertSurface(Surface, @PixelFmt_BGR, SDL_SWSURFACE); Converted := true; end; end; // Converts alpha-formats to RGBA, non-alpha to RGB, and leaves RGB(A) as is // sets converted to true if the surface needed to be converted function ConvertToRGB_RGBASurface(Surface: PSDL_Surface; out Converted: boolean): PSDL_Surface; var pixelFmt: PSDL_PixelFormat; begin pixelFmt := Surface.format; if (IsRGBSurface(pixelFmt) or IsRGBASurface(pixelFmt)) then begin Converted := false; Result := Surface; end else begin // invalid format -> needs conversion if (pixelFmt.AMask <> 0) then Result := SDL_ConvertSurface(Surface, @PixelFmt_RGBA, SDL_SWSURFACE) else Result := SDL_ConvertSurface(Surface, @PixelFmt_RGB, SDL_SWSURFACE); Converted := true; end; end; (******************************************************* * Image saving *******************************************************) (*************************** * PNG section *****************************) {$IFDEF HavePNG} // delphi does not support setjmp()/longjmp() -> define our own error-handler procedure user_error_fn(png_ptr: png_structp; error_msg: png_const_charp); cdecl; begin raise Exception.Create(error_msg); end; procedure user_read_data(png_ptr: png_structp; data: png_bytep; length: png_size_t); cdecl; var inFile: TStream; begin inFile := TStream(png_get_io_ptr(png_ptr)); inFile.Read(data^, length); end; procedure user_write_data(png_ptr: png_structp; data: png_bytep; length: png_size_t); cdecl; var outFile: TStream; begin outFile := TStream(png_get_io_ptr(png_ptr)); outFile.Write(data^, length); end; procedure user_flush_data(png_ptr: png_structp); cdecl; //var // outFile: TStream; begin // binary files are flushed automatically, Flush() works with Text-files only //outFile := TStream(png_get_io_ptr(png_ptr)); //outFile.Flush(); end; procedure DateTimeToPngTime(time: TDateTime; var pngTime: png_time); var year, month, day: word; hour, minute, second, msecond: word; begin DecodeDate(time, year, month, day); pngTime.year := png_uint_16(year); pngTime.month := png_byte(month); pngTime.day := png_byte(day); DecodeTime(time, hour, minute, second, msecond); pngTime.hour := png_byte(hour); pngTime.minute := png_byte(minute); pngTime.second := png_byte(second); end; (* * ImageData must be in RGB-format *) function WritePNGImage(const FileName: IPath; Surface: PSDL_Surface): boolean; var png_ptr: png_structp; info_ptr: png_infop; pngFile: TStream; row: integer; rowData: array of png_bytep; // rowStride: integer; converted: boolean; colorType: integer; // time: png_time; begin Result := false; // open file for writing try pngFile := TBinaryFileStream.Create(FileName, fmCreate); except Log.LogError('Could not open file: "' + FileName.ToNative + '"', 'WritePngImage'); Exit; end; // only 24bit (RGB) or 32bit (RGBA) data is supported, so convert to it Surface := ConvertToRGB_RGBASurface(Surface, converted); png_ptr := nil; try // initialize png (and enable a user-defined error-handler that throws an exception on error) png_ptr := png_create_write_struct(PNG_LIBPNG_VER_STRING, nil, @user_error_fn, nil); // the error-handler is called if png_create_write_struct() fails, so png_ptr should always be <> nil if (png_ptr = nil) then begin Log.LogError('png_create_write_struct() failed', 'WritePngImage'); if (converted) then SDL_FreeSurface(Surface); Exit; end; info_ptr := png_create_info_struct(png_ptr); if (Surface^.format^.BitsPerPixel = 24) then colorType := PNG_COLOR_TYPE_RGB else colorType := PNG_COLOR_TYPE_RGBA; // define write IO-functions (POSIX-style FILE-pointers are not available in Delphi) png_set_write_fn(png_ptr, pngFile, @user_write_data, @user_flush_data); png_set_IHDR( png_ptr, info_ptr, Surface.w, Surface.h, 8, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); // TODO: do we need the modification time? //DateTimeToPngTime(Now, time); //png_set_tIME(png_ptr, info_ptr, @time); if (SDL_MUSTLOCK(Surface)) then SDL_LockSurface(Surface); // setup data SetLength(rowData, Surface.h); for row := 0 to Surface.h-1 do begin // set rowData-elements to beginning of each image row // Note: the byte-count of a row is pitch (which is not width*bitsPerPixel if the image is aligned) rowData[row] := @PChar(Surface.pixels)[(Surface.h-row-1) * Surface.pitch]; end; if (SDL_MUSTLOCK(Surface)) then SDL_UnlockSurface(Surface); png_write_info(png_ptr, info_ptr); png_write_image(png_ptr, png_bytepp(rowData)); png_write_end(png_ptr, nil); Result := true; except on E: Exception do Log.LogError(E.message, 'WritePngImage'); end; // free row-data SetLength(rowData, 0); // free png-resources if (png_ptr <> nil) then png_destroy_write_struct(@png_ptr, nil); if (converted) then SDL_FreeSurface(Surface); // close file pngFile.Free; end; {$ENDIF} (*************************** * BMP section *****************************) {$IFDEF HaveBMP} {$IFNDEF MSWINDOWS} const (* constants for the biCompression field *) BI_RGB = 0; BI_RLE8 = 1; BI_RLE4 = 2; BI_BITFIELDS = 3; BI_JPEG = 4; BI_PNG = 5; type BITMAPINFOHEADER = record biSize: longword; biWidth: longint; biHeight: longint; biPlanes: word; biBitCount: word; biCompression: longword; biSizeImage: longword; biXPelsPerMeter: longint; biYPelsPerMeter: longint; biClrUsed: longword; biClrImportant: longword; end; LPBITMAPINFOHEADER = ^BITMAPINFOHEADER; TBITMAPINFOHEADER = BITMAPINFOHEADER; PBITMAPINFOHEADER = ^BITMAPINFOHEADER; RGBTRIPLE = record rgbtBlue: byte; rgbtGreen: byte; rgbtRed: byte; end; tagRGBTRIPLE = RGBTRIPLE; TRGBTRIPLE = RGBTRIPLE; PRGBTRIPLE = ^RGBTRIPLE; RGBQUAD = record rgbBlue: byte; rgbGreen: byte; rgbRed: byte; rgbReserved: byte; end; tagRGBQUAD = RGBQUAD; TRGBQUAD = RGBQUAD; PRGBQUAD = ^RGBQUAD; BITMAPINFO = record bmiHeader: BITMAPINFOHEADER; bmiColors: array[0..0] of RGBQUAD; end; LPBITMAPINFO = ^BITMAPINFO; PBITMAPINFO = ^BITMAPINFO; TBITMAPINFO = BITMAPINFO; {$PACKRECORDS 2} BITMAPFILEHEADER = record bfType: word; bfSize: longword; bfReserved1: word; bfReserved2: word; bfOffBits: longword; end; {$PACKRECORDS DEFAULT} {$ENDIF} (* * ImageData must be in BGR-format *) function WriteBMPImage(const FileName: IPath; Surface: PSDL_Surface): boolean; var bmpFile: TStream; FileInfo: BITMAPINFOHEADER; FileHeader: BITMAPFILEHEADER; Converted: boolean; Row: integer; RowSize: integer; begin Result := false; // open file for writing try bmpFile := TBinaryFileStream.Create(FileName, fmCreate); except Log.LogError('Could not open file: "' + FileName.ToNative + '"', 'WriteBMPImage'); Exit; end; // only 24bit (BGR) or 32bit (BGRA) data is supported, so convert to it Surface := ConvertToBGR_BGRASurface(Surface, Converted); // aligned (4-byte) row-size in bytes RowSize := ((Surface.w * Surface.format.BytesPerPixel + 3) div 4) * 4; // initialize bitmap info FillChar(FileInfo, SizeOf(BITMAPINFOHEADER), 0); with FileInfo do begin biSize := SizeOf(BITMAPINFOHEADER); biWidth := Surface.w; biHeight := Surface.h; biPlanes := 1; biBitCount := Surface^.format^.BitsPerPixel; biCompression := BI_RGB; biSizeImage := RowSize * Surface.h; end; // initialize header-data FillChar(FileHeader, SizeOf(BITMAPFILEHEADER), 0); with FileHeader do begin bfType := $4D42; // = 'BM' bfOffBits := SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER); bfSize := bfOffBits + FileInfo.biSizeImage; end; // and move the whole stuff into the file ;-) try // write headers bmpFile.Write(FileHeader, SizeOf(BITMAPFILEHEADER)); bmpFile.Write(FileInfo, SizeOf(BITMAPINFOHEADER)); // write image-data if (SDL_MUSTLOCK(Surface)) then SDL_LockSurface(Surface); // BMP needs 4-byte alignment if (Surface.pitch mod 4 = 0) then begin // aligned correctly -> write whole image at once bmpFile.Write(Surface.pixels^, FileInfo.biSizeImage); end else begin // misaligned -> write each line separately // Note: for the last line unassigned memory (> last Surface.pixels element) // will be copied to the padding area (last bytes of a row), // but we do not care because the content of padding data is ignored anyhow. for Row := 0 to Surface.h do bmpFile.Write(PChar(Surface.pixels)[Row * Surface.pitch], RowSize); end; if (SDL_MUSTLOCK(Surface)) then SDL_UnlockSurface(Surface); Result := true; finally Log.LogError('Could not write file: "' + FileName.ToNative + '"', 'WriteBMPImage'); end; if (Converted) then SDL_FreeSurface(Surface); // close file bmpFile.Free; end; {$ENDIF} (*************************** * JPG section *****************************) {$IFDEF HaveJPG} function WriteJPGImage(const FileName: IPath; Surface: PSDL_Surface; Quality: integer): boolean; var {$IFDEF Delphi} Bitmap: TBitmap; BitmapInfo: TBitmapInfo; Jpeg: TJpegImage; row: integer; FileStream: TBinaryFileStream; {$ELSE} cinfo: jpeg_compress_struct; jerr : jpeg_error_mgr; jpgFile: TBinaryFileStream; rowPtr: array[0..0] of JSAMPROW; {$ENDIF} converted: boolean; begin Result := false; {$IFDEF Delphi} // only 24bit (BGR) data is supported, so convert to it if (IsBGRSurface(Surface.format)) then converted := false else begin Surface := SDL_ConvertSurface(Surface, @PixelFmt_BGR, SDL_SWSURFACE); converted := true; end; // create and setup bitmap Bitmap := TBitmap.Create; Bitmap.PixelFormat := pf24bit; Bitmap.Width := Surface.w; Bitmap.Height := Surface.h; // setup bitmap info on source image (Surface parameter) ZeroMemory(@BitmapInfo, SizeOf(BitmapInfo)); with BitmapInfo.bmiHeader do begin biSize := SizeOf(BITMAPINFOHEADER); biWidth := Surface.w; biHeight := Surface.h; biPlanes := 1; biBitCount := 24; biCompression := BI_RGB; end; if (SDL_MUSTLOCK(Surface)) then SDL_LockSurface(Surface); // use fast Win32-API functions to copy data instead of Bitmap.Canvas.Pixels if (Surface.pitch mod 4 = 0) then begin // if the image is aligned (to a 4-byte boundary) -> copy all data at once // Note: surfaces created with SDL (e.g. with SDL_ConvertSurface) are aligned SetDIBits(0, Bitmap.Handle, 0, Bitmap.Height, Surface.pixels, BitmapInfo, DIB_RGB_COLORS); end else begin // wrong alignment -> copy each line separately. // Note: for the last line unassigned memory (> last Surface.pixels element) // will be copied to the padding area (last bytes of a row), // but we do not care because the content of padding data is ignored anyhow. for row := 0 to Surface.h do begin SetDIBits(0, Bitmap.Handle, row, 1, @PChar(Surface.pixels)[row * Surface.pitch], BitmapInfo, DIB_RGB_COLORS); end; end; if (SDL_MUSTLOCK(Surface)) then SDL_UnlockSurface(Surface); // assign Bitmap to JPEG and store the latter try // init with nil so Free() will not fail if an exception occurs Jpeg := nil; Bitmap := nil; FileStream := nil; try Jpeg := TJPEGImage.Create; Jpeg.Assign(Bitmap); // compress image (don't forget this line, otherwise it won't be compressed) Jpeg.CompressionQuality := Quality; Jpeg.Compress(); // Note: FileStream needed for unicode filename support FileStream := TBinaryFileStream.Create(Filename, fmCreate); Jpeg.SaveToStream(FileStream); finally FileStream.Free; Bitmap.Free; Jpeg.Free; end; except Log.LogError('Could not save file: "' + FileName.ToNative + '"', 'WriteJPGImage'); Exit; end; {$ELSE} // based on example.pas in FPC's packages/base/pasjpeg directory // only 24bit (RGB) data is supported, so convert to it if (IsRGBSurface(Surface.format)) then converted := false else begin Surface := SDL_ConvertSurface(Surface, @PixelFmt_RGB, SDL_SWSURFACE); converted := true; end; // allocate and initialize JPEG compression object cinfo.err := jpeg_std_error(jerr); // msg_level that will be displayed. (Nomssi) //jerr.trace_level := 3; // initialize the JPEG compression object jpeg_create_compress(@cinfo); // open file for writing try jpgFile := TBinaryFileStream.Create(FileName, fmCreate); except Log.LogError('Could not open file: "' + FileName.ToNative + '"', 'WriteJPGImage'); Exit; end; // specify data destination jpeg_stdio_dest(@cinfo, @jpgFile); // set parameters for compression cinfo.image_width := Surface.w; cinfo.image_height := Surface.h; cinfo.in_color_space := JCS_RGB; cinfo.input_components := 3; cinfo.data_precision := 8; // set default compression parameters jpeg_set_defaults(@cinfo); jpeg_set_quality(@cinfo, quality, true); // start compressor jpeg_start_compress(@cinfo, true); if (SDL_MUSTLOCK(Surface)) then SDL_LockSurface(Surface); while (cinfo.next_scanline < cinfo.image_height) do begin // Note: the byte-count of a row is pitch (which is not width*bitsPerPixel if the image is aligned) rowPtr[0] := JSAMPROW(@PChar(Surface.pixels)[(Surface.h-cinfo.next_scanline-1) * Surface.pitch]); jpeg_write_scanlines(@cinfo, JSAMPARRAY(@rowPtr), 1); end; if (SDL_MUSTLOCK(Surface)) then SDL_UnlockSurface(Surface); // finish compression jpeg_finish_compress(@cinfo); // close the output file jpgFile.Free; // release JPEG compression object jpeg_destroy_compress(@cinfo); {$ENDIF} if (converted) then SDL_FreeSurface(Surface); Result := true; end; {$ENDIF} (******************************************************* * Image loading *******************************************************) (* * Loads an image from the given file *) function LoadImage(const Filename: IPath): PSDL_Surface; var FilenameCaseAdj: IPath; FileStream: TBinaryFileStream; SDLStream: PSDL_RWops; begin Result := nil; // try to adjust filename's case and check if it exists FilenameCaseAdj := Filename.AdjustCase(false); if (not FilenameCaseAdj.IsFile) then begin Log.LogError('Image-File does not exist "' + FilenameCaseAdj.ToNative + '"', 'LoadImage'); Exit; end; // load from file try SDLStream := SDLStreamSetup(TBinaryFileStream.Create(FilenameCaseAdj, fmOpenRead)); Result := IMG_Load_RW(SDLStream, 1); // Note: TBinaryFileStream is freed by SDLStream. SDLStream by IMG_Load_RW(). except Log.LogError('Could not load from file "' + FilenameCaseAdj.ToNative + '"', 'LoadImage'); Exit; end; end; (******************************************************* * Image manipulation *******************************************************) function PixelFormatEquals(fmt1, fmt2: PSDL_PixelFormat): boolean; begin Result := (fmt1^.BitsPerPixel = fmt2^.BitsPerPixel) and (fmt1^.BytesPerPixel = fmt2^.BytesPerPixel) and (fmt1^.Rloss = fmt2^.Rloss) and (fmt1^.Gloss = fmt2^.Gloss) and (fmt1^.Bloss = fmt2^.Bloss) and (fmt1^.Rmask = fmt2^.Rmask) and (fmt1^.Gmask = fmt2^.Gmask) and (fmt1^.Bmask = fmt2^.Bmask) and (fmt1^.Rshift = fmt2^.Rshift) and (fmt1^.Gshift = fmt2^.Gshift) and (fmt1^.Bshift = fmt2^.Bshift) ; end; procedure ScaleImage(var ImgSurface: PSDL_Surface; Width, Height: cardinal); var TempSurface: PSDL_Surface; begin TempSurface := ImgSurface; ImgSurface := SDL_ScaleSurfaceRect(TempSurface, 0, 0, TempSurface^.W,TempSurface^.H, Width, Height); SDL_FreeSurface(TempSurface); end; procedure FitImage(var ImgSurface: PSDL_Surface; Width, Height: cardinal); var TempSurface: PSDL_Surface; ImgFmt: PSDL_PixelFormat; begin TempSurface := ImgSurface; // create a new surface with given width and height ImgFmt := TempSurface^.format; ImgSurface := SDL_CreateRGBSurface( SDL_SWSURFACE, Width, Height, ImgFmt^.BitsPerPixel, ImgFmt^.RMask, ImgFmt^.GMask, ImgFmt^.BMask, ImgFmt^.AMask); // copy image from temp- to new surface SDL_SetAlpha(ImgSurface, 0, 255); SDL_SetAlpha(TempSurface, 0, 255); SDL_BlitSurface(TempSurface, nil, ImgSurface, nil); SDL_FreeSurface(TempSurface); end; (* // Old slow floating point version of ColorizeTexture. // For an easier understanding of the faster fixed point version below. procedure ColorizeTexture(TexSurface: PSDL_Surface; Col: cardinal); var clr: array[0..2] of double; // [0: R, 1: G, 2: B] hsv: array[0..2] of double; // [0: H(ue), 1: S(aturation), 2: V(alue)] delta, f, p, q, t: double; max: double; begin clr[0] := PixelColors[0]/255; clr[1] := PixelColors[1]/255; clr[2] := PixelColors[2]/255; max := maxvalue(clr); delta := max - minvalue(clr); hsv[0] := DestinationHue; // set H(ue) hsv[2] := max; // set V(alue) // calc S(aturation) if (max = 0.0) then hsv[1] := 0.0 else hsv[1] := delta/max; //ColorizePixel(PByteArray(Pixel), DestinationHue); h_int := trunc(hsv[0]); // h_int = |_h_| f := hsv[0]-h_int; // f = h-h_int p := hsv[2]*(1.0-hsv[1]); // p = v*(1-s) q := hsv[2]*(1.0-(hsv[1]*f)); // q = v*(1-s*f) t := hsv[2]*(1.0-(hsv[1]*(1.0-f))); // t = v*(1-s*(1-f)) case h_int of 0: begin clr[0] := hsv[2]; clr[1] := t; clr[2] := p; end; // (v,t,p) 1: begin clr[0] := q; clr[1] := hsv[2]; clr[2] := p; end; // (q,v,p) 2: begin clr[0] := p; clr[1] := hsv[2]; clr[2] := t; end; // (p,v,t) 3: begin clr[0] := p; clr[1] := q; clr[2] := hsv[2]; end; // (p,q,v) 4: begin clr[0] := t; clr[1] := p; clr[2] := hsv[2]; end; // (t,p,v) 5: begin clr[0] := hsv[2]; clr[1] := p; clr[2] := q; end; // (v,p,q) end; // and store new rgb back into the image PixelColors[0] := trunc(255*clr[0]); PixelColors[1] := trunc(255*clr[1]); PixelColors[2] := trunc(255*clr[2]); end; *) procedure ColorizeImage(ImgSurface: PSDL_Surface; NewColor: longword); // First, the rgb colors are converted to hsv, second hue is replaced by // the NewColor, saturation and value remain unchanged, finally this // hsv color is converted back to rgb space. // For the conversion algorithms of colors from rgb to hsv space // and back simply check the wikipedia. // In order to speed up starting time of USDX the division of reals is // replaced by division of longints, shifted by 10 bits to keep // digits. // The use of longwards leeds to some type size mismatch warnings // whenever differences are formed. // This should not be a problem, since the results should all be positive. // replacing longword by longint would probably resolve this cosmetic fault :-) function ColorToHue(const Color: longword): longword; // returns hue within the range [0.0-6.0] but shl 10, ie. times 1024 var Red, Green, Blue: longint; Min, Max, Delta: longint; Hue: double; begin // extract the colors // division by 255 is omitted, since it is implicitly done // when deviding by delta Red := ((Color and $ff0000) shr 16); // R Green := ((Color and $ff00) shr 8); // G Blue := (Color and $ff) ; // B Min := Red; if Green < Min then Min := Green; if Blue < Min then Min := Blue; Max := Red; if Green > Max then Max := Green; if Blue > Max then Max := Blue; // calc hue Delta := Max - Min; // This gives a type size mismatch warning, because Delta is longword, ie. >= 0 // But the assignments above are easy enough to be sure, that Max - Min is >= 0. if (Delta = 0) then Result := 0 else begin // The division by Delta is done separately afterwards. // Necessary because Delphi did not do the type conversion from // longword to double as expected. // After the change to longint, we may not need it, but left for now // Something to check if (Max = Red ) then Hue := Green - Blue else if (Max = Green) then Hue := 2.0*Delta + Blue - Red else if (Max = Blue ) then Hue := 4.0*Delta + Red - Green; Hue := Hue / Delta; if (Hue < 0.0) then Hue := Hue + 6.0; Result := trunc(Hue*1024); // '*1024' is shl 10 // if NewColor = $000000 then // Log.LogError ('Hue: ' + FloatToStr(Hue), 'ColorToHue'); end; end; var PixelIndex: longword; Pixel: PByte; PixelColors: PByteArray; Red, Green, Blue: longword; Hue, Sat: longword; Min, Max, Delta: longword; HueInteger: longword; f, p, q, t: longword; GreyReal: real; Grey: byte; begin Pixel := ImgSurface^.Pixels; // check of the size of a pixel in bytes. // It should be always 4, but this // additional safeguard will show, // whether something went wrong up to here. if ImgSurface^.format.BytesPerPixel <> 4 then Log.LogError ('ColorizeImage: The pixel size should be 4, but it is ' + IntToStr(ImgSurface^.format.BytesPerPixel)); // Check whether the new color is white, grey or black, // because a greyscale must be created in a different // way. Red := ((NewColor and $ff0000) shr 16); // R Green := ((NewColor and $ff00) shr 8); // G Blue := (NewColor and $ff) ; // B if (Red = Green) and (Green = Blue) then // greyscale image begin // According to these recommendations (ITU-R BT.709-5) // the conversion parameters for rgb to greyscale are // 0.299, 0.587, 0.114 for PixelIndex := 0 to (ImgSurface^.W * ImgSurface^.H)-1 do begin PixelColors := PByteArray(Pixel); {$IFDEF FPC_BIG_ENDIAN} GreyReal := 0.299*PixelColors[3] + 0.587*PixelColors[2] + 0.114*PixelColors[1]; // PixelColors[0] is alpha and remains untouched {$ELSE} GreyReal := 0.299*PixelColors[0] + 0.587*PixelColors[1] + 0.114*PixelColors[2]; // PixelColors[3] is alpha and remains untouched {$ENDIF} Grey := round(GreyReal); {$IFDEF FPC_BIG_ENDIAN} PixelColors[3] := Grey; PixelColors[2] := Grey; PixelColors[1] := Grey; // PixelColors[0] is alpha and remains untouched {$ELSE} PixelColors[0] := Grey; PixelColors[1] := Grey; PixelColors[2] := Grey; // PixelColors[3] is alpha and remains untouched {$ENDIF} Inc(Pixel, ImgSurface^.format.BytesPerPixel); end; exit; // we are done with a greyscale image. end; Hue := ColorToHue(NewColor); // Hue is shl 10 f := Hue and $3ff; // f is the dezimal part of hue HueInteger := Hue shr 10; for PixelIndex := 0 to (ImgSurface^.W * ImgSurface^.H)-1 do begin PixelColors := PByteArray(Pixel); // inlined colorize per pixel // uses fixed point math // shl 10 is used for divisions // get color values {$IFDEF FPC_BIG_ENDIAN} Red := PixelColors[3]; Green := PixelColors[2]; Blue := PixelColors[1]; // PixelColors[0] is alpha and remains untouched {$ELSE} Red := PixelColors[0]; Green := PixelColors[1]; Blue := PixelColors[2]; // PixelColors[3] is alpha and remains untouched {$ENDIF} //calculate luminance and saturation from rgb Max := Red; if Green > Max then Max := Green; if Blue > Max then Max := Blue ; if (Max = 0) then // the color is black begin {$IFDEF FPC_BIG_ENDIAN} PixelColors[3] := 0; PixelColors[2] := 0; PixelColors[1] := 0; {$ELSE} PixelColors[0] := 0; PixelColors[1] := 0; PixelColors[2] := 0; {$ENDIF} end else begin Min := Red; if Green < Min then Min := Green; if Blue < Min then Min := Blue ; if (Min = 255) then // the color is white begin {$IFDEF FPC_BIG_ENDIAN} PixelColors[3] := 255; PixelColors[2] := 255; PixelColors[1] := 255; {$ELSE} PixelColors[0] := 255; PixelColors[1] := 255; PixelColors[2] := 255; {$ENDIF} end else // all colors except black and white begin Delta := Max - Min; // This gives a type size mismatch warning, because Delta is longword, ie. >= 0 // But the assignments above are easy enough to be sure, that Max - Min is >= 0. Sat := (Delta shl 10) div Max; // shl 10 // shr 10 corrects that Sat and f are shl 10 // the resulting p, q and t are unshifted p := (Max * (1024 - Sat )) shr 10; q := (Max * (1024 - (Sat * f ) shr 10)) shr 10; t := (Max * (1024 - (Sat * (1024 - f)) shr 10)) shr 10; // The above 3 lines give type size mismatch warning, but all variables are longword and the ranges should be ok. case HueInteger of 0: begin Red := Max; Green := t; Blue := p; end; // (v,t,p) 1: begin Red := q; Green := Max; Blue := p; end; // (q,v,p) 2: begin Red := p; Green := Max; Blue := t; end; // (p,v,t) 3: begin Red := p; Green := q; Blue := Max; end; // (p,q,v) 4: begin Red := t; Green := p; Blue := Max; end; // (t,p,v) 5: begin Red := Max; Green := p; Blue := q; end; // (v,p,q) end; {$IFDEF FPC_BIG_ENDIAN} PixelColors[3] := byte(Red); PixelColors[2] := byte(Green); PixelColors[1] := byte(Blue); {$ELSE} PixelColors[0] := byte(Red); PixelColors[1] := byte(Green); PixelColors[2] := byte(Blue); {$ENDIF} end; end; Inc(Pixel, ImgSurface^.format.BytesPerPixel); end; end; end.