// ==========================================================
// MNG / JNG helpers
//
// Design and implementation by
// - Hervé Drolon (drolon@infonie.fr)
//
// This file is part of FreeImage 3
//
// COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
// THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE
// OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED
// CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT
// THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY
// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL
// PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER
// THIS DISCLAIMER.
//
// Use at your own risk!
// ==========================================================

#include "FreeImage.h"
#include "Utilities.h"

/**
References
http://www.libpng.org/pub/mng/spec/jng.html
http://www.w3.org/TR/PNG/
http://libpng.org/pub/mng/spec/
*/

// --------------------------------------------------------------------------

#define MNG_INCLUDE_JNG

#ifdef MNG_INCLUDE_JNG
#define MNG_COLORTYPE_JPEGGRAY           8       /* JHDR */
#define MNG_COLORTYPE_JPEGCOLOR         10
#define MNG_COLORTYPE_JPEGGRAYA         12
#define MNG_COLORTYPE_JPEGCOLORA        14

#define MNG_BITDEPTH_JPEG8               8       /* JHDR */
#define MNG_BITDEPTH_JPEG12             12
#define MNG_BITDEPTH_JPEG8AND12         20

#define MNG_COMPRESSION_BASELINEJPEG     8       /* JHDR */

#define MNG_INTERLACE_SEQUENTIAL         0       /* JHDR */
#define MNG_INTERLACE_PROGRESSIVE        8
#endif /* MNG_INCLUDE_JNG */

// --------------------------------------------------------------------------

#define JNG_SUPPORTED

/** Size of a JDAT chunk on writing */
const DWORD JPEG_CHUNK_SIZE	= 8192;

/** PNG signature */
static const BYTE g_png_signature[8] = { 137, 80, 78, 71, 13, 10, 26, 10 };
/** JNG signature */
static const BYTE g_jng_signature[8] = { 139, 74, 78, 71, 13, 10, 26, 10 };

// --------------------------------------------------------------------------

/** Chunk type converted to enum */
enum eChunckType {
	UNKNOWN_CHUNCK,
	MHDR,
	BACK,
	BASI,
	CLIP,
	CLON,
	DEFI,
	DHDR,
	DISC,
	ENDL,
	FRAM,
	IEND,
	IHDR,
	JHDR,
	LOOP,
	MAGN,
	MEND,
	MOVE,
	PAST,
	PLTE,
	SAVE,
	SEEK,
	SHOW,
	TERM,
	bKGD,
	cHRM,
	gAMA,
	iCCP,
	nEED,
	pHYg,
	vpAg,
	pHYs,
	sBIT,
	sRGB,
	tRNS,
	IDAT,
	JDAT,
	JDAA,
	JdAA,
	JSEP,
	oFFs,
	hIST,
	iTXt,
	sPLT,
	sTER,
	tEXt,
	tIME,
	zTXt
};

/**
Helper for map<key, value> where value is a pointer to a string. 
Used to store tEXt metadata. 
*/
typedef std::map<std::string, std::string> tEXtMAP;

// --------------------------------------------------------------------------

/*
  Constant strings for known chunk types.  If you need to add a chunk,
  add a string holding the name here.   To make the code more
  portable, we use ASCII numbers like this, not characters.
*/

static BYTE mng_MHDR[5]={ 77,  72,  68,  82, (BYTE) '\0'};
static BYTE mng_BACK[5]={ 66,  65,  67,  75, (BYTE) '\0'};
static BYTE mng_BASI[5]={ 66,  65,  83,  73, (BYTE) '\0'};
static BYTE mng_CLIP[5]={ 67,  76,  73,  80, (BYTE) '\0'};
static BYTE mng_CLON[5]={ 67,  76,  79,  78, (BYTE) '\0'};
static BYTE mng_DEFI[5]={ 68,  69,  70,  73, (BYTE) '\0'};
static BYTE mng_DHDR[5]={ 68,  72,  68,  82, (BYTE) '\0'};
static BYTE mng_DISC[5]={ 68,  73,  83,  67, (BYTE) '\0'};
static BYTE mng_ENDL[5]={ 69,  78,  68,  76, (BYTE) '\0'};
static BYTE mng_FRAM[5]={ 70,  82,  65,  77, (BYTE) '\0'};
static BYTE mng_IEND[5]={ 73,  69,  78,  68, (BYTE) '\0'};
static BYTE mng_IHDR[5]={ 73,  72,  68,  82, (BYTE) '\0'};
static BYTE mng_JHDR[5]={ 74,  72,  68,  82, (BYTE) '\0'};
static BYTE mng_LOOP[5]={ 76,  79,  79,  80, (BYTE) '\0'};
static BYTE mng_MAGN[5]={ 77,  65,  71,  78, (BYTE) '\0'};
static BYTE mng_MEND[5]={ 77,  69,  78,  68, (BYTE) '\0'};
static BYTE mng_MOVE[5]={ 77,  79,  86,  69, (BYTE) '\0'};
static BYTE mng_PAST[5]={ 80,  65,  83,  84, (BYTE) '\0'};
static BYTE mng_PLTE[5]={ 80,  76,  84,  69, (BYTE) '\0'};
static BYTE mng_SAVE[5]={ 83,  65,  86,  69, (BYTE) '\0'};
static BYTE mng_SEEK[5]={ 83,  69,  69,  75, (BYTE) '\0'};
static BYTE mng_SHOW[5]={ 83,  72,  79,  87, (BYTE) '\0'};
static BYTE mng_TERM[5]={ 84,  69,  82,  77, (BYTE) '\0'};
static BYTE mng_bKGD[5]={ 98,  75,  71,  68, (BYTE) '\0'};
static BYTE mng_cHRM[5]={ 99,  72,  82,  77, (BYTE) '\0'};
static BYTE mng_gAMA[5]={103,  65,  77,  65, (BYTE) '\0'};
static BYTE mng_iCCP[5]={105,  67,  67,  80, (BYTE) '\0'};
static BYTE mng_nEED[5]={110,  69,  69,  68, (BYTE) '\0'};
static BYTE mng_pHYg[5]={112,  72,  89, 103, (BYTE) '\0'};
static BYTE mng_vpAg[5]={118, 112,  65, 103, (BYTE) '\0'};
static BYTE mng_pHYs[5]={112,  72,  89, 115, (BYTE) '\0'};
static BYTE mng_sBIT[5]={115,  66,  73,  84, (BYTE) '\0'};
static BYTE mng_sRGB[5]={115,  82,  71,  66, (BYTE) '\0'};
static BYTE mng_tRNS[5]={116,  82,  78,  83, (BYTE) '\0'};

