484 lines
12 KiB
JavaScript
Raw Normal View History

/**********************************************************************
*
*
*
**********************************************************************/
// XXX this is a hack...
// ...need a way to escape these so as not to load them in browser...
if(typeof(process) != 'undefined'){
var fs = require('fs')
var path = require('path')
var exiftool = require('exiftool')
var promise = require('promise')
}
define(function(require){ var module = {}
//var DEBUG = DEBUG != null ? DEBUG : true
var util = require('lib/util')
var toggler = require('lib/toggler')
var tasks = require('lib/tasks')
var keyboard = require('lib/keyboard')
var actions = require('lib/actions')
var core = require('features/core')
var base = require('features/base')
var browse = require('lib/widget/browse')
var overlay = require('lib/widget/overlay')
/*********************************************************************/
// XXX make metadata a prop of image... (???)
// XXX Q: should we standardise metadata field names and adapt them to
// lib???
var MetadataActions = actions.Actions({
getMetadata: ['- Image/Get metadata data',
function(image){
var gid = this.data.getImage(image)
if(this.images && this.images[gid]){
return this.images[gid].metadata || {}
}
return null
}],
setMetadata: ['- Image/Set metadata data',
function(image, metadata, merge){
var that = this
var gid = this.data.getImage(image)
if(this.images && this.images[gid]){
if(merge){
var m = this.images[gid].metadata
Object.keys(metadata).forEach(function(k){
m[k] = metadata[k]
})
} else {
this.images[gid].metadata = metadata
}
}
}]
})
var Metadata =
module.Metadata = core.ImageGridFeatures.Feature({
title: '',
doc: '',
tag: 'metadata',
depends: [
'base',
],
actions: MetadataActions,
})
//---------------------------------------------------------------------
// Metadata reader/writer...
// XXX add Metadata writer...
var MetadataReaderActions = actions.Actions({
// XXX should this be sync???
// XXX add support to taskqueue...
// XXX should this process multiple images???
// XXX also check the metadata/ folder (???)
// XXX this uses .markChanged(..) form filesystem.FileSystemWriter
// feature, but technically does not depend on it...
// XXX should we store metadata in an image (current) or in fs???
readMetadata: ['- Image/Get metadata data',
function(image, force){
var that = this
var gid = this.data.getImage(image)
var img = this.images && this.images[gid]
if(!image){
return
}
var full_path = path.normalize(img.base_path +'/'+ img.path)
return new promise(function(resolve, reject){
if(!force && img.metadata){
return resolve(img.metadata)
}
fs.readFile(full_path, function(err, file){
if(err){
return reject(err)
}
exiftool.metadata(file, function(err, data){
if(err){
reject(err)
} else if(data.error){
reject(data)
} else {
// store metadata...
// XXX
// convert to a real dict...
// NOTE: exiftool appears to return an array
// object rather than an actual dict/object
// and that is not JSON compatible....
var m = {}
Object.keys(data).forEach(function(k){ m[k] = data[k] })
that.images[gid].metadata = m
that.markChanged && that.markChanged(gid)
resolve(data)
}
resolve(data)
})
})
})
}],
// XXX STUB: add support for this to .readMetadata(..)
_readAllMetadata: ['- Image/Read all metadata',
function(){
var that = this
// XXX make this a global API...
var q = this.__reader_queue = this.__reader_queue || tasks.Queue()
var read = function(gid){
return function(){ return that.readMetadata(gid) } }
q.start()
this.images
&& this.images.forEach(function(gid){
q.enqueue('metadata', read(gid))
})
return q
}],
// XXX take image Metadata and write it to target...
writeMetadata: ['- Image/Set metadata data',
function(image, target){
// XXX
}]
})
var MetadataReader =
module.MetadataReader = core.ImageGridFeatures.Feature({
title: '',
doc: '',
tag: 'fs-metadata',
depends: [
'metadata',
],
isApplicable: function(){
return this.runtime == 'nw' || this.runtime == 'node' },
actions: MetadataReaderActions,
handlers: [
// XXX STUB: need a better strategy to read metadata...
['focusImage',
function(){
var gid = this.current
metadata = this.images && this.images[gid] && this.images[gid].metadata
metadata = metadata && (Object.keys(metadata).length > 0)
if(!metadata){
this.readMetadata(gid)
}
}]
],
})
//---------------------------------------------------------------------
// Metadata editor/viewer...
//
// NOTE: this is by-design platform independent...
//
// XXX first instinct is to use browse with editable fields as it will
// give us:
// - searchability
// - navigation
// - ...
// missing functionality:
// - editor/form on open event
// - inline (preferred)
// - modal-form
// - table-like layout
// - template???
// - script layout tweaking (post-update)
//
// ...need to think about this...
// XXX add ability to manually sort fields -- moving a field up/down
// edits .config...
// ...not sure how to go about this yet...
// XXX add combined fields...
// 'Make' + 'Camera Model Name'
// XXX add identical fields -- show first available and hide the rest...
// 'Shutter Speed', 'Exposure Time',
// 'Lens ID', 'Lens'
// XXX show all fields but make some of them hidden/disabled
// -- togglable via D
// XXX add field editing... (open)
// XXX might be good to split this to sections...
// - base info
// - general metadata
// - full metadata
// - EXIF
// - IPTC
// - ...
// XXX should this be a panel or a list (as is now...)????
var MetadataUIActions = actions.Actions({
config: {
'metadata-auto-select-modes': [
'none',
'on select',
'on open',
],
'metadata-auto-select-mode': 'on select',
'metadata-editable-fields': [
'Artist',
'Copyright',
'Comment',
],
'metadata-field-order': [
// base
'GID',
'File Name', 'Parent Directory', 'Full Path',
// metadata...
'Make', 'Camera Model Name', 'Lens ID', 'Lens', 'Lens Profile Name', 'Focal Length',
'Metering Mode', 'Exposure Program', 'Exposure Compensation',
'Shutter Speed Value', 'Aperture Value', 'Iso',
'Artist', 'Copyright',
'Date/time Original', 'Create Date', 'Modify Date',
'Mime Type',
],
},
toggleMetadataAutoSelect: ['Interface/Toggle metadata value auto-select',
core.makeConfigToggler('metadata-auto-select-mode',
function(){ return this.config['metadata-auto-select-modes'] })],
// XXX should we replace 'mode' with nested set of metadata???
// XXX make this support multiple images...
showMetadata: ['Image/Show metadata',
function(image, mode){
var that = this
image = this.data.getImage(image)
mode = mode || 'disabled'
var field_order = this.config['metadata-field-order'] || []
var x = field_order.length + 1
var img = this.images && this.images[image] || null
// get image metadata...
var metadata = img && img.metadata || {}
// helpers...
var _cmp = function(a, b){
a = field_order.indexOf(a[0].replace(/: $/, ''))
a = a == -1 ? x : a
b = field_order.indexOf(b[0].replace(/: $/, ''))
b = b == -1 ? x : b
return a - b
}
var _selectElemText = function(elem){
var range = document.createRange()
range.selectNodeContents(elem)
var sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
// XXX move these to an info feature...
// base fields...
var base = [
['GID: ', image],
// NOTE: these are 1-based and not 0-based...
['Index (ribbon): ',
this.data.getImageOrder('ribbon', image) + 1
+'/'+
this.data.getImages(image).len],
// show this only when cropped...
['Index (global): ',
this.data.getImageOrder(image) + 1
+'/'+
this.data.getImages('all').len],
]
// crop-specific stuff...
if(this.crop_stack && this.crop_stack.len > 0){
base = base.concat([
['Index (crop): ',
this.data.getImageOrder('loaded', image) + 1
+'/'+
this.data.getImages('loaded').len],
])
}
// fields that expect that image data is available...
var info = ['---']
if(img){
// XXX should these be here???
var _normalize = typeof(path) != 'undefined' ?
path.normalize
: function(e){ return e.replace(/\/\.\//, '') }
var _basename = typeof(path) != 'undefined' ?
path.basename
: function(e){ return e.split(/[\\\/]/g).pop() }
var _dirname = typeof(path) != 'undefined' ?
function(e){ return path.normalize(path.dirname(e)) }
: function(e){ return _normalize(e.split(/[\\\/]/g).slice(0, -1).join('/')) }
base = base.concat([
['File Name: ',
_basename(img.path)],
['Parent Directory: ',
_dirname((img.base_path || '.') +'/'+ img.path)],
['Full Path: ',
_normalize((img.base_path || '.') +'/'+ img.path)],
])
// comment and tags...
info.push(['Comment: ',
function(){ return img.comment || '' }])
}
info.push(['Tags: ',
function(){ return that.data.getTags().join(', ') || '' }])
// build fields...
var fields = []
Object.keys(metadata).forEach(function(k){
var n = k
// convert camel-case to human-case ;)
.replace(/([A-Z]+)/g, ' $1')
.capitalize()
// skip metadata stuff in short mode...
if(mode != 'full'
&& field_order.indexOf(n) == -1){
if(mode == 'short'){
return
} else if(mode == 'disabled') {
n = '- ' + n
}
}
fields.push([ n + ': ', metadata[k] ])
})
// sort fields...
base.sort(_cmp)
fields.sort(_cmp)
// add separator to base...
fields.length > 0 && info.push('---')
// XXX might be a good idea to directly bind ctrl-c to copy value...
var o = overlay.Overlay(this.ribbons.viewer,
browse.makeList(
null,
base
.concat(info)
.concat(fields),
{
showDisabled: false,
})
// select value of current item...
.on('select', function(evt, elem){
if(that.config['metadata-auto-select-mode'] == 'on select'){
_selectElemText($(elem).find('.text').last()[0])
}
})
// XXX start editing onkeydown...
.on('keydown', function(){
// XXX Enter + editable -> edit (only this???)
})
// path selected...
.open(function(evt, path){
var editable = RegExp(that.config['metadata-editable-fields']
.map(function(f){ return util.quoteRegExp(f) })
.join('|'))
var elem = o.client.filter(path).find('.text').last()
// handle select...
if(that.config['metadata-auto-select-mode'] == 'on open'){
_selectElemText(elem[0])
}
// skip non-editable fields...
if(editable.test(path)){
elem
.prop('contenteditable', true)
.focus()
.keydown(function(){
event.stopPropagation()
var n = keyboard.toKeyName(event.keyCode)
// reset to original value...
if(n == 'Esc'){
// XXX
// save value...
} else if(n == 'Enter' && event.ctrlKey){
event.preventDefault()
// XXX
}
})
}
}))
.close(function(){
// XXX
})
o.client.dom.addClass('metadata-view')
return o
}]
})
var MetadataUI =
module.MetadataUI = core.ImageGridFeatures.Feature({
title: '',
doc: '',
tag: 'ui-metadata',
depends: [
'ui',
'metadata',
],
actions: MetadataUIActions,
})
/**********************************************************************
* vim:set ts=4 sw=4 : */
return module })