unit UImage;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I ../switches.inc}

uses
  SDL;

{$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
  );


{$IFDEF HavePNG}
function WritePNGImage(const FileName: string; Surface: PSDL_Surface): boolean;
{$ENDIF}
{$IFDEF HaveBMP}
function WriteBMPImage(const FileName: string; Surface: PSDL_Surface): boolean;
{$ENDIF}
{$IFDEF HaveJPG}
function WriteJPGImage(const FileName: string; Surface: PSDL_Surface; Quality: integer): boolean;
{$ENDIF}

function LoadImage(const Identifier: string): PSDL_Surface;

implementation

uses
  SysUtils,
  Classes,
  {$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,
  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;

(***************************
 * 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: TFileStream;
begin
  inFile := TFileStream(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: TFileStream;
begin
  outFile := TFileStream(png_get_io_ptr(png_ptr));
  outFile.Write(data^, length);
end;

procedure user_flush_data(png_ptr: png_structp); cdecl;
//var
//  outFile: TFileStream;
begin
  // binary files are flushed automatically, Flush() works with Text-files only
  //outFile := TFileStream(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  := year;
  pngTime.month := month;
  pngTime.day   := day;
  DecodeTime(time, hour, minute, second, msecond);
  pngTime.hour   := hour;
  pngTime.minute := minute;
  pngTime.second := second;
end;

(*
 * ImageData must be in RGB-format
 *)
function WritePNGImage(const FileName: string; Surface: PSDL_Surface): boolean;
var
  png_ptr: png_structp;
  info_ptr: png_infop;
  pngFile: TFileStream;
  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 := TFileStream.Create(FileName, fmCreate);
  except
    Log.LogError('Could not open file: "' + FileName + '"', '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: string; Surface: PSDL_Surface): boolean;
var
  bmpFile:    TFileStream;
  FileInfo:   BITMAPINFOHEADER;
  FileHeader: BITMAPFILEHEADER;
  Converted:  boolean;
  Row:        integer;
  RowSize:    integer;
begin
  Result := false;

  // open file for writing
  try
    bmpFile := TFileStream.Create(FileName, fmCreate);
  except
    Log.LogError('Could not open file: "' + FileName + '"', '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 + '"', 'WriteBMPImage');
  end;

  if (Converted) then
    SDL_FreeSurface(Surface);

  // close file
  bmpFile.Free;
end;

{$ENDIF}

(***************************
 * JPG section
 *****************************)

{$IFDEF HaveJPG}

function WriteJPGImage(const FileName: string; Surface: PSDL_Surface; Quality: integer): boolean;
var
  {$IFDEF Delphi}
  Bitmap:    TBitmap;
  BitmapInfo: TBitmapInfo;
  Jpeg:      TJpegImage;
  row:       integer;
  {$ELSE}
  cinfo:     jpeg_compress_struct;
  jerr :     jpeg_error_mgr;
  jpgFile:   TFileStream;
  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
    Jpeg := TJPEGImage.Create;
    Jpeg.Assign(Bitmap);
    Bitmap.Free;
    Jpeg.CompressionQuality := Quality;
    try
      // compress image (don't forget this line, otherwise it won't be compressed)
      Jpeg.Compress();
      Jpeg.SaveToFile(FileName);
    except
      Log.LogError('Could not save file: "' + FileName + '"', 'WriteJPGImage');
      Exit;
    end;
    Jpeg.Free;
  {$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 := TFileStream.Create(FileName, fmCreate);
    except
      Log.LogError('Could not open file: "' + FileName + '"', '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}

(*
 * Loads an image from the given file or resource
 *)
function LoadImage(const Identifier: string): PSDL_Surface;
var
  TexRWops:  PSDL_RWops;
  TexStream: TStream;
  FileName: string;
begin
  Result   := nil;
  TexRWops := nil;

  if Identifier = '' then
    exit;
    
  //Log.LogStatus( Identifier, 'LoadImage' );

  FileName := Identifier;

  if (FileExistsInsensitive(FileName)) then
  begin
    // load from file
    //Log.LogStatus( 'Is File ( Loading : '+FileName+')', '  LoadImage' );
    try
      Result := IMG_Load(PChar(FileName));
      //Log.LogStatus( '       '+inttostr( integer( Result ) ), '  LoadImage' );
    except
      Log.LogError('Could not load from file "'+FileName+'"', 'TTextureUnit.LoadImage');
      Exit;
    end;
  end
  else
  begin
    //Log.LogStatus( 'IS Resource, because file does not exist.('+Identifier+')', '  LoadImage' );

    TexStream := GetResourceStream(Identifier, 'TEX');
    if (not assigned(TexStream)) then
    begin
      Log.LogError( 'Invalid file or resource "'+ Identifier+'"', 'TTextureUnit.LoadImage');
      Exit;
    end;

    TexRWops := RWopsFromStream(TexStream);
    if (TexRWops = nil) then
    begin
      Log.LogError( 'Could not assign resource "'+Identifier+'"', 'TTextureUnit.LoadImage');
      TexStream.Free();
      Exit;
    end;

    //Log.LogStatus( 'resource Assigned....' , Identifier);
    try
      Result := IMG_Load_RW(TexRWops, 0);
    except
      Log.LogError( 'Could not read resource "'+Identifier+'"', 'TTextureUnit.LoadImage');
    end;

    SDL_FreeRW(TexRWops);
    TexStream.Free();
  end;
end;

end.