import { logger } from "batch-cluster" ; import * as _path from "path" ; import { ExifDate } from "./ExifDate" ; import { ExifDateTime } from "./ExifDateTime" ; import { ExifTime } from "./ExifTime" ; import { ExifToolTask } from "./ExifToolTask" ; import { firstDefinedThunk , map } from "./Maybe" ; import { toF } from "./Number" ; import { blank , isString , toS } from "./String" ; import { Tags } from "./Tags" ; import { extractTzOffsetFromTags , extractTzOffsetFromUTCOffset , } from "./Timezones" ; const tzlookup = require ( "tz-lookup" ) ; /**
 * tag names we don't need to muck with:
 */ const PassthroughTags = [ "ExifToolVersion" , "DateStampMode" , "Sharpness" , "Firmware" , "DateDisplayFormat" , ] ; const nullishes = [ "undef" , "null" , "undefined" ] ; export function nullish ( s ) { return s == null || ( isString ( s ) && nullishes . includes ( s . trim ( ) ) ) ; } export class ReadTask extends ExifToolTask < Tags > { private readonly degroup : boolean ; /** May have keys that are group-prefixed */ private _raw : any = { } ; /** Always has non-group-prefixed keys */ private _tags : any = { } ; private readonly tags : Tags ; private lat : number | undefined ; private lon : number | undefined ; private invalidLatLon = false ; private tz : string | undefined ; private tzSource ? : string ; private constructor ( readonly sourceFile , override readonly args ) { super ( args ) ; this . degroup = this . args . indexOf ( "-G" ) !== - 1 ; this . tags = { SourceFile : sourceFile } as Tags ; this . tags . errors = this . errors ; } static for ( filename , numericTags , optionalArgs = [ ] ) { const sourceFile = _path . resolve ( filename ) ; const args = [ "-json" , "-struct" , ... optionalArgs , ] ; args . push ( ... numericTags . map ( ( ea ) => "-" + ea + "#" ) ) ; args . push ( "-all" , "-charset" , "filename=utf8" , sourceFile ) ; return new ReadTask ( sourceFile , args ) ; } override toString ( ) { return "ReadTask" + this . sourceFile + ")" ; } parse ( data , err ? ) { try { this . _raw = JSON . parse ( data ) [ 0 ] ; } catch ( jsonError ) { logger ( ) . warn ( "ExifTool.ReadTask(): Invalid JSON" , { data , err , jsonError , } ) ; throw err ?? jsonError ; } const SourceFile = _path . resolve ( this . _raw . SourceFile ) ; if ( SourceFile !== this . sourceFile ) { throw new Error ( `Internal error: unexpected SourceFile of ${ this . _raw . SourceFile } for file ${ this . sourceFile }` ) ; } if ( this . degroup ) { Object . keys ( this . _raw ) . forEach ( ( keyWithGroup ) => { this . _tags [ this . tagName ( keyWithGroup ) ] = this . _raw [ keyWithGroup ] ; } ) ; } else { this . _tags = this . _raw ; } return this . parseTags ( ) ; } private tagName ( k ) { return this . degroup ? k . split ( ":" ) [ 1 ] ?? k : k ; } private parseTags ( ) { this . extractLatLon ( ) ; this . extractTzOffset ( ) ; Object . keys ( this . _raw ) . forEach ( ( key ) => ( ( this . tags as any ) [ key ] = this . parseTag ( key , this . _raw [ key ] ) ) ) ; map ( this . tz , ( ea ) => ( this . tags . tz = ea ) ) ; map ( this . tzSource , ( ea ) => ( this . tags . tzSource = ea ) ) ; if ( this . errors . length > 0 ) this . tags . errors = this . errors ; return this . tags as Tags ; } private extractLatLon ( ) { this . lat ??= this . latlon ( "GPSLatitude" , "S" , 90 ) ; this . lon ??= this . latlon ( "GPSLongitude" , "W" , 180 ) ; if ( this . invalidLatLon ) { this . lat = this . lon = undefined ; } } private latlon ( tagName , negateRef , maxValid ) { const tagValue = this . _tags [ tagName ] ; const ref = this . _tags [ tagName + "Ref" ] ; const result = toF ( tagValue ) ; if ( result == null ) { return ; } else if ( Math . abs ( result ) > maxValid ) { this . errors . push ( `Invalid ${ tagName }: ${ JSON . stringify ( tagValue ) }` ) ; this . invalidLatLon = true ; return ; } else if ( blank ( ref ) ) { return result ; } else { const negative = toS ( ref ) . toUpperCase ( ) . startsWith ( negateRef ) ; return ( negative ? - 1 : 1 ) * Math . abs ( result ) ; } } private extractTzOffset ( ) { map ( firstDefinedThunk ( [ ( ) => extractTzOffsetFromTags ( this . _tags ) , ( ) => { if ( ! this . invalidLatLon && this . lat != null && this . lon != null ) { try { return map ( tzlookup ( this . lat , this . lon ) , ( tz ) => ( { tz , src : "from Lat/Lon" , } ) ) ; } catch ( err ) { } } return ; } , ( ) => extractTzOffsetFromUTCOffset ( this . _tags ) , ] ) , ( ea ) => ( { tz : this . tz , src : this . tzSource } = ea ) ) ; } private parseTag ( tagNameWithGroup , value ) { if ( nullish ( value ) ) return undefined ; const tagName = this . tagName ( tagNameWithGroup ) ; try { if ( PassthroughTags . indexOf ( tagName ) >= 0 ) { return value ; } if ( tagName === "GPSLatitude" ) { return this . lat ; } if ( tagName === "GPSLongitude" ) { return this . lon ; } const tz = tagName . includes ( "UTC" ) || tagName . startsWith ( "GPS" ) ? "UTC" : this . tz ; if ( typeof value === "string" && tagName . includes ( "DateTime" ) ) { const d = ExifDateTime . fromExifStrict ( value , tz ) ?? ExifDateTime . fromISO ( value , tz ) ; if ( d != null ) { return d ; } } if ( typeof value === "string" && tagName . includes ( "Date" ) ) { const d = ExifDateTime . fromExifStrict ( value , tz ) ?? ExifDateTime . fromISO ( value , tz ) ?? ExifDateTime . fromExifLoose ( value , tz ) ?? ExifDate . fromExifStrict ( value ) ?? ExifDate . fromISO ( value ) ?? ExifDate . fromExifLoose ( value ) ; if ( d != null ) { return d ; } } if ( typeof value === "string" && tagName . includes ( "Time" ) ) { const t = ExifTime . fromEXIF ( value ) ; if ( t != null ) return t ; } return value ; } catch ( e ) { this . addError ( `Failed to parse ${ tagName } with value ${ JSON . stringify ( value ) }: ${ e }` ) ; return value ; } } }