/* ConvertImage: another Dekkerware console application.

Converts an image file to another format, using the GDI+ Library.
Example:
  ConvertImage.exe C:\input.jpg D:\output.bmp

Optional additional parameters:
  /Q, /Quiet - Does not wait for the Enter key to be pressed.
  /P, /PropertyCount - Displays the property count of the input file.

Compiled by MSVC++ 2003 (Compiler Version 13.10.3077)
  CL.EXE /D "and=&&" /D "or=||" /D "not=!" /EHsc ConvertImage.cpp /link gdiplus.lib
Tested on Windows XP SP1.

Author: Niels Dekker
Mail(no_spam) << "dekkerware" << at << "xs4all" << dot << "nl"

-- With help from Lennert Ploeger
***********************************************************************
Update History:
2004-09-27  version 1.0
  Released!
2004-09-29  version 1.1
  Improved C++ style, using Standard Library stream classes and valarray,
  and "Resource Acquisition is Initialization" idiom.
2004-11-14  version 1.2
  valarray replaced by vector, because vector "should be used by
  default" (according to the C++ Standard), and just like a valarray,
  the elements of a vector are also stored contiguously.
  C-style cast replaced by reinterpret_cast.
  Application now prints information when no arguments are supplied.
*/

#include <cassert>

#include <vector>
#include <exception>
#include <iostream>

// Including _MAX_PATH:
#include <cstdlib>

// Including MultiByteToWideChar:
#include <windows.h>

// Including GdipLoadImageFromFile, typically from
// "C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include"
#include <ddraw.h>
#include <gdipluspixelformats.h>
#include <gdiplusenums.h>
#include <GdiplusTypes.h>
#include <GdiplusGpStubs.h>
#include <gdiplusimaging.h>
#include <gdiplusmetaheader.h>
#include <gdipluscolor.h>
#include <gdipluscolormatrix.h>
#include <GdiPlusFlat.h>
#include <GdiPlusInit.h>


namespace
{
  const char * StatusToString(const enum Status eStatus)
  {
    if ( eStatus < 0 )
    {
      return "Status is negative!";
    }
    else
    {
      const char * apszStatusStrings[] = 
      {
        "Ok",
        "GenericError",
        "InvalidParameter",
        "OutOfMemory",
        "ObjectBusy",
        "InsufficientBuffer",
        "NotImplemented",
        "Win32Error",
        "WrongState",
        "Aborted",
        "FileNotFound",
        "ValueOverflow",
        "AccessDenied",
        "UnknownImageFormat",
        "FontFamilyNotFound",
        "FontStyleNotFound",
        "NotTrueTypeFont",
        "UnsupportedGdiplusVersion",
        "GdiplusNotInitialized",
        "PropertyNotFound",
        "PropertyNotSupported"
      };
      if ( eStatus < sizeof(apszStatusStrings)/sizeof(apszStatusStrings[0]) )
      {
        return apszStatusStrings[eStatus];
      }
      else
      {
        return "Status is too big!"; 
      }
    }
  }

  class CBaseException
  {
    const enum Status m_eStatus;

    // No assignment supported.
    int operator=(const CBaseException &);
  public:
    explicit CBaseException(const enum Status eStatus)
      : m_eStatus(eStatus)
    {
    }
    enum Status GetStatus(void) const
    {
      return m_eStatus;
    }
  };

  // Following the idiom, "Resource Acquisition is Initialization", by
  // Bjarne Stroustrup.
  class CGdiplusResources
  {
    ULONG_PTR m_Token;

    // Disable copy constructor and assignment.
    int operator=(const CGdiplusResources &);
    CGdiplusResources(const CGdiplusResources &);
  public:
    class CException : public CBaseException
    {
    public:
      explicit CException(const enum Status eStatus)
        : CBaseException(eStatus)
      {
      }
    };
    CGdiplusResources(void)
    {
      const GdiplusStartupInput StartupInput;
      const enum Status eStatus =
        GdiplusStartup(&m_Token, &StartupInput, NULL);

      if (eStatus != Ok )
      {
        throw CException(eStatus);
      }
    }
    ~CGdiplusResources(void)
    {
      GdiplusShutdown(m_Token);
    }
  };

