mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-10-29 18:30: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 :                                                */
 |