aboutsummaryrefslogblamecommitdiffstats
path: root/cmake/src/base/UImage.pas
blob: 1866316ec676b0b7da799e24ee7d7b1c63fd7a4e (plain) (tree)



































                                                                        

        































































































                                                         
                                                                              

                
                                                                              

                
                                                                                                





                                                         
                                                        



































                                                                            
             




































































































                                                                                               
                  
     
                                             




                                                                                            
                   
     
                                              




                                                       
                     

                                                                               
                                                








                                                                    


                                     
                                                  


                                     




                                  
                                                                              


                         
                     










                                
                                                            
        
                                                                                      





























































































































































                                                                                                         
                                                                              
   
                      









                               
                                                            
        
                                                                                      






























































                                                                                    
                                                                                       
















                               
                                                                                                

                 
                      
                          


                                


                                  
                               

                                     
                      

























































                                                                                                
       




















                                                                                    
          
                                                                                        

           




















                                                                           
                                                              
          
                                                                                        
























































                                                                                                         
                                                        
   


                                
     
                
 


                                                         
       
                                                                                              




                   


                                                                                       
        
                                                                                             









                                                                  






                                                                                                         















































































                                                                                  
                                                                      






                                                                          
                                                                  

            




                                                                                 
                                                       
                                                                      
     

                              

















                                                              

                                                                                                            






                                                                     

                                                                          






                                                                  

                                                                  











                             

                 












                                                                         







































                                                                                     
                                                 
                                                                





























































                                                                        

                                                                                                                    

                                                 
                                                    

                                                 


                                                               
 

                                                                                                                         









                                                                         


                                      
               


                                      









                                                 
{* 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.