From 49245ba9d180c3441ee6beb2bb8a153ba4fdb014 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Tue, 3 Nov 2020 04:49:56 +0300 Subject: [PATCH] reworked metadata caching + updated peer feature (not done yet)... Signed-off-by: Alex A. Naanou --- Viewer/features/metadata.js | 51 +++++-------- Viewer/features/peer.js | 138 +++++------------------------------- Viewer/features/sharp.js | 138 +++++++++++++++++++++++++++++------- Viewer/package-lock.json | 6 +- Viewer/package.json | 2 +- 5 files changed, 151 insertions(+), 184 deletions(-) diff --git a/Viewer/features/metadata.js b/Viewer/features/metadata.js index 93e890a6..9f51df47 100755 --- a/Viewer/features/metadata.js +++ b/Viewer/features/metadata.js @@ -57,8 +57,7 @@ var MetadataActions = actions.Actions({ if(this.images && this.images[gid]){ return this.images[gid].metadata || {} } - return null - }], + return null }], setMetadata: ['- Image/Set metadata data', function(image, metadata, merge){ var that = this @@ -70,12 +69,8 @@ var MetadataActions = actions.Actions({ Object.keys(metadata).forEach(function(k){ m[k] = metadata[k] }) - } else { - this.images[gid].metadata = metadata - } - } - }] + this.images[gid].metadata = metadata } } }] }) var Metadata = @@ -119,21 +114,19 @@ var MetadataReaderActions = actions.Actions({ var img = this.images && this.images[gid] if(!image && !img){ - return false - } + return false } //var full_path = path.normalize(img.base_path +'/'+ img.path) var full_path = this.getImagePath(gid) return new Promise(function(resolve, reject){ - if(!force && img.metadata){ - return resolve(img.metadata) - } + if(!force + && !(img.metadata || {}).ImageGridPartialMetadata){ + return resolve(img.metadata) } fs.readFile(full_path, function(err, file){ if(err){ - return reject(err) - } + return reject(err) } // read stat... if(!that.images[gid].birthtime){ @@ -173,10 +166,7 @@ var MetadataReaderActions = actions.Actions({ that.markChanged && that.markChanged('images', [gid]) } - resolve(data) }) - }) - }) - }], + resolve(data) }) }) }) }], // XXX STUB: add support for this to .readMetadata(..) readAllMetadata: ['File/Read all metadata', @@ -202,8 +192,7 @@ var MetadataReaderActions = actions.Actions({ q.enqueue('metadata', read(gid)) }) - return q - }], + return q }], // XXX take image Metadata and write it to target... writeMetadata: ['- Image/Set metadata data', @@ -651,9 +640,7 @@ var MetadataUIActions = actions.Actions({ && (that.config['metadata-graph-config'] = { graph: this.graph.graph, mode: this.graph.mode, - }) - }) - })], + }) }) })], metadataSection: ['- Image/', { sortedActionPriority: 'normal' }, @@ -663,16 +650,18 @@ var MetadataUIActions = actions.Actions({ var field_order = this.config['metadata-field-order'] || [] var x = field_order.length + 1 + // NOTE: this is called on showMetadata.pre in the .handlers + // feature section... make.dialog.updateMetadata = function(metadata){ - metadata = metadata || that.getMetadata() + metadata = metadata + || that.getMetadata() // build new data set and update view... //this.options.data = _buildInfoList(image, metadata) this.update() - return this - } + return this } // build fields... var fields = [] @@ -691,15 +680,12 @@ var MetadataUIActions = actions.Actions({ return } else if(mode == 'disabled') { - opts.disabled = true - } - } + opts.disabled = true } } fields.push([ [ n + ': ', metadata[k] ], opts, - ]) - }) + ]) }) // make fields... fields @@ -717,8 +703,7 @@ var MetadataUIActions = actions.Actions({ this.length > 0 && make.Separator() }) .forEach(function(e){ - make(...e) }) - })], + make(...e) }) })], // shorthands... diff --git a/Viewer/features/peer.js b/Viewer/features/peer.js index f6545301..1feae936 100755 --- a/Viewer/features/peer.js +++ b/Viewer/features/peer.js @@ -19,114 +19,13 @@ var features = require('lib/features') var core = require('features/core') var object = require('lib/object') +var types = require('lib/types') /*********************************************************************/ // helpers... -// Cooperative promise object... -// -// This is like a promise but is not resolved internally, rather this -// resolves (is set) via a different promise of value passed to it via -// the .set(..) method... -// -// Example: -// // create a promise... -// var p = (new CooperativePromise()) -// // bind normally... -// .then(function(){ .. }) -// -// // this will resolve p and trigger all the .then(..) callbacks... -// p.set(new Promise(function(resolve, reject){ resolve() })) -// -// Note that .set(..) can be passed any value, passing a non-promise has -// the same effect as passing the same value to resolve(..) of a Promise -// object... -// -// XXX should this be a separate package??? -// XXX can we make this an instance of Promise for passing the -// x instanceof Promise test??? -var CooperativePromisePrototype = { - __base: null, - __promise: null, - - // XXX error if already set... - set: function(promise){ - if(this.__promise == null){ - // setting a non-promise... - if(promise.catch == null && promise.then == null){ - Object.defineProperty(this, '__promise', { - value: false, - enumerable: false, - }) - this.__resolve(promise) - - // setting a promise... - } else { - Object.defineProperty(this, '__promise', { - value: promise, - enumerable: false, - }) - - // connect the base and the set promises... - promise.catch(this.__reject.bind(this)) - promise.then(this.__resolve.bind(this)) - - // cleanup... - delete this.__base - } - - // cleanup... - delete this.__resolve - delete this.__reject - - } else { - // XXX throw err??? - console.error('Setting a cooperative promise twice', this) - } - }, - - // Promise API... - catch: function(func){ - return (this.__promise || this.__base).catch(func) }, - then: function(func){ - return (this.__promise || this.__base).then(func) }, - - __init__: function(){ - var that = this - var base = new Promise(function(resolve, reject){ - Object.defineProperties(that, { - __resolve: { - value: resolve, - enumerable: false, - configurable: true, - }, - __reject: { - value: reject, - enumerable: false, - configurable: true, - }, - }) - }) - - Object.defineProperty(this, '__base', { - value: base, - enumerable: false, - configurable: true, - }) - }, -} - -var CooperativePromise = -module.CooperativePromise = -object.Constructor('CooperativePromise', - Promise, - CooperativePromisePrototype) - - -//--------------------------------------------------------------------- - // XXX would be nice to list the protocols supported by the action in // an action attr... var makeProtocolHandler = @@ -192,8 +91,7 @@ var PeerActions = actions.Actions({ return that.getActionAttr(action, '__peer__') == id } // get all peer actions... : function(action){ - return that.getActionAttr(action, '__peer__') }) - }], + return that.getActionAttr(action, '__peer__') }) }], // XXX should this also check props??? isPeerAction: ['- System/Peer/', function(name){ @@ -208,9 +106,11 @@ var PeerActions = actions.Actions({ // XXX the events should get called on the peer too -- who is // responsible for this??? peerConnect: ['- System/Peer/', - function(id, options){ return new CooperativePromise() }], + function(id, options){ + return new Promise.cooperative() }], peerDisconnect: ['- System/Peer/', - function(id){ return new CooperativePromise() }], + function(id){ + return new Promise.cooperative() }], // events... // XXX do proper docs... @@ -229,13 +129,14 @@ var PeerActions = actions.Actions({ peerCall: ['- System/Peer/', function(id, action){ var args = [...arguments].slice(2) - return this.peerApply(id, action, args) - }], + return this.peerApply(id, action, args) }], peerApply: ['- System/Peer/', - function(id, action, args){ return new CooperativePromise() }], + function(id, action, args){ + return new Promise.cooperative() }], peerList: ['- System/Peer/', - function(){ return Object.keys(this.__peers || {}) }], + function(){ + return Object.keys(this.__peers || {}) }], // XXX do we need these??? // XXX format spec!!! @@ -287,8 +188,7 @@ var ChildProcessPeerActions = actions.Actions({ if(this.__peers && id in this.__peers && this.__peers[id].peer.connected){ - return resolve(id) - } + return resolve(id) } this.__peers = this.__peers || {} @@ -307,13 +207,9 @@ var ChildProcessPeerActions = actions.Actions({ callback && (delete this.__peer_result_callbacks[msg.id]) - && callback(msg.value, msg.error) - } - }).bind(this)) + && callback(msg.value, msg.error) } }).bind(this)) - resolve(id) - }).bind(this)) - })], + resolve(id) }).bind(this)) })], // XXX should this call .stop() on the child??? // ...does the child handle kill gracefully??? peerDisconnect: ['- System/Peer/', @@ -329,8 +225,7 @@ var ChildProcessPeerActions = actions.Actions({ that.__peers[id].peer.kill() delete that.__peers[id] - }).bind(this)) - })], + }).bind(this)) })], // XXX can we do sync??? // ...this would be useful to 100% match the action api and @@ -358,8 +253,7 @@ var ChildProcessPeerActions = actions.Actions({ var handlers = this.__peer_result_callbacks = this.__peer_result_callbacks || {} handlers[call_id] = function(res, err){ err ? reject(err) : resolve(res) } - }).bind(this)) - })], + }).bind(this)) })], }) diff --git a/Viewer/features/sharp.js b/Viewer/features/sharp.js index d2de84ff..72cd5a73 100755 --- a/Viewer/features/sharp.js +++ b/Viewer/features/sharp.js @@ -32,13 +32,19 @@ if(typeof(process) != 'undefined'){ /*********************************************************************/ +// helpers... if(typeof(process) != 'undefined'){ var copy = file.denodeify(fse.copy) var ensureDir = file.denodeify(fse.ensureDir) } -function normalizeOrientation(orientation){ + +//--------------------------------------------------------------------- + +var normalizeOrientation = +module.normalizeOrientation = +function(orientation){ return { orientation: ({ 0: 0, @@ -65,9 +71,94 @@ function normalizeOrientation(orientation){ } } -var exifReader2exiftool = { + +//--------------------------------------------------------------------- +// Convert image metadata from exif-reader output to format compatible +// with exiftool (features/metadata.js) + +// Format: +// { +// // simple key-key pair... +// 'path.to.value': 'output-key', +// +// // key with value handler... +// 'path.to.other.value': ['output-key', handler], +// +// // alias to handler... +// 'path.to.yet.another.value': ['output-key', 'path.to.other.value'], +// } +// +var EXIF_FORMAT = +module.EXIF_FORMAT = { + // camera / lens... + 'image.Make': 'make', + 'image.Model': 'cameraModelName', + 'image.Software': 'software', + 'exif.LensModel': 'lensModel', + + // exposure... + 'exif.ISO': 'iso', + 'exif.FNumber': ['fNumber', + function(v){ return 'f/'+v }], + 'exif.ExposureTime': ['exposureTime', + // NOTE: this is a bit of a brute-fore approach but for shutter + // speeds this should not matter... + function(v){ + if(v > 0.5){ + return ''+ v } + for(var d = 1; (v * d) % 1 != 0; d++){} + return (v * d) +'/'+ d }], + + // dates... + 'exif.DateTimeOriginal': ['date/timeOriginal', + function(v){ + return v.toShortDate() }], + 'image.ModifyDate': ['modifyDate', + 'exif.DateTimeOriginal'], + + // IPCT... + 'image.Artist': 'artist', + 'image.Copyright': 'copyright', + + // XXX anything else??? } +var exifReader2exiftool = +module.exifReader2exiftool = +function(data){ + return Object.entries(EXIF_FORMAT) + // handle exif/image/... + .reduce(function(res, [path, to]){ + var handler + ;[to, handler] = to instanceof Array ? + to + : [to] + // resolve handler reference/alias... + while(typeof(handler) == typeof('str')){ + handler = EXIF_FORMAT[handler][1] } + // resolve source path... + var value = path.split(/\./g) + .reduce(function(res, e){ + return res && res[e] }, data) + // set the value... + if(value !== undefined){ + res[to] = handler ? + handler(value) + : value } + return res }, {}) + // handle xmp... + .run(function(){ + var rating = data.xmp + // NOTE: we do not need the full XML + // fluff here, just get some values... + && parseInt( + (data.xmp.toString() + .match(/(?<(xmp:Rating)[^>]*>(?.*)<\/\2>)/i) + || {groups: {}}) + .groups.value) + rating + && (this.rating = rating) }) } + @@ -423,9 +514,10 @@ var SharpActions = actions.Actions({ .flat()) }], - // XXX should this update all images or just the ones that have no metadata??? + // XXX add support for offloading the processing to a thread/worker... // XXX would be nice to be able to abort this... // ...and/or have a generic abort protocol triggered when loading... + // ...use task queue??? // XXX make each section optional... // XXX revise name... cacheImageMetadata: ['- Sharp|Image/', @@ -498,6 +590,20 @@ var SharpActions = actions.Actions({ img.orientation = o.orientation || 0 img.flipped = o.flipped + // read the metadata... + var exif = metadata.exif + && exifReader(metadata.exif) + exif + && Object.assign( + (img.metadata = img.metadata || {}), + exifReader2exiftool(exif), + // mark metadata as partial read... + // + // NOTE: partial metadata will get reread by + // the metadata feature upon request... + // XXX revise name... + { ImageGridPartialMetadata: true }) + // if image too large, generate preview(s)... // XXX EXPERIMENTAL... var size_threshold = that.config['preview-generate-threshold'] @@ -513,31 +619,11 @@ var SharpActions = actions.Actions({ base_path, logger) } - // XXX EXIF -- keep compatible with exiftool... - // - dates - // - camera / lens / ... - var exif = metadata.exif - && exifReader(metadata.exif) - // XXX - - // xmp:Rating... - var rating = metadata.xmp - // NOTE: we do not need the full XML - // fluff here, just get some values... - && parseInt( - (metadata.xmp.toString() - .match(/(?<(xmp:Rating)[^>]*>(?.*)<\/\2>)/i) - || {groups: {}}) - .groups.value) - rating - && (img.metadata = img.metadata || {}) - && (img.metadata.rating = rating || 0) - that.markChanged('images', [gid]) logger && logger.emit('done', gid) - // update image to use the orientation... + // update loaded image to use the orientation... loaded && loaded.has(gid) && that.ribbons.updateImage(gid) @@ -564,9 +650,11 @@ module.Sharp = core.ImageGridFeatures.Feature({ handlers: [ /* XXX not sure if we need this... + // XXX this is best done in a thread + needs to be abortable... ['loadImages', function(){ - this.cacheImageMetadata('all', false) }], + //this.cacheImageMetadata('all', false) }], + this.cacheImageMetadata('all') }], //*/ // set orientation if not defined... diff --git a/Viewer/package-lock.json b/Viewer/package-lock.json index fc4d3fbc..79908f93 100755 --- a/Viewer/package-lock.json +++ b/Viewer/package-lock.json @@ -1117,9 +1117,9 @@ "integrity": "sha512-EzT4CP6d6lI8bnknNgT3W8mUQhSVXflO0yPbKD4dKsFcINiC6npjoEBz+8m3VQmWJhc+36pXD4JLwNxUEgzi+Q==" }, "ig-types": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/ig-types/-/ig-types-2.0.21.tgz", - "integrity": "sha512-s+Hu9MU50iohoS/5SUwuoS+P2EHk7Z2zKx9wX3syiBsaL9HYq8g/0Yp/4yz9JkME1zpbS8r9aviR0O2rjBXwHQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ig-types/-/ig-types-3.0.1.tgz", + "integrity": "sha512-oa0Tq+LFyy/2SoQHfhRoa39AtttslJFm95FaF7wH5FNcjxRn3dJ/C/4LscXIoeGaDcXe2DyneVIPNnvg8IOsNw==", "requires": { "ig-object": "^5.2.8", "object-run": "^1.0.1" diff --git a/Viewer/package.json b/Viewer/package.json index 5c4dea3f..220009ee 100755 --- a/Viewer/package.json +++ b/Viewer/package.json @@ -32,7 +32,7 @@ "ig-argv": "^2.15.0", "ig-features": "^3.4.2", "ig-object": "^5.2.8", - "ig-types": "^2.0.21", + "ig-types": "^3.0.1", "moment": "^2.29.1", "object-run": "^1.0.1", "requirejs": "^2.3.6",