{
  $Id: ImagingDds.pas 129 2008-08-06 20:01:30Z galfar $
  Vampyre Imaging Library
  by Marek Mauder
  http://imaginglib.sourceforge.net

  The contents of this file are used with permission, subject to the Mozilla
  Public License Version 1.1 (the "License"); you may not use this file except
  in compliance with the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL/MPL-1.1.html

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  the specific language governing rights and limitations under the License.

  Alternatively, the contents of this file may be used under the terms of the
  GNU Lesser General Public License (the  "LGPL License"), in which case the
  provisions of the LGPL License are applicable instead of those above.
  If you wish to allow use of your version of this file only under the terms
  of the LGPL License and not to allow others to use your version of this file
  under the MPL, indicate your decision by deleting  the provisions above and
  replace  them with the notice and other provisions required by the LGPL
  License.  If you do not delete the provisions above, a recipient may use
  your version of this file under either the MPL or the LGPL License.

  For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
}

{ This unit contains image format loader/saver for DirectDraw Surface images.}
unit ImagingDds;

{$I ImagingOptions.inc}

interface

uses
  ImagingTypes, Imaging, ImagingUtility, ImagingFormats;

type
  { Class for loading and saving Microsoft DirectDraw surfaces.
    It can load/save all D3D formats which have coresponding
    TImageFormat. It supports plain textures, cube textures and
    volume textures, all of these can have mipmaps. It can also
    load some formats which have no exact TImageFormat, but can be easily
    converted to one (bump map formats).
    You can get some information about last loaded DDS file by calling
    GetOption with ImagingDDSLoadedXXX options and you can set some
    saving options by calling SetOption with ImagingDDSSaveXXX or you can
    simply use properties of this class.
    Note that when saving cube maps and volumes input image array must contain
    at least number of images to build cube/volume based on current
    Depth and MipMapCount settings.}
  TDDSFileFormat = class(TImageFileFormat)
  protected
    FLoadedCubeMap: LongBool;
    FLoadedVolume: LongBool;
    FLoadedMipMapCount: LongInt;
    FLoadedDepth: LongInt;
    FSaveCubeMap: LongBool;
    FSaveVolume: LongBool;
    FSaveMipMapCount: LongInt;
    FSaveDepth: LongInt;
    procedure ComputeSubDimensions(Idx, Width, Height, MipMaps, Depth: LongInt;
      IsCubeMap, IsVolume: Boolean; var CurWidth, CurHeight: LongInt);
    function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
      OnlyFirstLevel: Boolean): Boolean; override;
    function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
      Index: LongInt): Boolean; override;
    procedure ConvertToSupported(var Image: TImageData;
      const Info: TImageFormatInfo); override;
  public
    constructor Create; override;
    function TestFormat(Handle: TImagingHandle): Boolean; override;
    procedure CheckOptionsValidity; override;
  published
    { True if last loaded DDS file was cube map.}
    property LoadedCubeMap: LongBool read FLoadedCubeMap write FLoadedCubeMap;
    { True if last loaded DDS file was volume texture.}
    property LoadedVolume: LongBool read FLoadedVolume write FLoadedVolume;
    { Number of mipmap levels of last loaded DDS image.}
    property LoadedMipMapCount: LongInt read FLoadedMipMapCount write FLoadedMipMapCount;
    { Depth (slices of volume texture or faces of cube map) of last loaded DDS image.}
    property LoadedDepth: LongInt read FLoadedDepth write FLoadedDepth;
    { True if next DDS file to be saved should be stored as cube map.}
    property SaveCubeMap: LongBool read FSaveCubeMap write FSaveCubeMap;
    { True if next DDS file to be saved should be stored as volume texture.}
    property SaveVolume: LongBool read FSaveVolume write FSaveVolume;
    { Sets the number of mipmaps which should be stored in the next saved DDS file.
      Only applies to cube maps and volumes, ordinary 2D textures save all
      levels present in input.}
    property SaveMipMapCount: LongInt read FSaveMipMapCount write FSaveMipMapCount;
    { Sets the depth (slices of volume texture or faces of cube map)
      of the next saved DDS file.}
    property SaveDepth: LongInt read FSaveDepth write FSaveDepth;
  end;

implementation

const
  SDDSFormatName = 'DirectDraw Surface';
  SDDSMasks      = '*.dds';
  DDSSupportedFormats: TImageFormats = [ifR8G8B8, ifA8R8G8B8, ifX8R8G8B8,
    ifA1R5G5B5, ifA4R4G4B4, ifX1R5G5B5, ifX4R4G4B4, ifR5G6B5, ifA16B16G16R16,
    ifR32F, ifA32B32G32R32F, ifR16F, ifA16B16G16R16F, ifR3G3B2, ifGray8, ifA8Gray8,
    ifGray16, ifDXT1, ifDXT3, ifDXT5, ifATI1N, ifATI2N];

