From 75a70a56f7f6a260521cecc66d542412de231adc Mon Sep 17 00:00:00 2001 From: James Chen Date: Thu, 23 Jan 2025 10:42:32 +0800 Subject: [PATCH] Fix compiler errors in particle-2d module and optimize TIFFReader usage. (#18230) * Fix compiler errors in particle-2d module * Better wrapParseInt/wrapParseFloat implementation * Update * Add type info for png-reader & tiff-reader and mangle them since they're internal modules. * Update type in png-reader.ts * tiffReader should be a singleton to avoid allocate TiffReader per ParticleSystem2D, and reset the tiff reader after decoding to avoid memory cached in it. --- cocos/particle-2d/particle-system-2d.ts | 116 ++++----- cocos/particle-2d/png-reader.ts | 166 +++++++++---- cocos/particle-2d/tiff-reader.ts | 316 +++++++++++++----------- 3 files changed, 344 insertions(+), 254 deletions(-) diff --git a/cocos/particle-2d/particle-system-2d.ts b/cocos/particle-2d/particle-system-2d.ts index 6562f9eb141..f3c95be7c08 100644 --- a/cocos/particle-2d/particle-system-2d.ts +++ b/cocos/particle-2d/particle-system-2d.ts @@ -36,7 +36,7 @@ import { ImageAsset } from '../asset/assets/image-asset'; import { ParticleAsset } from './particle-asset'; import { BlendFactor } from '../gfx'; import { PNGReader } from './png-reader'; -import { TiffReader } from './tiff-reader'; +import { tiffReader } from './tiff-reader'; import codec from '../../external/compression/ZipUtils'; import { IBatcher } from '../2d/renderer/i-batcher'; import { assetManager, builtinResMgr } from '../asset/asset-manager'; @@ -138,6 +138,9 @@ function getParticleComponents (node): ParticleSystem2D[] { return getParticleComponents(parent); } +const wrapParseInt: (str: string | number) => number = parseInt as any; +const wrapParseFloat: (str: string | number) => number = parseFloat as any; + /** * @en Particle System base class. * cocos2d also supports particles generated by Particle Designer (http://particledesigner.71squared.com/). @@ -743,7 +746,6 @@ export class ParticleSystem2D extends UIRenderer { private declare _previewTimer: number | null; private declare _focused: boolean; private declare _plistFile: string; - private declare _tiffReader; private _useFile: boolean; constructor () { @@ -917,7 +919,7 @@ export class ParticleSystem2D extends UIRenderer { if (!this._custom) { const isDiffFrame = this._spriteFrame !== file.spriteFrame; if (isDiffFrame) this.spriteFrame = file.spriteFrame; - this._initWithDictionary(file._nativeAsset); + this._initWithDictionary(file._nativeAsset as Record); } if (!this._spriteFrame) { @@ -937,7 +939,7 @@ export class ParticleSystem2D extends UIRenderer { */ public _initTextureWithDictionary (dict: any): boolean { if (dict.spriteFrameUuid) { - const spriteFrameUuid = dict.spriteFrameUuid; + const spriteFrameUuid: string = dict.spriteFrameUuid; assetManager.loadAny(spriteFrameUuid, (err: Error, spriteFrame: SpriteFrame): void => { if (err) { dict.spriteFrameUuid = undefined; @@ -949,7 +951,7 @@ export class ParticleSystem2D extends UIRenderer { }); } else { // texture - const imgPath = path.changeBasename(this._plistFile, dict.textureFileName || ''); + const imgPath = path.changeBasename(this._plistFile, dict.textureFileName as string || ''); if (dict.textureFileName) { // Try to get the texture from the cache assetManager.loadRemote(imgPath, (err: Error | null, imageAsset: ImageAsset): void => { @@ -994,10 +996,8 @@ export class ParticleSystem2D extends UIRenderer { const myPngObj = new PNGReader(buffer); myPngObj.render(canvasObj); } else { - if (!this._tiffReader) { - this._tiffReader = new TiffReader(); - } - this._tiffReader.parseTIFF(buffer, canvasObj); + tiffReader.parseTIFF(buffer, canvasObj); + tiffReader.reset(); // Reset the tiff reader to avoid memory cached in it. } imageAsset = new ImageAsset(canvasObj); assetManager.assets.add(imgPathName, imageAsset); @@ -1023,13 +1023,13 @@ export class ParticleSystem2D extends UIRenderer { /** * @deprecated since v3.5.0, this is an engine private interface that will be removed in the future. */ - public _initWithDictionary (dict: any): boolean { + public _initWithDictionary (dict: Record): boolean { this._useFile = true; - this.totalParticles = parseInt(dict.maxParticles || 0); + this.totalParticles = wrapParseInt(dict.maxParticles || 0); // life span - this.life = parseFloat(dict.particleLifespan || 0); - this.lifeVar = parseFloat(dict.particleLifespanVariance || 0); + this.life = wrapParseFloat(dict.particleLifespan || 0); + this.lifeVar = wrapParseFloat(dict.particleLifespanVariance || 0); // emission Rate const _tempEmissionRate = dict.emissionRate; @@ -1040,76 +1040,76 @@ export class ParticleSystem2D extends UIRenderer { } // duration - this.duration = parseFloat(dict.duration || 0); + this.duration = wrapParseFloat(dict.duration || 0); // blend function // remove when component remove blend function - this._srcBlendFactor = parseInt(dict.blendFuncSource || BlendFactor.SRC_ALPHA); - this._dstBlendFactor = parseInt(dict.blendFuncDestination || BlendFactor.ONE_MINUS_SRC_ALPHA); + this._srcBlendFactor = wrapParseInt(dict.blendFuncSource || BlendFactor.SRC_ALPHA); + this._dstBlendFactor = wrapParseInt(dict.blendFuncDestination || BlendFactor.ONE_MINUS_SRC_ALPHA); // color const locStartColor = this._startColor; - locStartColor.r = parseFloat(dict.startColorRed || 0) * 255; - locStartColor.g = parseFloat(dict.startColorGreen || 0) * 255; - locStartColor.b = parseFloat(dict.startColorBlue || 0) * 255; - locStartColor.a = parseFloat(dict.startColorAlpha || 0) * 255; + locStartColor.r = wrapParseFloat(dict.startColorRed || 0) * 255; + locStartColor.g = wrapParseFloat(dict.startColorGreen || 0) * 255; + locStartColor.b = wrapParseFloat(dict.startColorBlue || 0) * 255; + locStartColor.a = wrapParseFloat(dict.startColorAlpha || 0) * 255; const locStartColorVar = this._startColorVar; - locStartColorVar.r = parseFloat(dict.startColorVarianceRed || 0) * 255; - locStartColorVar.g = parseFloat(dict.startColorVarianceGreen || 0) * 255; - locStartColorVar.b = parseFloat(dict.startColorVarianceBlue || 0) * 255; - locStartColorVar.a = parseFloat(dict.startColorVarianceAlpha || 0) * 255; + locStartColorVar.r = wrapParseFloat(dict.startColorVarianceRed || 0) * 255; + locStartColorVar.g = wrapParseFloat(dict.startColorVarianceGreen || 0) * 255; + locStartColorVar.b = wrapParseFloat(dict.startColorVarianceBlue || 0) * 255; + locStartColorVar.a = wrapParseFloat(dict.startColorVarianceAlpha || 0) * 255; const locEndColor = this._endColor; - locEndColor.r = parseFloat(dict.finishColorRed || 0) * 255; - locEndColor.g = parseFloat(dict.finishColorGreen || 0) * 255; - locEndColor.b = parseFloat(dict.finishColorBlue || 0) * 255; - locEndColor.a = parseFloat(dict.finishColorAlpha || 0) * 255; + locEndColor.r = wrapParseFloat(dict.finishColorRed || 0) * 255; + locEndColor.g = wrapParseFloat(dict.finishColorGreen || 0) * 255; + locEndColor.b = wrapParseFloat(dict.finishColorBlue || 0) * 255; + locEndColor.a = wrapParseFloat(dict.finishColorAlpha || 0) * 255; const locEndColorVar = this._endColorVar; - locEndColorVar.r = parseFloat(dict.finishColorVarianceRed || 0) * 255; - locEndColorVar.g = parseFloat(dict.finishColorVarianceGreen || 0) * 255; - locEndColorVar.b = parseFloat(dict.finishColorVarianceBlue || 0) * 255; - locEndColorVar.a = parseFloat(dict.finishColorVarianceAlpha || 0) * 255; + locEndColorVar.r = wrapParseFloat(dict.finishColorVarianceRed || 0) * 255; + locEndColorVar.g = wrapParseFloat(dict.finishColorVarianceGreen || 0) * 255; + locEndColorVar.b = wrapParseFloat(dict.finishColorVarianceBlue || 0) * 255; + locEndColorVar.a = wrapParseFloat(dict.finishColorVarianceAlpha || 0) * 255; // particle size - this.startSize = parseFloat(dict.startParticleSize || 0); - this.startSizeVar = parseFloat(dict.startParticleSizeVariance || 0); - this.endSize = parseFloat(dict.finishParticleSize || 0); - this.endSizeVar = parseFloat(dict.finishParticleSizeVariance || 0); + this.startSize = wrapParseFloat(dict.startParticleSize || 0); + this.startSizeVar = wrapParseFloat(dict.startParticleSizeVariance || 0); + this.endSize = wrapParseFloat(dict.finishParticleSize || 0); + this.endSizeVar = wrapParseFloat(dict.finishParticleSizeVariance || 0); // position // Make empty positionType value and old version compatible - this.positionType = parseFloat(dict.positionType !== undefined ? dict.positionType : PositionType.FREE); + this.positionType = wrapParseFloat(dict.positionType !== undefined ? dict.positionType : PositionType.FREE); // for this.sourcePos.set(0, 0); - this.posVar.set(parseFloat(dict.sourcePositionVariancex || 0), parseFloat(dict.sourcePositionVariancey || 0)); + this.posVar.set(wrapParseFloat(dict.sourcePositionVariancex || 0), wrapParseFloat(dict.sourcePositionVariancey || 0)); // angle - this.angle = parseFloat(dict.angle || 0); - this.angleVar = parseFloat(dict.angleVariance || 0); + this.angle = wrapParseFloat(dict.angle || 0); + this.angleVar = wrapParseFloat(dict.angleVariance || 0); // Spinning - this.startSpin = parseFloat(dict.rotationStart || 0); - this.startSpinVar = parseFloat(dict.rotationStartVariance || 0); - this.endSpin = parseFloat(dict.rotationEnd || 0); - this.endSpinVar = parseFloat(dict.rotationEndVariance || 0); + this.startSpin = wrapParseFloat(dict.rotationStart || 0); + this.startSpinVar = wrapParseFloat(dict.rotationStartVariance || 0); + this.endSpin = wrapParseFloat(dict.rotationEnd || 0); + this.endSpinVar = wrapParseFloat(dict.rotationEndVariance || 0); - this.emitterMode = parseInt(dict.emitterType || EmitterMode.GRAVITY); + this.emitterMode = wrapParseInt(dict.emitterType || EmitterMode.GRAVITY); // Mode A: Gravity + tangential accel + radial accel if (this.emitterMode === EmitterMode.GRAVITY) { // gravity - this.gravity.set(parseFloat(dict.gravityx || 0), parseFloat(dict.gravityy || 0)); + this.gravity.set(wrapParseFloat(dict.gravityx || 0), wrapParseFloat(dict.gravityy || 0)); // speed - this.speed = parseFloat(dict.speed || 0); - this.speedVar = parseFloat(dict.speedVariance || 0); + this.speed = wrapParseFloat(dict.speed || 0); + this.speedVar = wrapParseFloat(dict.speedVariance || 0); // radial acceleration - this.radialAccel = parseFloat(dict.radialAcceleration || 0); - this.radialAccelVar = parseFloat(dict.radialAccelVariance || 0); + this.radialAccel = wrapParseFloat(dict.radialAcceleration || 0); + this.radialAccelVar = wrapParseFloat(dict.radialAccelVariance || 0); // tangential acceleration - this.tangentialAccel = parseFloat(dict.tangentialAcceleration || 0); - this.tangentialAccelVar = parseFloat(dict.tangentialAccelVariance || 0); + this.tangentialAccel = wrapParseFloat(dict.tangentialAcceleration || 0); + this.tangentialAccelVar = wrapParseFloat(dict.tangentialAccelVariance || 0); // rotation is dir let locRotationIsDir = dict.rotationIsDir || ''; @@ -1121,12 +1121,12 @@ export class ParticleSystem2D extends UIRenderer { } } else if (this.emitterMode === EmitterMode.RADIUS) { // or Mode B: radius movement - this.startRadius = parseFloat(dict.maxRadius || 0); - this.startRadiusVar = parseFloat(dict.maxRadiusVariance || 0); - this.endRadius = parseFloat(dict.minRadius || 0); - this.endRadiusVar = parseFloat(dict.minRadiusVariance || 0); - this.rotatePerS = parseFloat(dict.rotatePerSecond || 0); - this.rotatePerSVar = parseFloat(dict.rotatePerSecondVariance || 0); + this.startRadius = wrapParseFloat(dict.maxRadius || 0); + this.startRadiusVar = wrapParseFloat(dict.maxRadiusVariance || 0); + this.endRadius = wrapParseFloat(dict.minRadius || 0); + this.endRadiusVar = wrapParseFloat(dict.minRadiusVariance || 0); + this.rotatePerS = wrapParseFloat(dict.rotatePerSecond || 0); + this.rotatePerSVar = wrapParseFloat(dict.rotatePerSecondVariance || 0); } else { warnID(6009); return false; diff --git a/cocos/particle-2d/png-reader.ts b/cocos/particle-2d/png-reader.ts index 3f12e22b515..0bc0375bf26 100644 --- a/cocos/particle-2d/png-reader.ts +++ b/cocos/particle-2d/png-reader.ts @@ -29,17 +29,43 @@ import { getError } from '../core'; import zlib from '../../external/compression/zlib.min'; +interface PNGAnimationFrame { + width: number; + height: number; + xOffset: number; + yOffset: number; + delay: number; + disposeOp: number; + blendOp: number; + data: number[]; +} + +interface PNGTransparency { + indexed?: number[]; + rgb?: number[]; + grayscale?: number; +} + /** * A png file reader * @name PNGReader + * @mangle */ export class PNGReader { - private declare data; + private declare data: Uint8Array | number[]; private pos = 8; - private palette: any[] = []; + private palette: ArrayLike = []; private imgData: Uint8Array | number[] = []; - private declare transparency: any; - private declare animation: any; + private transparency: PNGTransparency = { + indexed: [], + rgb: [], + grayscale: 0, + }; + private declare animation: { + numFrames: number; + numPlays: number; + frames: PNGAnimationFrame[], + }; private text = {}; private width = 0; private height = 0; @@ -48,42 +74,40 @@ export class PNGReader { private compressionMethod = 0; private filterMethod = 0; private interlaceMethod = 0; - private colors: any = 0; + private colors: number | undefined = 0; private hasAlphaChannel = false; private pixelBitlength = 0; - private declare colorSpace: any; - private declare _decodedPalette: Uint8Array; + private declare colorSpace: string | undefined; + private _decodedPalette: Uint8Array | null = null; - constructor (data) { - this.data = data; - this.transparency = { - indexed: [], - rgb: 0, - grayscale: 0, - }; + constructor (data: number[]) { + const thisData = this.data = data; - let frame: any; - let i = 0; let _i = 0; let _j = 0; let chunkSize = 0; + let frame: PNGAnimationFrame | undefined; + let chunkSize = 0; while (true) { chunkSize = this.readUInt32(); - const section = (((): any[] => { - const _results: any[] = []; - for (i = _i = 0; _i < 4; i = ++_i) { - _results.push(String.fromCharCode(this.data[this.pos++])); + const section: string = (((): string[] => { + const results: string[] = []; + for (let _i = 0; _i < 4; ++_i) { + results.push(String.fromCharCode(thisData[this.pos++])); } - return _results; + return results; }).call(this)).join(''); + switch (section) { case 'IHDR': + { this.width = this.readUInt32(); this.height = this.readUInt32(); - this.bits = this.data[this.pos++]; - this.colorType = this.data[this.pos++]; - this.compressionMethod = this.data[this.pos++]; - this.filterMethod = this.data[this.pos++]; - this.interlaceMethod = this.data[this.pos++]; + this.bits = thisData[this.pos++]; + this.colorType = thisData[this.pos++]; + this.compressionMethod = thisData[this.pos++]; + this.filterMethod = thisData[this.pos++]; + this.interlaceMethod = thisData[this.pos++]; break; + } case 'acTL': this.animation = { numFrames: this.readUInt32(), @@ -95,6 +119,7 @@ export class PNGReader { this.palette = this.read(chunkSize); break; case 'fcTL': + { if (frame) { this.animation.frames.push(frame); } @@ -104,51 +129,66 @@ export class PNGReader { height: this.readUInt32(), xOffset: this.readUInt32(), yOffset: this.readUInt32(), + delay: 0, + disposeOp: 0, + blendOp: 0, + data: [], }; const delayNum = this.readUInt16(); const delayDen = this.readUInt16() || 100; frame.delay = 1000 * delayNum / delayDen; - frame.disposeOp = this.data[this.pos++]; - frame.blendOp = this.data[this.pos++]; - frame.data = []; + frame.disposeOp = thisData[this.pos++]; + frame.blendOp = thisData[this.pos++]; break; + } case 'IDAT': case 'fdAT': + { if (section === 'fdAT') { this.pos += 4; chunkSize -= 4; } - data = (frame != null ? frame.data : void 0) || this.imgData; - for (i = _i = 0; chunkSize >= 0 ? _i < chunkSize : _i > chunkSize; i = chunkSize >= 0 ? ++_i : --_i) { - data.push(this.data[this.pos++]); + // FIXME(cjh): Remove 'as number[]' since this.imgData is possible to be Uint8Array + data = (frame != null ? frame.data : undefined) || this.imgData as number[]; + for (let _i = 0; chunkSize >= 0 ? _i < chunkSize : _i > chunkSize; chunkSize >= 0 ? ++_i : --_i) { + data.push(thisData[this.pos++]); } break; + } case 'tRNS': this.transparency = {}; switch (this.colorType) { case 3: + { this.transparency.indexed = this.read(chunkSize); const ccshort = 255 - this.transparency.indexed.length; if (ccshort > 0) { - for (i = _j = 0; ccshort >= 0 ? _j < ccshort : _j > ccshort; i = ccshort >= 0 ? ++_j : --_j) { + for (let _j = 0; ccshort >= 0 ? _j < ccshort : _j > ccshort; ccshort >= 0 ? ++_j : --_j) { this.transparency.indexed.push(255); } } break; + } case 0: this.transparency.grayscale = this.read(chunkSize)[0]; break; case 2: this.transparency.rgb = this.read(chunkSize); + break; + default: + break; } break; case 'tEXt': + { const text = this.read(chunkSize); const index = text.indexOf(0); const key = String.fromCharCode.apply(String, text.slice(0, index)); this.text[key] = String.fromCharCode.apply(String, text.slice(index + 1)); break; + } case 'IEND': + { if (frame) { this.animation.frames.push(frame); } @@ -161,11 +201,13 @@ export class PNGReader { case 2: case 6: return 3; + default: + return undefined; } }).call(this); const _ref = this.colorType; this.hasAlphaChannel = _ref === 4 || _ref === 6; - const colors = this.colors + (this.hasAlphaChannel ? 1 : 0); + const colors = this.colors! + (this.hasAlphaChannel ? 1 : 0); this.pixelBitlength = this.bits * colors; this.colorSpace = ((): string | undefined => { switch (this.colors) { @@ -173,25 +215,29 @@ export class PNGReader { return 'DeviceGray'; case 3: return 'DeviceRGB'; + default: + return undefined; } }).call(this); if (!(this.imgData instanceof Uint8Array)) { this.imgData = new Uint8Array(this.imgData); } return; + } default: this.pos += chunkSize; } this.pos += 4; - if (this.pos > this.data.length) { + if (this.pos > thisData.length) { throw new Error(getError(6017)); } } } - public read (bytes): any[] { - let i = 0; let _i = 0; - const _results: any[] = []; + public read (bytes: number): number[] { + let i = 0; + let _i = 0; + const _results: number[] = []; for (i = _i = 0; bytes >= 0 ? _i < bytes : _i > bytes; i = bytes >= 0 ? ++_i : --_i) { _results.push(this.data[this.pos++]); } @@ -199,10 +245,11 @@ export class PNGReader { } public readUInt32 (): number { - const b1 = this.data[this.pos++] << 24; - const b2 = this.data[this.pos++] << 16; - const b3 = this.data[this.pos++] << 8; - const b4 = this.data[this.pos++]; + const data = this.data; + const b1 = data[this.pos++] << 24; + const b2 = data[this.pos++] << 16; + const b3 = data[this.pos++] << 8; + const b4 = data[this.pos++]; return b1 | b2 | b3 | b4; } @@ -212,7 +259,7 @@ export class PNGReader { return b1 | b2; } - public decodePixels (data): Uint8Array { + public decodePixels (data: Uint8Array | number[] | null): Uint8Array { if (data == null) { data = this.imgData; } @@ -220,14 +267,30 @@ export class PNGReader { return new Uint8Array(0); } const inflate = new zlib.Inflate(data, { index: 0, verify: false }); - data = inflate.decompress(); + data = inflate.decompress() as Uint8Array; const pixelBytes = this.pixelBitlength / 8; const scanlineLength = pixelBytes * this.width; const pixels = new Uint8Array(scanlineLength * this.height); const length = data.length; - let row = 0; let pos = 0; let c = 0; let ccbyte = 0; let col = 0; - let i = 0; let _i = 0; let _j = 0; let _k = 0; let _l = 0; let _m = 0; - let left = 0; let p = 0; let pa = 0; let paeth = 0; let pb = 0; let pc = 0; let upper = 0; let upperLeft = 0; + let row = 0; + let pos = 0; + let c = 0; + let ccbyte = 0; + let col = 0; + let i = 0; + let _i = 0; + let _j = 0; + let _k = 0; + let _l = 0; + let _m = 0; + let left = 0; + let p = 0; + let pa = 0; + let paeth = 0; + let pb = 0; + let pc = 0; + let upper = 0; + let upperLeft = 0; while (pos < length) { switch (data[pos++]) { case 0: @@ -292,9 +355,9 @@ export class PNGReader { return pixels; } - public copyToImageData (imageData, pixels): void { + public copyToImageData (imageData: ImageData, pixels: Uint8Array): void { let alpha = this.hasAlphaChannel; - let palette: any; + let palette: Uint8Array | undefined; let colors = this.colors; if (this.palette.length) { palette = this._decodedPalette != null ? this._decodedPalette : this._decodedPalette = this.decodePalette(); @@ -344,12 +407,13 @@ export class PNGReader { return ret; } - render (canvas): any { + render (canvas: HTMLCanvasElement): void { canvas.width = this.width; canvas.height = this.height; const ctx = canvas.getContext('2d'); + if (!ctx) return; const data = ctx.createImageData(this.width, this.height); this.copyToImageData(data, this.decodePixels(null)); - return ctx.putImageData(data, 0, 0); + ctx.putImageData(data, 0, 0); } } diff --git a/cocos/particle-2d/tiff-reader.ts b/cocos/particle-2d/tiff-reader.ts index d7ad92efb7e..8b734ec0547 100644 --- a/cocos/particle-2d/tiff-reader.ts +++ b/cocos/particle-2d/tiff-reader.ts @@ -32,40 +32,41 @@ import { getError, logID } from '../core'; import { ccwindow } from '../core/global-exports'; interface IFile { - type: string, - values: any[], + type: string; + values: number[] | string[]; } interface ISampleProperty { - bitsPerSample: number, - hasBytesPerSample: boolean, - bytesPerSample: any, + bitsPerSample: number; + hasBytesPerSample: boolean; + bytesPerSample: number | undefined; } /** * cc.tiffReader is a singleton object, it's a tiff file reader, it can parse byte array to draw into a canvas * @class * @name tiffReader + * @mangle */ export class TiffReader { private _littleEndian = false; - private _tiffData = []; - private _fileDirectories: any[] = []; - private declare _canvas; + private _tiffData: number[] = []; + private _fileDirectories: Record[] = []; + private _canvas: HTMLCanvasElement | null = null; constructor () { } - public getUint8 (offset): any { + public getUint8 (offset: number): number { return this._tiffData[offset]; } - public getUint16 (offset): number { + public getUint16 (offset: number): number { if (this._littleEndian) return (this._tiffData[offset + 1] << 8) | (this._tiffData[offset]); else return (this._tiffData[offset] << 8) | (this._tiffData[offset + 1]); } - public getUint32 (offset): number { + public getUint32 (offset: number): number { const a = this._tiffData; if (this._littleEndian) return (a[offset + 3] << 24) | (a[offset + 2] << 16) | (a[offset + 1] << 8) | (a[offset]); else return (a[offset] << 24) | (a[offset + 1] << 16) | (a[offset + 2] << 8) | (a[offset + 3]); @@ -79,6 +80,7 @@ export class TiffReader { } else if (BOM === 0x4D4D) { this._littleEndian = false; } else { + // eslint-disable-next-line no-console console.log(BOM); throw TypeError(getError(6019)); } @@ -95,7 +97,7 @@ export class TiffReader { return true; } - public getFieldTypeName (fieldType): any { + public getFieldTypeName (fieldType: FieldTypeNamesKey): FieldTypeNamesValue | null { const typeNames = fieldTypeNames; if (fieldType in typeNames) { return typeNames[fieldType]; @@ -103,18 +105,18 @@ export class TiffReader { return null; } - public getFieldTagName (fieldTag): any { + public getFieldTagName (fieldTag: FieldTagNamesKey): FieldTagNamesValue { const tagNames = fieldTagNames; if (fieldTag in tagNames) { return tagNames[fieldTag]; } else { logID(6021, fieldTag); - return `Tag${fieldTag}`; + return `Tag${fieldTag}` as FieldTagNamesValue; } } - public getFieldTypeLength (fieldTypeName): number { + public getFieldTypeLength (fieldTypeName: FieldTypeNamesValue): number { if (['BYTE', 'ASCII', 'SBYTE', 'UNDEFINED'].indexOf(fieldTypeName) !== -1) { return 1; } else if (['SHORT', 'SSHORT'].indexOf(fieldTypeName) !== -1) { @@ -128,8 +130,13 @@ export class TiffReader { return 0; } - public getFieldValues (fieldTagName, fieldTypeName, typeCount, valueOffset): any[] { - const fieldValues: any[] = []; + public getFieldValues ( + fieldTagName: FieldTagNamesValue, + fieldTypeName: FieldTypeNamesValue, + typeCount: number, + valueOffset: number, + ): string[] | number[] { + const fieldValues: number[] = []; const fieldTypeLength = this.getFieldTypeLength(fieldTypeName); const fieldValueSize = fieldTypeLength * typeCount; @@ -157,13 +164,13 @@ export class TiffReader { if (fieldTypeName === 'ASCII') { fieldValues.forEach((e, i, a): void => { - a[i] = String.fromCharCode(e); + (a as unknown as string[])[i] = String.fromCharCode(e); }); } return fieldValues; } - public getBytes (numBytes, offset): any { + public getBytes (numBytes: number, offset: number): number { if (numBytes <= 0) { logID(8001); } else if (numBytes <= 1) { @@ -181,7 +188,11 @@ export class TiffReader { return 0; } - getBits (numBits, byteOffset, bitOffset): { bits: number; byteOffset: any; bitOffset: number; } { + getBits (numBits: number, byteOffset: number, bitOffset: number): { + bits: number; + byteOffset: number; + bitOffset: number; + } { bitOffset = bitOffset || 0; const extraBytes = Math.floor(bitOffset / 8); const newByteOffset = byteOffset + extraBytes; @@ -212,9 +223,9 @@ export class TiffReader { }; } - parseFileDirectory (offset): void { + parseFileDirectory (offset: number): void { const numDirEntries = this.getUint16(offset); - const tiffFields: IFile[] = []; + const tiffFields = {} as Record; let i = 0; let entryCount = 0; @@ -224,11 +235,11 @@ export class TiffReader { const typeCount = this.getUint32(i + 4); const valueOffset = this.getUint32(i + 8); - const fieldTagName = this.getFieldTagName(fieldTag); - const fieldTypeName = this.getFieldTypeName(fieldType); - const fieldValues = this.getFieldValues(fieldTagName, fieldTypeName, typeCount, valueOffset); + const fieldTagName = this.getFieldTagName(fieldTag as FieldTagNamesKey); + const fieldTypeName = this.getFieldTypeName(fieldType as FieldTypeNamesKey); + const fieldValues = this.getFieldValues(fieldTagName, fieldTypeName as FieldTypeNamesValue, typeCount, valueOffset); - tiffFields[fieldTagName] = { type: fieldTypeName, values: fieldValues }; + tiffFields[fieldTagName] = { type: fieldTypeName!, values: fieldValues }; } this._fileDirectories.push(tiffFields); @@ -239,19 +250,20 @@ export class TiffReader { } } - clampColorSample (colorSample, bitsPerSample): number { - const multiplier = Math.pow(2, 8 - bitsPerSample); + clampColorSample (colorSample: number, bitsPerSample: number): number { + const multiplier = 2 ** (8 - bitsPerSample); return Math.floor((colorSample * multiplier) + (multiplier - 1)); } - /** - * @function - * @param {Array} tiffData - * @param {HTMLCanvasElement} canvas - * @returns {*} - */ - parseTIFF (tiffData, canvas): any { + reset (): void { + this._littleEndian = false; + this._tiffData = []; + this._fileDirectories = []; + this._canvas = null; + } + + parseTIFF (tiffData: number[], canvas: HTMLCanvasElement): void { canvas = canvas || ccwindow.document.createElement('canvas'); this._tiffData = tiffData; @@ -270,17 +282,17 @@ export class TiffReader { const fileDirectory = this._fileDirectories[0]; - const imageWidth = fileDirectory.ImageWidth.values[0]; - const imageLength = fileDirectory.ImageLength.values[0]; + const imageWidth = fileDirectory.ImageWidth.values[0] as number; + const imageLength = fileDirectory.ImageLength.values[0] as number; this._canvas.width = imageWidth; this._canvas.height = imageLength; - const strips: any[] = []; + const strips: Array>> = []; - const compression = (fileDirectory.Compression) ? fileDirectory.Compression.values[0] : 1; + const compression = (fileDirectory.Compression) ? fileDirectory.Compression.values[0] as number : 1; - const samplesPerPixel = fileDirectory.SamplesPerPixel.values[0]; + const samplesPerPixel = fileDirectory.SamplesPerPixel.values[0] as number; const sampleProperties: ISampleProperty[] = []; @@ -308,13 +320,13 @@ export class TiffReader { bytesPerPixel = bitsPerPixel / 8; } - const stripOffsetValues = fileDirectory.StripOffsets.values; + const stripOffsetValues = fileDirectory.StripOffsets.values as number[]; const numStripOffsetValues = stripOffsetValues.length; - let stripByteCountValues; + let stripByteCountValues: number[]; // StripByteCounts is supposed to be required, but see if we can recover anyway. if (fileDirectory.StripByteCounts) { - stripByteCountValues = fileDirectory.StripByteCounts.values; + stripByteCountValues = fileDirectory.StripByteCounts.values as number[]; } else { logID(8003); // Infer StripByteCounts, if possible. @@ -333,7 +345,8 @@ export class TiffReader { const stripByteCount = stripByteCountValues[i]; // Loop through pixels. - for (let byteOffset = 0, bitOffset = 0, jIncrement = 1, getHeader = true, pixel: number[] = [], numBytes = 0, sample = 0, currentSample = 0; + for (let byteOffset = 0, bitOffset = 0, jIncrement = 1, getHeader = true, + pixel: number[] = [], numBytes = 0, sample = 0, currentSample = 0; byteOffset < stripByteCount; byteOffset += jIncrement) { // Decompress strip. switch (compression) { @@ -342,11 +355,11 @@ export class TiffReader { pixel = []; // Loop through samples (sub-pixels). for (let m = 0; m < samplesPerPixel; m++) { - const s: any = sampleProperties[m]; + const s = sampleProperties[m]; if (s.hasBytesPerSample) { // XXX: This is wrong! - const sampleOffset = s.bytesPerSample * m; - pixel.push(this.getBytes(s.bytesPerSample, stripOffset + byteOffset + sampleOffset)); + const sampleOffset = s.bytesPerSample! * m; + pixel.push(this.getBytes(s.bytesPerSample!, stripOffset + byteOffset + sampleOffset)); } else { const sampleInfo = this.getBits(s.bitsPerSample, stripOffset + byteOffset, bitOffset); pixel.push(sampleInfo.bits); @@ -460,127 +473,132 @@ export class TiffReader { } } - if (canvas.getContext) { - const ctx = this._canvas.getContext('2d'); + const ctx = this._canvas.getContext('2d'); + if (!ctx) return; - // Set a default fill style. - ctx.fillStyle = 'rgba(255, 255, 255, 0)'; + // Set a default fill style. + ctx.fillStyle = 'rgba(255, 255, 255, 0)'; - // If RowsPerStrip is missing, the whole image is in one strip. - const rowsPerStrip = fileDirectory.RowsPerStrip ? fileDirectory.RowsPerStrip.values[0] : imageLength; + // If RowsPerStrip is missing, the whole image is in one strip. + const rowsPerStrip = fileDirectory.RowsPerStrip ? fileDirectory.RowsPerStrip.values[0] as number : imageLength; - const numStrips = strips.length; + const numStrips = strips.length; - const imageLengthModRowsPerStrip = imageLength % rowsPerStrip; - const rowsInLastStrip = (imageLengthModRowsPerStrip === 0) ? rowsPerStrip : imageLengthModRowsPerStrip; + const imageLengthModRowsPerStrip = imageLength % rowsPerStrip; + const rowsInLastStrip = (imageLengthModRowsPerStrip === 0) ? rowsPerStrip : imageLengthModRowsPerStrip; - let numRowsInStrip = rowsPerStrip; - let numRowsInPreviousStrip = 0; + let numRowsInStrip = rowsPerStrip; + let numRowsInPreviousStrip = 0; - const photometricInterpretation = fileDirectory.PhotometricInterpretation.values[0]; + const photometricInterpretation = fileDirectory.PhotometricInterpretation.values[0] as number; - let extraSamplesValues = []; - let numExtraSamples = 0; + let extraSamplesValues: number[] = []; + let numExtraSamples = 0; - if (fileDirectory.ExtraSamples) { - extraSamplesValues = fileDirectory.ExtraSamples.values; - numExtraSamples = extraSamplesValues.length; - } + if (fileDirectory.ExtraSamples) { + extraSamplesValues = fileDirectory.ExtraSamples.values as number[]; + numExtraSamples = extraSamplesValues.length; + } - let colorMapValues = []; - let colorMapSampleSize = 0; - if (fileDirectory.ColorMap) { - colorMapValues = fileDirectory.ColorMap.values; - colorMapSampleSize = Math.pow(2, (sampleProperties[0] as any).bitsPerSample); - } + let colorMapValues: number[] = []; + let colorMapSampleSize = 0; + if (fileDirectory.ColorMap) { + colorMapValues = fileDirectory.ColorMap.values as number[]; + colorMapSampleSize = 2 ** sampleProperties[0].bitsPerSample; + } - // Loop through the strips in the image. - for (let i = 0; i < numStrips; i++) { - // The last strip may be short. - if ((i + 1) === numStrips) { - numRowsInStrip = rowsInLastStrip; - } + // Loop through the strips in the image. + for (let i = 0; i < numStrips; i++) { + // The last strip may be short. + if ((i + 1) === numStrips) { + numRowsInStrip = rowsInLastStrip; + } - const numPixels = strips[i].length; - const yPadding = numRowsInPreviousStrip * i; + const numPixels = strips[i].length; + const yPadding = numRowsInPreviousStrip * i; - // Loop through the rows in the strip. - for (let y = 0, j = 0; y < numRowsInStrip && j < numPixels; y++) { - // Loop through the pixels in the row. - for (let x = 0; x < imageWidth; x++, j++) { - const pixelSamples = strips[i][j]; + // Loop through the rows in the strip. + for (let y = 0, j = 0; y < numRowsInStrip && j < numPixels; y++) { + // Loop through the pixels in the row. + for (let x = 0; x < imageWidth; x++, j++) { + const pixelSamples: number[] = strips[i][j]; - let red = 0; - let green = 0; - let blue = 0; - let opacity = 1.0; + let red = 0; + let green = 0; + let blue = 0; + let opacity = 1.0; - if (numExtraSamples > 0) { - for (let k = 0; k < numExtraSamples; k++) { - if (extraSamplesValues[k] === 1 || extraSamplesValues[k] === 2) { - // Clamp opacity to the range [0,1]. - opacity = pixelSamples[3 + k] / 256; + if (numExtraSamples > 0) { + for (let k = 0; k < numExtraSamples; k++) { + if (extraSamplesValues[k] === 1 || extraSamplesValues[k] === 2) { + // Clamp opacity to the range [0,1]. + opacity = pixelSamples[3 + k] / 256; - break; - } + break; } } + } - switch (photometricInterpretation) { - // Bilevel or Grayscale - // WhiteIsZero - case 0: - let invertValue = 0; - if ((sampleProperties[0] as any).hasBytesPerSample) { - invertValue = Math.pow(0x10, (sampleProperties[0] as any).bytesPerSample * 2); - } - - // Invert samples. - pixelSamples.forEach((sample, index, samples): void => { - samples[index] = invertValue - sample; - }); - - // Bilevel or Grayscale - // BlackIsZero - case 1: - red = green = blue = this.clampColorSample(pixelSamples[0], (sampleProperties[0] as any).bitsPerSample); - break; - - // RGB Full Color - case 2: - red = this.clampColorSample(pixelSamples[0], (sampleProperties[0] as any).bitsPerSample); - green = this.clampColorSample(pixelSamples[1], (sampleProperties[1] as any).bitsPerSample); - blue = this.clampColorSample(pixelSamples[2], (sampleProperties[2] as any).bitsPerSample); - break; - - // RGB Color Palette - case 3: - if (colorMapValues === undefined) { - throw Error(getError(6027)); - } - - const colorMapIndex = pixelSamples[0]; - - red = this.clampColorSample(colorMapValues[colorMapIndex], 16); - green = this.clampColorSample(colorMapValues[colorMapSampleSize + colorMapIndex], 16); - blue = this.clampColorSample(colorMapValues[(2 * colorMapSampleSize) + colorMapIndex], 16); - break; + switch (photometricInterpretation) { + // Bilevel or Grayscale + // WhiteIsZero + case 0: + { + let invertValue = 0; + if (sampleProperties[0].hasBytesPerSample) { + invertValue = 0x10 ** (sampleProperties[0].bytesPerSample! * 2); + } - // Unknown Photometric Interpretation - default: - throw RangeError(getError(6028, photometricInterpretation)); + // Invert samples. + pixelSamples.forEach((sample, index, samples): void => { + samples[index] = invertValue - sample; + }); + } + // Bilevel or Grayscale + // BlackIsZero + // + // FIXME(cjh): ESLint error: 'case' statement requires a 'break' statement. + // But I don't know whether it was supposed to be a 'break' statement here. + // For now, I'll just leave it as it is and disable the ESLint error. + // eslint-disable-next-line no-fallthrough + case 1: + { + red = green = blue = this.clampColorSample(pixelSamples[0], sampleProperties[0].bitsPerSample); + break; + } + // RGB Full Color + case 2: + red = this.clampColorSample(pixelSamples[0], sampleProperties[0].bitsPerSample); + green = this.clampColorSample(pixelSamples[1], sampleProperties[1].bitsPerSample); + blue = this.clampColorSample(pixelSamples[2], sampleProperties[2].bitsPerSample); + break; + + // RGB Color Palette + case 3: + { + if (colorMapValues === undefined) { + throw Error(getError(6027)); } - ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${opacity})`; - ctx.fillRect(x, yPadding + y, 1, 1); + const colorMapIndex = pixelSamples[0]; + + red = this.clampColorSample(colorMapValues[colorMapIndex], 16); + green = this.clampColorSample(colorMapValues[colorMapSampleSize + colorMapIndex], 16); + blue = this.clampColorSample(colorMapValues[(2 * colorMapSampleSize) + colorMapIndex], 16); + break; + } + // Unknown Photometric Interpretation + default: + throw RangeError(getError(6028, photometricInterpretation)); } - } - numRowsInPreviousStrip = numRowsInStrip; + ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${opacity})`; + ctx.fillRect(x, yPadding + y, 1, 1); + } } - } - return this._canvas; + numRowsInPreviousStrip = numRowsInStrip; + } } // See: http://www.digitizationguidelines.gov/guidelines/TIFF_Metadata_Final.pdf @@ -695,7 +713,10 @@ const fieldTagNames = { // Photoshop 0x8649: 'Photoshop', -}; +} as const; + +type FieldTagNamesKey = keyof typeof fieldTagNames; +type FieldTagNamesValue = typeof fieldTagNames[FieldTagNamesKey]; const fieldTypeNames = { 0x0001: 'BYTE', @@ -710,4 +731,9 @@ const fieldTypeNames = { 0x000A: 'SRATIONAL', 0x000B: 'FLOAT', 0x000C: 'DOUBLE', -}; +} as const; + +type FieldTypeNamesKey = keyof typeof fieldTypeNames; +type FieldTypeNamesValue = typeof fieldTypeNames[FieldTypeNamesKey]; + +export const tiffReader = new TiffReader();