mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
531 lines
14 KiB
JavaScript
Executable File
531 lines
14 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
|
|
//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 path = require('path')
|
|
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')
|
|
|
|
|
|
window.osPath = function(p){
|
|
return path
|
|
// we can have two types of path:
|
|
// file:///some/path -> /some/path
|
|
// file:///X:/some/other/path -> X:/some/other/path
|
|
.normalize(p.replace(/file:\/+([a-zA-Z]:\/|\/)/, '$1'))
|
|
}
|
|
window.execPathPush = function(p){
|
|
process.env.PATH += ';' + path.normalize(path.dirname(process.execPath) + '/' + p)
|
|
}
|
|
|
|
|
|
// paths to included utils...
|
|
execPathPush('./vips/bin')
|
|
|
|
// Things ImageGrid needs...
|
|
// XXX do we need assync versions??
|
|
window.listDir = function(path){
|
|
if(!fs.existsSync(osPath(path))){
|
|
return null
|
|
}
|
|
return fs.readdirSync(osPath(path))
|
|
}
|
|
// XXX make this work across fs...
|
|
// XXX this will not overwrite...
|
|
// XXX set ctime to the same value as the original...
|
|
window.copyFile = function(src, dst){
|
|
var deferred = $.Deferred()
|
|
src = osPath(src)
|
|
dst = osPath(dst)
|
|
|
|
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){
|
|
path = osPath(path)
|
|
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){
|
|
return fs.unlinkSync(osPath(path))
|
|
}
|
|
window.runSystem = function(path){
|
|
return proc.exec('"'+osPath(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 data = ''
|
|
var p = proc.spawn('vips', ['im_header_string', field, osPath(source)])
|
|
p.stdout.on('data', function(d){
|
|
data += d.toString()
|
|
})
|
|
p.stdout.on('end', function(){
|
|
getter.resolve(data.trim())
|
|
})
|
|
|
|
/* XXX do we need these???
|
|
p.on('error', function(code){
|
|
// XXX
|
|
})
|
|
p.on('close', function(code){
|
|
getter.resolve(data)
|
|
})
|
|
*/
|
|
|
|
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 cache_dir = CONFIG.cache_dir
|
|
|
|
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 = osPath(cache_path)
|
|
|
|
// 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){
|
|
|
|
// handle existing previews...
|
|
if(fs.existsSync(target_path +'/'+ name)){
|
|
// 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...
|
|
imageUpdated(gid)
|
|
}
|
|
//console.log('>>> Preview:', name, '('+size+'): Exists.')
|
|
deferred.notify(gid, size, 'exists')
|
|
return deferred.resolve()
|
|
|
|
// skip previews larger than cur image...
|
|
} else if(source_size <= size){
|
|
//console.log('>>> Preview:', name, '('+size+'): Skipped.')
|
|
deferred.notify(gid, size, 'skipped')
|
|
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 p = proc.spawn('vips', [
|
|
'im_shrink',
|
|
osPath(source) +':'+ rscale,
|
|
preview_path +':'+ compression,
|
|
factor,
|
|
factor
|
|
])
|
|
// XXX is this the correct wat to deal with errors???
|
|
var error = ''
|
|
p.stderr.on('data', function(data){
|
|
error += data.toString()
|
|
})
|
|
//p.stderr.on('end', function(data){
|
|
//})
|
|
p.on('close', function(code){
|
|
// error...
|
|
if(code != 0){
|
|
deferred.notify(gid, size, 'error', error)
|
|
deferred.reject()
|
|
|
|
// ok...
|
|
} else {
|
|
// NOTE: the size of the real preview
|
|
// generated might different from
|
|
// the target size...
|
|
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...
|
|
imageUpdated(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)
|
|
}))
|
|
}
|
|
|
|
// Queued version of makeImagesPreviews(...)
|
|
window.makeImagesPreviewsQ = function(gids, sizes, mode){
|
|
gids = gids == null ? getClosestGIDs() : gids
|
|
|
|
var queue = getWorkerQueue('Generate previews', 4)
|
|
.filling()
|
|
|
|
// 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(){ console.log(gid, 'done') })
|
|
})
|
|
|
|
return queue.doneFilling()
|
|
}
|
|
|
|
// format: "20130102-122315"
|
|
window.getEXIFDate = function(source){
|
|
var getter = $.Deferred()
|
|
getVipsField('exif-ifd0-Date and Time', source)
|
|
.done(function(date){
|
|
getter.resolve(date
|
|
// remove substrings in braces...
|
|
.replace(/\([^)]*\)/, '')
|
|
.trim()
|
|
.replace(/:/g, '')
|
|
.replace(/ /g, '-'))
|
|
})
|
|
return getter
|
|
}
|
|
|
|
window.getEXIFGID = 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),
|
|
getEXIFDate(source))
|
|
.done(function(artist, date){
|
|
// Artist...
|
|
artist = artist
|
|
// remove substrings in braces...
|
|
.replace(/\([^)]*\)/, '')
|
|
.trim()
|
|
artist = artist == '' ? 'Unknown' : artist
|
|
|
|
// Date...
|
|
// XXX if not set, get ctime...
|
|
// XXX
|
|
|
|
// 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 +' - '+ CONFIG.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()">×</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 : */
|