const
  { Four character codes.}
  DDSMagic    = LongWord(Byte('D') or (Byte('D') shl 8) or (Byte('S') shl 16) or
    (Byte(' ') shl 24));
  FOURCC_DXT1 = LongWord(Byte('D') or (Byte('X') shl 8) or (Byte('T') shl 16) or
    (Byte('1') shl 24));
  FOURCC_DXT3 = LongWord(Byte('D') or (Byte('X') shl 8) or (Byte('T') shl 16) or
    (Byte('3') shl 24));
  FOURCC_DXT5 = LongWord(Byte('D') or (Byte('X') shl 8) or (Byte('T') shl 16) or
    (Byte('5') shl 24));
  FOURCC_ATI1 = LongWord(Byte('A') or (Byte('T') shl 8) or (Byte('I') shl 16) or
    (Byte('1') shl 24));
  FOURCC_ATI2 = LongWord(Byte('A') or (Byte('T') shl 8) or (Byte('I') shl 16) or
    (Byte('2') shl 24));

  { Some D3DFORMAT values used in DDS files as FourCC value.}
  D3DFMT_A16B16G16R16  = 36;
  D3DFMT_R32F          = 114;
  D3DFMT_A32B32G32R32F = 116;
  D3DFMT_R16F          = 111;
  D3DFMT_A16B16G16R16F = 113;

  { Constans used by TDDSurfaceDesc2.Flags.}
  DDSD_CAPS            = $00000001;
  DDSD_HEIGHT          = $00000002;
  DDSD_WIDTH           = $00000004;
  DDSD_PITCH           = $00000008;
  DDSD_PIXELFORMAT     = $00001000;
  DDSD_MIPMAPCOUNT     = $00020000;
  DDSD_LINEARSIZE      = $00080000;
  DDSD_DEPTH           = $00800000;

  { Constans used by TDDSPixelFormat.Flags.}
  DDPF_ALPHAPIXELS     = $00000001;    // used by formats which contain alpha
  DDPF_FOURCC          = $00000004;    // used by DXT and large ARGB formats
  DDPF_RGB             = $00000040;    // used by RGB formats
  DDPF_LUMINANCE       = $00020000;    // used by formats like D3DFMT_L16
  DDPF_BUMPLUMINANCE   = $00040000;    // used by mixed signed-unsigned formats
  DDPF_BUMPDUDV        = $00080000;    // used by signed formats

  { Constans used by TDDSCaps.Caps1.}
  DDSCAPS_COMPLEX      = $00000008;
  DDSCAPS_TEXTURE      = $00001000;
  DDSCAPS_MIPMAP       = $00400000;

  { Constans used by TDDSCaps.Caps2.}
  DDSCAPS2_CUBEMAP     = $00000200;
  DDSCAPS2_POSITIVEX   = $00000400;
  DDSCAPS2_NEGATIVEX   = $00000800;
  DDSCAPS2_POSITIVEY   = $00001000;
  DDSCAPS2_NEGATIVEY   = $00002000;
  DDSCAPS2_POSITIVEZ   = $00004000;
  DDSCAPS2_NEGATIVEZ   = $00008000;
  DDSCAPS2_VOLUME      = $00200000;

  { Flags for TDDSurfaceDesc2.Flags used when saving DDS file.}
  DDS_SAVE_FLAGS = DDSD_CAPS or DDSD_PIXELFORMAT or DDSD_WIDTH or
    DDSD_HEIGHT or DDSD_LINEARSIZE;

type
  { Stores the pixel format information.}
  TDDPixelFormat = packed record
    Size: LongWord;       // Size of the structure = 32 bytes
    Flags: LongWord;      // Flags to indicate valid fields
    FourCC: LongWord;     // Four-char code for compressed textures (DXT)
    BitCount: LongWord;   // Bits per pixel if uncomp. usually 16,24 or 32
    RedMask: LongWord;    // Bit mask for the Red component
    GreenMask: LongWord;  // Bit mask for the Green component
    BlueMask: LongWord;   // Bit mask for the Blue component
    AlphaMask: LongWord;  // Bit mask for the Alpha component
  end;

  { Specifies capabilities of surface.}
  TDDSCaps = packed record
    Caps1: LongWord;      // Should always include DDSCAPS_TEXTURE
    Caps2: LongWord;      // For cubic environment maps
    Reserved: array[0..1] of LongWord; // Reserved
  end;

  { Record describing DDS file contents.}
  TDDSurfaceDesc2 = packed record
    Size: LongWord;       // Size of the structure = 124 Bytes
    Flags: LongWord;      // Flags to indicate valid fields
    Height: LongWord;     // Height of the main image in pixels
    Width: LongWord;      // Width of the main image in pixels
    PitchOrLinearSize: LongWord; // For uncomp formats number of bytes per
                          // scanline. For comp it is the size in
                          // bytes of the main image
    Depth: LongWord;      // Only for volume text depth of the volume
    MipMaps: LongInt;     // Total number of levels in the mipmap chain
    Reserved1: array[0..10] of LongWord; // Reserved
    PixelFormat: TDDPixelFormat; // Format of the pixel data
    Caps: TDDSCaps;       // Capabilities
    Reserved2: LongWord;  // Reserved
  end;

  { DDS file header.}
  TDDSFileHeader = packed record
    Magic: LongWord;       // File format magic
    Desc: TDDSurfaceDesc2; // Surface description
  end;