  class CGdiplusImage
  {
    GpImage *m_pImage;

    // Disable copy constructor and assignment.
    int operator=(const CGdiplusImage &);
    CGdiplusImage(const CGdiplusImage &);
public:
    class CException : public CBaseException
    {
    public:
      explicit CException(const enum Status eStatus)
        : CBaseException(eStatus)
      {
      }
    };
    explicit CGdiplusImage(const wchar_t (&rwszInputFile)[_MAX_PATH])
    {
      const enum Status eStatus =
        GdipLoadImageFromFile(rwszInputFile, &m_pImage);

      if (eStatus != Ok )
      {
        throw CException(eStatus);
      }
    }
    operator GpImage *(void)
    {
      return m_pImage;
    }
    ~CGdiplusImage(void)
    {
      GdipDisposeImage(m_pImage);
    }
  };


  void PrintGdiplusError(
    const char pszGdiplusFunction[],
    const enum Status eStatus)
  {
    std::cerr
      << "GDI+ function "
      << pszGdiplusFunction
      << " failed, result = "
      << eStatus
      << " ("
      << StatusToString(eStatus)
      << ")"
      << std::endl;
  }


  template<size_t NUM_CHARS>
  bool AnsiCharToWideChar(
    const char pszAnsiCharStr[],
    wchar_t (&rwszWideCharStr)[NUM_CHARS],
    const DWORD dwFlags = 0)
  {
    if (pszAnsiCharStr == NULL)
    {
      return false;
    }
    else
    {
      const size_t uStrLen = strlen(pszAnsiCharStr);

      if (uStrLen < NUM_CHARS)
      {
        const int cbMultiByte = static_cast<int>(uStrLen);
    
        return MultiByteToWideChar(
          CP_ACP, dwFlags, pszAnsiCharStr, cbMultiByte, rwszWideCharStr, NUM_CHARS) == cbMultiByte;
      }
      else
      {
        return false;
      }
    }
  }


  // Finds the image encoder for the specified filename extension.
  const ImageCodecInfo * FindImageEncoderForFileExt(
    const ImageCodecInfo pImageEncoders[],
    const UINT uNumEncoders,
    const wchar_t pwszFileExt[])
  {
    const size_t uStrLen = wcslen(pwszFileExt);
    UINT uIndex = uNumEncoders;

    while ( uIndex-- > 0 )
    {
      // FilenameExtension is a semicolon separated list of
      // filename extensions, e.g., "*.JPG;*.JPEG;*.JPE;*.JFIF"
      const wchar_t * pwsz = wcsstr(pImageEncoders[uIndex].FilenameExtension, L"*.");
      
      while (pwsz != NULL )
      {
        ++pwsz;
        if ( _wcsnicmp(pwsz, pwszFileExt, uStrLen) == 0 )
        {
          pwsz += uStrLen;

          if ( (*pwsz == L'\0') or (*pwsz == L';') )
          {
            return pImageEncoders + uIndex;
          }
        }
        pwsz = wcsstr(pwsz, L"*.");
      }
    }
    return NULL;
  }


  // Gets the class identifier (CLSID) for the image of the specified
  // filename extension.
  // Inspired by GetEncoderClsid
  // From MSDN "Retrieving the Class Identifier for an Encoder" 
  // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdicpp/gdiplus/usinggdiplus/usingimageencodersanddecoders/retrievingthe.asp
  bool GetClassIdFromFileExt(
    CLSID& rClassId,
    const wchar_t pwszFileExt[])
  {
    UINT uNumEncoders = 0;
    UINT uSize = 0;

    const enum Status eEncodersSizeStatus =
      GdipGetImageEncodersSize(&uNumEncoders, &uSize);

    if ( eEncodersSizeStatus == Ok )
    {
      if ( uSize > 0 )
      {
        try
        {
          std::vector<unsigned char> BytesForEncoders(uSize);
          ImageCodecInfo* const pEncoders =
            reinterpret_cast<ImageCodecInfo*>(&BytesForEncoders[0]);

          const enum Status eEncodersStatus =
            GdipGetImageEncoders(uNumEncoders, uSize, pEncoders);

          if ( eEncodersStatus == Ok )
          {
            const ImageCodecInfo *const pEncoderForFileExt = FindImageEncoderForFileExt(
              pEncoders, uNumEncoders, pwszFileExt);

            if ( pEncoderForFileExt == NULL )
            {
              std::cerr
                << "No GDI+ image codec encoder found for filename extension ";
              std::wcerr << pwszFileExt << std::endl;
            }
            else
            {
              rClassId = pEncoderForFileExt->Clsid;
              return true;
            }
          }
          else
          {
            PrintGdiplusError("GdipGetImageEncoders", eEncodersStatus);
          }
        }
        catch(const std::bad_alloc &)
        {
          std::cerr << "Failed to allocate image encoders" << std::endl;
        }
      }
    }
    else
    {
      PrintGdiplusError("GdipGetImageEncodersSize", eEncodersSizeStatus);
    }
    return false;
  }