#if defined(JNG_SUPPORTED)
static BYTE mng_IDAT[5]={ 73,  68,  65,  84, (BYTE) '\0'};
static BYTE mng_JDAT[5]={ 74,  68,  65,  84, (BYTE) '\0'};
static BYTE mng_JDAA[5]={ 74,  68,  65,  65, (BYTE) '\0'};
static BYTE mng_JdAA[5]={ 74, 100,  65,  65, (BYTE) '\0'};
static BYTE mng_JSEP[5]={ 74,  83,  69,  80, (BYTE) '\0'};
static BYTE mng_oFFs[5]={111,  70,  70, 115, (BYTE) '\0'};
#endif

static BYTE mng_hIST[5]={104,  73,  83,  84, (BYTE) '\0'};
static BYTE mng_iTXt[5]={105,  84,  88, 116, (BYTE) '\0'};
static BYTE mng_sPLT[5]={115,  80,  76,  84, (BYTE) '\0'};
static BYTE mng_sTER[5]={115,  84,  69,  82, (BYTE) '\0'};
static BYTE mng_tEXt[5]={116,  69,  88, 116, (BYTE) '\0'};
static BYTE mng_tIME[5]={116,  73,  77,  69, (BYTE) '\0'};
static BYTE mng_zTXt[5]={122,  84,  88, 116, (BYTE) '\0'};


// --------------------------------------------------------------------------

/**
Convert a chunk name to a unique ID
*/
static eChunckType 
mng_GetChunckType(const BYTE *mChunkName) {
	if(memcmp(mChunkName, mng_MHDR, 4) == 0) {
		return MHDR;
	}
	if(memcmp(mChunkName, mng_LOOP, 4) == 0) {
		return LOOP;
	}
	if(memcmp(mChunkName, mng_DEFI, 4) == 0) {
		return DEFI;
	}
	if(memcmp(mChunkName, mng_PLTE, 4) == 0) {
		return PLTE;
	}
	if(memcmp(mChunkName, mng_tRNS, 4) == 0) {
		return tRNS;
	}
	if(memcmp(mChunkName, mng_IHDR, 4) == 0) {
		return IHDR;
	}
	if(memcmp(mChunkName, mng_JHDR, 4) == 0) {
		return JHDR;
	}
	if(memcmp(mChunkName, mng_MEND, 4) == 0) {
		return MEND;
	}
	if(memcmp(mChunkName, mng_IEND, 4) == 0) {
		return IEND;
	}
	if(memcmp(mChunkName, mng_JDAT, 4) == 0) {
		return JDAT;
	}
	if(memcmp(mChunkName, mng_IDAT, 4) == 0) {
		return IDAT;
	}
	if(memcmp(mChunkName, mng_JDAA, 4) == 0) {
		return JDAA;
	}
	if(memcmp(mChunkName, mng_gAMA, 4) == 0) {
		return gAMA;
	}
	if(memcmp(mChunkName, mng_pHYs, 4) == 0) {
		return pHYs;
	}
	if(memcmp(mChunkName, mng_bKGD, 4) == 0) {
		return bKGD;
	}
	if(memcmp(mChunkName, mng_tEXt, 4) == 0) {
		return tEXt;
	}

	return UNKNOWN_CHUNCK;
}

inline void
mng_SwapShort(WORD *sp) {
#ifndef FREEIMAGE_BIGENDIAN
	SwapShort(sp);
#endif
}

inline void
mng_SwapLong(DWORD *lp) {
#ifndef FREEIMAGE_BIGENDIAN
	SwapLong(lp);
#endif
}

/**
Returns the size, in bytes, of a FreeImageIO stream, from the current position. 
*/
static long
mng_LOF(FreeImageIO *io, fi_handle handle) {
	long start_pos = io->tell_proc(handle);
	io->seek_proc(handle, 0, SEEK_END);
	long file_length = io->tell_proc(handle);
	io->seek_proc(handle, start_pos, SEEK_SET);
	return file_length;
}

/**
Count the number of bytes in a PNG stream, from IHDR to IEND. 
If successful, the stream position, as given by io->tell_proc(handle), 
should be the end of the PNG stream at the return of the function. 
@param io
@param handle
@param inPos
@param m_TotalBytesOfChunks
@return Returns TRUE if successful, returns FALSE otherwise
*/
static BOOL 
mng_CountPNGChunks(FreeImageIO *io, fi_handle handle, long inPos, unsigned *m_TotalBytesOfChunks) {
	long mLOF;
	long mPos;
	BOOL mEnd = FALSE;
	DWORD mLength = 0;
	BYTE mChunkName[5];

	*m_TotalBytesOfChunks = 0;

	// get the length of the file
	mLOF = mng_LOF(io, handle);

	// go to the start of the file
	io->seek_proc(handle, inPos, SEEK_SET);

	try {
		// parse chunks
		while(mEnd == FALSE) {
			// chunk length
			mPos = io->tell_proc(handle);
			if(mPos + 4 > mLOF) {
				throw(1);
			}
			io->read_proc(&mLength, 1, 4, handle);
			mng_SwapLong(&mLength);
			// chunk name
			mPos = io->tell_proc(handle);
			if(mPos + 4 > mLOF) {
				throw(1);
			}
			io->read_proc(&mChunkName[0], 1, 4, handle);
			mChunkName[4] = '\0';

			// go to next chunk
			mPos = io->tell_proc(handle);
			// 4 = size of the CRC
			if(mPos + (long)mLength + 4 > mLOF) {
				throw(1);
			}
			io->seek_proc(handle, mLength + 4, SEEK_CUR);

			switch( mng_GetChunckType(mChunkName) ) {
				case IHDR:
					if(mLength != 13) {
						throw(1);
					}
					break;
				
				case IEND:
					mEnd = TRUE;		
					// the length below includes 4 bytes CRC, but no bytes for Length
					*m_TotalBytesOfChunks = io->tell_proc(handle) - inPos;
					break;		
				
				case UNKNOWN_CHUNCK:
				default:
					break;
			}

		} // while(!mEnd)

		return TRUE;
		
	} catch(int) {
		return FALSE;
	}
}

