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 * cc.tiffReader is a singleton object, it's a tiff file reader, it can parse byte array to draw into a canvas 32 * @class 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 };