export class Fraction extends Number { numerator : number ; denominator : number ; } export interface IImageExtended extends HTMLImageElement { exifdata : any ; iptcdata : any ; } export class Debug { public static log ( ... args ) { if ( Exif . debug ) { console . log ( args ) ; } } } export class Exif { public static debug : boolean = false ; public static IptcFieldMap : any = { 0x78 : "caption" , 0x6e : "credit" , 0x19 : "keywords" , 0x37 : "dateCreated" , 0x50 : "byline" , 0x55 : "bylineTitle" , 0x7a : "captionWriter" , 0x69 : "headline" , 0x74 : "copyright" , 0x0f : "category" } ; public static Tags : any = { 0x9000 : "ExifVersion" , 0xa000 : "FlashpixVersion" , 0xa001 : "ColorSpace" , 0xa002 : "PixelXDimension" , 0xa003 : "PixelYDimension" , 0x9101 : "ComponentsConfiguration" , 0x9102 : "CompressedBitsPerPixel" , 0x927c : "MakerNote" , 0x9286 : "UserComment" , 0xa004 : "RelatedSoundFile" , 0x9003 : "DateTimeOriginal" , 0x9004 : "DateTimeDigitized" , 0x9290 : "SubsecTime" , 0x9291 : "SubsecTimeOriginal" , 0x9292 : "SubsecTimeDigitized" , 0x829a : "ExposureTime" , 0x829d : "FNumber" , 0x8822 : "ExposureProgram" , 0x8824 : "SpectralSensitivity" , 0x8827 : "ISOSpeedRatings" , 0x8828 : "OECF" , 0x9201 : "ShutterSpeedValue" , 0x9202 : "ApertureValue" , 0x9203 : "BrightnessValue" , 0x9204 : "ExposureBias" , 0x9205 : "MaxApertureValue" , 0x9206 : "SubjectDistance" , 0x9207 : "MeteringMode" , 0x9208 : "LightSource" , 0x9209 : "Flash" , 0x9214 : "SubjectArea" , 0x920a : "FocalLength" , 0xa20b : "FlashEnergy" , 0xa20c : "SpatialFrequencyResponse" , 0xa20e : "FocalPlaneXResolution" , 0xa20f : "FocalPlaneYResolution" , 0xa210 : "FocalPlaneResolutionUnit" , 0xa214 : "SubjectLocation" , 0xa215 : "ExposureIndex" , 0xa217 : "SensingMethod" , 0xa300 : "FileSource" , 0xa301 : "SceneType" , 0xa302 : "CFAPattern" , 0xa401 : "CustomRendered" , 0xa402 : "ExposureMode" , 0xa403 : "WhiteBalance" , 0xa404 : "DigitalZoomRation" , 0xa405 : "FocalLengthIn35mmFilm" , 0xa406 : "SceneCaptureType" , 0xa407 : "GainControl" , 0xa408 : "Contrast" , 0xa409 : "Saturation" , 0xa40a : "Sharpness" , 0xa40b : "DeviceSettingDescription" , 0xa40c : "SubjectDistanceRange" , 0xa005 : "InteroperabilityIFDPointer" , 0xa420 : "ImageUniqueID" } ; public static TiffTags : any = { 0x0100 : "ImageWidth" , 0x0101 : "ImageHeight" , 0x8769 : "ExifIFDPointer" , 0x8825 : "GPSInfoIFDPointer" , 0xa005 : "InteroperabilityIFDPointer" , 0x0102 : "BitsPerSample" , 0x0103 : "Compression" , 0x0106 : "PhotometricInterpretation" , 0x0112 : "Orientation" , 0x0115 : "SamplesPerPixel" , 0x011c : "PlanarConfiguration" , 0x0212 : "YCbCrSubSampling" , 0x0213 : "YCbCrPositioning" , 0x011a : "XResolution" , 0x011b : "YResolution" , 0x0128 : "ResolutionUnit" , 0x0111 : "StripOffsets" , 0x0116 : "RowsPerStrip" , 0x0117 : "StripByteCounts" , 0x0201 : "JPEGInterchangeFormat" , 0x0202 : "JPEGInterchangeFormatLength" , 0x012d : "TransferFunction" , 0x013e : "WhitePoint" , 0x013f : "PrimaryChromaticities" , 0x0211 : "YCbCrCoefficients" , 0x0214 : "ReferenceBlackWhite" , 0x0132 : "DateTime" , 0x010e : "ImageDescription" , 0x010f : "Make" , 0x0110 : "Model" , 0x0131 : "Software" , 0x013b : "Artist" , 0x8298 : "Copyright" } ; public static GPSTags : any = { 0x0000 : "GPSVersionID" , 0x0001 : "GPSLatitudeRef" , 0x0002 : "GPSLatitude" , 0x0003 : "GPSLongitudeRef" , 0x0004 : "GPSLongitude" , 0x0005 : "GPSAltitudeRef" , 0x0006 : "GPSAltitude" , 0x0007 : "GPSTimeStamp" , 0x0008 : "GPSSatellites" , 0x0009 : "GPSStatus" , 0x000a : "GPSMeasureMode" , 0x000b : "GPSDOP" , 0x000c : "GPSSpeedRef" , 0x000d : "GPSSpeed" , 0x000e : "GPSTrackRef" , 0x000f : "GPSTrack" , 0x0010 : "GPSImgDirectionRef" , 0x0011 : "GPSImgDirection" , 0x0012 : "GPSMapDatum" , 0x0013 : "GPSDestLatitudeRef" , 0x0014 : "GPSDestLatitude" , 0x0015 : "GPSDestLongitudeRef" , 0x0016 : "GPSDestLongitude" , 0x0017 : "GPSDestBearingRef" , 0x0018 : "GPSDestBearing" , 0x0019 : "GPSDestDistanceRef" , 0x001a : "GPSDestDistance" , 0x001b : "GPSProcessingMethod" , 0x001c : "GPSAreaInformation" , 0x001d : "GPSDateStamp" , 0x001e : "GPSDifferential" } ; public static StringValues : any = { ExposureProgram : { 0 : "Not defined" , 1 : "Manual" , 2 : "Normal program" , 3 : "Aperture priority" , 4 : "Shutter priority" , 5 : "Creative program" , 6 : "Action program" , 7 : "Portrait mode" , 8 : "Landscape mode" } , MeteringMode : { 0 : "Unknown" , 1 : "Average" , 2 : "CenterWeightedAverage" , 3 : "Spot" , 4 : "MultiSpot" , 5 : "Pattern" , 6 : "Partial" , 255 : "Other" } , LightSource : { 0 : "Unknown" , 1 : "Daylight" , 2 : "Fluorescent" , 3 : "Tungsten (incandescent light)" , 4 : "Flash" , 9 : "Fine weather" , 10 : "Cloudy weather" , 11 : "Shade" , 12 : "Daylight fluorescent (D 5700 - 7100K)" , 13 : "Day white fluorescent (N 4600 - 5400K)" , 14 : "Cool white fluorescent (W 3900 - 4500K)" , 15 : "White fluorescent (WW 3200 - 3700K)" , 17 : "Standard light A" , 18 : "Standard light B" , 19 : "Standard light C" , 20 : "D55" , 21 : "D65" , 22 : "D75" , 23 : "D50" , 24 : "ISO studio tungsten" , 255 : "Other" } , Flash : { 0x0000 : "Flash did not fire" , 0x0001 : "Flash fired" , 0x0005 : "Strobe return light not detected" , 0x0007 : "Strobe return light detected" , 0x0009 : "Flash fired, compulsory flash mode" , 0x000d : "Flash fired, compulsory flash mode, return light not detected" , 0x000f : "Flash fired, compulsory flash mode, return light detected" , 0x0010 : "Flash did not fire, compulsory flash mode" , 0x0018 : "Flash did not fire, auto mode" , 0x0019 : "Flash fired, auto mode" , 0x001d : "Flash fired, auto mode, return light not detected" , 0x001f : "Flash fired, auto mode, return light detected" , 0x0020 : "No flash function" , 0x0041 : "Flash fired, red-eye reduction mode" , 0x0045 : "Flash fired, red-eye reduction mode, return light not detected" , 0x0047 : "Flash fired, red-eye reduction mode, return light detected" , 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode" , 0x004d : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected" , 0x004f : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected" , 0x0059 : "Flash fired, auto mode, red-eye reduction mode" , 0x005d : "Flash fired, auto mode, return light not detected, red-eye reduction mode" , 0x005f : "Flash fired, auto mode, return light detected, red-eye reduction mode" } , SensingMethod : { 1 : "Not defined" , 2 : "One-chip color area sensor" , 3 : "Two-chip color area sensor" , 4 : "Three-chip color area sensor" , 5 : "Color sequential area sensor" , 7 : "Trilinear sensor" , 8 : "Color sequential linear sensor" } , SceneCaptureType : { 0 : "Standard" , 1 : "Landscape" , 2 : "Portrait" , 3 : "Night scene" } , SceneType : { 1 : "Directly photographed" } , CustomRendered : { 0 : "Normal process" , 1 : "Custom process" } , WhiteBalance : { 0 : "Auto white balance" , 1 : "Manual white balance" } , GainControl : { 0 : "None" , 1 : "Low gain up" , 2 : "High gain up" , 3 : "Low gain down" , 4 : "High gain down" } , Contrast : { 0 : "Normal" , 1 : "Soft" , 2 : "Hard" } , Saturation : { 0 : "Normal" , 1 : "Low saturation" , 2 : "High saturation" } , Sharpness : { 0 : "Normal" , 1 : "Soft" , 2 : "Hard" } , SubjectDistanceRange : { 0 : "Unknown" , 1 : "Macro" , 2 : "Close view" , 3 : "Distant view" } , FileSource : { 3 : "DSC" } , Components : { 0 : "" , 1 : "Y" , 2 : "Cb" , 3 : "Cr" , 4 : "R" , 5 : "G" , 6 : "B" } } ; public static addEvent ( element , event , handler ) { if ( element . addEventListener ) { element . addEventListener ( event , handler , false ) ; } else { if ( element . attachEvent ) { element . attachEvent ( "on" + event , handler ) ; } } } public static imageHasData ( img ) { return ! ! img . exifdata ; } public static base64ToArrayBuffer ( base64 ) { base64 = base64 . replace ( /^data:([^;]+);base64,/gim , "" ) ; let binary = atob ( base64 ) ; let len = binary . length ; let buffer = new ArrayBuffer ( len ) ; let view = new Uint8Array ( buffer ) ; for ( let i = 0 ; i < len ; i ++ ) { view [ i ] = binary . charCodeAt ( i ) ; } return buffer ; } public static objectURLToBlob ( url , callback ) { let http = new XMLHttpRequest ( ) ; http . open ( "GET" , url , true ) ; http . responseType = "blob" ; http . onload = function ( ) { if ( http . status === 200 || http . status === 0 ) { callback ( http . response ) ; } } ; http . send ( ) ; } public static getImageData ( img , callback ) { function handleBinaryFile ( binFile ) { let data = Exif . findEXIFinJPEG ( binFile ) ; let iptcdata = Exif . findIPTCinJPEG ( binFile ) ; ( img as IImageExtended ) . exifdata = data || { } ; ( img as IImageExtended ) . iptcdata = iptcdata || { } ; if ( callback ) { callback . call ( img ) ; } } if ( "src" in img && ( img as IImageExtended ) . src ) { if ( /^data:/i . test ( ( img as IImageExtended ) . src ) ) { let arrayBuffer = Exif . base64ToArrayBuffer ( ( img as IImageExtended ) . src ) ; handleBinaryFile ( arrayBuffer ) ; } else { if ( /^blob:/i . test ( ( img as IImageExtended ) . src ) ) { let fileReader = new FileReader ( ) ; fileReader . onload = ( e ) => { handleBinaryFile ( e . target . result ) ; } ; Exif . objectURLToBlob ( ( img as IImageExtended ) . src , ( blob ) => { fileReader . readAsArrayBuffer ( blob ) ; } ) ; } else { let http = new XMLHttpRequest ( ) ; http . onload = function ( ) { if ( http . status === 200 || http . status === 0 ) { handleBinaryFile ( http . response ) ; } else { throw "Could not load image" ; } } ; http . open ( "GET" , ( img as IImageExtended ) . src , true ) ; http . responseType = "arraybuffer" ; http . send ( null ) ; } } } else { if ( FileReader && ( img instanceof Blob || img instanceof File ) ) { let fileReader = new FileReader ( ) ; fileReader . onload = function ( e ) { Debug . log ( "Got file of length " + e . target . result . byteLength ) ; handleBinaryFile ( e . target . result ) ; } ; fileReader . readAsArrayBuffer ( img ) ; } } } public static findEXIFinJPEG ( file ) { let dataView = new DataView ( file ) ; Debug . log ( "Got file of length " + file . byteLength ) ; if ( dataView . getUint8 ( 0 ) !== 0xff || dataView . getUint8 ( 1 ) !== 0xd8 ) { Debug . log ( "Not a valid JPEG" ) ; return false ; } let offset = 2 ; let length = file . byteLength ; let marker ; while ( offset < length ) { if ( dataView . getUint8 ( offset ) !== 0xff ) { Debug . log ( "Not a valid marker at offset " + offset + ", found: " + dataView . getUint8 ( offset ) ) ; return false ; } marker = dataView . getUint8 ( offset + 1 ) ; Debug . log ( marker ) ; if ( marker === 225 ) { Debug . log ( "Found 0xFFE1 marker" ) ; return Exif . readEXIFData ( dataView , offset + 4 ) ; } else { offset += 2 + dataView . getUint16 ( offset + 2 ) ; } } } public static findIPTCinJPEG ( file ) { let dataView = new DataView ( file ) ; Debug . log ( "Got file of length " + file . byteLength ) ; if ( dataView . getUint8 ( 0 ) !== 0xff || dataView . getUint8 ( 1 ) !== 0xd8 ) { Debug . log ( "Not a valid JPEG" ) ; return false ; } let offset = 2 ; let length = file . byteLength ; let isFieldSegmentStart = function ( _dataView , _offset ) { return ( _dataView . getUint8 ( _offset ) === 0x38 && _dataView . getUint8 ( _offset + 1 ) === 0x42 && _dataView . getUint8 ( _offset + 2 ) === 0x49 && _dataView . getUint8 ( _offset + 3 ) === 0x4d && _dataView . getUint8 ( _offset + 4 ) === 0x04 && _dataView . getUint8 ( _offset + 5 ) === 0x04 ) ; } ; while ( offset < length ) { if ( isFieldSegmentStart ( dataView , offset ) ) { let nameHeaderLength = dataView . getUint8 ( offset + 7 ) ; if ( nameHeaderLength % 2 !== 0 ) { nameHeaderLength += 1 ; } if ( nameHeaderLength === 0 ) { nameHeaderLength = 4 ; } let startOffset = offset + 8 + nameHeaderLength ; let sectionLength = dataView . getUint16 ( offset + 6 + nameHeaderLength ) ; return Exif . readIPTCData ( file , startOffset , sectionLength ) ; } offset ++ ; } } public static readIPTCData ( file , startOffset , sectionLength ) { let dataView = new DataView ( file ) ; let data = { } ; let fieldValue , fieldName , dataSize , segmentType , segmentSize ; let segmentStartPos = startOffset ; while ( segmentStartPos < startOffset + sectionLength ) { if ( dataView . getUint8 ( segmentStartPos ) === 0x1c && dataView . getUint8 ( segmentStartPos + 1 ) === 0x02 ) { segmentType = dataView . getUint8 ( segmentStartPos + 2 ) ; if ( segmentType in Exif . IptcFieldMap ) { dataSize = dataView . getInt16 ( segmentStartPos + 3 ) ; segmentSize = dataSize + 5 ; fieldName = Exif . IptcFieldMap [ segmentType ] ; fieldValue = Exif . getStringFromDB ( dataView , segmentStartPos + 5 , dataSize ) ; if ( data . hasOwnProperty ( fieldName ) ) { if ( data [ fieldName ] instanceof Array ) { data [ fieldName ] . push ( fieldValue ) ; } else { data [ fieldName ] = [ data [ fieldName ] , fieldValue ] ; } } else { data [ fieldName ] = fieldValue ; } } } segmentStartPos ++ ; } return data ; } public static readTags ( file , tiffStart , dirStart , strings , bigEnd ) { let entries = file . getUint16 ( dirStart , ! bigEnd ) ; let tags = { } ; let entryOffset ; let tag ; for ( let i = 0 ; i < entries ; i ++ ) { entryOffset = dirStart + i * 12 + 2 ; tag = strings [ file . getUint16 ( entryOffset , ! bigEnd ) ] ; if ( ! tag ) { Debug . log ( "Unknown tag: " + file . getUint16 ( entryOffset , ! bigEnd ) ) ; } tags [ tag ] = Exif . readTagValue ( file , entryOffset , tiffStart , dirStart , bigEnd ) ; } return tags ; } public static readTagValue ( file , entryOffset , tiffStart , dirStart , bigEnd ) { let type = file . getUint16 ( entryOffset + 2 , ! bigEnd ) ; let numValues = file . getUint32 ( entryOffset + 4 , ! bigEnd ) ; let valueOffset = file . getUint32 ( entryOffset + 8 , ! bigEnd ) + tiffStart ; let offset ; let vals , val , n ; let numerator ; let denominator ; switch ( type ) { case 1 : case 7 : if ( numValues === 1 ) { return file . getUint8 ( entryOffset + 8 , ! bigEnd ) ; } else { offset = numValues > 4 ? valueOffset : entryOffset + 8 ; vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { vals [ n ] = file . getUint8 ( offset + n ) ; } return vals ; } case 2 : offset = numValues > 4 ? valueOffset : entryOffset + 8 ; return Exif . getStringFromDB ( file , offset , numValues - 1 ) ; case 3 : if ( numValues === 1 ) { return file . getUint16 ( entryOffset + 8 , ! bigEnd ) ; } else { offset = numValues > 2 ? valueOffset : entryOffset + 8 ; vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { vals [ n ] = file . getUint16 ( offset + 2 * n , ! bigEnd ) ; } return vals ; } case 4 : if ( numValues === 1 ) { return file . getUint32 ( entryOffset + 8 , ! bigEnd ) ; } else { vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { vals [ n ] = file . getUint32 ( valueOffset + 4 * n , ! bigEnd ) ; } return vals ; } case 5 : if ( numValues === 1 ) { numerator = file . getUint32 ( valueOffset , ! bigEnd ) ; denominator = file . getUint32 ( valueOffset + 4 , ! bigEnd ) ; val = new Fraction ( numerator / denominator ) ; val . numerator = numerator ; val . denominator = denominator ; return val ; } else { vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { numerator = file . getUint32 ( valueOffset + 8 * n , ! bigEnd ) ; denominator = file . getUint32 ( valueOffset + 4 + 8 * n , ! bigEnd ) ; vals [ n ] = new Fraction ( numerator / denominator ) ; vals [ n ] . numerator = numerator ; vals [ n ] . denominator = denominator ; } return vals ; } case 9 : if ( numValues === 1 ) { return file . getInt32 ( entryOffset + 8 , ! bigEnd ) ; } else { vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { vals [ n ] = file . getInt32 ( valueOffset + 4 * n , ! bigEnd ) ; } return vals ; } case 10 : if ( numValues === 1 ) { return ( file . getInt32 ( valueOffset , ! bigEnd ) / file . getInt32 ( valueOffset + 4 , ! bigEnd ) ) ; } else { vals = [ ] ; for ( n = 0 ; n < numValues ; n ++ ) { vals [ n ] = file . getInt32 ( valueOffset + 8 * n , ! bigEnd ) / file . getInt32 ( valueOffset + 4 + 8 * n , ! bigEnd ) ; } return vals ; } default : break ; } } public static getStringFromDB ( buffer , start , length ) { let outstr = "" ; for ( let n = start ; n < start + length ; n ++ ) { outstr += String . fromCharCode ( buffer . getUint8 ( n ) ) ; } return outstr ; } public static readEXIFData ( file , start ) { if ( Exif . getStringFromDB ( file , start , 4 ) !== "Exif" ) { Debug . log ( "Not valid EXIF data! " + Exif . getStringFromDB ( file , start , 4 ) ) ; return false ; } let bigEnd , tags , tag , exifData , gpsData , tiffOffset = start + 6 ; if ( file . getUint16 ( tiffOffset ) === 0x4949 ) { bigEnd = false ; } else { if ( file . getUint16 ( tiffOffset ) === 0x4d4d ) { bigEnd = true ; } else { Debug . log ( "Not valid TIFF data! (no 0x4949 or 0x4D4D)" ) ; return false ; } } if ( file . getUint16 ( tiffOffset + 2 , ! bigEnd ) !== 0x002a ) { Debug . log ( "Not valid TIFF data! (no 0x002A)" ) ; return false ; } let firstIFDOffset = file . getUint32 ( tiffOffset + 4 , ! bigEnd ) ; if ( firstIFDOffset < 0x00000008 ) { Debug . log ( "Not valid TIFF data! (First offset less than 8)" , file . getUint32 ( tiffOffset + 4 , ! bigEnd ) ) ; return false ; } tags = Exif . readTags ( file , tiffOffset , tiffOffset + firstIFDOffset , Exif . TiffTags , bigEnd ) ; if ( tags . ExifIFDPointer ) { exifData = Exif . readTags ( file , tiffOffset , tiffOffset + tags . ExifIFDPointer , Exif . Tags , bigEnd ) ; for ( tag in exifData ) { if ( { } . hasOwnProperty . call ( exifData , tag ) ) { switch ( tag ) { case "LightSource" : case "Flash" : case "MeteringMode" : case "ExposureProgram" : case "SensingMethod" : case "SceneCaptureType" : case "SceneType" : case "CustomRendered" : case "WhiteBalance" : case "GainControl" : case "Contrast" : case "Saturation" : case "Sharpness" : case "SubjectDistanceRange" : case "FileSource" : exifData [ tag ] = Exif . StringValues [ tag ] [ exifData [ tag ] ] ; break ; case "ExifVersion" : case "FlashpixVersion" : exifData [ tag ] = String . fromCharCode ( exifData [ tag ] [ 0 ] , exifData [ tag ] [ 1 ] , exifData [ tag ] [ 2 ] , exifData [ tag ] [ 3 ] ) ; break ; case "ComponentsConfiguration" : let compopents = "Components" ; exifData [ tag ] = Exif . StringValues [ compopents ] [ exifData [ tag ] [ 0 ] ] + Exif . StringValues [ compopents ] [ exifData [ tag ] [ 1 ] ] + Exif . StringValues [ compopents ] [ exifData [ tag ] [ 2 ] ] + Exif . StringValues [ compopents ] [ exifData [ tag ] [ 3 ] ] ; break ; default : break ; } tags [ tag ] = exifData [ tag ] ; } } } if ( tags . GPSInfoIFDPointer ) { gpsData = Exif . readTags ( file , tiffOffset , tiffOffset + tags . GPSInfoIFDPointer , Exif . GPSTags , bigEnd ) ; for ( tag in gpsData ) { if ( { } . hasOwnProperty . call ( gpsData , tag ) ) { switch ( tag ) { case "GPSVersionID" : gpsData [ tag ] = gpsData [ tag ] [ 0 ] + "." + gpsData [ tag ] [ 1 ] + "." + gpsData [ tag ] [ 2 ] + "." + gpsData [ tag ] [ 3 ] ; break ; default : break ; } tags [ tag ] = gpsData [ tag ] ; } } } return tags ; } private static checkImageType ( img ) { return img instanceof Image || img instanceof HTMLImageElement ; } public static getData ( img , callback ) { if ( this . checkImageType ( img ) && ! img . complete ) { return false ; } if ( ! Exif . imageHasData ( img as IImageExtended ) ) { Exif . getImageData ( img as IImageExtended , callback ) ; } else { if ( callback ) { callback . call ( img ) ; } } return true ; } public static getTag ( img , tag ) { if ( ! Exif . imageHasData ( img ) ) { return ; } return img . exifdata [ tag ] ; } public static getAllTags ( img ) { if ( ! Exif . imageHasData ( img ) ) { return { } ; } let a , data = img . exifdata , tags = { } ; for ( a in data ) { if ( data . hasOwnProperty ( a ) ) { tags [ a ] = data [ a ] ; } } return tags ; } public static pretty ( img ) { if ( ! Exif . imageHasData ( img ) ) { return "" ; } let a , data = img . exifdata , strPretty = "" ; for ( a in data ) { if ( data . hasOwnProperty ( a ) ) { if ( typeof data [ a ] === "object" ) { if ( data [ a ] instanceof Number ) { strPretty += `${ a } : ${ data [ a ] } [${ data [ a ] . numerator }/${ data [ a ] . denominator }]\r\n` ; } else { strPretty += `${ a } : [${ data [ a ] . length } values]\r\n` ; } } else { strPretty += `${ a } : ${ data [ a ] }\r\n` ; } } } return strPretty ; } public static readFromBinaryFile ( file ) { return Exif . findEXIFinJPEG ( file ) ; } }