ImageGrid/ui/compatibility.js

507 lines
13 KiB
JavaScript
Raw Normal View History

/**********************************************************************
*
*
*
**********************************************************************/
//var DEBUG = DEBUG != null ? DEBUG : true
// XXX move this to the config...
var PREVIEW_SIZES = [
// NOTE: this is first so as to prevent the hi-res from loading...
// XXX this is best to be screen sized or just a little bigger...
1280,
150,
350,
900
]
/*********************************************************************/
// load the target-specific handlers...
// CEF
if(window.CEF_dumpJSON != null){
console.log('CEF mode: loading...')
var dumpJSON = CEF_dumpJSON
var listDir = CEF_listDir
var removeFile = CEF_removeFile
var runSystem = CEF_runSystem
// node-webkit
} else if(window.require != null){
console.log('node-webkit mode: loading...')
var fs = require('fs')
var fse = require('fs.extra')
var proc = require('child_process')
var node_crypto = require('crypto')
//var exif = require('exif2')
var gui = require('nw.gui')
var fp = /file:\/\/\//
// paths to included utils...
process.env.PATH += ';' + process.cwd() + '/vips/bin'
// Things ImageGrid needs...
// XXX do we need assync versions??
window.listDir = function(path){
if(fp.test(path)){
// XXX will this work on Mac???
path = path.replace(fp, '')
}
return fs.readdirSync(path)
}
// XXX make this work across fs...
// XXX this will not overwrite...
window.copyFile = function(src, dst){
var deferred = $.Deferred()
if(fp.test(src)){
// XXX will this work on Mac???
src = src.replace(fp, '')
}
if(fp.test(dst)){
// XXX will this work on Mac???
dst = dst.replace(fp, '')
}
var path = dst.split('/')
path.pop()
path = path.join('/')
// make dirs...
if(!fs.existsSync(path)){
console.log('making:', path)
fse.mkdirRecursiveSync(path)
}
if(!fs.existsSync(dst)){
// NOTE: this is not sync...
fse.copy(src, dst, function(err){
if(err){
deferred.reject(err)
} else {
deferred.resolve()
}
})
return deferred
}
deferred.notify(dst, 'exists')
return deferred.resolve()
}
window.dumpJSON = function(path, data){
if(fp.test(path)){
// XXX will this work on Mac???
path = path.replace(fp, '')
}
var dirs = path.split(/[\\\/]/)
dirs.pop()
dirs = dirs.join('/')
// build path...
if(!fs.existsSync(dirs)){
console.log('making:', path)
fse.mkdirRecursiveSync(path)
}
return fs.writeFileSync(path, JSON.stringify(data), encoding='utf8')
}
window.removeFile = function(path){
if(fp.test(path)){
// XXX will this work on Mac???
path = path.replace(fp, '')
}
return fs.unlinkSync(path)
}
window.runSystem = function(path){
if(fp.test(path)){
// XXX will this work on Mac???
path = path.replace(fp, '')
}
return proc.exec('"'+path+'"', function(error, stdout, stderr){
if(error != null){
console.error(stderr)
}
})
}
// XXX this uses vips...
window.getVipsField = function(field, source){
if(source in IMAGES){
var img = IMAGES[source]
var source = normalizePath(img.path)
}
var getter = $.Deferred()
var cmd = 'vips im_header_string "$FIELD" "$IN"'
.replace(/\$IN/g, source.replace(fp, ''))
.replace(/\$FIELD/g, field)
proc.exec(cmd, function(error, stdout, stderr){
getter.resolve(stdout.trim())
})
return getter
}
// NOTE: source can be either gid or a path...
window.getImageOrientation = function(source){
var getter = $.Deferred()
getVipsField('exif-ifd0-Orientation', source)
.done(function(o){
getter.resolve(orientationExif2ImageGrid(parseInt(o)))
})
return getter
}
// NOTE: source can be either gid or a path...
// XXX handle errors...
window._getImageSize = function(dimension, source){
if(source in IMAGES){
var img = IMAGES[source]
var source = normalizePath(img.path)
}
var getter = $.Deferred()
// get max/min dimension...
if(dimension == 'max' || dimension == 'min'){
$.when(
_getImageSize('width', source),
_getImageSize('height', source))
.done(function(w, h){
getter.resolve(Math[dimension](w, h))
})
// get dimension...
} else if(dimension == 'width' || dimension == 'height') {
getVipsField(dimension, source)
.done(function(res){
getter.resolve(parseInt(res))
})
// wrong dimension...
} else {
return getter.reject('unknown dimension:' + dimension)
}
return getter
}
// XXX API to add to $PATH...
// preview generation...
//
// possible modes:
// - optimized
// use closest rscale and minimal factor
// previews might get artifacts associated with small scale factors
// 0.5x time
// - best
// only use scale factor (rscale=1)
// 1x time (fixed set of previews: 1280, 150, 350, 900)
// - fast_r
// make previews using nearest rscale (factor is rounded)
// will produce inexact preview sizes
// 0.4x time
// - fast_f
// same as fast_r but factor is floored rather than rounded
// will priduce previews the same size or larger than requested
// - rscale
// only use rscale (factor=1)
// produces only fixed size previews
// 0.3x time
//
// NOTE: rscale should be used for exactly tuned preview sizes...
// NOTE: this will add already existing previews to IMAGES[gid]...
//
// XXX make this not just vips-specific...
// XXX path handling is a mess...
// XXX looks a bit too complex for what it is -- revise!
window.makeImagePreviews = function(gid, sizes, mode, no_update_loaded){
mode = mode == null ? 'fast_f' : mode
var img = IMAGES[gid]
var source = normalizePath(img.path)
var name = gid +' - '+ source.split('/').pop()
var compression = 90
var previews = []
// prepare the sizes we are going to be working with...
if(sizes == null){
sizes = PREVIEW_SIZES
} else if(typeof(sizes) == typeof(123)){
sizes = [ sizes ]
}
// build usable local path (without 'file:///')...
var cache_path = normalizePath(CACHE_DIR)
cache_path = cache_path.replace(fp, '')
// get cur image size...
var size_getter = _getImageSize('max', source)
for(var i=0; i < sizes.length; i++){
var size = sizes[i]
// XXX get this from config...
var target_path = [ cache_path, size+'px' ].join('/')
var deferred = $.Deferred()
previews.push(deferred)
[function(size, target_path, deferred){
// wait for current image size if needed...
size_getter.done(function(source_size){
// skip previews larger than cur image...
if(fs.existsSync(target_path +'/'+ name) || source_size <= size){
// see if we know about the preview...
if(img.preview == null || !((size+'px') in img.preview)){
var preview_path = [target_path, name].join('/')
// add the preview to the image object...
img.preview[size+'px'] = './' + CACHE_DIR +'/'+ preview_path.split(CACHE_DIR).pop()
// mark image dirty...
if(IMAGES_UPDATED.indexOf(gid) < 0){
IMAGES_UPDATED.push(gid)
}
}
//console.log('>>> Preview:', name, '('+size+'): Skipped.')
deferred.notify(gid, size, 'exists')
return deferred.resolve()
}
// create the directory then go to its content...
// XXX check for errors...
fse.mkdirRecursive(target_path, function(err){
var preview_path = [target_path, name].join('/')
var factor = source_size / size
// this can be 1, 2, 4 or 8...
var rscale = 1
// speed things up with read-scaling and rounding the scale factor...
if(['fast_r', 'fast_f', 'optimized', 'rscale'].indexOf(mode) >= 0){
while(rscale < 8){
if(rscale*2 >= factor){
break
}
rscale *= 2
}
factor = factor / rscale
}
// factor processing...
if(mode == 'fast_r'){
factor = Math.max(Math.round(factor), 1)
} else if(mode == 'fast_f'){
// NOTE: .floor(...) will make the images larger than
// the requested size, this will avaoid scale-up
// artifacts...
factor = Math.max(Math.floor(factor), 1)
} else if(mode == 'rscale'){
factor = 1
}
var cmd = 'vips im_shrink "$IN:$RSCALE" "$OUT:$COMPRESSION" $FACTOR $FACTOR'
.replace(/\$IN/g, source.replace(fp, ''))
.replace(/\$RSCALE/g, rscale)
.replace(/\$OUT/g, preview_path)
.replace(/\$COMPRESSION/g, compression)
.replace(/\$FACTOR/g, factor)
//console.log(cmd)
proc.exec(cmd, function(error, stdout, stderr){
if(error != null){
//console.error('>>> Error: preview:', stderr)
deferred.notify(gid, size, 'error', stderr)
deferred.reject()
} else {
// XXX use real size of the preview generated (???)
//console.log('>>> Preview:', name, '('+size+'): Done.')
deferred.notify(gid, size, 'done')
// update the image structure...
if(!('preview' in img)){
img.preview = {}
}
img.preview[size+'px'] = './' + CACHE_DIR +'/'+ preview_path.split(CACHE_DIR).pop()
// mark image dirty...
if(IMAGES_UPDATED.indexOf(gid) < 0){
IMAGES_UPDATED.push(gid)
}
// we are done...
deferred.resolve()
}
})
})
})
}(size, target_path, deferred)]
}
var res = $.when.apply(null, previews)
// update loaded images...
if(!no_update_loaded){
res.done(function(){
var o = getImage(gid)
if(o.length > 0){
updateImage(o)
}
})
}
return res
}
// XXX needs more testing...
// - for some reason this is a bit slower than the queued version
// ...in spite of being managed by node.js
// - will this be faster on SMP/multi-core?
window.makeImagesPreviews = function(gids, sizes, mode){
gids = gids == null ? getClosestGIDs() : gids
return $.when.apply(null, gids.map(function(gid){
return makeImagePreviews(gid, sizes, mode)
}))
}
window._PREVIW_CREATE_QUEUE = null
// Queued version of makeImagesPreviews(...)
window.makeImagesPreviewsQ = function(gids, sizes, mode){
gids = gids == null ? getClosestGIDs() : gids
// attach the the previous queue...
if(_PREVIW_CREATE_QUEUE == null){
var queue = makeDeferredsQ()
_PREVIW_CREATE_QUEUE = queue.start()
} else {
var queue = _PREVIW_CREATE_QUEUE
}
// attach the workers to the queue...
$.each(gids, function(_, gid){
queue.enqueue(makeImagePreviews, gid, sizes, mode)
// XXX do we need to report seporate previews???
//.progress(function(state){ queue.notify(state) })
.always(function(){ queue.notify(gid, 'done') })
})
return queue
}
window.makeImageGID = function(source, make_text_gid){
if(source in IMAGES){
var img = IMAGES[source]
var source = normalizePath(img.path)
}
var getter = $.Deferred()
$.when(
getVipsField('exif-ifd0-Artist', source),
getVipsField('exif-ifd0-Date and Time', source))
.done(function(artist, date){
// Artist...
artist = artist
.replace(/\([^)]*\)/, '')
.trim()
artist = artist == '' ? 'Unknown' : artist
// Date...
// format: "20130102-122315"
// XXX if not set, get ctime...
date = date
.replace(/\([^)]*\)/, '')
.trim()
.replace(/:/g, '')
.replace(/ /g, '-')
// File name...
var name = source.split(/[\\\/]/).pop().split('.')[0]
var text_gid = artist +'-'+ date +'-'+ name
// text gid...
if(make_text_gid){
getter.resolve(text_gid)
// hex gid...
} else {
var h = node_crypto.createHash('sha1')
h.update(text_gid)
var hex_gid = h.digest('hex')
getter.resolve(hex_gid)
}
})
// XXX handle arrors in a more informative way...
.fail(function(){
getter.reject()
})
return getter
}
// UI-specific...
window.toggleFullscreenMode = createCSSClassToggler(
document.body,
'.full-screen-mode',
function(action){
gui.Window.get().toggleFullscreen()
})
window.closeWindow = function(){
gui.Window.get().close()
}
window.showDevTools = function(){
gui.Window.get().showDevTools()
}
window.reload = function(){
gui.Window.get().reload()
}
window.setWindowTitle = function(text){
var title = text +' - '+ APP_NAME
gui.Window.get().title = title
$('.title-bar .title').text(title)
}
// load UI stuff...
$(function(){
$('<div class="title-bar"/>')
.append($('<div class="title"></div>')
.text($('title').text()))
.append($('<div class="button close" onclick="closeWindow()">&times;</div>'))
.appendTo($('body'))
})
// PhoneGap
} else if(false){
console.log('PhoneGap mode: loading...')
// XXX
// stubs...
window.toggleFullscreenMode = function(){}
window.closeWindow = function(){}
window.showDevTools = function(){}
window.reload = function(){}
// Bare Chrome...
} else {
console.log('Chrome mode: loading...')
// stubs...
window.toggleFullscreenMode = function(){}
window.closeWindow = function(){}
window.showDevTools = function(){}
window.reload = function(){}
}
/**********************************************************************
* vim:set ts=4 sw=4 : */