1 /****************************************************************************
  2  Copyright (c) 2011 Gordon P. Hemsley
  3  http://gphemsley.org/
  4 
  5  Copyright (c) 2008-2010 Ricardo Quesada
  6  Copyright (c) 2011-2012 cocos2d-x.org
  7  Copyright (c) 2013-2014 Chukong Technologies Inc.
  8 
  9  http://www.cocos2d-x.org
 10 
 11  Permission is hereby granted, free of charge, to any person obtaining a copy
 12  of this software and associated documentation files (the "Software"), to deal
 13  in the Software without restriction, including without limitation the rights
 14  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 15  copies of the Software, and to permit persons to whom the Software is
 16  furnished to do so, subject to the following conditions:
 17 
 18  The above copyright notice and this permission notice shall be included in
 19  all copies or substantial portions of the Software.
 20 
 21  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 22  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 23  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 24  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 25  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 26  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 27  THE SOFTWARE.
 28  ****************************************************************************/
 29 
 30 /**
 31  * A tiff file reader, it can parse byte array to draw into a canvas
 32  * @namespace
 33  * @name cc.tiffReader
 34  */
 35 cc.tiffReader = /** @lends cc.tiffReader# */{
 36     _littleEndian: false,
 37     _tiffData: null,
 38     _fileDirectories: [],
 39 
 40     getUint8: function (offset) {
 41         return this._tiffData[offset];
 42     },
 43 
 44     getUint16: function (offset) {
 45         if (this._littleEndian)
 46             return (this._tiffData[offset + 1] << 8) | (this._tiffData[offset]);
 47         else
 48             return (this._tiffData[offset] << 8) | (this._tiffData[offset + 1]);
 49     },
 50 
 51     getUint32: function (offset) {
 52         var a = this._tiffData;
 53         if (this._littleEndian)
 54             return (a[offset + 3] << 24) | (a[offset + 2] << 16) | (a[offset + 1] << 8) | (a[offset]);
 55         else
 56             return (a[offset] << 24) | (a[offset + 1] << 16) | (a[offset + 2] << 8) | (a[offset + 3]);
 57     },
 58 
 59     checkLittleEndian: function () {
 60         var BOM = this.getUint16(0);
 61 
 62         if (BOM === 0x4949) {
 63             this.littleEndian = true;
 64         } else if (BOM === 0x4D4D) {
 65             this.littleEndian = false;
 66         } else {
 67             console.log(BOM);
 68             throw TypeError("Invalid byte order value.");
 69         }
 70 
 71         return this.littleEndian;
 72     },
 73 
 74     hasTowel: function () {
 75         // Check for towel.
 76         if (this.getUint16(2) !== 42) {
 77             throw RangeError("You forgot your towel!");
 78             return false;
 79         }
 80 
 81         return true;
 82     },
 83 
 84     getFieldTypeName: function (fieldType) {
 85         var typeNames = this.fieldTypeNames;
 86         if (fieldType in typeNames) {
 87             return typeNames[fieldType];
 88         }
 89         return null;
 90     },
 91 
 92     getFieldTagName: function (fieldTag) {
 93         var tagNames = this.fieldTagNames;
 94 
 95         if (fieldTag in tagNames) {
 96             return tagNames[fieldTag];
 97         } else {
 98             console.log("Unknown Field Tag:", fieldTag);
 99             return "Tag" + fieldTag;
100         }
101     },
102 
103     getFieldTypeLength: function (fieldTypeName) {
104         if (['BYTE', 'ASCII', 'SBYTE', 'UNDEFINED'].indexOf(fieldTypeName) !== -1) {
105             return 1;
106         } else if (['SHORT', 'SSHORT'].indexOf(fieldTypeName) !== -1) {
107             return 2;
108         } else if (['LONG', 'SLONG', 'FLOAT'].indexOf(fieldTypeName) !== -1) {
109             return 4;
110         } else if (['RATIONAL', 'SRATIONAL', 'DOUBLE'].indexOf(fieldTypeName) !== -1) {
111             return 8;
112         }
113         return null;
114     },
115 
116     getFieldValues: function (fieldTagName, fieldTypeName, typeCount, valueOffset) {
117         var fieldValues = [];
118         var fieldTypeLength = this.getFieldTypeLength(fieldTypeName);
119         var fieldValueSize = fieldTypeLength * typeCount;
120 
121         if (fieldValueSize <= 4) {
122             // The value is stored at the big end of the valueOffset.
123             if (this.littleEndian === false)
124                 fieldValues.push(valueOffset >>> ((4 - fieldTypeLength) * 8));
125             else
126                 fieldValues.push(valueOffset);
127         } else {
128             for (var i = 0; i < typeCount; i++) {
129                 var indexOffset = fieldTypeLength * i;
130                 if (fieldTypeLength >= 8) {
131                     if (['RATIONAL', 'SRATIONAL'].indexOf(fieldTypeName) !== -1) {
132                         // Numerator
133                         fieldValues.push(this.getUint32(valueOffset + indexOffset));
134                         // Denominator
135                         fieldValues.push(this.getUint32(valueOffset + indexOffset + 4));
136                     } else {
137                         cc.log("Can't handle this field type or size");
138                     }
139                 } else {
140                     fieldValues.push(this.getBytes(fieldTypeLength, valueOffset + indexOffset));
141                 }
142             }
143         }
144 
145         if (fieldTypeName === 'ASCII') {
146             fieldValues.forEach(function (e, i, a) {
147                 a[i] = String.fromCharCode(e);
148             });
149         }
150         return fieldValues;
151     },
152 
153     getBytes: function (numBytes, offset) {
154         if (numBytes <= 0) {
155             cc.log("No bytes requested");
156         } else if (numBytes <= 1) {
157             return this.getUint8(offset);
158         } else if (numBytes <= 2) {
159             return this.getUint16(offset);
160         } else if (numBytes <= 3) {
161             return this.getUint32(offset) >>> 8;
162         } else if (numBytes <= 4) {
163             return this.getUint32(offset);
164         } else {
165             cc.log("Too many bytes requested");
166         }
167     },
168 
169     getBits: function (numBits, byteOffset, bitOffset) {
170         bitOffset = bitOffset || 0;
171         var extraBytes = Math.floor(bitOffset / 8);
172         var newByteOffset = byteOffset + extraBytes;
173         var totalBits = bitOffset + numBits;
174         var shiftRight = 32 - numBits;
175         var shiftLeft,rawBits;
176 
177         if (totalBits <= 0) {
178             console.log("No bits requested");
179         } else if (totalBits <= 8) {
180             shiftLeft = 24 + bitOffset;
181             rawBits = this.getUint8(newByteOffset);
182         } else if (totalBits <= 16) {
183             shiftLeft = 16 + bitOffset;
184             rawBits = this.getUint16(newByteOffset);
185         } else if (totalBits <= 32) {
186             shiftLeft = bitOffset;
187             rawBits = this.getUint32(newByteOffset);
188         } else {
189             console.log( "Too many bits requested" );
190         }
191 
192         return {
193             'bits': ((rawBits << shiftLeft) >>> shiftRight),
194             'byteOffset': newByteOffset + Math.floor(totalBits / 8),
195             'bitOffset': totalBits % 8
196         };
197     },
198 
199     parseFileDirectory: function (byteOffset) {
200         var numDirEntries = this.getUint16(byteOffset);
201         var tiffFields = [];
202 
203         for (var i = byteOffset + 2, entryCount = 0; entryCount < numDirEntries; i += 12, entryCount++) {
204             var fieldTag = this.getUint16(i);
205             var fieldType = this.getUint16(i + 2);
206             var typeCount = this.getUint32(i + 4);
207             var valueOffset = this.getUint32(i + 8);
208 
209             var fieldTagName = this.getFieldTagName(fieldTag);
210             var fieldTypeName = this.getFieldTypeName(fieldType);
211             var fieldValues = this.getFieldValues(fieldTagName, fieldTypeName, typeCount, valueOffset);
212 
213             tiffFields[fieldTagName] = { type: fieldTypeName, values: fieldValues };
214         }
215 
216         this._fileDirectories.push(tiffFields);
217 
218         var nextIFDByteOffset = this.getUint32(i);
219         if (nextIFDByteOffset !== 0x00000000) {
220             this.parseFileDirectory(nextIFDByteOffset);
221         }
222     },
223 
224     clampColorSample: function(colorSample, bitsPerSample) {
225         var multiplier = Math.pow(2, 8 - bitsPerSample);
226 
227         return Math.floor((colorSample * multiplier) + (multiplier - 1));
228     },
229 
230     /**
231      * @function
232      * @param {Array} tiffData
233      * @param {HTMLCanvasElement} canvas
234      * @returns {*}
235      */
236     parseTIFF: function (tiffData, canvas) {
237         canvas = canvas || cc.newElement('canvas');
238 
239         this._tiffData = tiffData;
240         this.canvas = canvas;
241 
242         this.checkLittleEndian();
243 
244         if (!this.hasTowel()) {
245             return;
246         }
247 
248         var firstIFDByteOffset = this.getUint32(4);
249 
250         this._fileDirectories.length = 0;
251         this.parseFileDirectory(firstIFDByteOffset);
252 
253         var fileDirectory = this._fileDirectories[0];
254 
255         var imageWidth = fileDirectory['ImageWidth'].values[0];
256         var imageLength = fileDirectory['ImageLength'].values[0];
257 
258         this.canvas.width = imageWidth;
259         this.canvas.height = imageLength;
260 
261         var strips = [];
262 
263         var compression = (fileDirectory['Compression']) ? fileDirectory['Compression'].values[0] : 1;
264 
265         var samplesPerPixel = fileDirectory['SamplesPerPixel'].values[0];
266 
267         var sampleProperties = [];
268 
269         var bitsPerPixel = 0;
270         var hasBytesPerPixel = false;
271 
272         fileDirectory['BitsPerSample'].values.forEach(function (bitsPerSample, i, bitsPerSampleValues) {
273             sampleProperties[i] = {
274                 bitsPerSample: bitsPerSample,
275                 hasBytesPerSample: false,
276                 bytesPerSample: undefined
277             };
278 
279             if ((bitsPerSample % 8) === 0) {
280                 sampleProperties[i].hasBytesPerSample = true;
281                 sampleProperties[i].bytesPerSample = bitsPerSample / 8;
282             }
283 
284             bitsPerPixel += bitsPerSample;
285         }, this);
286 
287         if ((bitsPerPixel % 8) === 0) {
288             hasBytesPerPixel = true;
289             var bytesPerPixel = bitsPerPixel / 8;
290         }
291 
292         var stripOffsetValues = fileDirectory['StripOffsets'].values;
293         var numStripOffsetValues = stripOffsetValues.length;
294 
295         // StripByteCounts is supposed to be required, but see if we can recover anyway.
296         if (fileDirectory['StripByteCounts']) {
297             var stripByteCountValues = fileDirectory['StripByteCounts'].values;
298         } else {
299             cc.log("Missing StripByteCounts!");
300 
301             // Infer StripByteCounts, if possible.
302             if (numStripOffsetValues === 1) {
303                 var stripByteCountValues = [Math.ceil((imageWidth * imageLength * bitsPerPixel) / 8)];
304             } else {
305                 throw Error("Cannot recover from missing StripByteCounts");
306             }
307         }
308 
309         // Loop through strips and decompress as necessary.
310         for (var i = 0; i < numStripOffsetValues; i++) {
311             var stripOffset = stripOffsetValues[i];
312             strips[i] = [];
313 
314             var stripByteCount = stripByteCountValues[i];
315 
316             // Loop through pixels.
317             for (var byteOffset = 0, bitOffset = 0, jIncrement = 1, getHeader = true, pixel = [], numBytes = 0, sample = 0, currentSample = 0;
318                  byteOffset < stripByteCount; byteOffset += jIncrement) {
319                 // Decompress strip.
320                 switch (compression) {
321                     // Uncompressed
322                     case 1:
323                         // Loop through samples (sub-pixels).
324                         for (var m = 0, pixel = []; m < samplesPerPixel; m++) {
325                             if (sampleProperties[m].hasBytesPerSample) {
326                                 // XXX: This is wrong!
327                                 var sampleOffset = sampleProperties[m].bytesPerSample * m;
328                                 pixel.push(this.getBytes(sampleProperties[m].bytesPerSample, stripOffset + byteOffset + sampleOffset));
329                             } else {
330                                 var sampleInfo = this.getBits(sampleProperties[m].bitsPerSample, stripOffset + byteOffset, bitOffset);
331                                 pixel.push(sampleInfo.bits);
332                                 byteOffset = sampleInfo.byteOffset - stripOffset;
333                                 bitOffset = sampleInfo.bitOffset;
334 
335                                 throw RangeError("Cannot handle sub-byte bits per sample");
336                             }
337                         }
338 
339                         strips[i].push(pixel);
340 
341                         if (hasBytesPerPixel) {
342                             jIncrement = bytesPerPixel;
343                         } else {
344                             jIncrement = 0;
345                             throw RangeError("Cannot handle sub-byte bits per pixel");
346                         }
347                         break;
348 
349                     // CITT Group 3 1-Dimensional Modified Huffman run-length encoding
350                     case 2:
351                         // XXX: Use PDF.js code?
352                         break;
353 
354                     // Group 3 Fax
355                     case 3:
356                         // XXX: Use PDF.js code?
357                         break;
358 
359                     // Group 4 Fax
360                     case 4:
361                         // XXX: Use PDF.js code?
362                         break;
363 
364                     // LZW
365                     case 5:
366                         // XXX: Use PDF.js code?
367                         break;
368 
369                     // Old-style JPEG (TIFF 6.0)
370                     case 6:
371                         // XXX: Use PDF.js code?
372                         break;
373 
374                     // New-style JPEG (TIFF Specification Supplement 2)
375                     case 7:
376                         // XXX: Use PDF.js code?
377                         break;
378 
379                     // PackBits
380                     case 32773:
381                         // Are we ready for a new block?
382                         if (getHeader) {
383                             getHeader = false;
384 
385                             var blockLength = 1;
386                             var iterations = 1;
387 
388                             // The header byte is signed.
389                             var header = this.getInt8(stripOffset + byteOffset);
390 
391                             if ((header >= 0) && (header <= 127)) { // Normal pixels.
392                                 blockLength = header + 1;
393                             } else if ((header >= -127) && (header <= -1)) { // Collapsed pixels.
394                                 iterations = -header + 1;
395                             } else /*if (header === -128)*/ { // Placeholder byte?
396                                 getHeader = true;
397                             }
398                         } else {
399                             var currentByte = this.getUint8(stripOffset + byteOffset);
400 
401                             // Duplicate bytes, if necessary.
402                             for (var m = 0; m < iterations; m++) {
403                                 if (sampleProperties[sample].hasBytesPerSample) {
404                                     // We're reading one byte at a time, so we need to handle multi-byte samples.
405                                     currentSample = (currentSample << (8 * numBytes)) | currentByte;
406                                     numBytes++;
407 
408                                     // Is our sample complete?
409                                     if (numBytes === sampleProperties[sample].bytesPerSample) {
410                                         pixel.push(currentSample);
411                                         currentSample = numBytes = 0;
412                                         sample++;
413                                     }
414                                 } else {
415                                     throw RangeError("Cannot handle sub-byte bits per sample");
416                                 }
417 
418                                 // Is our pixel complete?
419                                 if (sample === samplesPerPixel) {
420                                     strips[i].push(pixel);
421                                     pixel = [];
422                                     sample = 0;
423                                 }
424                             }
425 
426                             blockLength--;
427 
428                             // Is our block complete?
429                             if (blockLength === 0) {
430                                 getHeader = true;
431                             }
432                         }
433 
434                         jIncrement = 1;
435                         break;
436 
437                     // Unknown compression algorithm
438                     default:
439                         // Do not attempt to parse the image data.
440                         break;
441                 }
442             }
443         }
444 
445         if (canvas.getContext) {
446             var ctx = this.canvas.getContext("2d");
447 
448             // Set a default fill style.
449             ctx.fillStyle = "rgba(255, 255, 255, 0)";
450 
451             // If RowsPerStrip is missing, the whole image is in one strip.
452             var rowsPerStrip = fileDirectory['RowsPerStrip'] ? fileDirectory['RowsPerStrip'].values[0] : imageLength;
453 
454             var numStrips = strips.length;
455 
456             var imageLengthModRowsPerStrip = imageLength % rowsPerStrip;
457             var rowsInLastStrip = (imageLengthModRowsPerStrip === 0) ? rowsPerStrip : imageLengthModRowsPerStrip;
458 
459             var numRowsInStrip = rowsPerStrip;
460             var numRowsInPreviousStrip = 0;
461 
462             var photometricInterpretation = fileDirectory['PhotometricInterpretation'].values[0];
463 
464             var extraSamplesValues = [];
465             var numExtraSamples = 0;
466 
467             if (fileDirectory['ExtraSamples']) {
468                 extraSamplesValues = fileDirectory['ExtraSamples'].values;
469                 numExtraSamples = extraSamplesValues.length;
470             }
471 
472             if (fileDirectory['ColorMap']) {
473                 var colorMapValues = fileDirectory['ColorMap'].values;
474                 var colorMapSampleSize = Math.pow(2, sampleProperties[0].bitsPerSample);
475             }
476 
477             // Loop through the strips in the image.
478             for (var i = 0; i < numStrips; i++) {
479                 // The last strip may be short.
480                 if ((i + 1) === numStrips) {
481                     numRowsInStrip = rowsInLastStrip;
482                 }
483 
484                 var numPixels = strips[i].length;
485                 var yPadding = numRowsInPreviousStrip * i;
486 
487                 // Loop through the rows in the strip.
488                 for (var y = 0, j = 0; y < numRowsInStrip, j < numPixels; y++) {
489                     // Loop through the pixels in the row.
490                     for (var x = 0; x < imageWidth; x++, j++) {
491                         var pixelSamples = strips[i][j];
492 
493                         var red = 0;
494                         var green = 0;
495                         var blue = 0;
496                         var opacity = 1.0;
497 
498                         if (numExtraSamples > 0) {
499                             for (var k = 0; k < numExtraSamples; k++) {
500                                 if (extraSamplesValues[k] === 1 || extraSamplesValues[k] === 2) {
501                                     // Clamp opacity to the range [0,1].
502                                     opacity = pixelSamples[3 + k] / 256;
503 
504                                     break;
505                                 }
506                             }
507                         }
508 
509                         switch (photometricInterpretation) {
510                             // Bilevel or Grayscale
511                             // WhiteIsZero
512                             case 0:
513                                 if (sampleProperties[0].hasBytesPerSample) {
514                                     var invertValue = Math.pow(0x10, sampleProperties[0].bytesPerSample * 2);
515                                 }
516 
517                                 // Invert samples.
518                                 pixelSamples.forEach(function (sample, index, samples) {
519                                     samples[index] = invertValue - sample;
520                                 });
521 
522                             // Bilevel or Grayscale
523                             // BlackIsZero
524                             case 1:
525                                 red = green = blue = this.clampColorSample(pixelSamples[0], sampleProperties[0].bitsPerSample);
526                                 break;
527 
528                             // RGB Full Color
529                             case 2:
530                                 red = this.clampColorSample(pixelSamples[0], sampleProperties[0].bitsPerSample);
531                                 green = this.clampColorSample(pixelSamples[1], sampleProperties[1].bitsPerSample);
532                                 blue = this.clampColorSample(pixelSamples[2], sampleProperties[2].bitsPerSample);
533                                 break;
534 
535                             // RGB Color Palette
536                             case 3:
537                                 if (colorMapValues === undefined) {
538                                     throw Error("Palette image missing color map");
539                                 }
540 
541                                 var colorMapIndex = pixelSamples[0];
542 
543                                 red = this.clampColorSample(colorMapValues[colorMapIndex], 16);
544                                 green = this.clampColorSample(colorMapValues[colorMapSampleSize + colorMapIndex], 16);
545                                 blue = this.clampColorSample(colorMapValues[(2 * colorMapSampleSize) + colorMapIndex], 16);
546                                 break;
547 
548                             // Unknown Photometric Interpretation
549                             default:
550                                 throw RangeError('Unknown Photometric Interpretation:', photometricInterpretation);
551                                 break;
552                         }
553 
554                         ctx.fillStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + opacity + ")";
555                         ctx.fillRect(x, yPadding + y, 1, 1);
556                     }
557                 }
558 
559                 numRowsInPreviousStrip = numRowsInStrip;
560             }
561         }
562 
563         return this.canvas;
564     },
565 
566     // See: http://www.digitizationguidelines.gov/guidelines/TIFF_Metadata_Final.pdf
567     // See: http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
568     fieldTagNames: {
569         // TIFF Baseline
570         0x013B: 'Artist',
571         0x0102: 'BitsPerSample',
572         0x0109: 'CellLength',
573         0x0108: 'CellWidth',
574         0x0140: 'ColorMap',
575         0x0103: 'Compression',
576         0x8298: 'Copyright',
577         0x0132: 'DateTime',
578         0x0152: 'ExtraSamples',
579         0x010A: 'FillOrder',
580         0x0121: 'FreeByteCounts',
581         0x0120: 'FreeOffsets',
582         0x0123: 'GrayResponseCurve',
583         0x0122: 'GrayResponseUnit',
584         0x013C: 'HostComputer',
585         0x010E: 'ImageDescription',
586         0x0101: 'ImageLength',
587         0x0100: 'ImageWidth',
588         0x010F: 'Make',
589         0x0119: 'MaxSampleValue',
590         0x0118: 'MinSampleValue',
591         0x0110: 'Model',
592         0x00FE: 'NewSubfileType',
593         0x0112: 'Orientation',
594         0x0106: 'PhotometricInterpretation',
595         0x011C: 'PlanarConfiguration',
596         0x0128: 'ResolutionUnit',
597         0x0116: 'RowsPerStrip',
598         0x0115: 'SamplesPerPixel',
599         0x0131: 'Software',
600         0x0117: 'StripByteCounts',
601         0x0111: 'StripOffsets',
602         0x00FF: 'SubfileType',
603         0x0107: 'Threshholding',
604         0x011A: 'XResolution',
605         0x011B: 'YResolution',
606 
607         // TIFF Extended
608         0x0146: 'BadFaxLines',
609         0x0147: 'CleanFaxData',
610         0x0157: 'ClipPath',
611         0x0148: 'ConsecutiveBadFaxLines',
612         0x01B1: 'Decode',
613         0x01B2: 'DefaultImageColor',
614         0x010D: 'DocumentName',
615         0x0150: 'DotRange',
616         0x0141: 'HalftoneHints',
617         0x015A: 'Indexed',
618         0x015B: 'JPEGTables',
619         0x011D: 'PageName',
620         0x0129: 'PageNumber',
621         0x013D: 'Predictor',
622         0x013F: 'PrimaryChromaticities',
623         0x0214: 'ReferenceBlackWhite',
624         0x0153: 'SampleFormat',
625         0x022F: 'StripRowCounts',
626         0x014A: 'SubIFDs',
627         0x0124: 'T4Options',
628         0x0125: 'T6Options',
629         0x0145: 'TileByteCounts',
630         0x0143: 'TileLength',
631         0x0144: 'TileOffsets',
632         0x0142: 'TileWidth',
633         0x012D: 'TransferFunction',
634         0x013E: 'WhitePoint',
635         0x0158: 'XClipPathUnits',
636         0x011E: 'XPosition',
637         0x0211: 'YCbCrCoefficients',
638         0x0213: 'YCbCrPositioning',
639         0x0212: 'YCbCrSubSampling',
640         0x0159: 'YClipPathUnits',
641         0x011F: 'YPosition',
642 
643         // EXIF
644         0x9202: 'ApertureValue',
645         0xA001: 'ColorSpace',
646         0x9004: 'DateTimeDigitized',
647         0x9003: 'DateTimeOriginal',
648         0x8769: 'Exif IFD',
649         0x9000: 'ExifVersion',
650         0x829A: 'ExposureTime',
651         0xA300: 'FileSource',
652         0x9209: 'Flash',
653         0xA000: 'FlashpixVersion',
654         0x829D: 'FNumber',
655         0xA420: 'ImageUniqueID',
656         0x9208: 'LightSource',
657         0x927C: 'MakerNote',
658         0x9201: 'ShutterSpeedValue',
659         0x9286: 'UserComment',
660 
661         // IPTC
662         0x83BB: 'IPTC',
663 
664         // ICC
665         0x8773: 'ICC Profile',
666 
667         // XMP
668         0x02BC: 'XMP',
669 
670         // GDAL
671         0xA480: 'GDAL_METADATA',
672         0xA481: 'GDAL_NODATA',
673 
674         // Photoshop
675         0x8649: 'Photoshop'
676     },
677 
678     fieldTypeNames: {
679         0x0001: 'BYTE',
680         0x0002: 'ASCII',
681         0x0003: 'SHORT',
682         0x0004: 'LONG',
683         0x0005: 'RATIONAL',
684         0x0006: 'SBYTE',
685         0x0007: 'UNDEFINED',
686         0x0008: 'SSHORT',
687         0x0009: 'SLONG',
688         0x000A: 'SRATIONAL',
689         0x000B: 'FLOAT',
690         0x000C: 'DOUBLE'
691     }
692 };