{ TDDSFileFormat class implementation }

constructor TDDSFileFormat.Create;
begin
  inherited Create;
  FName := SDDSFormatName;
  FCanLoad := True;
  FCanSave := True;
  FIsMultiImageFormat := True;
  FSupportedFormats := DDSSupportedFormats;

  FSaveCubeMap := False;
  FSaveVolume := False;
  FSaveMipMapCount := 1;
  FSaveDepth := 1;

  AddMasks(SDDSMasks);

  RegisterOption(ImagingDDSLoadedCubeMap, @FLoadedCubeMap);
  RegisterOption(ImagingDDSLoadedVolume, @FLoadedVolume);
  RegisterOption(ImagingDDSLoadedMipMapCount, @FLoadedMipMapCount);
  RegisterOption(ImagingDDSLoadedDepth, @FLoadedDepth);
  RegisterOption(ImagingDDSSaveCubeMap, @FSaveCubeMap);
  RegisterOption(ImagingDDSSaveVolume, @FSaveVolume);
  RegisterOption(ImagingDDSSaveMipMapCount, @FSaveMipMapCount);
  RegisterOption(ImagingDDSSaveDepth, @FSaveDepth);
end;

procedure TDDSFileFormat.CheckOptionsValidity;
begin
  if FSaveCubeMap then
    FSaveVolume := False;
  if FSaveVolume then
    FSaveCubeMap := False;
  if FSaveDepth < 1 then
    FSaveDepth := 1;
  if FSaveMipMapCount < 1 then
    FSaveMipMapCount := 1;
end;

procedure TDDSFileFormat.ComputeSubDimensions(Idx, Width, Height, MipMaps, Depth: LongInt;
  IsCubeMap, IsVolume: Boolean; var CurWidth, CurHeight: LongInt);
var
  I, Last, Shift: LongInt;
begin
  CurWidth := Width;
  CurHeight := Height;
  if MipMaps > 1 then
  begin
    if not IsVolume then
    begin
      if IsCubeMap then
      begin
        // Cube maps are stored like this
        // Face 0 mimap 0
        // Face 0 mipmap 1
        // ...
        // Face 1 mipmap 0
        // Face 1 mipmap 1
        // ...

        // Modify index so later in for loop we iterate less times
        Idx := Idx - ((Idx div MipMaps) * MipMaps);
      end;
      for I := 0 to Idx - 1 do
      begin
        CurWidth := ClampInt(CurWidth shr 1, 1, CurWidth);
        CurHeight := ClampInt(CurHeight shr 1, 1, CurHeight);
      end;
    end
    else
    begin
      // Volume textures are stored in DDS files like this:
      // Slice 0 mipmap 0
      // Slice 1 mipmap 0
      // Slice 2 mipmap 0
      // Slice 3 mipmap 0
      // Slice 0 mipmap 1
      // Slice 1 mipmap 1
      // Slice 0 mipmap 2
      // Slice 0 mipmap 3 ...
      Shift := 0;
      Last := Depth;
      while Idx > Last - 1 do
      begin
        CurWidth := ClampInt(CurWidth shr 1, 1, CurWidth);
        CurHeight := ClampInt(CurHeight shr 1, 1, CurHeight);
        if (CurWidth = 1) and (CurHeight = 1) then
          Break;
        Inc(Shift);
        Inc(Last, ClampInt(Depth shr Shift, 1, Depth));
      end;
    end;
  end;
end;

