reworked metadata caching + updated peer feature (not done yet)...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2020-11-03 04:49:56 +03:00
parent f9398462c8
commit 49245ba9d1
5 changed files with 151 additions and 184 deletions

View File

@ -57,8 +57,7 @@ var MetadataActions = actions.Actions({
if(this.images && this.images[gid]){ if(this.images && this.images[gid]){
return this.images[gid].metadata || {} return this.images[gid].metadata || {}
} }
return null return null }],
}],
setMetadata: ['- Image/Set metadata data', setMetadata: ['- Image/Set metadata data',
function(image, metadata, merge){ function(image, metadata, merge){
var that = this var that = this
@ -70,12 +69,8 @@ var MetadataActions = actions.Actions({
Object.keys(metadata).forEach(function(k){ Object.keys(metadata).forEach(function(k){
m[k] = metadata[k] m[k] = metadata[k]
}) })
} else { } else {
this.images[gid].metadata = metadata this.images[gid].metadata = metadata } } }]
}
}
}]
}) })
var Metadata = var Metadata =
@ -119,21 +114,19 @@ var MetadataReaderActions = actions.Actions({
var img = this.images && this.images[gid] var img = this.images && this.images[gid]
if(!image && !img){ if(!image && !img){
return false return false }
}
//var full_path = path.normalize(img.base_path +'/'+ img.path) //var full_path = path.normalize(img.base_path +'/'+ img.path)
var full_path = this.getImagePath(gid) var full_path = this.getImagePath(gid)
return new Promise(function(resolve, reject){ return new Promise(function(resolve, reject){
if(!force && img.metadata){ if(!force
return resolve(img.metadata) && !(img.metadata || {}).ImageGridPartialMetadata){
} return resolve(img.metadata) }
fs.readFile(full_path, function(err, file){ fs.readFile(full_path, function(err, file){
if(err){ if(err){
return reject(err) return reject(err) }
}
// read stat... // read stat...
if(!that.images[gid].birthtime){ if(!that.images[gid].birthtime){
@ -173,10 +166,7 @@ var MetadataReaderActions = actions.Actions({
that.markChanged that.markChanged
&& that.markChanged('images', [gid]) } && that.markChanged('images', [gid]) }
resolve(data) }) resolve(data) }) }) }) }],
})
})
}],
// XXX STUB: add support for this to .readMetadata(..) // XXX STUB: add support for this to .readMetadata(..)
readAllMetadata: ['File/Read all metadata', readAllMetadata: ['File/Read all metadata',
@ -202,8 +192,7 @@ var MetadataReaderActions = actions.Actions({
q.enqueue('metadata', read(gid)) q.enqueue('metadata', read(gid))
}) })
return q return q }],
}],
// XXX take image Metadata and write it to target... // XXX take image Metadata and write it to target...
writeMetadata: ['- Image/Set metadata data', writeMetadata: ['- Image/Set metadata data',
@ -651,9 +640,7 @@ var MetadataUIActions = actions.Actions({
&& (that.config['metadata-graph-config'] = { && (that.config['metadata-graph-config'] = {
graph: this.graph.graph, graph: this.graph.graph,
mode: this.graph.mode, mode: this.graph.mode,
}) }) }) })],
})
})],
metadataSection: ['- Image/', metadataSection: ['- Image/',
{ sortedActionPriority: 'normal' }, { sortedActionPriority: 'normal' },
@ -663,16 +650,18 @@ var MetadataUIActions = actions.Actions({
var field_order = this.config['metadata-field-order'] || [] var field_order = this.config['metadata-field-order'] || []
var x = field_order.length + 1 var x = field_order.length + 1
// NOTE: this is called on showMetadata.pre in the .handlers
// feature section...
make.dialog.updateMetadata = make.dialog.updateMetadata =
function(metadata){ function(metadata){
metadata = metadata || that.getMetadata() metadata = metadata
|| that.getMetadata()
// build new data set and update view... // build new data set and update view...
//this.options.data = _buildInfoList(image, metadata) //this.options.data = _buildInfoList(image, metadata)
this.update() this.update()
return this return this }
}
// build fields... // build fields...
var fields = [] var fields = []
@ -691,15 +680,12 @@ var MetadataUIActions = actions.Actions({
return return
} else if(mode == 'disabled') { } else if(mode == 'disabled') {
opts.disabled = true opts.disabled = true } }
}
}
fields.push([ fields.push([
[ n + ': ', metadata[k] ], [ n + ': ', metadata[k] ],
opts, opts,
]) ]) })
})
// make fields... // make fields...
fields fields
@ -717,8 +703,7 @@ var MetadataUIActions = actions.Actions({
this.length > 0 this.length > 0
&& make.Separator() }) && make.Separator() })
.forEach(function(e){ .forEach(function(e){
make(...e) }) make(...e) }) })],
})],
// shorthands... // shorthands...