  // Extracts the extension part of a file name.
  // (Inspired by Borland Delphi Visual Component Library.)
  const wchar_t * ExtractFileExt(const wchar_t pwszFile[])
  {
    if ( pwszFile != NULL )
    {
      size_t uIndex = wcslen(pwszFile);

      while ( uIndex-- > 0 )
      {
        const wchar_t wch = pwszFile[uIndex];

        if ( (wch == L'.') )
        {
          // Dot found!
          return pwszFile + uIndex;
        }
        if ( (wch == L'\\') or (wch == L'/') or (wch == L':') )
        {
          // No dot in filename!
          break;
        }
      }
    }
    return L"";
  }


  // Saves the specified image to a file, converting it according to
  // the extension of the specified filename.
  bool SaveImageToFile(
    GpImage *const pImage,
    const char pszOutputFile[])
  {
    CLSID ClassId = { 0 };

    wchar_t wszOutputFile[_MAX_PATH] = L"";

    if ( AnsiCharToWideChar(pszOutputFile, wszOutputFile) )
    {
      if ( GetClassIdFromFileExt(ClassId, ExtractFileExt(wszOutputFile) ) )
      {
        const enum Status eStatus = GdipSaveImageToFile(
          pImage, wszOutputFile, &ClassId, NULL);

        if (eStatus == Ok)
        {
          return true;
        }
        else
        {
          PrintGdiplusError("GdipSaveImageToFile", eStatus);
        }
      }
      else
      {
        std::cerr << "GetClassIdFromFileExt failed" << std::endl;
      }
    }
    else
    {
      std::cerr
        << "Conversion of output filename to wide characters failed."
        << std::endl;
    }
    return false;
  }


  template <size_t N>
  const char * GetFileArgument(
   const char * const ppszArguments[],
    const size_t uNumArguments)
  {
    size_t uNumFileArguments = 0;

    for ( size_t uIndex = 0; uIndex < uNumArguments; ++uIndex )
    {
      const char *const pszArgument = ppszArguments[uIndex];

      if ( (pszArgument != NULL) and (pszArgument[0] != '/') )
      {
        if (uNumFileArguments == N)
        {
          return pszArgument;
        }
        else
        {
          ++uNumFileArguments;
        }
      }
    }
    return NULL;
  }


  bool ArrayContainsString(
    const char * const ppszArray[],
    const size_t uNumElems,
    const char pszString[])
  {
    size_t uIndex = uNumElems;

    if ( ppszArray != NULL )
    {
      while ( uIndex > 0 )
      {
        const char * const pszStringFromArray = ppszArray[--uIndex];

        if ( pszStringFromArray == NULL )
        {
          if ( pszString == NULL )
          {
            return true;
          }
        }
        else
        {
          if ( stricmp(pszString, pszStringFromArray) == 0 )
          {
            return true;
          }
        }
      }
    }
    return false;
  }

  void SkipLine(void)
  {
    do ; while ( (std::cin.get() != '\n') and std::cin.good() );
  }

  // Extracts the name and extension parts of a file name.
  // (Inspired by Borland Delphi Visual Component Library.)
  const char * ExtractFileName(const char pszFile[])
  {
    if ( pszFile == NULL )
    {
      return "";
    }
    else
    {
      size_t uIndex = strlen(pszFile) + 1;

      while ( --uIndex != 0 )
      {
        const char ch = pszFile[uIndex - 1];

        if ( (ch == '\\') or (ch == '/') or (ch == ':') )
        {
          break;
        }
      }
      return pszFile + uIndex;
    }
}

} // namespace


