View Javadoc

1   package de.desy.acop.video.timageio;
2   
3   import java.awt.image.BufferedImage;
4   import java.math.BigInteger;
5   import java.security.MessageDigest;
6   import java.security.NoSuchAlgorithmException;
7   import java.util.Calendar;
8   import java.util.GregorianCalendar;
9   import java.util.HashMap;
10  import java.util.Hashtable;
11  import java.util.Map;
12  
13  import javax.imageio.metadata.IIOMetadata;
14  
15  import com.sun.imageio.plugins.png.PNGMetadata;
16  
17  import de.desy.acop.video.displayer.ImageFlag;
18  import de.desy.acop.video.displayer.ImageFormat;
19  import de.desy.acop.video.displayer.timage.TImageUtils;
20  import de.desy.tine.types.IMAGE;
21  import de.desy.tine.types.IMAGE.FrameHeader;
22  import de.desy.tine.types.IMAGE.SourceHeader;
23  
24  /**
25   * Represents metadata (non-image data) associated with TINE images.
26   * 
27   * @author David Melkumyan, DESY Zeuthen
28   * @since October 2009
29   */
30  public class TImageMetadata {
31  
32  	private static final String DEFLT_SOFTWARE = "TImageIO - Java TINE Image I/O API for loading/saving TINE images (betta ver. 0.0.02)";
33  	private static final String DEFLT_COMMENT = "Betta version: 0.0.03";
34  
35  	public static final String KEY_SOFTWARE = "Software";
36  	public static final String KEY_COMMENT = "Comment";
37  
38  	public static final String KEY_CAMERA_PORT_ID = "VSv3.hdr.src.cameraPortId";
39  	public static final String KEY_VERSION_TAG = "VSv3.hdr.src.versionTag";
40  	public static final String KEY_TIMESTAMP_SECONDS = "VSv3.hdr.src.timestampSeconds";
41  	public static final String KEY_TIMESTAMP_MICROSECONDS = "VSv3.hdr.src.timestampMicroseconds";
42  	public static final String KEY_CAMERA_PORT_NAME = "VSv3.hdr.src.cameraPortName";
43  	public static final String KEY_SOURCE_WIDTH = "VSv3.hdr.frm.sourceWidth";
44  	public static final String KEY_SOURCE_HEIGHT = "VSv3.hdr.frm.sourceHeight";
45  	public static final String KEY_AOI_WIDTH = "VSv3.hdr.frm.aoiWidth";
46  	public static final String KEY_AOI_HEIGHT = "VSv3.hdr.frm.aoiHeight";
47  	public static final String KEY_XSTART = "VSv3.hdr.frm.xStart";
48  	public static final String KEY_YSTART = "VSv3.hdr.frm.yStart";
49  	public static final String KEY_BYTES_PER_PIXEL = "VSv3.hdr.frm.bytesPerPixel";
50  	public static final String KEY_EFFECTIVE_BITS_PER_PIXEL = "VSv3.hdr.frm.effectiveBitsPerPixel";
51  	public static final String KEY_HORIZONTAL_BINNING = "VSv3.hdr.frm.horizontalBinning";
52  	public static final String KEY_VERTICAL_BINNING = "VSv3.hdr.frm.verticalBinning";
53  	public static final String KEY_SOURCE_FORMAT = "VSv3.hdr.frm.sourceFormat";
54  	public static final String KEY_IMAGE_FORMAT = "VSv3.hdr.frm.imageFormat";
55  	public static final String KEY_FRAME_NUMBER = "VSv3.hdr.frm.frameNumber";
56  	public static final String KEY_EVENT_NUMBER = "VSv3.hdr.frm.eventNumber";
57  	public static final String KEY_XSCALE = "VSv3.hdr.frm.xScale";
58  	public static final String KEY_YSCALE = "VSv3.hdr.frm.yScale";
59  	public static final String KEY_IMAGE_ROTATION = "VSv3.hdr.frm.imageRotation";
60  	public static final String KEY_IMAGE_FLAGS = "VSv3.hdr.frm.imageFlags";
61  	public static final String KEY_APPENDED_FRAME_SIZE = "VSv3.hdr.frm.appendedFrameSize";
62  	public static final String KEY_MD5HASH = "VSV3.frameBuffer.md5hash";
63  	public static final String KEY_ISPARE1 = "VSv3.hdr.frm.ispare1";
64  	public static final String KEY_ISPARE2 = "VSv3.hdr.frm.ispare2";
65  	public static final String KEY_ISPARE3 = "VSv3.hdr.frm.ispare3";
66  	public static final String KEY_FSPARE1 = "VSv3.hdr.frm.fspare1";
67  	public static final String KEY_FSPARE2 = "VSv3.hdr.frm.fspare2";
68  	public static final String KEY_FSPARE3 = "VSv3.hdr.frm.fspare3";
69  	public static final String KEY_OPT_TEXT = "VSv3.opt.text";
70  	public static final String KEY_VTOB = "vtOb";
71  
72  	// /**
73  	// * PNG Color Type:
74  	// * <p>
75  	// * <ul>
76  	// * <li>PNG_COLOR_GRAY = 0
77  	// * <li>PNG_COLOR_RGB = 2
78  	// * <li>PNG_COLOR_PALETTE = 3
79  	// * <li>PNG_COLOR_GRAY_ALPHA = 4
80  	// * <li>PNG_COLOR_RGB_ALPHA = 6
81  	// * </ul>
82  	// */
83  	// private int pngColorType = -1;
84  
85  	/**
86  	 * optional
87  	 */
88  	public String software = DEFLT_SOFTWARE;
89  
90  	/**
91  	 * optional
92  	 */
93  	public String comment = DEFLT_COMMENT;
94  
95  	/**
96  	 * @see de.desy.tine.types.TBufferedImage.SourceHeader#cameraPortId
97  	 */
98  	public Long cameraPortId;
99  
100 	/**
101 	 * @see de.desy.tine.types.TBufferedImage.SourceHeader#versionTag
102 	 */
103 	public Long versionTag;
104 
105 	/**
106 	 * @see de.desy.tine.types.TBufferedImage.SourceHeader#timestampSeconds
107 	 */
108 	public Integer timestampSeconds;
109 
110 	/**
111 	 * @see de.desy.tine.types.TBufferedImage.SourceHeader#timestampMicroseconds
112 	 */
113 	public Integer timestampMicroseconds;
114 
115 	/**
116 	 * @see de.desy.tine.types.TBufferedImage.SourceHeader#cameraPortName
117 	 */
118 	public String cameraPortName;
119 
120 	/**
121 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#sourceWidth
122 	 */
123 	public Integer sourceWidth;
124 
125 	/**
126 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#sourceHeight
127 	 */
128 	public Integer sourceHeight;
129 
130 	/**
131 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#aoiWidth
132 	 */
133 	public Integer aoiWidth;
134 
135 	/**
136 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#aoiHeight
137 	 */
138 	public Integer aoiHeight;
139 
140 	/**
141 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#xStart
142 	 */
143 	public Integer xStart;
144 
145 	/**
146 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#yStart
147 	 */
148 	public Integer yStart;
149 
150 	/**
151 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#bytesPerPixel
152 	 */
153 	public Integer bytesPerPixel;
154 
155 	/**
156 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#effectiveBitsPerPixel
157 	 */
158 	public Integer effectiveBitsPerPixel;
159 
160 	/**
161 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#horizontalBinning
162 	 */
163 	public Integer horizontalBinning;
164 
165 	/**
166 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#verticalBinning
167 	 */
168 	public Integer verticalBinning;
169 
170 	/**
171 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#sourceFormat
172 	 */
173 	public ImageFormat sourceFormat;
174 
175 	/**
176 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#imageFormat
177 	 */
178 	public ImageFormat imageFormat;
179 
180 	/**
181 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#frameNumber
182 	 */
183 	public Long frameNumber;
184 
185 	/**
186 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#eventNumber
187 	 */
188 	public Long eventNumber;
189 
190 	/**
191 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#xScale
192 	 */
193 	public Float xScale;
194 
195 	/**
196 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#yScale
197 	 */
198 	public Float yScale;
199 
200 	/**
201 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#imageRotation
202 	 */
203 	public Float imageRotation;
204 
205 	/**
206 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#imageFlags
207 	 */
208 	public Integer imageFlags;
209 
210 	/**
211 	 * @see de.desy.tine.types.TBufferedImage.FrameHeader#appendedFrameSize
212 	 */
213 	public Integer appendedFrameSize;
214 
215 	/**
216 	 * The md5 hash of image bits.
217 	 * <p>
218 	 * An md5 checksum (32 chars of text) is calculated out of the first
219 	 * 'appendedFrameSize' bytes found in TImage's appended frame buffer.
220 	 */
221 	public String md5hash;
222 
223 	/**
224 	 * Optional user text tags.
225 	 * <p>
226 	 * The text tags are passed by the user as key value pairs, where each key
227 	 * and value must consist of only Latin1 characters (see PNG reference
228 	 * documentation on this subject).<br>
229 	 * The key must be smaller or equal to 64 characters in length.<br>
230 	 * Each key string is prefixed with "VSv3.opt.text." and then encoded to PNG
231 	 * tEXt chunk.
232 	 */
233 	public Map<String, String> optVars = new HashMap<String, String>();
234 
235 	public int ispare1 = -1; // optional
236 	public int ispare2 = -1; // optional
237 	public int ispare3 = -1; // optional
238 
239 	public float fspare1 = -1; // optional
240 	public float fspare2 = -1; // optional
241 	public float fspare3 = -1; // optional
242 
243 	/**
244 	 * Videosystem Three Optional Binary data.
245 	 * <p>
246 	 * The user/programmer has the option to save a binary object as a byte
247 	 * array.
248 	 */
249 	private byte[] vtOb; // optional
250 
251 	/**
252 	 * Constructs TImageMetadata from BufferedImage with TINE image metadata in
253 	 * hashtable
254 	 * 
255 	 * @param bi
256 	 *            - Java BufferedImage with TINE image metadata in hashtable
257 	 */
258 	public TImageMetadata(BufferedImage bi) {
259 		read(bi, true);
260 	}
261 
262 	/**
263 	 * @param bi
264 	 *            - BufferedImage
265 	 * @param rejectNonArchival
266 	 *            - if <code>true</code> and <code>bi</code> contains invalid
267 	 *            metadata throws <code>IllegalArgumentException</code>
268 	 */
269 	public TImageMetadata(BufferedImage bi, boolean rejectNonArchival) {
270 		read(bi, rejectNonArchival);
271 	}
272 
273 	/**
274 	 * Creates image Metadata from known Image I/O Metadata.
275 	 * 
276 	 * @param iioMetadata
277 	 *            Image I/O Metadata
278 	 * 
279 	 * @exception NullPointerException
280 	 *                If <code>iioMetadata</code> is <code>null</code>.
281 	 * @exception NullPointerException
282 	 *                If <code>iioMetadata</code> is unsupported.
283 	 */
284 	public TImageMetadata(IIOMetadata iioMetadata) {
285 		if (iioMetadata == null)
286 			throw new NullPointerException("iioMetadata == null!");
287 
288 		if (iioMetadata instanceof PNGMetadata)
289 			read((PNGMetadata) iioMetadata);
290 		else
291 			throw new IllegalArgumentException("Unsupported class of metadata: " + iioMetadata.getClass().getName());
292 	}
293 
294 	/**
295 	 * Creates image Metadata from PNG Metadata.
296 	 * 
297 	 * @param pngMetadata
298 	 *            PNG Metadata
299 	 */
300 	public TImageMetadata(PNGMetadata pngMetadata) {
301 		read(pngMetadata);
302 	}
303 
304 	/**
305 	 * Creates image Metadata from TINE image.
306 	 * 
307 	 * @param timage
308 	 *            TINE Image
309 	 */
310 	public TImageMetadata(IMAGE timage) {
311 		read(timage);
312 	}
313 
314 	/**
315 	 * Initializes metadata from PNG Metadata
316 	 * 
317 	 * @param pngMetadata
318 	 *            - PNG Metadata
319 	 * 
320 	 * @exception NullPointerException
321 	 *                If <code>pngMetadata</code> is <code>null</code>.
322 	 * @exception NumberFormatException
323 	 *                If the numerical string cannot be parsed as a number.
324 	 * @exception IllegalArgumentException
325 	 *                If the specified image format type has no constant with
326 	 *                the specified name.
327 	 * 
328 	 */
329 	private void read(PNGMetadata pngMetadata) {
330 		if (pngMetadata == null)
331 			throw new NullPointerException("pngMetadata == null!");
332 
333 		int size = pngMetadata.tEXt_keyword.size();
334 		for (int index = 0; index < size; index++) {
335 			String key = (String) pngMetadata.tEXt_keyword.get(index);
336 
337 			if (KEY_SOFTWARE.equals(key)) {
338 				software = (String) pngMetadata.tEXt_text.get(index);
339 			} else if (KEY_COMMENT.equals(key)) {
340 				comment = (String) pngMetadata.tEXt_text.get(index);
341 			} else if (KEY_CAMERA_PORT_ID.equals(key)) {
342 				cameraPortId = Long.valueOf((String) pngMetadata.tEXt_text.get(index));
343 			} else if (KEY_VERSION_TAG.equals(key)) {
344 				versionTag = Long.decode((String) pngMetadata.tEXt_text.get(index));
345 			} else if (KEY_TIMESTAMP_SECONDS.equals(key)) {
346 				timestampSeconds = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
347 			} else if (KEY_TIMESTAMP_MICROSECONDS.equals(key)) {
348 				timestampMicroseconds = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
349 			} else if (KEY_CAMERA_PORT_NAME.equals(key)) {
350 				cameraPortName = (String) pngMetadata.tEXt_text.get(index);
351 			} else if (KEY_SOURCE_WIDTH.equals(key)) {
352 				sourceWidth = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
353 			} else if (KEY_SOURCE_HEIGHT.equals(key)) {
354 				sourceHeight = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
355 			} else if (KEY_AOI_WIDTH.equals(key)) {
356 				aoiWidth = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
357 			} else if (KEY_AOI_HEIGHT.equals(key)) {
358 				aoiHeight = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
359 			} else if (KEY_XSTART.equals(key)) {
360 				xStart = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
361 			} else if (KEY_YSTART.equals(key)) {
362 				yStart = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
363 			} else if (KEY_BYTES_PER_PIXEL.equals(key)) {
364 				bytesPerPixel = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
365 			} else if (KEY_EFFECTIVE_BITS_PER_PIXEL.equals(key)) {
366 				effectiveBitsPerPixel = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
367 			} else if (KEY_HORIZONTAL_BINNING.equals(key)) {
368 				horizontalBinning = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
369 			} else if (KEY_VERTICAL_BINNING.equals(key)) {
370 				verticalBinning = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
371 			} else if (KEY_SOURCE_FORMAT.equals(key)) {
372 				sourceFormat = ImageFormat.valueOf((String) pngMetadata.tEXt_text.get(index));
373 			} else if (KEY_IMAGE_FORMAT.equals(key)) {
374 				imageFormat = ImageFormat.valueOf((String) pngMetadata.tEXt_text.get(index));
375 			} else if (KEY_FRAME_NUMBER.equals(key)) {
376 				frameNumber = Long.valueOf((String) pngMetadata.tEXt_text.get(index));
377 			} else if (KEY_EVENT_NUMBER.equals(key)) {
378 				eventNumber = Long.valueOf((String) pngMetadata.tEXt_text.get(index));
379 			} else if (KEY_XSCALE.equals(key)) {
380 				xScale = Float.valueOf((String) pngMetadata.tEXt_text.get(index));
381 			} else if (KEY_YSCALE.equals(key)) {
382 				yScale = Float.valueOf((String) pngMetadata.tEXt_text.get(index));
383 			} else if (KEY_IMAGE_ROTATION.equals(key)) {
384 				imageRotation = Float.valueOf((String) pngMetadata.tEXt_text.get(index));
385 			} else if (KEY_IMAGE_FLAGS.equals(key)) {
386 				imageFlags = Integer.decode((String) pngMetadata.tEXt_text.get(index));
387 			} else if (KEY_APPENDED_FRAME_SIZE.equals(key)) {
388 				appendedFrameSize = Integer.valueOf((String) pngMetadata.tEXt_text.get(index));
389 			} else if (KEY_MD5HASH.equals(key)) {
390 				md5hash = (new BigInteger((String) pngMetadata.tEXt_text.get(index), 16)).toString(16);
391 			} else if (KEY_ISPARE1.equals(key)) {
392 				ispare1 = Integer.parseInt((String) pngMetadata.tEXt_text.get(index));
393 			} else if (KEY_ISPARE2.equals(key)) {
394 				ispare2 = Integer.parseInt((String) pngMetadata.tEXt_text.get(index));
395 			} else if (KEY_ISPARE3.equals(key)) {
396 				ispare3 = Integer.parseInt((String) pngMetadata.tEXt_text.get(index));
397 			} else if (KEY_FSPARE1.equals(key)) {
398 				fspare1 = Float.parseFloat((String) pngMetadata.tEXt_text.get(index));
399 			} else if (KEY_FSPARE2.equals(key)) {
400 				fspare2 = Float.parseFloat((String) pngMetadata.tEXt_text.get(index));
401 			} else if (KEY_FSPARE3.equals(key)) {
402 				fspare3 = Float.parseFloat((String) pngMetadata.tEXt_text.get(index));
403 			} else if (key.startsWith(KEY_OPT_TEXT)) {
404 				if (optVars.containsKey(key))
405 					throw new IllegalArgumentException("Duplicate keyword: " + key);
406 				optVars.put(key, (String) pngMetadata.tEXt_text.get(index));
407 			}
408 		}
409 
410 		// initialize V3 optional binary data
411 		size = pngMetadata.unknownChunkType.size();
412 		for (int index = 0; index < size; index++) {
413 			String type = (String) pngMetadata.unknownChunkType.get(index);
414 			if (KEY_VTOB.equals(type))
415 				vtOb = (byte[]) pngMetadata.unknownChunkData.get(index);
416 		}
417 
418 		if (!isArchival())
419 			setDefault(pngMetadata);
420 		validateArchivalMetadata(pngMetadata);
421 	}
422 
423 	/**
424 	 * Initializes metadata from TINE Image
425 	 * 
426 	 * @param timage
427 	 *            TINE Image
428 	 * @exception NullPointerException
429 	 *                If <code>timage</code> is <code>null</code>.
430 	 */
431 	private void read(IMAGE timage) throws IllegalArgumentException {
432 		if (timage == null)
433 			throw new NullPointerException("timage == null!");
434 
435 		// ----------------------- Source Header -----------------------
436 		SourceHeader srcHeader = timage.getSourceHeader();
437 		cameraPortId = srcHeader.cameraPortId;
438 		cameraPortName = srcHeader.cameraPortName;
439 		versionTag = srcHeader.versionTag;
440 		timestampSeconds = srcHeader.timestampSeconds;
441 		timestampMicroseconds = srcHeader.timestampMicroseconds;
442 
443 		// ------------------------ Frame Header ------------------------
444 		FrameHeader frameHeader = timage.getFrameHeader();
445 		sourceWidth = frameHeader.sourceWidth;
446 		sourceHeight = frameHeader.sourceHeight;
447 		aoiWidth = frameHeader.aoiWidth;
448 		aoiHeight = frameHeader.aoiHeight;
449 		xStart = frameHeader.xStart;
450 		yStart = frameHeader.yStart;
451 		bytesPerPixel = frameHeader.bytesPerPixel;
452 		effectiveBitsPerPixel = frameHeader.effectiveBitsPerPixel;
453 		horizontalBinning = frameHeader.horizontalBinning;
454 		verticalBinning = frameHeader.verticalBinning;
455 		sourceFormat = ImageFormat.valueOf(frameHeader.sourceFormat);
456 		imageFormat = ImageFormat.valueOf(frameHeader.imageFormat);
457 		frameNumber = frameHeader.frameNumber;
458 		eventNumber = frameHeader.eventNumber;
459 		xScale = frameHeader.xScale;
460 		yScale = frameHeader.yScale;
461 		imageRotation = frameHeader.imageRotation;
462 		imageFlags = frameHeader.imageFlags;
463 		appendedFrameSize = frameHeader.appendedFrameSize;
464 		ispare1 = frameHeader.ispare1;
465 		ispare2 = frameHeader.ispare2;
466 		ispare3 = frameHeader.ispare3;
467 		fspare1 = frameHeader.fspare1;
468 		fspare2 = frameHeader.fspare2;
469 		fspare3 = frameHeader.fspare3;
470 
471 		md5hash = md5hash(timage.getImageFrameBuffer());
472 	}
473 
474 	private Object readProperty(BufferedImage bi, String name, Class<?> clazz, Object deflt, boolean reject)
475 			throws IllegalArgumentException {
476 		Object obj = bi.getProperty(name);
477 		if (obj == null || obj == java.awt.Image.UndefinedProperty) {
478 			if (reject)
479 				throw new IllegalArgumentException("Undefined archival metadata: " + name);
480 			return deflt;
481 		}
482 		try {
483 			return clazz.cast(obj);
484 		} catch (ClassCastException e) {
485 			throw new IllegalArgumentException("Illegal archival metadata: " + name);
486 		}
487 	}
488 
489 	/**
490 	 * Initializes metadata from Java <code>BufferedImage</code>
491 	 * 
492 	 * @param bi
493 	 *            - Java BufferedImage
494 	 * @param rejectNonArchival
495 	 *            - if <code>true</code> and <code>bi</code> contains invalid
496 	 *            metadata throws <code>IllegalArgumentException</code>
497 	 * @exception NullPointerException
498 	 *                If <code>image</code> is <code>null</code>.
499 	 */
500 	private void read(BufferedImage bi, boolean rejectNonArchival) throws IllegalArgumentException {
501 
502 		if (bi == null)
503 			throw new NullPointerException("image == null!");
504 
505 		frameNumber = (Long) readProperty(bi, KEY_FRAME_NUMBER, Long.class, 0L, rejectNonArchival);
506 
507 		eventNumber = (Long) readProperty(bi, KEY_EVENT_NUMBER, Long.class, 0L, rejectNonArchival);
508 
509 		xStart = (Integer) readProperty(bi, KEY_XSTART, Integer.class, 0, rejectNonArchival);
510 		yStart = (Integer) readProperty(bi, KEY_YSTART, Integer.class, 0, rejectNonArchival);
511 
512 		xScale = (Float) readProperty(bi, KEY_XSCALE, Float.class, -1F, rejectNonArchival);
513 		yScale = (Float) readProperty(bi, KEY_YSCALE, Float.class, -1F, rejectNonArchival);
514 
515 		imageRotation = (Float) readProperty(bi, KEY_IMAGE_ROTATION, Float.class, 0F, rejectNonArchival);
516 
517 		horizontalBinning = (Integer) readProperty(bi, KEY_HORIZONTAL_BINNING, Integer.class, 0, rejectNonArchival);
518 		verticalBinning = (Integer) readProperty(bi, KEY_VERTICAL_BINNING, Integer.class, 0, rejectNonArchival);
519 
520 		imageFlags = (Integer) readProperty(bi, KEY_IMAGE_FLAGS, Integer.class, 0x1, rejectNonArchival);
521 
522 		ispare1 = (Integer) readProperty(bi, KEY_ISPARE1, Integer.class, -1, rejectNonArchival);
523 		ispare2 = (Integer) readProperty(bi, KEY_ISPARE2, Integer.class, -1, rejectNonArchival);
524 		ispare3 = (Integer) readProperty(bi, KEY_ISPARE3, Integer.class, -1, rejectNonArchival);
525 
526 		fspare1 = (Float) readProperty(bi, KEY_FSPARE1, Float.class, -1F, rejectNonArchival);
527 		fspare2 = (Float) readProperty(bi, KEY_FSPARE2, Float.class, -1F, rejectNonArchival);
528 		fspare3 = (Float) readProperty(bi, KEY_FSPARE3, Float.class, -1F, rejectNonArchival);
529 
530 		cameraPortName = (String) readProperty(bi, KEY_CAMERA_PORT_NAME, String.class, IMAGE.DEFAULT_CAMERA_PORT_NAME,
531 				rejectNonArchival);
532 		cameraPortId = (Long) readProperty(bi, KEY_CAMERA_PORT_ID, Long.class, IMAGE.DEFAULT_CAMERA_PORT_ID,
533 				rejectNonArchival);
534 
535 		versionTag = (Long) readProperty(bi, KEY_VERSION_TAG, Long.class, IMAGE.IMAGE_INITIAL_VERSION,
536 				rejectNonArchival);
537 
538 		timestampSeconds = (Integer) readProperty(bi, KEY_TIMESTAMP_SECONDS, Integer.class,
539 				(int) (new GregorianCalendar().getTimeInMillis() / 1000), rejectNonArchival);
540 
541 		timestampMicroseconds = (Integer) readProperty(bi, KEY_TIMESTAMP_MICROSECONDS, Integer.class, 0,
542 				rejectNonArchival);
543 
544 		Integer value = (Integer) readProperty(bi, KEY_IMAGE_FORMAT, Integer.class, null, rejectNonArchival);
545 		imageFormat = (value == null ? getImageFormat(bi) : ImageFormat.valueOf(value));
546 
547 		value = (Integer) readProperty(bi, KEY_SOURCE_FORMAT, Integer.class, null, rejectNonArchival);
548 		sourceFormat = (value == null ? imageFormat : ImageFormat.valueOf(value));
549 
550 		bytesPerPixel = (Integer) readProperty(bi, KEY_BYTES_PER_PIXEL, Integer.class, null, rejectNonArchival);
551 		if (bytesPerPixel == null)
552 			bytesPerPixel = getBytesPerPixel(bi);
553 
554 		effectiveBitsPerPixel = (Integer) readProperty(bi, KEY_EFFECTIVE_BITS_PER_PIXEL, Integer.class, null,
555 				rejectNonArchival);
556 		if (effectiveBitsPerPixel == null)
557 			effectiveBitsPerPixel = getEffBytesPerPixel(bi);
558 
559 		sourceWidth = (Integer) readProperty(bi, KEY_SOURCE_WIDTH, Integer.class, bi.getWidth(), rejectNonArchival);
560 		sourceHeight = (Integer) readProperty(bi, KEY_SOURCE_HEIGHT, Integer.class, bi.getHeight(), rejectNonArchival);
561 
562 		aoiWidth = (Integer) readProperty(bi, KEY_AOI_WIDTH, Integer.class, -1, rejectNonArchival);
563 		if (aoiWidth != -1 && aoiWidth != sourceWidth)
564 			throw new IllegalArgumentException("Invalid archival metadata: aoiWidth == " + aoiWidth);
565 
566 		aoiHeight = (Integer) readProperty(bi, KEY_AOI_HEIGHT, Integer.class, -1, rejectNonArchival);
567 		if (aoiHeight != -1 && aoiHeight != sourceWidth)
568 			throw new IllegalArgumentException("Invalid archival metadata: aoiHeight == " + aoiHeight);
569 
570 		appendedFrameSize = bytesPerPixel * sourceWidth * sourceHeight;
571 	}
572 
573 	private ImageFormat getImageFormat(BufferedImage bi) {
574 		int imageType = bi.getType();
575 		switch (imageType) {
576 		case BufferedImage.TYPE_BYTE_GRAY:
577 		case BufferedImage.TYPE_USHORT_GRAY:
578 			return ImageFormat.IMAGE_FORMAT_GRAY;
579 		default:
580 			return ImageFormat.IMAGE_FORMAT_RGB;
581 		}
582 	}
583 
584 	private int getBytesPerPixel(BufferedImage bi) {
585 		int imageType = bi.getType();
586 		switch (imageType) {
587 		case BufferedImage.TYPE_BYTE_GRAY:
588 			return 1;
589 		case BufferedImage.TYPE_USHORT_GRAY:
590 			return 2;
591 		default:
592 			return 3;
593 		}
594 	}
595 
596 	private int getEffBytesPerPixel(BufferedImage bi) {
597 		int[] cs = bi.getColorModel().getComponentSize();
598 		int effBits = 0;
599 		for (int componentSize : cs)
600 			effBits += componentSize;
601 		return Math.min(effBits, 24);
602 	}
603 
604 	public void write(IMAGE ti) {
605 		if (ti == null)
606 			throw new NullPointerException("ti == null!");
607 
608 		// ********************* Frame Header ********************************
609 		IMAGE.FrameHeader frmHdr = ti.getFrameHeader();
610 		frmHdr.bytesPerPixel = bytesPerPixel;
611 		frmHdr.effectiveBitsPerPixel = effectiveBitsPerPixel;
612 		frmHdr.appendedFrameSize = appendedFrameSize;
613 		frmHdr.imageFormat = imageFormat.getId();
614 		frmHdr.sourceFormat = sourceFormat.getId();
615 		frmHdr.sourceWidth = sourceWidth;
616 		frmHdr.sourceHeight = sourceHeight;
617 		frmHdr.frameNumber = frameNumber;
618 		frmHdr.eventNumber = eventNumber;
619 		frmHdr.aoiHeight = aoiHeight;
620 		frmHdr.aoiWidth = aoiWidth;
621 		frmHdr.xStart = xStart;
622 		frmHdr.yStart = yStart;
623 		frmHdr.xScale = xScale;
624 		frmHdr.yScale = yScale;
625 		frmHdr.ispare1 = ispare1;
626 		frmHdr.ispare2 = ispare2;
627 		frmHdr.ispare3 = ispare3;
628 		frmHdr.fspare1 = fspare1;
629 		frmHdr.fspare2 = fspare2;
630 		frmHdr.fspare3 = fspare3;
631 		frmHdr.horizontalBinning = horizontalBinning;
632 		frmHdr.verticalBinning = verticalBinning;
633 		frmHdr.imageFlags = imageFlags;
634 
635 		// ********************* Source Header ********************************
636 		SourceHeader srcHdr = ti.getSourceHeader();
637 		srcHdr.cameraPortName = cameraPortName;
638 		srcHdr.timestampSeconds = timestampSeconds;
639 		srcHdr.timestampMicroseconds = timestampMicroseconds;
640 		srcHdr.cameraPortId = cameraPortId;
641 		srcHdr.versionTag = versionTag;
642 		srcHdr.totalLength = IMAGE.HEADER_SIZE + frmHdr.appendedFrameSize;
643 	}
644 
645 	static Hashtable<String, Object> createProperties(IMAGE ti) {
646 		Hashtable<String, Object> props = new Hashtable<String, Object>(32);
647 
648 		// ----------------------- Source Header -----------------------
649 		SourceHeader srcHdr = ti.getSourceHeader();
650 		props.put(KEY_CAMERA_PORT_ID, srcHdr.cameraPortId);
651 		props.put(KEY_CAMERA_PORT_NAME, srcHdr.cameraPortName);
652 		props.put(KEY_VERSION_TAG, srcHdr.versionTag);
653 		props.put(KEY_TIMESTAMP_SECONDS, srcHdr.timestampSeconds);
654 		props.put(KEY_TIMESTAMP_MICROSECONDS, srcHdr.timestampMicroseconds);
655 
656 		// ------------------------ Frame Header ------------------------
657 		FrameHeader frmHdr = ti.getFrameHeader();
658 		props.put(KEY_SOURCE_WIDTH, frmHdr.sourceWidth);
659 		props.put(KEY_SOURCE_HEIGHT, frmHdr.sourceHeight);
660 		props.put(KEY_AOI_WIDTH, frmHdr.aoiWidth);
661 		props.put(KEY_AOI_HEIGHT, frmHdr.aoiHeight);
662 		props.put(KEY_XSTART, frmHdr.xStart);
663 		props.put(KEY_YSTART, frmHdr.yStart);
664 		props.put(KEY_BYTES_PER_PIXEL, frmHdr.bytesPerPixel);
665 		props.put(KEY_EFFECTIVE_BITS_PER_PIXEL, frmHdr.effectiveBitsPerPixel);
666 		props.put(KEY_HORIZONTAL_BINNING, frmHdr.horizontalBinning);
667 		props.put(KEY_VERTICAL_BINNING, frmHdr.verticalBinning);
668 		props.put(KEY_SOURCE_FORMAT, frmHdr.sourceFormat);
669 		props.put(KEY_IMAGE_FORMAT, frmHdr.imageFormat);
670 		props.put(KEY_FRAME_NUMBER, frmHdr.frameNumber);
671 		props.put(KEY_EVENT_NUMBER, frmHdr.eventNumber);
672 		props.put(KEY_XSCALE, frmHdr.xScale);
673 		props.put(KEY_YSCALE, frmHdr.yScale);
674 		props.put(KEY_IMAGE_ROTATION, frmHdr.imageRotation);
675 		props.put(KEY_IMAGE_FLAGS, frmHdr.imageFlags);
676 		props.put(KEY_APPENDED_FRAME_SIZE, frmHdr.appendedFrameSize);
677 		props.put(KEY_ISPARE1, frmHdr.ispare1);
678 		props.put(KEY_ISPARE2, frmHdr.ispare2);
679 		props.put(KEY_ISPARE3, frmHdr.ispare3);
680 		props.put(KEY_FSPARE1, frmHdr.fspare1);
681 		props.put(KEY_FSPARE2, frmHdr.fspare2);
682 		props.put(KEY_FSPARE3, frmHdr.fspare3);
683 
684 		return props;
685 	}
686 
687 	/**
688 	 * Converts metadata to a PNG Metadata
689 	 * 
690 	 * @return PNG Metadata
691 	 */
692 	public PNGMetadata toPngMetadata() {
693 		PNGMetadata pngMetadata = new PNGMetadata();
694 		addText(pngMetadata, KEY_SOFTWARE, software);
695 		addText(pngMetadata, KEY_COMMENT, comment);
696 		if (isArchival()) {
697 			addText(pngMetadata, KEY_CAMERA_PORT_ID, cameraPortId.toString());
698 			addText(pngMetadata, KEY_VERSION_TAG, String.format("0x%01x", versionTag));
699 			addText(pngMetadata, KEY_TIMESTAMP_SECONDS, timestampSeconds.toString());
700 			addText(pngMetadata, KEY_TIMESTAMP_MICROSECONDS, timestampMicroseconds.toString());
701 			addText(pngMetadata, KEY_CAMERA_PORT_NAME, cameraPortName);
702 			addText(pngMetadata, KEY_SOURCE_WIDTH, sourceWidth.toString());
703 			addText(pngMetadata, KEY_SOURCE_HEIGHT, sourceHeight.toString());
704 			addText(pngMetadata, KEY_AOI_WIDTH, aoiWidth.toString());
705 			addText(pngMetadata, KEY_AOI_HEIGHT, aoiHeight.toString());
706 			addText(pngMetadata, KEY_XSTART, xStart.toString());
707 			addText(pngMetadata, KEY_YSTART, yStart.toString());
708 			addText(pngMetadata, KEY_BYTES_PER_PIXEL, bytesPerPixel.toString());
709 			addText(pngMetadata, KEY_EFFECTIVE_BITS_PER_PIXEL, effectiveBitsPerPixel.toString());
710 			addText(pngMetadata, KEY_HORIZONTAL_BINNING, horizontalBinning.toString());
711 			addText(pngMetadata, KEY_VERTICAL_BINNING, verticalBinning.toString());
712 			addText(pngMetadata, KEY_SOURCE_FORMAT, sourceFormat.toString());
713 			addText(pngMetadata, KEY_IMAGE_FORMAT, imageFormat.toString());
714 			addText(pngMetadata, KEY_FRAME_NUMBER, frameNumber.toString());
715 			addText(pngMetadata, KEY_EVENT_NUMBER, eventNumber.toString());
716 			addText(pngMetadata, KEY_XSCALE, xScale.toString());
717 			addText(pngMetadata, KEY_YSCALE, yScale.toString());
718 			addText(pngMetadata, KEY_IMAGE_ROTATION, imageRotation.toString());
719 			addText(pngMetadata, KEY_IMAGE_FLAGS, String.format("0x%01x", imageFlags |= ImageFlag.IMAGE_LOSSLESS
720 					.getId()));
721 			addText(pngMetadata, KEY_APPENDED_FRAME_SIZE, appendedFrameSize.toString());
722 			addText(pngMetadata, KEY_ISPARE1, Integer.toString(ispare1));
723 			addText(pngMetadata, KEY_ISPARE2, Integer.toString(ispare2));
724 			addText(pngMetadata, KEY_ISPARE3, Integer.toString(ispare3));
725 			addText(pngMetadata, KEY_FSPARE1, Float.toString(fspare1));
726 			addText(pngMetadata, KEY_FSPARE2, Float.toString(fspare2));
727 			addText(pngMetadata, KEY_FSPARE3, Float.toString(fspare3));
728 
729 			if (imageFormat == ImageFormat.IMAGE_FORMAT_GRAY && effectiveBitsPerPixel < 8 * bytesPerPixel) {
730 				 pngMetadata.sBIT_present = true;
731 				 pngMetadata.sBIT_grayBits = effectiveBitsPerPixel;
732 			}
733 
734 			if (xScale != -1 || yScale != -1) {
735 				pngMetadata.pHYs_present = true;
736 				pngMetadata.pHYs_unitSpecifier = 1;
737 				pngMetadata.pHYs_pixelsPerUnitXAxis = (int) (1000.0F * xScale);
738 				pngMetadata.pHYs_pixelsPerUnitYAxis = (int) (1000.0F * yScale);
739 			}
740 		}
741 
742 		// add time chunk
743 		addTimeChunk(pngMetadata, System.currentTimeMillis());
744 
745 		// add md5hash string
746 		// TODO: only for testing/developing of loading/saving images
747 		// if (!exportMode) // TODO: uncomment later
748 		if (md5hash != null)
749 			addText(pngMetadata, KEY_MD5HASH, md5hash);
750 
751 		// add optional strings
752 		for (String key : optVars.keySet()) {
753 			String value = optVars.get(key);
754 			if (value != null)
755 				addText(pngMetadata, key, value);
756 		}
757 
758 		// add vtOb chunk
759 		if (vtOb != null)
760 			addUserChunk(pngMetadata, KEY_VTOB, vtOb);
761 
762 		return pngMetadata;
763 	}
764 
765 	@SuppressWarnings("unchecked")
766 	private void addText(PNGMetadata pngMetadata, String keyword, String text) {
767 		pngMetadata.tEXt_keyword.add(keyword);
768 		pngMetadata.tEXt_text.add(text);
769 	}
770 
771 	@SuppressWarnings("unchecked")
772 	private void addUserChunk(PNGMetadata pngMetadata, String keyword, byte[] data) {
773 		pngMetadata.unknownChunkType.add(keyword);
774 		pngMetadata.unknownChunkData.add(data);
775 	}
776 
777 	private void addTimeChunk(PNGMetadata pngMetadata, long millis) {
778 		Calendar cal = new GregorianCalendar();
779 		cal.setTimeInMillis(millis);
780 		pngMetadata.tIME_year = cal.get(Calendar.YEAR);
781 		pngMetadata.tIME_month = cal.get(Calendar.MONTH) + 1;
782 		pngMetadata.tIME_day = cal.get(Calendar.DAY_OF_MONTH);
783 		pngMetadata.tIME_hour = cal.get(Calendar.HOUR_OF_DAY);
784 		pngMetadata.tIME_minute = cal.get(Calendar.MINUTE);
785 		pngMetadata.tIME_second = cal.get(Calendar.SECOND);
786 		pngMetadata.tIME_present = true;
787 	}
788 
789 	@Override
790 	public String toString() {
791 
792 		StringBuilder sb = new StringBuilder();
793 
794 		if (software != null)
795 			sb.append(KEY_SOFTWARE).append(" = ").append(software).append("\n");
796 		if (comment != null)
797 			sb.append(KEY_COMMENT).append(" = ").append(comment).append("\n");
798 
799 		if (versionTag != null)
800 			sb.append(KEY_VERSION_TAG).append(" = ").append(String.format("0x%01x", versionTag)).append("\n");
801 		if (cameraPortId != null)
802 			sb.append(KEY_CAMERA_PORT_ID).append(" = ").append(cameraPortId).append("\n");
803 		if (cameraPortName != null)
804 			sb.append(KEY_CAMERA_PORT_NAME).append(" = ").append(cameraPortName).append("\n");
805 
806 		if (timestampSeconds != null)
807 			sb.append(KEY_TIMESTAMP_SECONDS).append(" = ").append(timestampSeconds).append("\n");
808 		if (timestampMicroseconds != null)
809 			sb.append(KEY_TIMESTAMP_MICROSECONDS).append(" = ").append(timestampMicroseconds).append("\n");
810 
811 		if (sourceFormat != null)
812 			sb.append(KEY_SOURCE_FORMAT).append(" = ").append(sourceFormat).append("\n");
813 		if (sourceWidth != null)
814 			sb.append(KEY_SOURCE_WIDTH).append(" = ").append(sourceWidth).append("\n");
815 		if (sourceHeight != null)
816 			sb.append(KEY_SOURCE_HEIGHT).append(" = ").append(sourceHeight).append("\n");
817 
818 		if (aoiWidth != null)
819 			sb.append(KEY_AOI_WIDTH).append(" = ").append(aoiWidth).append("\n");
820 		if (aoiHeight != null)
821 			sb.append(KEY_AOI_HEIGHT).append(" = ").append(aoiHeight).append("\n");
822 
823 		if (xStart != null)
824 			sb.append(KEY_XSTART).append(" = ").append(xStart).append("\n");
825 		if (yStart != null)
826 			sb.append(KEY_YSTART).append(" = ").append(yStart).append("\n");
827 
828 		if (bytesPerPixel != null)
829 			sb.append(KEY_BYTES_PER_PIXEL).append(" = ").append(bytesPerPixel).append("\n");
830 		if (effectiveBitsPerPixel != null)
831 			sb.append(KEY_EFFECTIVE_BITS_PER_PIXEL).append(" = ").append(effectiveBitsPerPixel).append("\n");
832 
833 		if (horizontalBinning != null)
834 			sb.append(KEY_HORIZONTAL_BINNING).append(" = ").append(horizontalBinning).append("\n");
835 		if (verticalBinning != null)
836 			sb.append(KEY_VERTICAL_BINNING).append(" = ").append(verticalBinning).append("\n");
837 
838 		if (imageFormat != null)
839 			sb.append(KEY_IMAGE_FORMAT).append(" = ").append(imageFormat).append("\n");
840 
841 		if (frameNumber != null)
842 			sb.append(KEY_FRAME_NUMBER).append(" = ").append(frameNumber).append("\n");
843 		if (eventNumber != null)
844 			sb.append(KEY_EVENT_NUMBER).append(" = ").append(eventNumber).append("\n");
845 
846 		if (xScale != null)
847 			sb.append(KEY_XSCALE).append(" = ").append(xScale).append("\n");
848 		if (yScale != null)
849 			sb.append(KEY_YSCALE).append(" = ").append(yScale).append("\n");
850 
851 		if (imageRotation != null)
852 			sb.append(KEY_IMAGE_ROTATION).append(" = ").append(imageRotation).append("\n");
853 		if (imageFlags != null)
854 			sb.append(KEY_IMAGE_FLAGS).append(" = ").append(String.format("0x%01x", imageFlags)).append("\n");
855 
856 		if (appendedFrameSize != null)
857 			sb.append(KEY_APPENDED_FRAME_SIZE).append(" = ").append(appendedFrameSize).append("\n");
858 
859 		sb.append(KEY_ISPARE1).append(" = ").append(ispare1).append("\n");
860 		sb.append(KEY_ISPARE2).append(" = ").append(ispare2).append("\n");
861 		sb.append(KEY_ISPARE3).append(" = ").append(ispare3).append("\n");
862 
863 		sb.append(KEY_FSPARE1).append(" = ").append(ispare1).append("\n");
864 		sb.append(KEY_FSPARE2).append(" = ").append(ispare2).append("\n");
865 		sb.append(KEY_FSPARE3).append(" = ").append(ispare3).append("\n");
866 
867 		if (md5hash != null)
868 			sb.append(KEY_MD5HASH).append(" = ").append(TImageUtils.padString(md5hash, 32, '0', true)).append("\n");
869 
870 		if (vtOb != null)
871 			sb.append(KEY_VTOB).append(" = ").append(new String(vtOb)).append("\n");
872 
873 		for (String key : optVars.keySet())
874 			sb.append(key).append(" = ").append(optVars.get(key)).append("\n");
875 
876 		return sb.toString();
877 	}
878 
879 	/**
880 	 * Check whether metadata fulfills "archival" image requirements
881 	 * 
882 	 * @exception IllegalArgumentException
883 	 *                if <code>metadata</code> is <code>non-archival</code>.
884 	 */
885 	private boolean validateArchivalMetadata(PNGMetadata pngMetadata) throws IllegalArgumentException {
886 
887 		int bits = bytesPerPixel * 8;
888 		int effbits = effectiveBitsPerPixel;
889 		if (effbits > bits)
890 			throw new IllegalArgumentException("effectiveBitsPerPixel == " + effbits + ", max: " + bits);
891 
892 		int pngColorType = pngMetadata.IHDR_colorType;
893 		switch (pngColorType) {
894 		case 0: // "Grayscale"
895 			if (imageFormat != ImageFormat.IMAGE_FORMAT_GRAY)
896 				throw new IllegalArgumentException("imageFormat == " + imageFormat + ", expected: "
897 						+ ImageFormat.IMAGE_FORMAT_GRAY);
898 
899 			if (effbits != pngMetadata.IHDR_bitDepth)
900 				throw new IllegalArgumentException("bitDepth == " + effbits + ", expected: "
901 						+ pngMetadata.IHDR_bitDepth);
902 
903 			if (pngMetadata.sBIT_present && effbits != pngMetadata.sBIT_grayBits) //
904 				throw new IllegalArgumentException("effectiveBitsPerPixel == " + effbits + ", expected: "
905 						+ pngMetadata.sBIT_grayBits);
906 
907 			break;
908 
909 		case 2: // "RGB"
910 		case 6: // "RGBA"
911 		case 3: // "Palette"
912 			if (imageFormat != ImageFormat.IMAGE_FORMAT_RGB
913 					&& (imageFormat != ImageFormat.IMAGE_FORMAT_GRAY && bytesPerPixel == 3))
914 				throw new IllegalArgumentException("imageFormat == " + imageFormat + ", expected: "
915 						+ (bytesPerPixel == 3 ? ImageFormat.IMAGE_FORMAT_GRAY : ImageFormat.IMAGE_FORMAT_RGB));
916 
917 			break;
918 
919 		default:
920 			throw new IllegalArgumentException("pngColorType == " + pngColorType);
921 		}
922 
923 		if (aoiWidth <= 0) {
924 			if (aoiHeight > 0)
925 				throw new IllegalArgumentException("aoiWidth <= 0, but aoiHeight = " + aoiHeight);
926 			if (sourceWidth != pngMetadata.IHDR_width)
927 				throw new IllegalArgumentException("sourceWidth == " + sourceWidth + ", excepted value: "
928 						+ pngMetadata.IHDR_width);
929 			if (xStart > 0)
930 				throw new IllegalArgumentException("aoiWidth <= 0, but xStart = " + xStart);
931 		}
932 
933 		if (aoiHeight <= 0) {
934 			if (aoiWidth > 0)
935 				throw new IllegalArgumentException("aoiHeight <= 0, but aoiWidth = " + aoiWidth);
936 			if (sourceHeight != pngMetadata.IHDR_height)
937 				throw new IllegalArgumentException("sourceHeight == " + sourceHeight + ", excepted value: "
938 						+ pngMetadata.IHDR_height);
939 			if (yStart > 0)
940 				throw new IllegalArgumentException("aoiHeight <= 0, but yStart = " + yStart);
941 		}
942 
943 		if (pngMetadata.pHYs_present && pngMetadata.pHYs_unitSpecifier == 1) {
944 			if (xScale != 1000.0F / pngMetadata.pHYs_pixelsPerUnitXAxis)
945 				throw new IllegalArgumentException("xScale == " + xScale + ", expected value: " + 1000.0F
946 						/ pngMetadata.pHYs_pixelsPerUnitXAxis);
947 
948 			if (yScale != 1000.0F / pngMetadata.pHYs_pixelsPerUnitYAxis)
949 				throw new IllegalArgumentException("yScale == " + yScale + ", expected value: " + 1000.0F
950 						/ pngMetadata.pHYs_pixelsPerUnitYAxis);
951 
952 		} else if (xScale != -1F)
953 			throw new IllegalArgumentException("yScale == " + yScale + ", expected value: " + (-1F));
954 
955 		else if (yScale != -1F)
956 			throw new IllegalArgumentException("yScale == " + yScale + ", expected value: " + (-1F));
957 
958 		// ... TODO: continue validation
959 
960 		return true;
961 	}
962 
963 	/**
964 	 * Sets default values to 'archival' metadata.
965 	 */
966 	private void setDefault(PNGMetadata pngMetadata) {
967 		versionTag = IMAGE.IMAGE_VERSION;
968 		cameraPortId = IMAGE.DEFAULT_CAMERA_PORT_ID;
969 		cameraPortName = IMAGE.DEFAULT_CAMERA_PORT_NAME;
970 		aoiWidth = aoiHeight = -1;
971 		xStart = yStart = 0;
972 		horizontalBinning = verticalBinning = 0;
973 		frameNumber = eventNumber = 0L;
974 		imageRotation = 0F;
975 		imageFlags = ImageFlag.LITTLE_ENDIAN_BYTE_ORDER.getId(); // TODO: check
976 		ispare1 = ispare2 = ispare3 = -1;
977 		fspare1 = fspare2 = fspare3 = -1f;
978 
979 		sourceWidth = pngMetadata.IHDR_width;
980 		sourceHeight = pngMetadata.IHDR_height;
981 
982 		Calendar cal = pngMetadata.tIME_present ? new GregorianCalendar(pngMetadata.tIME_year,
983 				pngMetadata.tIME_month - 1, pngMetadata.tIME_day, //
984 				pngMetadata.tIME_hour, pngMetadata.tIME_minute, pngMetadata.tIME_second) : new GregorianCalendar();
985 
986 		timestampSeconds = (int) (cal.getTimeInMillis() / 1000);
987 		timestampMicroseconds = 0;
988 
989 		if (pngMetadata.pHYs_present && pngMetadata.pHYs_unitSpecifier == 1) {
990 			xScale = pngMetadata.pHYs_pixelsPerUnitXAxis / 1000.0F;
991 			yScale = pngMetadata.pHYs_pixelsPerUnitYAxis / 1000.0F;
992 		} else
993 			xScale = yScale = -1F;
994 
995 		int pngColorType = pngMetadata.IHDR_colorType;
996 		switch (pngColorType) {
997 		case 0: // PNG_COLOR_GRAY
998 		case 4: // PNG_COLOR_GRAY_ALPHA
999 			sourceFormat = imageFormat = ImageFormat.IMAGE_FORMAT_GRAY;
1000 			bytesPerPixel = pngMetadata.IHDR_bitDepth <= 8 ? 1 : 2;
1001 			effectiveBitsPerPixel = pngMetadata.IHDR_bitDepth;
1002 			break;
1003 
1004 		case 2: // PNG_COLOR_RGB
1005 		case 6: // PNG_COLOR_RGB_ALPHA
1006 		case 3: // PNG_COLOR_PALETTE
1007 			sourceFormat = imageFormat = ImageFormat.IMAGE_FORMAT_RGB;
1008 			bytesPerPixel = 3;
1009 			effectiveBitsPerPixel = 24;
1010 			break;
1011 
1012 		default:
1013 			throw new IllegalArgumentException("pngColorType == " + pngColorType);
1014 		}
1015 		appendedFrameSize = sourceWidth * sourceHeight * bytesPerPixel;
1016 	}
1017 
1018 	// /**
1019 	// * Converts TINE Image format to PNG image color type
1020 	// *
1021 	// * @param timageFormat
1022 	// * - Tine IMAGE format
1023 	// * @return PNG image color type
1024 	// *
1025 	// * @see FrameHeader#imageFormat
1026 	// * @see TImageMetadata#pngColorType
1027 	// */
1028 	// public int toPngColorType() {
1029 	// if (imageFormat == null)
1030 	// throw new NullPointerException("timageFormat == null!");
1031 	//
1032 	// switch (imageFormat) {
1033 	// case IMAGE_FORMAT_GRAY:
1034 	// case IMAGE_FORMAT_HUFFYUV:
1035 	// return 0; // PNG_COLOR_GRAY
1036 	//
1037 	// case IMAGE_FORMAT_RGB: // RGB24
1038 	// return 2; // PNG_COLOR_RGB
1039 	//
1040 	// case IMAGE_FORMAT_RGBA:
1041 	// return 6; // PNG_COLOR_RGB_ALPHA
1042 	//
1043 	// case IMAGE_FORMAT_JPEG:
1044 	// if (bytesPerPixel == 1)
1045 	// return 0;
1046 	// return 2;
1047 	//
1048 	// default:
1049 	// throw new IllegalArgumentException("Unsupported Tine Image format: " +
1050 	// imageFormat);
1051 	// }
1052 	// }
1053 
1054 	public static String md5hash(byte[] data) {
1055 		MessageDigest m;
1056 		try {
1057 			m = MessageDigest.getInstance("MD5");
1058 
1059 		} catch (NoSuchAlgorithmException e) {
1060 			throw new RuntimeException("Invalid cryptographic algorithm: " + e.getMessage());
1061 		}
1062 
1063 		m.update(data);
1064 		byte[] digest = m.digest();
1065 		BigInteger bigInt = new BigInteger(1, digest);
1066 		return bigInt.toString(16);
1067 	}
1068 
1069 	/**
1070 	 * Return <code>true</code> if image metadata is archival (valid), otherwise
1071 	 * <code>false</code>
1072 	 * 
1073 	 * @return <code>true</code> if image metadata is archival (valid),
1074 	 *         otherwise <code>false</code>
1075 	 */
1076 	public boolean isArchival() {
1077 		return versionTag != null //
1078 				&& cameraPortId != null //
1079 				&& cameraPortName != null //
1080 				&& timestampSeconds != null //
1081 				&& timestampMicroseconds != null //
1082 				&& sourceWidth != null //
1083 				&& sourceHeight != null // 
1084 				&& aoiWidth != null //
1085 				&& aoiHeight != null //
1086 				&& xStart != null //
1087 				&& yStart != null //
1088 				&& bytesPerPixel != null //
1089 				&& effectiveBitsPerPixel != null //
1090 				&& horizontalBinning != null //
1091 				&& verticalBinning != null //
1092 				&& sourceFormat != null //
1093 				&& imageFormat != null //
1094 				&& frameNumber != null //
1095 				&& eventNumber != null //
1096 				&& xScale != null //
1097 				&& yScale != null //
1098 				&& imageRotation != null //
1099 				&& imageFlags != null //
1100 				&& appendedFrameSize != null;
1101 	}
1102 
1103 }