/**
Retrieve the position of a chunk in a PNG stream
@param hPngMemory PNG stream handle
@param chunk_name Name of the chunk to be found
@param offset Start of the search in the stream
@param start_pos [returned value] Start position of the chunk
@param next_pos [returned value] Start position of the next chunk
@return Returns TRUE if successful, returns FALSE otherwise
*/
static BOOL 
mng_FindChunk(FIMEMORY *hPngMemory, BYTE *chunk_name, long offset, DWORD *start_pos, DWORD *next_pos) {
	DWORD mLength = 0;

	BYTE *data = NULL;
	DWORD size_in_bytes = 0;

	*start_pos = 0;
	*next_pos = 0;

	// get a pointer to the stream buffer
	FreeImage_AcquireMemory(hPngMemory, &data, &size_in_bytes);
	if(!(data && size_in_bytes) || (size_in_bytes < 20) || (size_in_bytes - offset < 20)) {
		// not enough space to read a signature(8 bytes) + a chunk(at least 12 bytes)
		return FALSE;
	}

	try {
	
		// skip the signature and/or any following chunk(s)
		DWORD chunk_pos = offset;

		while(1) {
			// get chunk length
			if(chunk_pos + 4 > size_in_bytes) {
				break;
			}

			memcpy(&mLength, &data[chunk_pos], 4);
			mng_SwapLong(&mLength);
			chunk_pos += 4;

			const DWORD next_chunk_pos = chunk_pos + 4 + mLength + 4;
			if(next_chunk_pos > size_in_bytes) {
				break;
			}

			// get chunk name
			if(memcmp(&data[chunk_pos], chunk_name, 4) == 0) {
				chunk_pos -= 4;	// found chunk
				*start_pos = chunk_pos;
				*next_pos = next_chunk_pos;
				return TRUE;
			}
			
			chunk_pos = next_chunk_pos;
		}

		return FALSE;

	} catch(int) {
		return FALSE;
	}
}

/**
Remove a chunk located at (start_pos, next_pos) in the PNG stream
@param hPngMemory PNG stream handle
@param start_pos Start position of the chunk
@param next_pos Start position of the next chunk
@return Returns TRUE if successfull, returns FALSE otherwise
*/
static BOOL 
mng_CopyRemoveChunks(FIMEMORY *hPngMemory, DWORD start_pos, DWORD next_pos) {
	BYTE *data = NULL;
	DWORD size_in_bytes = 0;

	// length of the chunk to remove
	DWORD chunk_length = next_pos - start_pos;
	if(chunk_length == 0) {
		return TRUE;
	}

	// get a pointer to the stream buffer
	FreeImage_AcquireMemory(hPngMemory, &data, &size_in_bytes);
	if(!(data && size_in_bytes) || (size_in_bytes < 20) || (chunk_length >= size_in_bytes)) {
		// not enough space to read a signature(8 bytes) + a chunk(at least 12 bytes)
		return FALSE;
	}
	
	// new file length
	unsigned buffer_size = size_in_bytes + chunk_length;

	BYTE *buffer = (BYTE*)malloc(buffer_size * sizeof(BYTE));
	if(!buffer) {
		return FALSE;
	}
	memcpy(&buffer[0], &data[0], start_pos);
	memcpy(&buffer[start_pos], &data[next_pos], size_in_bytes - next_pos);

	// seek to the start of the stream
	FreeImage_SeekMemory(hPngMemory, 0, SEEK_SET);
	// re-write the stream
	FreeImage_WriteMemory(buffer, 1, buffer_size, hPngMemory);

	free(buffer);

	return TRUE;
}

/**
Insert a chunk just before the inNextChunkName chunk
@param hPngMemory PNG stream handle
@param start_pos Start position of the inNextChunkName chunk
@param next_pos Start position of the next chunk
@return Returns TRUE if successfull, returns FALSE otherwise
*/
static BOOL 
mng_CopyInsertChunks(FIMEMORY *hPngMemory, BYTE *inNextChunkName, BYTE *inInsertChunk, DWORD inChunkLength, DWORD start_pos, DWORD next_pos) {
	BYTE *data = NULL;
	DWORD size_in_bytes = 0;

	// length of the chunk to check
	DWORD chunk_length = next_pos - start_pos;
	if(chunk_length == 0) {
		return TRUE;
	}

	// get a pointer to the stream buffer
	FreeImage_AcquireMemory(hPngMemory, &data, &size_in_bytes);
	if(!(data && size_in_bytes) || (size_in_bytes < 20) || (chunk_length >= size_in_bytes)) {
		// not enough space to read a signature(8 bytes) + a chunk(at least 12 bytes)
		return FALSE;
	}
	
	// new file length
	unsigned buffer_size = inChunkLength + size_in_bytes;

	BYTE *buffer = (BYTE*)malloc(buffer_size * sizeof(BYTE));
	if(!buffer) {
		return FALSE;
	}
	unsigned p = 0;
	memcpy(&buffer[p], &data[0], start_pos);
	p += start_pos;
	memcpy(&buffer[p], inInsertChunk, inChunkLength);
	p += inChunkLength;
	memcpy(&buffer[p], &data[start_pos], size_in_bytes - start_pos);

	// seek to the start of the stream
	FreeImage_SeekMemory(hPngMemory, 0, SEEK_SET);
	// re-write the stream
	FreeImage_WriteMemory(buffer, 1, buffer_size, hPngMemory);

	free(buffer);

	return TRUE;
}