function TDDSFileFormat.LoadData(Handle: TImagingHandle;
  var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
var
  Hdr: TDDSFileHeader;
  SrcFormat: TImageFormat;
  FmtInfo: TImageFormatInfo;
  NeedsSwapChannels: Boolean;
  CurrentWidth, CurrentHeight, ImageCount, LoadSize, I, PitchOrLinear: LongInt;
  Data: PByte;
  UseAsPitch: Boolean;
  UseAsLinear: Boolean;

  function MasksEqual(const DDPF: TDDPixelFormat; PF: PPixelFormatInfo): Boolean;
  begin
    Result := (DDPF.AlphaMask = PF.ABitMask) and
      (DDPF.RedMask = PF.RBitMask) and (DDPF.GreenMask = PF.GBitMask) and
      (DDPF.BlueMask = PF.BBitMask);
  end;

begin
  Result := False;
  ImageCount := 1;
  FLoadedMipMapCount := 1;
  FLoadedDepth := 1;
  FLoadedVolume := False;
  FLoadedCubeMap := False;

  with GetIO, Hdr, Hdr.Desc.PixelFormat do
  begin
    Read(Handle, @Hdr, SizeOF(Hdr));
    {
    // Set position to the end of the header (for possible future versions
    // ith larger header)
    Seek(Handle, Hdr.Desc.Size + SizeOf(Hdr.Magic) - SizeOf(Hdr),
      smFromCurrent);
    }
    SrcFormat := ifUnknown;
    NeedsSwapChannels := False;
    // Get image data format
    if (Flags and DDPF_FOURCC) = DDPF_FOURCC then
    begin
      // Handle FourCC and large ARGB formats
      case FourCC of
        D3DFMT_A16B16G16R16: SrcFormat := ifA16B16G16R16;
        D3DFMT_R32F: SrcFormat := ifR32F;
        D3DFMT_A32B32G32R32F: SrcFormat := ifA32B32G32R32F;
        D3DFMT_R16F: SrcFormat := ifR16F;
        D3DFMT_A16B16G16R16F: SrcFormat := ifA16B16G16R16F;
        FOURCC_DXT1: SrcFormat := ifDXT1;
        FOURCC_DXT3: SrcFormat := ifDXT3;
        FOURCC_DXT5: SrcFormat := ifDXT5;
        FOURCC_ATI1: SrcFormat := ifATI1N;
        FOURCC_ATI2: SrcFormat := ifATI2N;
      end;
    end
    else if (Flags and DDPF_RGB) = DDPF_RGB then
    begin
      // Handle RGB formats
      if (Flags and DDPF_ALPHAPIXELS) = DDPF_ALPHAPIXELS then
      begin
        // Handle RGB with alpha formats
        case BitCount of
          16:
            begin
              if MasksEqual(Desc.PixelFormat,
                GetFormatInfo(ifA4R4G4B4).PixelFormat) then
                SrcFormat := ifA4R4G4B4;
              if MasksEqual(Desc.PixelFormat,
                GetFormatInfo(ifA1R5G5B5).PixelFormat) then
                SrcFormat := ifA1R5G5B5;
            end;
          32:
            begin
              SrcFormat := ifA8R8G8B8;
              if BlueMask = $00FF0000 then
                NeedsSwapChannels := True;
            end;
        end;
      end
      else
      begin
        // Handle RGB without alpha formats
        case BitCount of
          8:
            if MasksEqual(Desc.PixelFormat,
              GetFormatInfo(ifR3G3B2).PixelFormat) then
              SrcFormat := ifR3G3B2;
          16:
            begin
              if MasksEqual(Desc.PixelFormat,
                GetFormatInfo(ifX4R4G4B4).PixelFormat) then
                SrcFormat := ifX4R4G4B4;
              if MasksEqual(Desc.PixelFormat,
                GetFormatInfo(ifX1R5G5B5).PixelFormat) then
                SrcFormat := ifX1R5G5B5;
              if MasksEqual(Desc.PixelFormat,
                GetFormatInfo(ifR5G6B5).PixelFormat) then
                SrcFormat := ifR5G6B5;
            end;
          24: SrcFormat := ifR8G8B8;
          32:
            begin
              SrcFormat := ifX8R8G8B8;
              if BlueMask = $00FF0000 then
                NeedsSwapChannels := True;
            end;
        end;
      end;
    end
    else if (Flags and DDPF_LUMINANCE) = DDPF_LUMINANCE then
    begin
      // Handle luminance formats
      if (Flags and DDPF_ALPHAPIXELS) = DDPF_ALPHAPIXELS then
      begin
        // Handle luminance with alpha formats
        if BitCount = 16 then
          SrcFormat := ifA8Gray8;
      end
      else
      begin
        // Handle luminance without alpha formats
        case BitCount of
          8: SrcFormat := ifGray8;
          16: SrcFormat := ifGray16;
        end;
      end;
    end
    else if (Flags and DDPF_BUMPLUMINANCE) = DDPF_BUMPLUMINANCE then
    begin
      // Handle mixed bump-luminance formats like D3DFMT_X8L8V8U8
      case BitCount of
        32:
          if BlueMask = $00FF0000 then
          begin
            SrcFormat := ifX8R8G8B8; // D3DFMT_X8L8V8U8
            NeedsSwapChannels := True;
          end;
      end;
    end
    else if (Flags and DDPF_BUMPDUDV) = DDPF_BUMPDUDV then
    begin
      // Handle bumpmap formats like D3DFMT_Q8W8V8U8
      case BitCount of
        16: SrcFormat := ifA8Gray8; // D3DFMT_V8U8
        32:
          if AlphaMask = $FF000000 then
          begin
            SrcFormat := ifA8R8G8B8; // D3DFMT_Q8W8V8U8
            NeedsSwapChannels := True;
          end;
        64: SrcFormat := ifA16B16G16R16; // D3DFMT_Q16W16V16U16
      end;
    end;

    // If DDS format is not supported we will exit
    if SrcFormat = ifUnknown then Exit;

    // File contains mipmaps for each subimage.
    { Some DDS writers ignore setting proper Caps and Flags so
      this check is not usable:
    if ((Desc.Caps.Caps1 and DDSCAPS_MIPMAP) = DDSCAPS_MIPMAP) and
      ((Desc.Flags and DDSD_MIPMAPCOUNT) = DDSD_MIPMAPCOUNT) then}
    if Desc.MipMaps > 1 then
    begin
      FLoadedMipMapCount := Desc.MipMaps;
      ImageCount := Desc.MipMaps;
    end;

    // File stores volume texture
    if ((Desc.Caps.Caps2 and DDSCAPS2_VOLUME) = DDSCAPS2_VOLUME) and
      ((Desc.Flags and DDSD_DEPTH) = DDSD_DEPTH) then
    begin
      FLoadedVolume := True;
      FLoadedDepth := Desc.Depth;
      ImageCount := GetVolumeLevelCount(Desc.Depth, ImageCount);
    end;

    // File stores cube texture
    if (Desc.Caps.Caps2 and DDSCAPS2_CUBEMAP) = DDSCAPS2_CUBEMAP then
    begin
      FLoadedCubeMap := True;
      I := 0;
      if (Desc.Caps.Caps2 and DDSCAPS2_POSITIVEX) = DDSCAPS2_POSITIVEX then Inc(I);
      if (Desc.Caps.Caps2 and DDSCAPS2_POSITIVEY) = DDSCAPS2_POSITIVEY then Inc(I);
      if (Desc.Caps.Caps2 and DDSCAPS2_POSITIVEZ) = DDSCAPS2_POSITIVEZ then Inc(I);
      if (Desc.Caps.Caps2 and DDSCAPS2_NEGATIVEX) = DDSCAPS2_NEGATIVEX then Inc(I);
      if (Desc.Caps.Caps2 and DDSCAPS2_NEGATIVEY) = DDSCAPS2_NEGATIVEY then Inc(I);
      if (Desc.Caps.Caps2 and DDSCAPS2_NEGATIVEZ) = DDSCAPS2_NEGATIVEZ then Inc(I);
      FLoadedDepth := I;
      ImageCount := ImageCount * I;
    end;

    // Allocate and load all images in file
    FmtInfo := GetFormatInfo(SrcFormat);
    SetLength(Images, ImageCount);

    // Compute the pitch or get if from file if present
    UseAsPitch := (Desc.Flags and DDSD_PITCH) = DDSD_PITCH;
    UseAsLinear := (Desc.Flags and DDSD_LINEARSIZE) = DDSD_LINEARSIZE;
    // Use linear as default if none is set
    if not UseAsPitch and not UseAsLinear then
      UseAsLinear := True;
    // Main image pitch or linear size
    PitchOrLinear := Desc.PitchOrLinearSize;

    for I := 0 to ImageCount - 1 do
    begin
      // Compute dimensions of surrent subimage based on texture type and
      // number of mipmaps
      ComputeSubDimensions(I, Desc.Width, Desc.Height, Desc.MipMaps, Desc.Depth,
        FloadedCubeMap, FLoadedVolume, CurrentWidth, CurrentHeight);
      NewImage(CurrentWidth, CurrentHeight, SrcFormat, Images[I]);

      if (I > 0) or (PitchOrLinear = 0) then
      begin
        // Compute pitch or linear size for mipmap levels, or even for main image
        // since some formats do not fill pitch nor size
        if UseAsLinear then
          PitchOrLinear := FmtInfo.GetPixelsSize(SrcFormat, CurrentWidth, CurrentHeight)
        else
          PitchOrLinear := (CurrentWidth * FmtInfo.BytesPerPixel + 3) div 4 * 4; // must be DWORD aligned
      end;

      if UseAsLinear then
        LoadSize := PitchOrLinear
      else
        LoadSize := CurrentHeight * PitchOrLinear;

      if UseAsLinear or (LoadSize = Images[I].Size) then
      begin
        // If DDS does not use Pitch we can simply copy data
        Read(Handle, Images[I].Bits, LoadSize)
      end
      else
      begin
        // If DDS uses Pitch we must load aligned scanlines
        // and then remove padding
        GetMem(Data, LoadSize);
        try
          Read(Handle, Data, LoadSize);
          RemovePadBytes(Data, Images[I].Bits, CurrentWidth, CurrentHeight,
            FmtInfo.BytesPerPixel, PitchOrLinear);
       finally
          FreeMem(Data);
        end;
      end;

      if NeedsSwapChannels then
        SwapChannels(Images[I], ChannelRed, ChannelBlue);
    end;
    Result := True;
  end;
end;

function TDDSFileFormat.SaveData(Handle: TImagingHandle;
  const Images: TDynImageDataArray; Index: LongInt): Boolean;
var
  Hdr: TDDSFileHeader;
  MainImage, ImageToSave: TImageData;
  I, MainIdx, Len, ImageCount: LongInt;
  J: LongWord;
  FmtInfo: TImageFormatInfo;
  MustBeFreed: Boolean;
  Is2DTexture, IsCubeMap, IsVolume: Boolean;
  MipMapCount, CurrentWidth, CurrentHeight: LongInt;
  NeedsResize: Boolean;
  NeedsConvert: Boolean;
begin
  Result := False;
  FillChar(Hdr, Sizeof(Hdr), 0);

  MainIdx := FFirstIdx;
  Len := FLastIdx - MainIdx + 1;
  // Some DDS saving rules:
  //   2D textures: Len is used as mipmap count (FSaveMipMapCount not used!).
  //   Cube maps:   FSaveDepth * FSaveMipMapCount images are used, if Len is
  //                smaller than this file is saved as regular 2D texture.
  //   Volume maps: GetVolumeLevelCount(FSaveDepth, FSaveMipMapCount) images are
  //                used, if Len is smaller than this file is
  //                saved as regular 2D texture.

  IsCubeMap := FSaveCubeMap;
  IsVolume := FSaveVolume;
  MipMapCount := FSaveMipMapCount;

  if IsCubeMap then
  begin
    // Check if we have enough images on Input to save cube map
    if Len < FSaveDepth * FSaveMipMapCount then
      IsCubeMap := False;
  end
  else if IsVolume then
  begin
    // Check if we have enough images on Input to save volume texture
    if Len < GetVolumeLevelCount(FSaveDepth, FSaveMipMapCount) then
      IsVolume := False;
  end;

  Is2DTexture := not IsCubeMap and not IsVolume;
  if Is2DTexture then
  begin
    // Get number of mipmaps used with 2D texture
    MipMapCount := Min(Len, GetNumMipMapLevels(Images[MainIdx].Width, Images[MainIdx].Height));
  end;

  // we create compatible main image and fill headers
  if MakeCompatible(Images[MainIdx], MainImage, MustBeFreed) then
  with GetIO, MainImage, Hdr do
  try
    FmtInfo := GetFormatInfo(Format);
    Magic := DDSMagic;
    Desc.Size := SizeOf(Desc);
    Desc.Width := Width;
    Desc.Height := Height;
    Desc.Flags := DDS_SAVE_FLAGS;
    Desc.Caps.Caps1 := DDSCAPS_TEXTURE;
    Desc.PixelFormat.Size := SizeOf(Desc.PixelFormat);
    Desc.PitchOrLinearSize := MainImage.Size;
    ImageCount := MipMapCount;

    if MipMapCount > 1 then
    begin
      // Set proper flags if we have some mipmaps to be saved
      Desc.Flags := Desc.Flags or DDSD_MIPMAPCOUNT;
      Desc.Caps.Caps1 := Desc.Caps.Caps1 or DDSCAPS_MIPMAP or DDSCAPS_COMPLEX;
      Desc.MipMaps := MipMapCount;
    end;

    if IsCubeMap then
    begin
      // Set proper cube map flags - number of stored faces is taken
      // from FSaveDepth
      Desc.Caps.Caps1 := Desc.Caps.Caps1 or DDSCAPS_COMPLEX;
      Desc.Caps.Caps2 := Desc.Caps.Caps2 or DDSCAPS2_CUBEMAP;
      J := DDSCAPS2_POSITIVEX;
      for I := 0 to FSaveDepth - 1 do
      begin
        Desc.Caps.Caps2 := Desc.Caps.Caps2 or J;
        J := J shl 1;
      end;
      ImageCount := FSaveDepth * FSaveMipMapCount;
    end
    else if IsVolume then
    begin
      // Set proper flags for volume texture
      Desc.Flags := Desc.Flags or DDSD_DEPTH;
      Desc.Caps.Caps1 := Desc.Caps.Caps1 or DDSCAPS_COMPLEX;
      Desc.Caps.Caps2 := Desc.Caps.Caps2 or DDSCAPS2_VOLUME;
      Desc.Depth := FSaveDepth;
      ImageCount := GetVolumeLevelCount(FSaveDepth, FSaveMipMapCount);
    end;

    // Now we set DDS pixel format for main image
    if FmtInfo.IsSpecial or FmtInfo.IsFloatingPoint or
      (FmtInfo.BytesPerPixel > 4) then
    begin
      Desc.PixelFormat.Flags := DDPF_FOURCC;
      case Format of
        ifA16B16G16R16:  Desc.PixelFormat.FourCC := D3DFMT_A16B16G16R16;
        ifR32F:          Desc.PixelFormat.FourCC := D3DFMT_R32F;
        ifA32B32G32R32F: Desc.PixelFormat.FourCC := D3DFMT_A32B32G32R32F;
        ifR16F:          Desc.PixelFormat.FourCC := D3DFMT_R16F;
        ifA16B16G16R16F: Desc.PixelFormat.FourCC := D3DFMT_A16B16G16R16F;
        ifDXT1:          Desc.PixelFormat.FourCC := FOURCC_DXT1;
        ifDXT3:          Desc.PixelFormat.FourCC := FOURCC_DXT3;
        ifDXT5:          Desc.PixelFormat.FourCC := FOURCC_DXT5;
        ifATI1N:         Desc.PixelFormat.FourCC := FOURCC_ATI1;
        ifATI2N:         Desc.PixelFormat.FourCC := FOURCC_ATI2;
      end;
    end
    else if FmtInfo.HasGrayChannel then
    begin
      Desc.PixelFormat.Flags := DDPF_LUMINANCE;
      Desc.PixelFormat.BitCount := FmtInfo.BytesPerPixel * 8;
      case Format of
        ifGray8:  Desc.PixelFormat.RedMask := 255;
        ifGray16: Desc.PixelFormat.RedMask := 65535;
        ifA8Gray8:
          begin
            Desc.PixelFormat.Flags := Desc.PixelFormat.Flags or DDPF_ALPHAPIXELS;
            Desc.PixelFormat.RedMask := 255;
            Desc.PixelFormat.AlphaMask := 65280;
          end;
      end;
    end
    else
    begin
      Desc.PixelFormat.Flags := DDPF_RGB;
      Desc.PixelFormat.BitCount := FmtInfo.BytesPerPixel * 8;
      if FmtInfo.HasAlphaChannel then
      begin
        Desc.PixelFormat.Flags := Desc.PixelFormat.Flags or DDPF_ALPHAPIXELS;
        Desc.PixelFormat.AlphaMask := $FF000000;
      end;
      if FmtInfo.BytesPerPixel > 2 then
      begin
        Desc.PixelFormat.RedMask :=   $00FF0000;
        Desc.PixelFormat.GreenMask := $0000FF00;
        Desc.PixelFormat.BlueMask :=  $000000FF;
      end
      else
      begin
        Desc.PixelFormat.AlphaMask := FmtInfo.PixelFormat.ABitMask;
        Desc.PixelFormat.RedMask := FmtInfo.PixelFormat.RBitMask;
        Desc.PixelFormat.GreenMask := FmtInfo.PixelFormat.GBitMask;
        Desc.PixelFormat.BlueMask := FmtInfo.PixelFormat.BBitMask;
      end;
    end;

    // Header and main image are written to output
    Write(Handle, @Hdr, SizeOf(Hdr));
    Write(Handle, MainImage.Bits, MainImage.Size);

    // Write the rest of the images and convert them to
    // the same format as main image if necessary and ensure proper mipmap
    // simensions too.
    for I := MainIdx + 1 to MainIdx + ImageCount - 1 do
    begin
      // Get proper dimensions for this level
      ComputeSubDimensions(I, Desc.Width, Desc.Height, Desc.MipMaps, Desc.Depth,
        IsCubeMap, IsVolume, CurrentWidth, CurrentHeight);

      // Check if input image for this level has the right size and format
      NeedsResize := not ((Images[I].Width = CurrentWidth) and (Images[I].Height = CurrentHeight));
      NeedsConvert := not (Images[I].Format = Format);

      if NeedsResize or NeedsConvert then
      begin
        // Input image must be resized or converted to different format
        // to become valid mipmap level
        InitImage(ImageToSave);
        CloneImage(Images[I], ImageToSave);
        if NeedsConvert then
          ConvertImage(ImageToSave, Format);
        if NeedsResize then
          ResizeImage(ImageToSave, CurrentWidth, CurrentHeight, rfBilinear);
      end
      else
        // Input image can be used without any changes
        ImageToSave := Images[I];

      // Write level data and release temp image if necessary
      Write(Handle, ImageToSave.Bits, ImageToSave.Size);
      if Images[I].Bits <> ImageToSave.Bits then
        FreeImage(ImageToSave);
    end;

    Result := True;
  finally
    if MustBeFreed then
      FreeImage(MainImage);
  end;
end;

procedure TDDSFileFormat.ConvertToSupported(var Image: TImageData;
  const Info: TImageFormatInfo);
var
  ConvFormat: TImageFormat;
begin
  if Info.IsIndexed or Info.IsSpecial then
    // convert indexed and unsupported special formatd to A8R8G8B8
    ConvFormat := ifA8R8G8B8
  else if Info.IsFloatingPoint then
  begin
    if Info.Format = ifA16R16G16B16F then
      // only swap channels here
      ConvFormat := ifA16B16G16R16F
    else
      // convert other floating point formats to A32B32G32R32F
      ConvFormat := ifA32B32G32R32F
  end
  else if Info.HasGrayChannel then
  begin
    if Info.HasAlphaChannel then
      // convert grayscale with alpha to A8Gray8
      ConvFormat := ifA8Gray8
    else if Info.BytesPerPixel = 1 then
      // convert 8bit grayscale to Gray8
      ConvFormat := ifGray8
    else
      // convert 16-64bit grayscales to Gray16
      ConvFormat := ifGray16;
  end
  else if Info.BytesPerPixel > 4 then
    ConvFormat := ifA16B16G16R16
  else if Info.HasAlphaChannel then
    // convert the other images with alpha channel to A8R8G8B8
    ConvFormat := ifA8R8G8B8
  else
    // convert the other formats to X8R8G8B8
    ConvFormat := ifX8R8G8B8;

  ConvertImage(Image, ConvFormat);
end;

function TDDSFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
var
  Hdr: TDDSFileHeader;
  ReadCount: LongInt;
begin
  Result := False;
  if Handle <> nil then
    with GetIO do
    begin
      ReadCount := Read(Handle, @Hdr, SizeOf(Hdr));
      Seek(Handle, -ReadCount, smFromCurrent);
      Result := (Hdr.Magic = DDSMagic) and (ReadCount = SizeOf(Hdr)) and
        ((Hdr.Desc.Caps.Caps1 and DDSCAPS_TEXTURE) = DDSCAPS_TEXTURE);
    end;
end;

initialization
  RegisterImageFileFormat(TDDSFileFormat);

{
  File Notes:

  -- TODOS ----------------------------------------------------
    - nothing now

  -- 0.25.0 Changes/Bug Fixes ---------------------------------
    - Added support for 3Dc ATI1/2 formats.

  -- 0.23 Changes/Bug Fixes -----------------------------------
    - Saved DDS with mipmaps now correctly defineds COMPLEX flag.
    - Fixed loading of RGB DDS files that use pitch and have mipmaps -
      mipmaps were loaded wrongly.

  -- 0.21 Changes/Bug Fixes -----------------------------------
    - Changed saving behaviour a bit: mipmaps are inlcuded automatically for
      2D textures if input image array has more than 1 image (no need to
      set SaveMipMapCount manually).
    - Mipmap levels are now saved with proper dimensions when saving DDS files.
    - Made some changes to not be so strict when loading DDS files.
      Many programs seem to save them in non-standard format
      (by MS DDS File Reference).
    - Added missing ifX8R8G8B8 to SupportedFormats, MakeCompatible failed
      when image was converted to this format (inside).
    - MakeCompatible method moved to base class, put ConvertToSupported here.
      GetSupportedFormats removed, it is now set in constructor.
    - Fixed bug that sometimes saved non-standard DDS files and another
      one that caused crash when these files were loaded.
    - Changed extensions to filename masks.
    - Changed SaveData, LoadData, and MakeCompatible methods according
      to changes in base class in Imaging unit.

  -- 0.19 Changes/Bug Fixes -----------------------------------
    - added support for half-float image formats
    - change in LoadData to allow support for more images
      in one stream loading

  -- 0.17 Changes/Bug Fixes -----------------------------------
    - fixed bug in TestFormat which does not recognize many DDS files
    - changed pitch/linearsize handling in DDS loading code to
      load DDS files produced by NVidia's Photoshop plugin
}

end.

