mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-11-03 04:40:10 +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 :                                                */
 |