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