static BOOL 
mng_RemoveChunk(FIMEMORY *hPngMemory, BYTE *chunk_name) {
	BOOL bResult = FALSE;

	DWORD start_pos = 0;
	DWORD next_pos = 0;
	
	bResult = mng_FindChunk(hPngMemory, chunk_name, 8, &start_pos, &next_pos);
	if(!bResult) {
		return FALSE;
	}

	bResult = mng_CopyRemoveChunks(hPngMemory, start_pos, next_pos);
	if(!bResult) {
		return FALSE;
	}

	return TRUE;
}

static BOOL 
mng_InsertChunk(FIMEMORY *hPngMemory, BYTE *inNextChunkName, BYTE *inInsertChunk, unsigned chunk_length) {
	BOOL bResult = FALSE;

	DWORD start_pos = 0;
	DWORD next_pos = 0;
	
	bResult = mng_FindChunk(hPngMemory, inNextChunkName, 8, &start_pos, &next_pos);
	if(!bResult) {
		return FALSE;
	}

	bResult = mng_CopyInsertChunks(hPngMemory, inNextChunkName, inInsertChunk, chunk_length, start_pos, next_pos);
	if(!bResult) {
		return FALSE;
	}

	return TRUE;
}

static FIBITMAP* 
mng_LoadFromMemoryHandle(FIMEMORY *hmem, int flags = 0) {
	long offset = 0;
	FIBITMAP *dib = NULL;

	if(hmem) {
		// seek to the start of the stream
		FreeImage_SeekMemory(hmem, offset, SEEK_SET);

		// check the file signature and deduce its format
		// (the second argument is currently not used by FreeImage)
		FREE_IMAGE_FORMAT fif = FreeImage_GetFileTypeFromMemory(hmem, 0);
		if(fif != FIF_UNKNOWN) {
			dib = FreeImage_LoadFromMemory(fif, hmem, flags);
		}
	}
	
	return dib;
}

/**
Write a chunk in a PNG stream from the current position. 
@param chunk_name Name of the chunk
@param chunk_data Chunk array
@param length Chunk length
@param hPngMemory PNG stream handle
*/
static void
mng_WriteChunk(BYTE *chunk_name, BYTE *chunk_data, DWORD length, FIMEMORY *hPngMemory) {
	DWORD crc_file = 0;
	// write a PNG chunk ...
	// - length
	mng_SwapLong(&length);
	FreeImage_WriteMemory(&length, 1, 4, hPngMemory);
	mng_SwapLong(&length);
	// - chunk name
	FreeImage_WriteMemory(chunk_name, 1, 4, hPngMemory);
	if(chunk_data && length) {
		// - chunk data
		FreeImage_WriteMemory(chunk_data, 1, length, hPngMemory);
		// - crc
		crc_file = FreeImage_ZLibCRC32(0, chunk_name, 4);
		crc_file = FreeImage_ZLibCRC32(crc_file, chunk_data, length);
		mng_SwapLong(&crc_file);
		FreeImage_WriteMemory(&crc_file, 1, 4, hPngMemory);
	} else {
		// - crc
		crc_file = FreeImage_ZLibCRC32(0, chunk_name, 4);
		mng_SwapLong(&crc_file);
		FreeImage_WriteMemory(&crc_file, 1, 4, hPngMemory);
	}

}

/**
Wrap a IDAT chunk as a PNG stream. 
The stream has the structure { g_png_signature, IHDR, IDAT, IEND }
The image is assumed to be a greyscale image. 

@param jng_width Image width
@param jng_height Image height
@param jng_alpha_sample_depth Bits per pixel
@param mChunk PNG grayscale IDAT format
@param mLength IDAT chunk length
@param hPngMemory Output memory stream
*/
static void 
mng_WritePNGStream(DWORD jng_width, DWORD jng_height, BYTE jng_alpha_sample_depth, BYTE *mChunk, DWORD mLength, FIMEMORY *hPngMemory) {
	// PNG grayscale IDAT format

	BYTE data[14];

	// wrap the IDAT chunk as a PNG stream

	// write PNG file signature
	FreeImage_WriteMemory(g_png_signature, 1, 8, hPngMemory);

	// write a IHDR chunk ...
	/*
	The IHDR chunk must appear FIRST. It contains:
	Width:              4 bytes
	Height:             4 bytes
	Bit depth:          1 byte
	Color type:         1 byte
	Compression method: 1 byte
	Filter method:      1 byte
	Interlace method:   1 byte
	*/
	// - chunk data
	mng_SwapLong(&jng_width);
	mng_SwapLong(&jng_height);
	memcpy(&data[0], &jng_width, 4);
	memcpy(&data[4], &jng_height, 4);
	mng_SwapLong(&jng_width);
	mng_SwapLong(&jng_height);
	data[8] = jng_alpha_sample_depth;
	data[9] = 0;	// color_type gray (jng_color_type)
	data[10] = 0;	// compression method 0 (jng_alpha_compression_method)
	data[11] = 0;	// filter_method 0 (jng_alpha_filter_method)
	data[12] = 0;	// interlace_method 0 (jng_alpha_interlace_method)

	mng_WriteChunk(mng_IHDR, &data[0], 13, hPngMemory);

	// write a IDAT chunk ...
	mng_WriteChunk(mng_IDAT, mChunk, mLength, hPngMemory);

	// write a IEND chunk ...
	mng_WriteChunk(mng_IEND, NULL, 0, hPngMemory);

}

// --------------------------------------------------------------------------