int main(int argc, char* argv[])
{
  assert(argc > 0);
  assert(argv != NULL);

  const char *const *const pszArguments = argv + 1;
  const size_t uNumArguments = argc - 1;

  const bool bQuietMode = 
    ArrayContainsString(pszArguments, uNumArguments, "/Q") or
    ArrayContainsString(pszArguments, uNumArguments, "/Quiet");

  std::cout << std::endl;

  if ( uNumArguments == 0 )
  {
    assert(argv[0] != NULL);
    std::cout
      << "Dekkerware ConvertImage 1.2 - "
        "http://www.xs4all.nl/~nd/dekkerware/console/"
        "\n\n"
        "Converts an image from one file to another.\n"
      << std::endl;
    std::cout
      << ExtractFileName(argv[0]) << " input output [/Q]\n"
      << std::endl;
    std::cout
      <<
        "  input   The name of an existing image file\n"
        "  output  The name of a file that the image will be converted to.\n"
        "  /Q      Quiet mode: no prompt to press Enter (optional)\n"
        "\n"
        "Converts the file format, according to the output filename extension.\n"
        "This application depends on the Microsoft Windows GDI+ API.\n" 
      << std::endl;
  }
  else
  {
    const bool bPropertyCount =
      ArrayContainsString(pszArguments, uNumArguments, "/P") or
      ArrayContainsString(pszArguments, uNumArguments, "/PropertyCount");

    const char *const pszInputFile = GetFileArgument<0>(pszArguments, uNumArguments);
    const char *const pszOutputFile = GetFileArgument<1>(pszArguments, uNumArguments);

    if ( (pszInputFile == NULL) or (pszOutputFile == NULL) )
    {
      std::cout
        << "Arguments expected: an input file and an output file"
        << std::endl;
    }
    else
    {
      wchar_t wszInputFile[_MAX_PATH] = L"";

      if ( AnsiCharToWideChar(pszInputFile, wszInputFile) )
      {
        try
        {
          CGdiplusResources GdiplusResources;

          if (not bQuietMode)
          {
            std::cout
              << "Loading image from " << pszInputFile << std::endl;
            std::cout
              << "Press Enter to continue" << std::endl;
            SkipLine();
          }

          CGdiplusImage GdiplusImage(wszInputFile);

          if ( bPropertyCount )
          {
            UINT uPropertyCount = 0;
            const enum Status ePropertyStatus =
              GdipGetPropertyCount(GdiplusImage, &uPropertyCount);

            if ( ePropertyStatus == Ok )
            {
              std::cout
                << "PropertyCount = " << uPropertyCount << std::endl;
            }
            else
            {
              PrintGdiplusError("GdipGetPropertyCount", ePropertyStatus);
            }
          }
          if (not bQuietMode)
          {
            std::cout
              << "Saving image to " << pszOutputFile << std::endl;
            std::cout
              << "Press Enter to continue" << std::endl;
            SkipLine();
          }
          SaveImageToFile(GdiplusImage, pszOutputFile);
        }
        catch(const CGdiplusResources::CException & rException)
        {
          const enum Status eExceptionStatus = rException.GetStatus();
          std::cerr
            << "Failed to aqcuire GDI+ resources, status = "
            << eExceptionStatus
            << " ("
            << StatusToString(eExceptionStatus)
            << ")"
            << std::endl;
        }
        catch(const CGdiplusImage::CException & rException)
        {
          const enum Status eExceptionStatus = rException.GetStatus();
          std::cerr
            << "Failed to load the file into a GDI+ image, status = "
            << eExceptionStatus
            << " ("
            << StatusToString(eExceptionStatus)
            << ")"
            << std::endl;
        }
      }
      else
      {
        std::cerr
          << "Conversion of input filename to wide characters failed."
          << std::endl;
      }
    }
  }
  if (not bQuietMode)
  {
    std::cout << "Press Enter to quit" << std::endl;
    SkipLine();
  }
  return 0;
}