View File

@ -19,114 +19,13 @@ var features = require('lib/features')
var core = require('features/core') var core = require('features/core')
var object = require('lib/object') var object = require('lib/object')
var types = require('lib/types')
/*********************************************************************/ /*********************************************************************/
// helpers... // 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 // XXX would be nice to list the protocols supported by the action in
// an action attr... // an action attr...
var makeProtocolHandler = var makeProtocolHandler =
@ -192,8 +91,7 @@ var PeerActions = actions.Actions({
return that.getActionAttr(action, '__peer__') == id } return that.getActionAttr(action, '__peer__') == id }
// get all peer actions... // get all peer actions...
: function(action){ : function(action){
return that.getActionAttr(action, '__peer__') }) return that.getActionAttr(action, '__peer__') }) }],
}],
// XXX should this also check props??? // XXX should this also check props???
isPeerAction: ['- System/Peer/', isPeerAction: ['- System/Peer/',
function(name){ function(name){
@ -208,9 +106,11 @@ var PeerActions = actions.Actions({
// XXX the events should get called on the peer too -- who is // XXX the events should get called on the peer too -- who is
// responsible for this??? // responsible for this???
peerConnect: ['- System/Peer/', peerConnect: ['- System/Peer/',
function(id, options){ return new CooperativePromise() }], function(id, options){
return new Promise.cooperative() }],
peerDisconnect: ['- System/Peer/', peerDisconnect: ['- System/Peer/',
function(id){ return new CooperativePromise() }], function(id){
return new Promise.cooperative() }],
// events... // events...
// XXX do proper docs... // XXX do proper docs...
@ -229,13 +129,14 @@ var PeerActions = actions.Actions({
peerCall: ['- System/Peer/', peerCall: ['- System/Peer/',
function(id, action){ function(id, action){
var args = [...arguments].slice(2) var args = [...arguments].slice(2)
return this.peerApply(id, action, args) return this.peerApply(id, action, args) }],
}],
peerApply: ['- System/Peer/', peerApply: ['- System/Peer/',
function(id, action, args){ return new CooperativePromise() }], function(id, action, args){
return new Promise.cooperative() }],
peerList: ['- System/Peer/', peerList: ['- System/Peer/',
function(){ return Object.keys(this.__peers || {}) }], function(){
return Object.keys(this.__peers || {}) }],
// XXX do we need these??? // XXX do we need these???
// XXX format spec!!! // XXX format spec!!!
@ -287,8 +188,7 @@ var ChildProcessPeerActions = actions.Actions({
if(this.__peers if(this.__peers
&& id in this.__peers && id in this.__peers
&& this.__peers[id].peer.connected){ && this.__peers[id].peer.connected){
return resolve(id) return resolve(id) }
}
this.__peers = this.__peers || {} this.__peers = this.__peers || {}
@ -307,13 +207,9 @@ var ChildProcessPeerActions = actions.Actions({
callback callback
&& (delete this.__peer_result_callbacks[msg.id]) && (delete this.__peer_result_callbacks[msg.id])
&& callback(msg.value, msg.error) && callback(msg.value, msg.error) } }).bind(this))
}
}).bind(this))
resolve(id) resolve(id) }).bind(this)) })],
}).bind(this))
})],
// XXX should this call .stop() on the child??? // XXX should this call .stop() on the child???
// ...does the child handle kill gracefully??? // ...does the child handle kill gracefully???
peerDisconnect: ['- System/Peer/', peerDisconnect: ['- System/Peer/',
@ -329,8 +225,7 @@ var ChildProcessPeerActions = actions.Actions({
that.__peers[id].peer.kill() that.__peers[id].peer.kill()
delete that.__peers[id] delete that.__peers[id]
}).bind(this)) }).bind(this)) })],
})],
// XXX can we do sync??? // XXX can we do sync???
// ...this would be useful to 100% match the action api and // ...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 || {} var handlers = this.__peer_result_callbacks = this.__peer_result_callbacks || {}
handlers[call_id] = function(res, err){ err ? reject(err) : resolve(res) } handlers[call_id] = function(res, err){ err ? reject(err) : resolve(res) }
}).bind(this)) }).bind(this)) })],
})],
}) })

View File