/**
Build and set a FITAG whose type is FIDT_ASCII. 
The tag must be destroyed by the caller using FreeImage_DeleteTag.
@param model Metadata model to be filled
@param dib Image to be filled
@param key Tag key
@param value Tag value
@return Returns TRUE if successful, returns FALSE otherwise
*/
static BOOL 
mng_SetKeyValue(FREE_IMAGE_MDMODEL model, FIBITMAP *dib, const char *key, const char *value) {
	if(!dib || !key || !value) {
		return FALSE;
	}
	// create a tag
	FITAG *tag = FreeImage_CreateTag();
	if(tag) {
		BOOL bSuccess = TRUE;
		// fill the tag
		DWORD tag_length = (DWORD)(strlen(value) + 1);
		bSuccess &= FreeImage_SetTagKey(tag, key);
		bSuccess &= FreeImage_SetTagLength(tag, tag_length);
		bSuccess &= FreeImage_SetTagCount(tag, tag_length);
		bSuccess &= FreeImage_SetTagType(tag, FIDT_ASCII);
		bSuccess &= FreeImage_SetTagValue(tag, value);
		if(bSuccess) {
			// set the tag
			FreeImage_SetMetadata(model, dib, FreeImage_GetTagKey(tag), tag);
		}
		FreeImage_DeleteTag(tag);
		return bSuccess;
	}

	return FALSE;
}

/**
Read a tEXt chunk and extract the key/value pair. 
@param key_value_pair [returned value] Array of key/value pairs
@param mChunk Chunk data
@param mLength Chunk length
@return Returns TRUE if successful, returns FALSE otherwise
*/
static BOOL 
mng_SetMetadata_tEXt(tEXtMAP &key_value_pair, const BYTE *mChunk, DWORD mLength) {
	std::string key;
	std::string value;
	BYTE *buffer = (BYTE*)malloc(mLength * sizeof(BYTE));
	if(!buffer) {
		return FALSE;
	}
	DWORD pos = 0;

	memset(buffer, 0, mLength * sizeof(BYTE));

	for(DWORD i = 0; i < mLength; i++) {
		buffer[pos++] = mChunk[i];
		if(mChunk[i] == '\0') {
			if(key.size() == 0) {
				key = (char*)buffer;
				pos = 0;
				memset(buffer, 0, mLength * sizeof(BYTE));
			} else {
				break;
			}
		}
	}
	value = (char*)buffer;
	free(buffer);

	key_value_pair[key] = value;

	return TRUE;
}

// --------------------------------------------------------------------------

