import * as bc from "batch-cluster" ; import * as _cp from "child_process" ; import * as _fs from "fs" ; import * as _os from "os" ; import * as _path from "path" ; import * as _p from "process" ; import { retryOnReject } from "./AsyncRetry" ; import { BinaryExtractionTask } from "./BinaryExtractionTask" ; import { BinaryToBufferTask } from "./BinaryToBufferTask" ; import { DeleteAllTagsArgs } from "./DeleteAllTagsArgs" ; import { ExifDate } from "./ExifDate" ; import { ExifDateTime } from "./ExifDateTime" ; import { ExifToolTask } from "./ExifToolTask" ; import { lazy } from "./Lazy" ; import { PreviewTag } from "./PreviewTag" ; import { ReadRawTask } from "./ReadRawTask" ; import { ReadTask } from "./ReadTask" ; import { RewriteAllTagsTask } from "./RewriteAllTagsTask" ; import { blank , notBlank } from "./String" ; import { Tags } from "./Tags" ; import { VersionTask } from "./VersionTask" ; import { WriteTask } from "./WriteTask" ; export { ExifDate } from "./ExifDate" ; export { ExifDateTime } from "./ExifDateTime" ; export { ExifTime } from "./ExifTime" ; export { ExifToolTask } from "./ExifToolTask" ; export { parseJSON } from "./JSON" ; export { offsetMinutesToZoneName , UnsetZone , UnsetZoneName , UnsetZoneOffsetMinutes , } from "./Timezones" ; export type { Tags } ; const isWin32 = lazy ( ( ) => _os . platform ( ) === "win32" ) ; function findExiftool ( ) { const path = require ( `exiftool-vendored.${ isWin32 ( ) ? "exe" : "pl" }` ) ; const fixedPath = path . split ( _path . sep ) . map ( ( ea ) => ( ea === "app.asar" ? "app.asar.unpacked" : ea ) ) . join ( _path . sep ) ; if ( _fs . existsSync ( fixedPath ) ) { return fixedPath ; } if ( _fs . existsSync ( path ) ) { return path ; } throw new Error ( `Vendored ExifTool does not exist at ${ path }` ) ; } export const DefaultExifToolPath = findExiftool ( ) ; export const DefaultExiftoolArgs = [ "-stay_open" , "True" , "-@" , "-" ] ; const _ignoreShebang = lazy ( ( ) => ! isWin32 ( ) && ! _fs . existsSync ( "/usr/bin/perl" ) ) ; see https export interface ShortcutTags { /**
     * Shortcut for writing the "common EXIF date/time tags": `DateTimeOriginal`,
     * `CreateDate`, and `ModifyDate` tags.
     *
     * Only used by `write`. This tag is not returned by `read`.
     */ AllDates ? : string ; } type AdditionalWriteTags = { "Orientation#" ? : number ; /**
     * Included because it's so rare, it doesn't always make the Tags build:
     */ TimeZoneOffset ? : number | string ; } ; type ExpandedDateTags = { [ K in keyof Tags ] : ( Tags [ K ] extends number ? number : Tags [ K ] extends ExifDateTime ? ExifDate | ExifDateTime : Tags [ K ] ) | string ; } ; type NotUndefined < T > = T extends undefined ? never : T ; export type DefinedOrNullValued < T > = { [ P in keyof T ] : NotUndefined < T [ P ] > | null ; } ; export type WriteTags = DefinedOrNullValued < ShortcutTags & AdditionalWriteTags & ExpandedDateTags > ; export const DefaultMaxProcs = Math . max ( 1 , Math . floor ( _os . cpus ( ) . length / 4 ) ) ; export interface ExifToolOptions extends bc . BatchClusterOptions , bc . BatchProcessOptions , bc . ChildProcessFactory { /**
     * The maximum number of ExifTool child processes to spawn when load merits.
     *
     * Defaults to 1/4 the number of CPUs, minimally 1.
     */ maxProcs : number ; /**
     * The maximum number of requests a given ExifTool process will service before
     * being retired.
     *
     * Defaults to 500, to balance performance with memory usage.
     */ maxTasksPerProcess : number ; /**
     * Spawning new ExifTool processes must not take longer than
     * `spawnTimeoutMillis` millis before it times out and a new attempt is made.
     * Be pessimistic here--windows can regularly take several seconds to spin up
     * a process, thanks to antivirus shenanigans. This can't be set to a value
     * less than 100ms.
     *
     * Defaults to 30 seconds, to accommodate slow Windows machines.
     */ spawnTimeoutMillis : number ; /**
     * If requests to ExifTool take longer than this,
     * presume the underlying process is dead and we should restart the task. This
     * can't be set to a value less than 10ms, and really should be set to at more
     * than a second unless `taskRetries` is sufficiently large or all writes will
     * be to a fast local disk. Defaults to 10 seconds.
     */ taskTimeoutMillis : number ; /**
     * An interval timer is scheduled to do periodic maintenance of underlying
     * child processes with this periodicity.
     *
     * Defaults to 2 seconds.
     */ onIdleIntervalMillis : number ; /**
     * The number of times a task can error or timeout and be retried.
     *
     * Defaults to 1 (every task gets 2 chances).
     */ taskRetries : number ; /**
     * Allows for non-standard paths to ExifTool. Defaults to the perl or windows
     * binaries provided by `exiftool-vendored.pl` or `exiftool-vendored.exe`.
     */ exiftoolPath : string ; /**
     * Args passed to exiftool on launch.
     */ exiftoolArgs : string [ ] ; /**
     * Environment variables passed to ExifTool (besides `EXIFTOOL_HOME`)
     */ exiftoolEnv : NodeJS . ProcessEnv ; /**
     * Tag names (which can have '*' glob matchers) which you want numeric values,
     * rather than ExifTool's "Print Conversion."
     *
     * If the tag value is only for human consumption, you may want to leave this
     * blank. The default is `["*Duration*"]`, but you may want to include
     * "Orientation" as well.
     */ numericTags : string [ ] ; /**
     * `ExifTool` has a shebang line that assumes a valid `perl` is installed at
     * `/usr/bin/perl`.
     *
     * Some environments may not include a valid `/usr/bin/perl` (like AWS
     * Lambda), but `perl` may be available in your `PATH` some place else (like
     * `/opt/bin/perl`), if you pull in a perl layer.
     *
     * This will default to `true` in those environments as a workaround in these
     * situations. Note also that `perl` will be spawned in a sub-shell.
     */ ignoreShebang : boolean ; } type Omit < T , K > = Pick < T , Exclude < keyof T , K > > ; /**
 * Default values for `ExifToolOptions`, except for `processFactory` (which is
 * created by the ExifTool constructor)
 */ export const DefaultExifToolOptions = Object . freeze ( { ... new bc . BatchClusterOptions ( ) , maxProcs : DefaultMaxProcs , maxTasksPerProcess : 500 , spawnTimeoutMillis : 30000 , taskTimeoutMillis : 20000 , onIdleIntervalMillis : 2000 , taskRetries : 1 , exiftoolPath : DefaultExifToolPath , exiftoolArgs : DefaultExiftoolArgs , exiftoolEnv : { } , pass : "{ready}" , fail : "{ready}" , exitCommand : "-stay_open\nFalse\n" , versionCommand : new VersionTask ( ) . command , numericTags : [ "*Duration*" , "GPSAltitude" , "GPSLatitude" , "GPSLongitude" , "GPSPosition" , "Orientation" , ] , } ) ; /**
 * Manages delegating calls to a vendored running instance of ExifTool.
 *
 * Instances should be shared: consider using the exported singleton
 * instance of this class, `exiftool`.
 */ export class ExifTool { readonly options : ExifToolOptions ; private readonly batchCluster : bc . BatchCluster ; constructor ( options = { } ) { if ( options != null && typeof options !== "object" ) { throw new Error ( "Please update caller to the new ExifTool constructor API" ) ; } const o = { ... DefaultExifToolOptions , ... options , } ; const ignoreShebang = o . ignoreShebang ?? _ignoreShebang ( ) ; const env = { ... o . exiftoolEnv , LANG : "C" } ; if ( notBlank ( _p . env . EXIFTOOL_HOME ) && blank ( env . EXIFTOOL_HOME ) ) { env . EXIFTOOL_HOME = _p . env . EXIFTOOL_HOME ; } const spawnOpts = { stdio : "pipe" , shell : ignoreShebang , detached : false , env , } ; const processFactory = ( ) => ignoreShebang ? _cp . spawn ( "perl" , [ o . exiftoolPath , ... o . exiftoolArgs ] , spawnOpts ) : _cp . spawn ( o . exiftoolPath , o . exiftoolArgs , spawnOpts ) ; this . options = { ... o , ignoreShebang , processFactory , exitCommand : o . exitCommand , versionCommand : o . versionCommand , ... options , } ; this . batchCluster = new bc . BatchCluster ( this . options ) ; } /**
     * Register life cycle event listeners. Delegates to BatchProcess.
     */ readonly on : bc . BatchCluster [ "on" ] = ( event , listener ) => this . batchCluster . on ( event , listener ) ; return version ( ) { return this . enqueueTask ( ( ) => new VersionTask ( ) ) ; } param { string } file param { string [ ] } args returns { Promise < Tags > } memberof read < T extends Tags = Tags > ( file , args = [ "-fast" ] ) { return this . enqueueTask ( ( ) => ReadTask . for ( file , this . options . numericTags , args ) ) as any ; } param args return see https readRaw ( file , args ) { return this . enqueueTask ( ( ) => ReadRawTask . for ( file , args ) ) ; } param { string } file param { Tags } tags param { string [ ] } args returns { Promise < void > } memberof write < T extends WriteTags = WriteTags > ( file , tags , args ? ) { return this . enqueueTask ( ( ) => WriteTask . for ( file , tags , args ) ) ; } /**
     * This will strip `file` of all metadata tags. The original file (with the
     * name `${FILENAME}_original`) will be retained. Note that some tags, like
     * stat information and image dimensions, are intrinsic to the file and will
     * continue to exist if you re-`read` the file.
     */ deleteAllTags ( file ) { return this . write ( file , { } , DeleteAllTagsArgs ) ; } return extractThumbnail ( imageFile , thumbnailFile ) { return this . extractBinaryTag ( "ThumbnailImage" , imageFile , thumbnailFile ) ; } return extractPreview ( imageFile , previewFile ) { return this . extractBinaryTag ( "PreviewImage" , imageFile , previewFile ) ; } return extractJpgFromRaw ( imageFile , outputFile ) { return this . extractBinaryTag ( "JpgFromRaw" , imageFile , outputFile ) ; } return async extractBinaryTag ( tagname , src , dest ) { const maybeError = await this . enqueueTask ( ( ) => BinaryExtractionTask . for ( tagname , src , dest ) ) ; if ( maybeError != null ) { throw new Error ( maybeError ) ; } } return async extractBinaryTagToBuffer ( tagname , imageFile ) { const result = await this . enqueueTask ( ( ) => BinaryToBufferTask . for ( tagname , imageFile ) ) ; if ( Buffer . isBuffer ( result ) ) { return result ; } else if ( result instanceof Error ) { throw result ; } else { throw new Error ( "Unexpected result from BinaryToBufferTask: " + JSON . stringify ( result ) ) ; } } /**
     * Attempt to fix metadata problems in JPEG images by deleting all metadata
     * and rebuilding from scratch. After repairing an image you should be able to
     * write to it without errors, but some metadata from the original image may
     * be lost in the process.
     *
     * This should only be applied as a last resort to images whose metadata is
     * not readable via  {@link .read()} .
     *
     *  see https param { string } inputFile param { string } outputFile param { boolean } allowMakerNoteRepair return { Promise < void > } rewriteAllTags ( inputFile , outputFile , allowMakerNoteRepair = false ) { return this . enqueueTask ( ( ) => RewriteAllTagsTask . for ( inputFile , outputFile , allowMakerNoteRepair ) ) ; } /**
     * Shut down running ExifTool child processes. No subsequent requests will be
     * accepted.
     *
     * This may need to be called in `after` or `finally` clauses in tests or
     * scripts for them to exit cleanly.
     */ end ( gracefully = true ) { return this . batchCluster . end ( gracefully ) . promise ; } return get ended ( ) { return this . batchCluster . ended ; } see BinaryExtractionTask enqueueTask < T > ( task ) { return retryOnReject ( async ( ) => this . batchCluster . enqueueTask ( await task ( ) ) , this . options . taskRetries ) ; } return get pids ( ) : Promise < number [ ] > { return this . batchCluster . pids ( ) ; } return get pendingTasks ( ) : number { return this . batchCluster . pendingTaskCount ; } return get spawnedProcs ( ) : number { return this . batchCluster . spawnedProcCount ; } return get busyProcs ( ) : number { return this . batchCluster . busyProcCount ; } return childEndCounts ( ) { return this . batchCluster . childEndCounts ; } } /**
 * Use this singleton rather than instantiating new ExifTool instances in order
 * to leverage a single running ExifTool process. As of v3.0, its `maxProcs` is
 * set to the number of CPUs on the current system; no more than `maxProcs`
 * instances of `exiftool` will be spawned. You may want to experiment with
 * smaller or larger values for `maxProcs`, depending on CPU and disk speed of
 * your system and performance tradeoffs.
 *
 * Note that each child process consumes between 10 and 50 MB of RAM. If you
 * have limited system resources you may want to use a smaller `maxProcs` value.
 */ export const exiftool = new ExifTool ( ) ;