@ -32,13 +32,19 @@ if(typeof(process) != 'undefined'){
/*********************************************************************/ /*********************************************************************/
// helpers...
if(typeof(process) != 'undefined'){ if(typeof(process) != 'undefined'){
var copy = file.denodeify(fse.copy) var copy = file.denodeify(fse.copy)
var ensureDir = file.denodeify(fse.ensureDir) var ensureDir = file.denodeify(fse.ensureDir)
} }
function normalizeOrientation(orientation){
//---------------------------------------------------------------------
var normalizeOrientation =
module.normalizeOrientation =
function(orientation){
return { return {
orientation: ({ orientation: ({
0: 0, 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(/(?<match><(xmp:Rating)[^>]*>(?<value>.*)<\/\2>)/i)
|| {groups: {}})
.groups.value)
rating
&& (this.rating = rating) }) }
@ -423,9 +514,10 @@ var SharpActions = actions.Actions({
.flat()) }], .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... // XXX would be nice to be able to abort this...
// ...and/or have a generic abort protocol triggered when loading... // ...and/or have a generic abort protocol triggered when loading...
// ...use task queue???
// XXX make each section optional... // XXX make each section optional...
// XXX revise name... // XXX revise name...
cacheImageMetadata: ['- Sharp|Image/', cacheImageMetadata: ['- Sharp|Image/',
@ -498,6 +590,20 @@ var SharpActions = actions.Actions({
img.orientation = o.orientation || 0 img.orientation = o.orientation || 0
img.flipped = o.flipped 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)... // if image too large, generate preview(s)...
// XXX EXPERIMENTAL... // XXX EXPERIMENTAL...
var size_threshold = that.config['preview-generate-threshold'] var size_threshold = that.config['preview-generate-threshold']
@ -513,31 +619,11 @@ var SharpActions = actions.Actions({
base_path, base_path,
logger) } 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(/(?<match><(xmp:Rating)[^>]*>(?<value>.*)<\/\2>)/i)
|| {groups: {}})
.groups.value)
rating
&& (img.metadata = img.metadata || {})
&& (img.metadata.rating = rating || 0)
that.markChanged('images', [gid]) that.markChanged('images', [gid])
logger && logger.emit('done', gid) logger && logger.emit('done', gid)
// update image to use the orientation... // update loaded image to use the orientation...
loaded loaded
&& loaded.has(gid) && loaded.has(gid)
&& that.ribbons.updateImage(gid) && that.ribbons.updateImage(gid)
@ -564,9 +650,11 @@ module.Sharp = core.ImageGridFeatures.Feature({
handlers: [ handlers: [
/* XXX not sure if we need this... /* XXX not sure if we need this...
// XXX this is best done in a thread + needs to be abortable...
['loadImages', ['loadImages',
function(){ function(){
this.cacheImageMetadata('all', false) }], //this.cacheImageMetadata('all', false) }],
this.cacheImageMetadata('all') }],
//*/ //*/
// set orientation if not defined... // set orientation if not defined...

View File

@ -1117,9 +1117,9 @@
"integrity": "sha512-EzT4CP6d6lI8bnknNgT3W8mUQhSVXflO0yPbKD4dKsFcINiC6npjoEBz+8m3VQmWJhc+36pXD4JLwNxUEgzi+Q==" "integrity": "sha512-EzT4CP6d6lI8bnknNgT3W8mUQhSVXflO0yPbKD4dKsFcINiC6npjoEBz+8m3VQmWJhc+36pXD4JLwNxUEgzi+Q=="
}, },
"ig-types": { "ig-types": {
"version": "2.0.21", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ig-types/-/ig-types-2.0.21.tgz", "resolved": "https://registry.npmjs.org/ig-types/-/ig-types-3.0.1.tgz",
"integrity": "sha512-s+Hu9MU50iohoS/5SUwuoS+P2EHk7Z2zKx9wX3syiBsaL9HYq8g/0Yp/4yz9JkME1zpbS8r9aviR0O2rjBXwHQ==", "integrity": "sha512-oa0Tq+LFyy/2SoQHfhRoa39AtttslJFm95FaF7wH5FNcjxRn3dJ/C/4LscXIoeGaDcXe2DyneVIPNnvg8IOsNw==",
"requires": { "requires": {
"ig-object": "^5.2.8", "ig-object": "^5.2.8",
"object-run": "^1.0.1" "object-run": "^1.0.1"

View File

@ -32,7 +32,7 @@
"ig-argv": "^2.15.0", "ig-argv": "^2.15.0",
"ig-features": "^3.4.2", "ig-features": "^3.4.2",
"ig-object": "^5.2.8", "ig-object": "^5.2.8",
"ig-types": "^2.0.21", "ig-types": "^3.0.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"object-run": "^1.0.1", "object-run": "^1.0.1",
"requirejs": "^2.3.6", "requirejs": "^2.3.6",