/**
Load a FIBITMAP from a MNG or a JNG stream
@param format_id ID of the caller
@param io Stream i/o functions
@param handle Stream handle
@param Offset Start of the first chunk
@param flags Loading flags
@return Returns a dib if successful, returns NULL otherwise
*/
FIBITMAP* 
mng_ReadChunks(int format_id, FreeImageIO *io, fi_handle handle, long Offset, int flags = 0) {
	DWORD mLength = 0;
	BYTE mChunkName[5];
	BYTE *mChunk = NULL;
	DWORD crc_file;
	long LastOffset;
	long mOrigPos;
	BYTE *PLTE_file_chunk = NULL;	// whole PLTE chunk (lentgh, name, array, crc)
	DWORD PLTE_file_size = 0;		// size of PLTE chunk

	BOOL m_HasGlobalPalette = FALSE; // may turn to TRUE in PLTE chunk
	unsigned m_TotalBytesOfChunks = 0;
	FIBITMAP *dib = NULL;
	FIBITMAP *dib_alpha = NULL;

	FIMEMORY *hJpegMemory = NULL;
	FIMEMORY *hPngMemory = NULL;
	FIMEMORY *hIDATMemory = NULL;

	// ---
	DWORD jng_width = 0;
	DWORD jng_height = 0;
	BYTE jng_color_type = 0;
	BYTE jng_image_sample_depth = 0;
	BYTE jng_image_compression_method = 0;

	BYTE jng_alpha_sample_depth = 0;
	BYTE jng_alpha_compression_method = 0;
	BYTE jng_alpha_filter_method = 0;
	BYTE jng_alpha_interlace_method = 0;

	DWORD mng_frame_width = 0;
	DWORD mng_frame_height = 0;
	DWORD mng_ticks_per_second = 0;
	DWORD mng_nominal_layer_count = 0;
	DWORD mng_nominal_frame_count = 0;
	DWORD mng_nominal_play_time = 0;
	DWORD mng_simplicity_profile = 0;


	DWORD res_x = 2835;	// 72 dpi
	DWORD res_y = 2835;	// 72 dpi
	RGBQUAD rgbBkColor = {0, 0, 0, 0};
	WORD bk_red, bk_green, bk_blue;
	BOOL hasBkColor = FALSE;
	BOOL mHasIDAT = FALSE;

	tEXtMAP key_value_pair;

	// ---

	BOOL header_only = (flags & FIF_LOAD_NOPIXELS) == FIF_LOAD_NOPIXELS;
	
	// get the file size
	const long mLOF = mng_LOF(io, handle);
	// go to the first chunk
	io->seek_proc(handle, Offset, SEEK_SET);

	try {
		BOOL mEnd = FALSE;

		while(mEnd == FALSE) {
			// start of the chunk
			LastOffset = io->tell_proc(handle);
			// read length
			mLength = 0;			
			io->read_proc(&mLength, 1, sizeof(mLength), handle);
			mng_SwapLong(&mLength);
			// read name			
			io->read_proc(&mChunkName[0], 1, 4, handle);
			mChunkName[4] = '\0';

			if(mLength > 0) {
				mChunk = (BYTE*)realloc(mChunk, mLength);
				if(!mChunk) {
					FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: out of memory", mChunkName);
					throw (const char*)NULL;
				}				
				Offset = io->tell_proc(handle);
				if(Offset + (long)mLength > mLOF) {
					FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: unexpected end of file", mChunkName);
					throw (const char*)NULL;
				}
				// read chunk
				io->read_proc(mChunk, 1, mLength, handle);
			}
			// read crc
			io->read_proc(&crc_file, 1, sizeof(crc_file), handle);
			mng_SwapLong(&crc_file);
			// check crc
			DWORD crc_check = FreeImage_ZLibCRC32(0, &mChunkName[0], 4);
			crc_check = FreeImage_ZLibCRC32(crc_check, mChunk, mLength);
			if(crc_check != crc_file) {
				FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: bad CRC", mChunkName);
				throw (const char*)NULL;
			}		

			switch( mng_GetChunckType(mChunkName) ) {
				case MHDR:
					// The MHDR chunk is always first in all MNG datastreams except for those 
					// that consist of a single PNG or JNG datastream with a PNG or JNG signature. 
					if(mLength == 28) {
						memcpy(&mng_frame_width, &mChunk[0], 4);
						memcpy(&mng_frame_height, &mChunk[4], 4);
						memcpy(&mng_ticks_per_second, &mChunk[8], 4);
						memcpy(&mng_nominal_layer_count, &mChunk[12], 4);
						memcpy(&mng_nominal_frame_count, &mChunk[16], 4);
						memcpy(&mng_nominal_play_time, &mChunk[20], 4);
						memcpy(&mng_simplicity_profile, &mChunk[24], 4);

						mng_SwapLong(&mng_frame_width);
						mng_SwapLong(&mng_frame_height);
						mng_SwapLong(&mng_ticks_per_second);
						mng_SwapLong(&mng_nominal_layer_count);
						mng_SwapLong(&mng_nominal_frame_count);
						mng_SwapLong(&mng_nominal_play_time);
						mng_SwapLong(&mng_simplicity_profile);

					} else {
						FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: size is %d instead of 28", mChunkName, mLength);
					}
					break;

				case MEND:
					mEnd = TRUE;
					break;

				case LOOP:
				case ENDL:
					break;
				case DEFI:
					break;
				case SAVE:
				case SEEK:
				case TERM:
					break;
				case BACK:
					break;

					// Global "PLTE" and "tRNS" (if any).  PNG "PLTE" will be of 0 byte, as it uses global data.
				case PLTE:	// Global
					m_HasGlobalPalette = TRUE;
					PLTE_file_size = mLength + 12; // (lentgh, name, array, crc) = (4, 4, mLength, 4)
					PLTE_file_chunk = (BYTE*)realloc(PLTE_file_chunk, PLTE_file_size);
					if(!PLTE_file_chunk) {
						FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: out of memory", mChunkName);
						throw (const char*)NULL;
					} else {
						mOrigPos = io->tell_proc(handle);
						// seek to the start of the chunk
						io->seek_proc(handle, LastOffset, SEEK_SET);
						// load the whole chunk
						io->read_proc(PLTE_file_chunk, 1, PLTE_file_size, handle);
						// go to the start of the next chunk
						io->seek_proc(handle, mOrigPos, SEEK_SET);
					}
					break;

				case tRNS:	// Global
					break;
					
				case IHDR:
					Offset = LastOffset;
					// parse the PNG file and get its file size
					if(mng_CountPNGChunks(io, handle, Offset, &m_TotalBytesOfChunks) == FALSE) {
						// reach an unexpected end of file
						mEnd = TRUE;
						FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: unexpected end of PNG file", mChunkName);
						break;
					}
					
					// wrap the { IHDR, ..., IEND } chunks as a PNG stream
					if(hPngMemory == NULL) {
						hPngMemory = FreeImage_OpenMemory();
					}

					mOrigPos = io->tell_proc(handle);

					// write PNG file signature
					FreeImage_SeekMemory(hPngMemory, 0, SEEK_SET);
					FreeImage_WriteMemory(g_png_signature, 1, 8, hPngMemory);

					mChunk = (BYTE*)realloc(mChunk, m_TotalBytesOfChunks);
					if(!mChunk) {
						FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: out of memory", mChunkName);
						throw (const char*)NULL;
					}
					
					// on calling CountPNGChunks earlier, we were in Offset pos,
					// go back there
					io->seek_proc(handle, Offset, SEEK_SET);
					io->read_proc(mChunk, 1, m_TotalBytesOfChunks, handle);
					// Put back to original pos
					io->seek_proc(handle, mOrigPos, SEEK_SET);
					// write the PNG chunks
					FreeImage_WriteMemory(mChunk, 1, m_TotalBytesOfChunks, hPngMemory);

					// plug in global PLTE if local PLTE exists
					if(m_HasGlobalPalette) {
						// ensure we remove some local chunks, so that global
						// "PLTE" can be inserted right before "IDAT".
						mng_RemoveChunk(hPngMemory, mng_PLTE);
						mng_RemoveChunk(hPngMemory, mng_tRNS);
						mng_RemoveChunk(hPngMemory, mng_bKGD);
						// insert global "PLTE" chunk in its entirety before "IDAT"
						mng_InsertChunk(hPngMemory, mng_IDAT, PLTE_file_chunk, PLTE_file_size);
					}

					if(dib) FreeImage_Unload(dib);
					dib = mng_LoadFromMemoryHandle(hPngMemory, flags);

					// stop after the first image
					mEnd = TRUE;
					break;

				case JHDR:
					if(mLength == 16) {
						memcpy(&jng_width, &mChunk[0], 4);
						memcpy(&jng_height, &mChunk[4], 4);
						mng_SwapLong(&jng_width);
						mng_SwapLong(&jng_height);

						jng_color_type = mChunk[8];
						jng_image_sample_depth = mChunk[9];
						jng_image_compression_method = mChunk[10];
						//BYTE jng_image_interlace_method = mChunk[11];	// for debug only

						jng_alpha_sample_depth = mChunk[12];
						jng_alpha_compression_method = mChunk[13];
						jng_alpha_filter_method = mChunk[14];
						jng_alpha_interlace_method = mChunk[15];
					} else {
						FreeImage_OutputMessageProc(format_id, "Error while parsing %s chunk: invalid chunk length", mChunkName);
						throw (const char*)NULL;
					}
					break;

				case JDAT:
					if(hJpegMemory == NULL) {
						hJpegMemory = FreeImage_OpenMemory();
					}
					// as there may be several JDAT chunks, concatenate them
					FreeImage_WriteMemory(mChunk, 1, mLength, hJpegMemory);
					break;

				case IDAT:
					if(!header_only && (jng_alpha_compression_method == 0)) {
						// PNG grayscale IDAT format
						if(hIDATMemory == NULL) {
							hIDATMemory = FreeImage_OpenMemory();
							mHasIDAT = TRUE;
						}
						// as there may be several IDAT chunks, concatenate them
						FreeImage_WriteMemory(mChunk, 1, mLength, hIDATMemory);
					}
					break;

				case IEND:
					if(!hJpegMemory) {
						mEnd = TRUE;
						break;
					}
					// load the JPEG
					if(dib) {
						FreeImage_Unload(dib);
					}
					dib = mng_LoadFromMemoryHandle(hJpegMemory, flags);

					// load the PNG alpha layer
					if(mHasIDAT) {
						BYTE *data = NULL;
						DWORD size_in_bytes = 0;

						// get a pointer to the IDAT buffer
						FreeImage_AcquireMemory(hIDATMemory, &data, &size_in_bytes);
						if(data && size_in_bytes) {
							// wrap the IDAT chunk as a PNG stream
							if(hPngMemory == NULL) {
								hPngMemory = FreeImage_OpenMemory();
							}
							mng_WritePNGStream(jng_width, jng_height, jng_alpha_sample_depth, data, size_in_bytes, hPngMemory);
							// load the PNG
							if(dib_alpha) {
								FreeImage_Unload(dib_alpha);
							}
							dib_alpha = mng_LoadFromMemoryHandle(hPngMemory, flags);
						}
					}
					// stop the parsing
					mEnd = TRUE;
					break;

				case JDAA:
					break;

				case gAMA:
					break;

				case pHYs:
					// unit is pixels per meter
					memcpy(&res_x, &mChunk[0], 4);
					mng_SwapLong(&res_x);
					memcpy(&res_y, &mChunk[4], 4);
					mng_SwapLong(&res_y);
					break;

				case bKGD:
					memcpy(&bk_red, &mChunk[0], 2);
					mng_SwapShort(&bk_red);
					rgbBkColor.rgbRed = (BYTE)bk_red;
					memcpy(&bk_green, &mChunk[2], 2);
					mng_SwapShort(&bk_green);
					rgbBkColor.rgbGreen = (BYTE)bk_green;
					memcpy(&bk_blue, &mChunk[4], 2);
					mng_SwapShort(&bk_blue);
					rgbBkColor.rgbBlue = (BYTE)bk_blue;
					hasBkColor = TRUE;
					break;
				
				case tEXt:
					mng_SetMetadata_tEXt(key_value_pair, mChunk, mLength);
					break;

				case UNKNOWN_CHUNCK:
				default:
					break;


			} // switch( GetChunckType )
		} // while(!mEnd)

		FreeImage_CloseMemory(hJpegMemory);
		FreeImage_CloseMemory(hPngMemory);
		FreeImage_CloseMemory(hIDATMemory);
		free(mChunk);
		free(PLTE_file_chunk);

		// convert to 32-bit if a transparent layer is available
		if(!header_only && dib_alpha) {
			FIBITMAP *dst = FreeImage_ConvertTo32Bits(dib);
			if((FreeImage_GetBPP(dib_alpha) == 8) && (FreeImage_GetImageType(dib_alpha) == FIT_BITMAP)) {
				FreeImage_SetChannel(dst, dib_alpha, FICC_ALPHA);
			} else {
				FIBITMAP *dst_alpha = FreeImage_ConvertTo8Bits(dib_alpha);
				FreeImage_SetChannel(dst, dst_alpha, FICC_ALPHA);
				FreeImage_Unload(dst_alpha);
			}			
			FreeImage_Unload(dib);
			dib = dst;
		}
		FreeImage_Unload(dib_alpha);

		if(dib) {
			// set metadata
			FreeImage_SetDotsPerMeterX(dib, res_x);
			FreeImage_SetDotsPerMeterY(dib, res_y);
			if(hasBkColor) {
				FreeImage_SetBackgroundColor(dib, &rgbBkColor);
			}
			if(key_value_pair.size()) {
				for(tEXtMAP::iterator j = key_value_pair.begin(); j != key_value_pair.end(); j++) {
					std::string key = (*j).first;
					std::string value = (*j).second;
					mng_SetKeyValue(FIMD_COMMENTS, dib, key.c_str(), value.c_str());
				}
			}
		}
			
		return dib;

	} catch(const char *text) {
		FreeImage_CloseMemory(hJpegMemory);
		FreeImage_CloseMemory(hPngMemory);
		FreeImage_CloseMemory(hIDATMemory);
		free(mChunk);
		free(PLTE_file_chunk);
		FreeImage_Unload(dib);
		FreeImage_Unload(dib_alpha);
		if(text) {
			FreeImage_OutputMessageProc(format_id, text);
		}
		return NULL;
	}
}

