exiftool-vendored
Advanced tools
Comparing version 0.3.0 to 0.4.0
@@ -7,2 +7,5 @@ export declare function compact<T>(array: T[]): T[]; | ||
} | ||
/** | ||
* Encodes an ExifTime (which may not have a timezone offset) | ||
*/ | ||
export declare class ExifTime { | ||
@@ -18,2 +21,5 @@ readonly hour: number; | ||
} | ||
/** | ||
* Encodes an ExifDate | ||
*/ | ||
export declare class ExifDate { | ||
@@ -28,2 +34,5 @@ readonly year: number; | ||
} | ||
/** | ||
* Encodes an ExifDateTime. | ||
*/ | ||
export declare class ExifDateTime extends Base { | ||
@@ -38,4 +47,12 @@ readonly year: number; | ||
static regex: RegExp; | ||
/** | ||
* Note that this may have fractional precision (123.456ms) | ||
*/ | ||
readonly millis: number; | ||
constructor(year: number, month: number, day: number, hour: number, minute: number, second: number, secondFraction?: number, tzoffsetMinutes?: number); | ||
/** | ||
* Note that this is most likely incorrect if the timezone offset is not set. | ||
* | ||
* See the README for details. | ||
*/ | ||
toDate(): Date; | ||
@@ -42,0 +59,0 @@ toISOString(): string; |
@@ -43,4 +43,11 @@ "use strict"; | ||
exports.Base = Base; | ||
/** | ||
* Encodes an ExifTime (which may not have a timezone offset) | ||
*/ | ||
class ExifTime { | ||
constructor(hour, minute, second, secondFraction, tzoffsetMinutes) { | ||
constructor(hour, // 1-23 | ||
minute, // 0-59 | ||
second, // 0-59 | ||
secondFraction, // 0-999 | ||
tzoffsetMinutes) { | ||
this.hour = hour; | ||
@@ -58,8 +65,13 @@ this.minute = minute; | ||
exports.ExifTime = ExifTime; | ||
/** | ||
* Encodes an ExifDate | ||
*/ | ||
class ExifDate { | ||
constructor(year, month, day) { | ||
constructor(year, // four-digit year | ||
month, // 1-12, (no crazy 0-11 nonsense from Date!) | ||
day) { | ||
this.year = year; | ||
this.month = month; | ||
this.day = day; | ||
} | ||
} // tslint:disable-line | ||
toString() { | ||
@@ -74,4 +86,13 @@ return `${this.year}-${pad2(this.month)}-${pad2(this.day)}`; | ||
exports.ExifDate = ExifDate; | ||
/** | ||
* Encodes an ExifDateTime. | ||
*/ | ||
class ExifDateTime extends Base { | ||
constructor(year, month, day, hour, minute, second, secondFraction, tzoffsetMinutes) { | ||
constructor(year, month, // 1-12, no crazy 0-11 nonsense | ||
day, // 1-31 | ||
hour, // 1-23 | ||
minute, // 0-59 | ||
second, // 0-59 | ||
secondFraction, // 0-999 | ||
tzoffsetMinutes) { | ||
super(); | ||
@@ -87,2 +108,7 @@ this.year = year; | ||
} | ||
/** | ||
* Note that this is most likely incorrect if the timezone offset is not set. | ||
* | ||
* See the README for details. | ||
*/ | ||
toDate() { | ||
@@ -96,2 +122,3 @@ if (this.tzoffsetMinutes === undefined) { | ||
else if (this.tzoffsetMinutes === 0) { | ||
// Don't leave it up to string parsing | ||
return new Date(Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second, this.millis)); | ||
@@ -108,2 +135,3 @@ } | ||
} | ||
// The timezone offset will be extricated prior to this regex: | ||
ExifDateTime.regex = /^(\d{4})[ :]+(\d{2})[ :]+(\d{2})[ :]+(\d{2})[ :]+(\d{2})[ :]+(\d{2})(?:\.(\d{1,6}))?$/; | ||
@@ -161,2 +189,3 @@ exports.ExifDateTime = ExifDateTime; | ||
exports.ExifTimeZoneOffset = ExifTimeZoneOffset; | ||
// workaround for the fact that the spread operator doesn't work for constructors (!!?): | ||
const newDateTime = _new(ExifDateTime.regex, (a) => { | ||
@@ -171,3 +200,3 @@ return new ExifDateTime(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); | ||
}); | ||
const emptyRe = /^[\s:]*$/; | ||
const emptyRe = /^[\s:0]*$/; // Some empty datetimes come back as " : : " | ||
function parse(tagName, rawTagValue, globalTzOffset) { | ||
@@ -178,2 +207,3 @@ if (rawTagValue === undefined || emptyRe.exec(rawTagValue)) { | ||
const tz = new ExifTimeZoneOffset(tagName, rawTagValue); | ||
// If it's just a timezone: | ||
if (tz.tzOffsetMinutes !== undefined && emptyRe.exec(tz.inputWithoutTimezone)) { | ||
@@ -190,2 +220,1 @@ return tz; | ||
exports.parse = parse; | ||
//# sourceMappingURL=datetime.js.map |
@@ -47,3 +47,3 @@ "use strict"; | ||
describe('example strings with tz', () => { | ||
const dt = _dt.parse('DateTimeOriginal', '2013:12:30 11:04:15-05:00'); | ||
const dt = _dt.parse('DateTimeOriginal', '2013:12:30 11:04:15-05:00'); // non-local offset | ||
it('year/month/day', () => { | ||
@@ -139,2 +139,8 @@ chai_1.expect([dt.year, dt.month, dt.day]).to.eql([2013, 12, 30]); | ||
}); | ||
//# sourceMappingURL=datetime.spec.js.map | ||
describe('parsing empty/invalid input', () => { | ||
['', ' ', '0000:00:00 00:00:00', ' : : : : '].forEach(bad => { | ||
it(bad, () => { | ||
chai_1.expect(_dt.parse('DateTimeOriginal', bad)).to.eql(bad); | ||
}); | ||
}); | ||
}); |
@@ -23,2 +23,1 @@ "use strict"; | ||
exports.Deferred = Deferred; | ||
//# sourceMappingURL=deferred.js.map |
@@ -6,2 +6,5 @@ import { Task } from './task'; | ||
export declare function ellipsize(str: string, max: number): string; | ||
/** | ||
* Manages a child process. Callers need to restart if ended. | ||
*/ | ||
export declare class ExifToolProcess { | ||
@@ -17,3 +20,9 @@ private readonly taskProvider; | ||
end(): void; | ||
/** | ||
* @return true if `end()` was called, or the child process has closed | ||
*/ | ||
readonly ended: boolean; | ||
/** | ||
* @return true if the child process has closed | ||
*/ | ||
readonly closed: boolean; | ||
@@ -20,0 +29,0 @@ readonly closedPromise: Promise<void>; |
@@ -17,2 +17,5 @@ "use strict"; | ||
exports.ellipsize = ellipsize; | ||
/** | ||
* Manages a child process. Callers need to restart if ended. | ||
*/ | ||
class ExifToolProcess { | ||
@@ -25,6 +28,6 @@ constructor(taskProvider) { | ||
this.proc = cp.spawn(exiftoolPath, ['-stay_open', 'True', '-@', '-']); | ||
this.proc.unref(); | ||
this.proc.unref(); // don't let node count ExifTool as a reason to stay alive | ||
this.proc.stdout.on('data', d => this.onData(d)); | ||
this.proc.stderr.on('data', d => this.onError(d)); | ||
this.proc.on('close', (code) => { | ||
this.proc.on('close', () => { | ||
this._ended = true; | ||
@@ -43,5 +46,11 @@ this._closedDeferred.resolve(); | ||
} | ||
/** | ||
* @return true if `end()` was called, or the child process has closed | ||
*/ | ||
get ended() { | ||
return this._ended; | ||
} | ||
/** | ||
* @return true if the child process has closed | ||
*/ | ||
get closed() { | ||
@@ -64,3 +73,3 @@ return this._closedDeferred.fulfilled; | ||
'-execute', | ||
'' | ||
'' // Need to end -execute with a newline | ||
].join('\n'); | ||
@@ -90,3 +99,3 @@ this.proc.stdin.write(cmd); | ||
this.currentTask = undefined; | ||
this.workIfIdle(); | ||
this.workIfIdle(); // start the next job running before parsing this one to minimize latency | ||
if (task === undefined) { | ||
@@ -106,2 +115,1 @@ if (buff.length > 0) { | ||
exports.ExifToolProcess = ExifToolProcess; | ||
//# sourceMappingURL=exiftool_process.js.map |
@@ -15,2 +15,1 @@ "use strict"; | ||
}); | ||
//# sourceMappingURL=exiftool_process.spec.js.map |
@@ -7,4 +7,16 @@ /// <reference types="node" /> | ||
export interface ExifToolAPI { | ||
/** | ||
* @return a promise holding the version number of the vendored ExifTool | ||
*/ | ||
version(): Promise<string>; | ||
/** | ||
* @return a Promise holding the metadata tags found in `file`. | ||
*/ | ||
read(file: string): Promise<Tags>; | ||
/** | ||
* Request graceful shut down of any running ExifTool child processes. | ||
* | ||
* This may need to be called in `after` or `finally` clauses in tests | ||
* or scripts for them to exit cleanly. | ||
*/ | ||
end(): void; | ||
@@ -17,4 +29,16 @@ } | ||
} | ||
/** | ||
* Assign your custom logger to this instance. | ||
*/ | ||
export declare let logger: Console; | ||
/** | ||
* This is the version of the `exiftool-vendored` npm module. | ||
* The package.json value is made to match this value by `npm run update`. | ||
*/ | ||
export declare const ExifToolVendoredVersion: string; | ||
/** | ||
* Manages delegating calls to a vendored running instance of ExifTool. | ||
* | ||
* Instantiation is expensive: use the exported singleton instance of this class, `exiftool`. | ||
*/ | ||
export declare class ExifTool implements ExifToolAPI { | ||
@@ -24,6 +48,21 @@ readonly maxProcs: number; | ||
private _tasks; | ||
/** | ||
* @param maxProcs the maximum number of ExifTool child processes to spawn when load merits | ||
*/ | ||
constructor(maxProcs?: number); | ||
/** | ||
* @return a Promise to the vendored ExifTool's version | ||
*/ | ||
version(): Promise<string>; | ||
/** | ||
* @return a Promise holding the metadata tags found in `file`. | ||
*/ | ||
read(file: string): Promise<Tags>; | ||
end(): Promise<void>; | ||
/** | ||
* Request graceful shut down of any running ExifTool child processes. | ||
* | ||
* This may need to be called in `after` or `finally` clauses in tests | ||
* or scripts for them to exit cleanly. | ||
*/ | ||
end(): Promise<any>; | ||
enqueueTask<T>(task: Task<T>): Task<T>; | ||
@@ -34,2 +73,6 @@ private dequeueTask(); | ||
} | ||
/** | ||
* Use this singleton rather than instantiating new ExifTool instances | ||
* in order to leverage a single running ExifTool process. | ||
*/ | ||
export declare const exiftool: ExifToolAPI; |
@@ -10,5 +10,20 @@ "use strict"; | ||
exports.ExifTimeZoneOffset = datetime_1.ExifTimeZoneOffset; | ||
/** | ||
* Assign your custom logger to this instance. | ||
*/ | ||
exports.logger = console; | ||
exports.ExifToolVendoredVersion = '0.3.0'; | ||
/** | ||
* This is the version of the `exiftool-vendored` npm module. | ||
* The package.json value is made to match this value by `npm run update`. | ||
*/ | ||
exports.ExifToolVendoredVersion = '0.4.0'; | ||
/** | ||
* Manages delegating calls to a vendored running instance of ExifTool. | ||
* | ||
* Instantiation is expensive: use the exported singleton instance of this class, `exiftool`. | ||
*/ | ||
class ExifTool { | ||
/** | ||
* @param maxProcs the maximum number of ExifTool child processes to spawn when load merits | ||
*/ | ||
constructor(maxProcs = 1) { | ||
@@ -18,9 +33,21 @@ this.maxProcs = maxProcs; | ||
this._tasks = []; | ||
} | ||
} // tslint:disable-line | ||
/** | ||
* @return a Promise to the vendored ExifTool's version | ||
*/ | ||
version() { | ||
return this.enqueueTask(new version_task_1.VersionTask()).promise; | ||
} | ||
/** | ||
* @return a Promise holding the metadata tags found in `file`. | ||
*/ | ||
read(file) { | ||
return this.enqueueTask(tags_task_1.TagsTask.for(file)).promise; | ||
} | ||
/** | ||
* Request graceful shut down of any running ExifTool child processes. | ||
* | ||
* This may need to be called in `after` or `finally` clauses in tests | ||
* or scripts for them to exit cleanly. | ||
*/ | ||
end() { | ||
@@ -52,3 +79,6 @@ this._procs.forEach(p => p.end()); | ||
exports.ExifTool = ExifTool; | ||
/** | ||
* Use this singleton rather than instantiating new ExifTool instances | ||
* in order to leverage a single running ExifTool process. | ||
*/ | ||
exports.exiftool = new ExifTool(); | ||
//# sourceMappingURL=exiftool.js.map |
@@ -29,3 +29,3 @@ "use strict"; | ||
function normalize(tagNames) { | ||
return tagNames.filter(i => i !== "FileInodeChangeDate" && i !== 'FileCreateDate').sort(); | ||
return tagNames.filter(i => i !== 'FileInodeChangeDate' && i !== 'FileCreateDate').sort(); | ||
} | ||
@@ -62,2 +62,1 @@ it('returns no exif metadata for an image with no headers', () => { | ||
}); | ||
//# sourceMappingURL=exiftool.spec.js.map |
"use strict"; | ||
//# sourceMappingURL=parser.js.map |
@@ -25,4 +25,7 @@ "use strict"; | ||
this.rawTags = JSON.parse(data)[0]; | ||
// ExifTool does humorous things to paths, like flip slashes. resolve() undoes that. | ||
const SourceFile = _path.resolve(this.rawTags.SourceFile); | ||
// Sanity check that the result is for the file we want: | ||
if (SourceFile !== this.SourceFile) { | ||
// Throw an error rather than add an errors string because this is *really* bad: | ||
throw new Error(`Internal error: unexpected SourceFile of ${this.rawTags.SourceFile} for file ${this.SourceFile}`); | ||
@@ -36,2 +39,3 @@ } | ||
extractTzoffset() { | ||
// TimeZone just wins if we're just handed it, then use it: | ||
const tze = new _dt.ExifTimeZoneOffset('TimeZone', this.rawTags.TimeZone); | ||
@@ -45,2 +49,3 @@ if (tze.tzOffsetMinutes !== undefined) { | ||
if (gps && local) { | ||
// timezone offsets are never less than 30 minutes. | ||
const gpsToHalfHour = gps.toDate().getTime() / (30 * 60 * 1000); | ||
@@ -63,3 +68,3 @@ const localToHalfHour = local.toDate().getTime() / (30 * 60 * 1000); | ||
|| tagName.endsWith('Firmware') || tagName.endsWith('DateDisplayFormat')) { | ||
return value.toString(); | ||
return value.toString(); // force to string | ||
} | ||
@@ -79,3 +84,3 @@ else if (tagName.endsWith('BitsPerSample')) { | ||
if (ref === undefined) { | ||
return value; | ||
return value; // give up | ||
} | ||
@@ -99,2 +104,1 @@ else { | ||
exports.TagsTask = TagsTask; | ||
//# sourceMappingURL=tags_task.js.map |
@@ -14,2 +14,11 @@ "use strict"; | ||
describe('Lat/Lon parsing', () => { | ||
/* Example: | ||
$ exiftool -j -coordFormat '%.8f' -fast ../test-images/important/Apple_iPhone7Plus.jpg | grep itude | ||
"GPSLatitudeRef": "North", | ||
"GPSLongitudeRef": "East", | ||
"GPSAltitudeRef": "Above Sea Level", | ||
"GPSAltitude": "73 m Above Sea Level", | ||
"GPSLatitude": "22.33543889 N", | ||
"GPSLongitude": "114.16401667 E", | ||
*/ | ||
it('N lat is positive', () => { | ||
@@ -82,2 +91,1 @@ chai_1.expect(parse({ GPSLatitude: '22.33543889 N' }).GPSLatitude).to.be.closeTo(22.33543889, 0.00001); | ||
}); | ||
//# sourceMappingURL=tags_task.spec.js.map |
"use strict"; | ||
//# sourceMappingURL=tags.js.map |
import { Deferred } from './deferred'; | ||
/** | ||
* Emodies both a command (`args`), and a handler for the resulting output | ||
*/ | ||
export declare abstract class Task<T> extends Deferred<T> { | ||
@@ -3,0 +6,0 @@ readonly args: string[]; |
"use strict"; | ||
const deferred_1 = require('./deferred'); | ||
/** | ||
* Emodies both a command (`args`), and a handler for the resulting output | ||
*/ | ||
class Task extends deferred_1.Deferred { | ||
@@ -18,2 +21,1 @@ constructor(args) { | ||
exports.Task = Task; | ||
//# sourceMappingURL=task.js.map |
@@ -0,2 +1,17 @@ | ||
/** | ||
* If the vendored ExifTool version is different from | ||
* the current latest version, download and extract | ||
* into the appropriate sibling directories, and update | ||
* their package.json. | ||
* | ||
* Running tests, committing to git, and publishing the npm are | ||
* manual subsequent steps. | ||
*/ | ||
export declare class Checksums { | ||
/** | ||
* @param checksums is a newline-separated text file with the following format: | ||
* $HASHTYPE($FILENAME) = $HASH | ||
* | ||
* We only care about SHA1, so we're pulling just those out. | ||
*/ | ||
private static readonly regex; | ||
@@ -3,0 +18,0 @@ private readonly store; |
"use strict"; | ||
const io_1 = require('./io'); | ||
/** | ||
* If the vendored ExifTool version is different from | ||
* the current latest version, download and extract | ||
* into the appropriate sibling directories, and update | ||
* their package.json. | ||
* | ||
* Running tests, committing to git, and publishing the npm are | ||
* manual subsequent steps. | ||
*/ | ||
class Checksums { | ||
@@ -21,4 +30,9 @@ constructor(checksums) { | ||
} | ||
/** | ||
* @param checksums is a newline-separated text file with the following format: | ||
* $HASHTYPE($FILENAME) = $HASH | ||
* | ||
* We only care about SHA1, so we're pulling just those out. | ||
*/ | ||
Checksums.regex = /SHA1 ?\((\S+)\) ?= ?([a-f0-9]+)/i; | ||
exports.Checksums = Checksums; | ||
//# sourceMappingURL=checksums.js.map |
@@ -14,3 +14,3 @@ "use strict"; | ||
this.version = version; | ||
} | ||
} // tslint:disable-line | ||
static parsedPath(url) { | ||
@@ -57,4 +57,4 @@ const parsedUrlPathname = _url.parse(url).pathname; | ||
} | ||
// The file suffix and the path will already be stripped out at this point | ||
Enclosure.regex = /.*?([\d\.]+)(\.tar\.gz|\.zip)$/; | ||
exports.Enclosure = Enclosure; | ||
//# sourceMappingURL=enclosure.js.map |
@@ -194,2 +194,1 @@ "use strict"; | ||
exports.mkdir = mkdir; | ||
//# sourceMappingURL=io.js.map |
@@ -9,2 +9,3 @@ "use strict"; | ||
const _path = require('path'); | ||
// ☠☠ THIS IS PRETTY GRISLY CODE. SCROLL DOWN AT YOUR OWN PERIL ☠☠ | ||
const globule = require('globule'); | ||
@@ -56,6 +57,7 @@ function usage() { | ||
this.values = []; | ||
} | ||
} // tslint:disable-line | ||
get group() { return this.tag.split(':')[0]; } | ||
get withoutGroup() { return this.tag.split(':')[1]; } | ||
get valueType() { | ||
// hard-coded because the ☆☆☆ ITPC tag doesn't match the ★★★ Composite tag, causing Tags not to compile | ||
if (this.withoutGroup === 'DateCreated') { | ||
@@ -67,2 +69,3 @@ return 'ExifDate'; | ||
const byCount = cm.byCountDesc().slice(0, 2); | ||
// If an "Exif*" type is common, let's use that instead of string. | ||
if (byCount[0] === 'string' && ('' + byCount[1]).startsWith('Exif')) { | ||
@@ -135,3 +138,3 @@ byCount.shift(); | ||
const saneTagRe = /^[a-z0-9_]+:[a-z0-9_]+$/i; | ||
const exiftool = new exiftool_1.ExifTool(4); | ||
const exiftool = new exiftool_1.ExifTool(4); // so I can use `enqueueTask` | ||
const start = Date.now(); | ||
@@ -159,3 +162,3 @@ Promise.all(files.map(file => { | ||
const tagWriter = _fs.createWriteStream(destFile); | ||
tagWriter.write('/* tslint:disable:class-name */\n'); | ||
tagWriter.write('/* tslint:disable:class-name */\n'); // because of ICC_Profile | ||
tagWriter.write(`import { ExifDate, ExifTime, ExifDateTime, ExifTimeZoneOffset } from './datetime'\n\n`); | ||
@@ -184,3 +187,2 @@ tagWriter.write(`// Autogenerated by "npm run mktags" by ExifTool ${version} on ${new Date().toDateString()}.\n\n`); | ||
tagWriter.write('}\n'); | ||
tagWriter.write('\n'); | ||
tagWriter.end(); | ||
@@ -192,2 +194,1 @@ }).catch(err => { | ||
}); | ||
//# sourceMappingURL=mktags.js.map |
@@ -19,3 +19,4 @@ "use strict"; | ||
downloadMaybeAndVerify() { | ||
return this.verify().catch(err => io.rmrf(this.dlDest, true) | ||
// tslint:disable-next-line: handle-callback-err | ||
return this.verify().catch(() => io.rmrf(this.dlDest, true) | ||
.then(() => this.download()) | ||
@@ -70,2 +71,3 @@ .then(() => this.verify())); | ||
.then(() => { | ||
// The tarball is prefixed with "Image-ExifTool-VERSION". Move that subdirectory into bin proper. | ||
const subdir = globule.find(_path.join(tmpUnpack, `Image-ExifTool*${_path.sep}`)); | ||
@@ -115,2 +117,1 @@ if (subdir.length !== 1) { | ||
update(); | ||
//# sourceMappingURL=update.js.map |
@@ -19,2 +19,1 @@ "use strict"; | ||
exports.VersionTask = VersionTask; | ||
//# sourceMappingURL=version_task.js.map |
{ | ||
"name": "exiftool-vendored", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Efficient, cross-platform access to ExifTool", | ||
@@ -18,2 +18,3 @@ "main": "./lib/exiftool.js", | ||
"cleantest": "run-s clean lint fmt pdm test", | ||
"preversion": "run-s lint pdm", | ||
"pdm": "platform-dependent-modules", | ||
@@ -79,8 +80,10 @@ "postinstall": "npm run pdm" | ||
"mocha": "^3.1.2", | ||
"np": "^2.9.0", | ||
"npm-run-all": "^3.1.1", | ||
"rimraf": "^2.5.4", | ||
"tar-fs": "^1.14.0", | ||
"tslint": "^3.15.1", | ||
"tslint-config-standard": "^1.5.0", | ||
"typescript": "^2.0.3", | ||
"typescript-formatter": "^3.1.0", | ||
"typescript": "^2.0.8", | ||
"typescript-formatter": "^4.0.0", | ||
"xmldom": "^0.1.22", | ||
@@ -87,0 +90,0 @@ "xpath": "0.0.23" |
@@ -11,4 +11,6 @@ # exiftool-vendored | ||
1. **High performance** via [`-stay_open`](#stay_open) and [multithreading](#parallelism), with [7-300x faster](#performance) than competing packages | ||
1. **High performance** via [`-stay_open`](#stay_open) and [multithreading](#parallelism). [7-300x faster](#performance) than competing packages | ||
1. Support for [Mac, Linux](https://travis-ci.org/mceachen/exiftool-vendored), and [Windows](https://ci.appveyor.com/project/mceachen/exiftool-vendored/branch/master). | ||
1. Proper extraction of | ||
@@ -115,2 +117,7 @@ - **dates** with [correct timezone offset encoding, when available](#dates)) | ||
### v0.4.0 | ||
* Fixed packaging (maintain jsdocs in .d.ts) | ||
* Using [np](https://www.npmjs.com/package/np) for packaging | ||
### v0.3.0 | ||
@@ -117,0 +124,0 @@ |
Sorry, the diff of this file is too big to display
4271
135
155442
22
39