// --------------------------------------------------------------------------

/**
Write a FIBITMAP to a JNG stream
@param format_id ID of the caller
@param io Stream i/o functions
@param dib Image to be saved
@param handle Stream handle
@param flags Saving flags
@return Returns TRUE if successful, returns FALSE otherwise
*/
BOOL 
mng_WriteJNG(int format_id, FreeImageIO *io, FIBITMAP *dib, fi_handle handle, int flags) {
	DWORD jng_width = 0;
	DWORD jng_height = 0;
	BYTE jng_color_type = 0;
	BYTE jng_image_sample_depth = 8;
	BYTE jng_image_compression_method = 8;	//  8: ISO-10918-1 Huffman-coded baseline JPEG.
	BYTE jng_image_interlace_method = 0;

	BYTE jng_alpha_sample_depth = 0;
	BYTE jng_alpha_compression_method = 0;
	BYTE jng_alpha_filter_method = 0;
	BYTE jng_alpha_interlace_method = 0;

	BYTE buffer[16];

	FIMEMORY *hJngMemory = NULL;
	FIMEMORY *hJpegMemory = NULL;
	FIMEMORY *hPngMemory = NULL;

	FIBITMAP *dib_rgb = NULL;
	FIBITMAP *dib_alpha = NULL;

	if(!dib || (FreeImage_GetImageType(dib) != FIT_BITMAP)) {
		return FALSE;
	}

	unsigned bpp = FreeImage_GetBPP(dib);

	switch(bpp) {
		case 8:
			if(FreeImage_GetColorType(dib) == FIC_MINISBLACK) {
				dib_rgb = dib;
				jng_color_type = MNG_COLORTYPE_JPEGGRAY;
			} else {
				// JPEG plugin will convert other types (FIC_MINISWHITE, FIC_PALETTE) to 24-bit on the fly
				//dib_rgb = FreeImage_ConvertTo24Bits(dib);
				dib_rgb = dib;
				jng_color_type = MNG_COLORTYPE_JPEGCOLOR;

			}
			break;
		case 24:
			dib_rgb = dib;
			jng_color_type = MNG_COLORTYPE_JPEGCOLOR;
			break;
		case 32:
			dib_rgb = FreeImage_ConvertTo24Bits(dib);
			jng_color_type = MNG_COLORTYPE_JPEGCOLORA;
			jng_alpha_sample_depth = 8;
			break;
		default:
			return FALSE;
	}

	jng_width = (DWORD)FreeImage_GetWidth(dib);
	jng_height = (DWORD)FreeImage_GetHeight(dib);

	try {
		hJngMemory = FreeImage_OpenMemory();

		// --- write JNG file signature ---
		FreeImage_WriteMemory(g_jng_signature, 1, 8, hJngMemory);

		// --- write a JHDR chunk ---
		SwapLong(&jng_width);
		SwapLong(&jng_height);
		memcpy(&buffer[0], &jng_width, 4);
		memcpy(&buffer[4], &jng_height, 4);
		SwapLong(&jng_width);
		SwapLong(&jng_height);
		buffer[8] = jng_color_type;
		buffer[9] = jng_image_sample_depth;
		buffer[10] = jng_image_compression_method;
		buffer[11] = jng_image_interlace_method;
		buffer[12] = jng_alpha_sample_depth;
		buffer[13] = jng_alpha_compression_method;
		buffer[14] = jng_alpha_filter_method;
		buffer[15] = jng_alpha_interlace_method;
		mng_WriteChunk(mng_JHDR, &buffer[0], 16, hJngMemory);

		// --- write a sequence of JDAT chunks ---
		hJpegMemory = FreeImage_OpenMemory();
		flags |= JPEG_BASELINE;
		if(!FreeImage_SaveToMemory(FIF_JPEG, dib_rgb, hJpegMemory, flags)) {
			throw (const char*)NULL;
		}
		if(dib_rgb != dib) {
			FreeImage_Unload(dib_rgb);
			dib_rgb = NULL;
		}
		{
			BYTE *jpeg_data = NULL;
			DWORD size_in_bytes = 0;
			
			// get a pointer to the stream buffer
			FreeImage_AcquireMemory(hJpegMemory, &jpeg_data, &size_in_bytes);
			// write chunks
			for(DWORD k = 0; k < size_in_bytes;) {
				DWORD bytes_left = size_in_bytes - k;
				DWORD chunk_size = MIN(JPEG_CHUNK_SIZE, bytes_left);
				mng_WriteChunk(mng_JDAT, &jpeg_data[k], chunk_size, hJngMemory);
				k += chunk_size;
			}
		}
		FreeImage_CloseMemory(hJpegMemory);
		hJpegMemory = NULL;

		// --- write alpha layer as a sequence of IDAT chunk ---
		if((bpp == 32) && (jng_color_type == MNG_COLORTYPE_JPEGCOLORA)) {
			dib_alpha = FreeImage_GetChannel(dib, FICC_ALPHA);

			hPngMemory = FreeImage_OpenMemory();
			if(!FreeImage_SaveToMemory(FIF_PNG, dib_alpha, hPngMemory, PNG_DEFAULT)) {
				throw (const char*)NULL;
			}
			FreeImage_Unload(dib_alpha);
			dib_alpha = NULL;
			// get the IDAT chunk
			{		
				BOOL bResult = FALSE;
				DWORD start_pos = 0;
				DWORD next_pos = 0;
				long offset = 8;
				
				do {
					// find the next IDAT chunk from 'offset' position
					bResult = mng_FindChunk(hPngMemory, mng_IDAT, offset, &start_pos, &next_pos);
					if(!bResult) break;
					
					BYTE *png_data = NULL;
					DWORD size_in_bytes = 0;
					
					// get a pointer to the stream buffer
					FreeImage_AcquireMemory(hPngMemory, &png_data, &size_in_bytes);
					// write the IDAT chunk
					mng_WriteChunk(mng_IDAT, &png_data[start_pos+8], next_pos - start_pos - 12, hJngMemory);

					offset = next_pos;

				} while(bResult);
			}

			FreeImage_CloseMemory(hPngMemory);
			hPngMemory = NULL;
		}

		// --- write a IEND chunk ---
		mng_WriteChunk(mng_IEND, NULL, 0, hJngMemory);

		// write the JNG on output stream
		{
			BYTE *jng_data = NULL;
			DWORD size_in_bytes = 0;
			FreeImage_AcquireMemory(hJngMemory, &jng_data, &size_in_bytes);
			io->write_proc(jng_data, 1, size_in_bytes, handle);			
		}

		FreeImage_CloseMemory(hJngMemory);
		FreeImage_CloseMemory(hJpegMemory);
		FreeImage_CloseMemory(hPngMemory);

		return TRUE;

	} catch(const char *text) {
		FreeImage_CloseMemory(hJngMemory);
		FreeImage_CloseMemory(hJpegMemory);
		FreeImage_CloseMemory(hPngMemory);
		if(dib_rgb && (dib_rgb != dib)) {
			FreeImage_Unload(dib_rgb);
		}
		FreeImage_Unload(dib_alpha);
		if(text) {
			FreeImage_OutputMessageProc(format_id, text);
		}
		return FALSE;
	}
}
