mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-11-04 05:10:07 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			959 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			959 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**********************************************************************
 | 
						|
* 
 | 
						|
*
 | 
						|
*
 | 
						|
**********************************************************************/
 | 
						|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
 | 
						|
(function(require){ var module={} // make module AMD/node compatible...
 | 
						|
/*********************************************************************/
 | 
						|
 | 
						|
var runner = require('lib/types/runner')
 | 
						|
 | 
						|
var actions = require('lib/actions')
 | 
						|
var features = require('lib/features')
 | 
						|
 | 
						|
var core = require('features/core')
 | 
						|
 | 
						|
try{
 | 
						|
	var sharp = requirejs('sharp')
 | 
						|
 | 
						|
} catch(err){
 | 
						|
	var sharp = null
 | 
						|
}
 | 
						|
 | 
						|
if(typeof(process) != 'undefined'){
 | 
						|
	var cp = requirejs('child_process')
 | 
						|
	var fse = requirejs('fs-extra')
 | 
						|
	var pathlib = requirejs('path')
 | 
						|
	var glob = requirejs('glob')
 | 
						|
	var exifReader = requirejs('exif-reader')
 | 
						|
 | 
						|
	var file = require('imagegrid/file')
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/*********************************************************************/
 | 
						|
// helpers...
 | 
						|
 | 
						|
if(typeof(process) != 'undefined'){
 | 
						|
	var copy = file.denodeify(fse.copy)
 | 
						|
	var ensureDir = file.denodeify(fse.ensureDir)
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
 | 
						|
var normalizeOrientation =
 | 
						|
module.normalizeOrientation =
 | 
						|
function(orientation){
 | 
						|
	return {
 | 
						|
		orientation: ({
 | 
						|
				0: 0,
 | 
						|
				1: 0,
 | 
						|
				2: 0,
 | 
						|
				3: 180,
 | 
						|
				4: 0,
 | 
						|
				5: 90,
 | 
						|
				6: 90,
 | 
						|
				7: 90, 
 | 
						|
				8: 270,
 | 
						|
			})[orientation],
 | 
						|
		flipped: ({
 | 
						|
				0: null,
 | 
						|
				1: null,
 | 
						|
				2: ['horizontal'],
 | 
						|
				3: null,
 | 
						|
				4: ['vertical'],
 | 
						|
				5: ['vertical'],
 | 
						|
				6: null,
 | 
						|
				7: ['horizontal'],
 | 
						|
				8: null,
 | 
						|
			})[orientation],
 | 
						|
	} }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
//---------------------------------------------------------------------
 | 
						|
// Convert image metadata from exif-reader output to format compatible 
 | 
						|
// with exiftool (features/metadata.js)
 | 
						|
 | 
						|
// Format:
 | 
						|
// 	{
 | 
						|
// 		// simple key-key pair...
 | 
						|
// 		'path.to.value': 'output-key',
 | 
						|
//
 | 
						|
// 		// key with value handler...
 | 
						|
// 		'path.to.other.value': ['output-key', handler],
 | 
						|
//
 | 
						|
// 		// alias to handler...
 | 
						|
// 		'path.to.yet.another.value': ['output-key', 'path.to.other.value'],
 | 
						|
// 	}
 | 
						|
//
 | 
						|
var EXIF_FORMAT =
 | 
						|
module.EXIF_FORMAT = {
 | 
						|
	// camera / lens...
 | 
						|
	'image.Make': 'make',
 | 
						|
	'image.Model': 'cameraModelName',
 | 
						|
	'image.Software': 'software',
 | 
						|
	'exif.LensModel': 'lensModel',
 | 
						|
 | 
						|
	// exposure...
 | 
						|
	'exif.ISO': 'iso',
 | 
						|
	'exif.FNumber': [
 | 
						|
		'fNumber', 
 | 
						|
		function(v){ 
 | 
						|
			return 'f/'+v }],
 | 
						|
	'exif.ExposureTime': [
 | 
						|
		'exposureTime',
 | 
						|
		// NOTE: this is a bit of a brute-fore approach but for shutter 
 | 
						|
		// 		speeds this should not matter...
 | 
						|
		function(v){
 | 
						|
			if(v > 0.5){
 | 
						|
				return ''+ v }
 | 
						|
			for(var d = 1; (v * d) % 1 != 0; d++){}
 | 
						|
			return (v * d) +'/'+ d }],
 | 
						|
 | 
						|
	// dates...
 | 
						|
	'exif.DateTimeOriginal': [
 | 
						|
		'date/timeOriginal',
 | 
						|
		function(v){
 | 
						|
			return v.toShortDate() }],
 | 
						|
	'image.ModifyDate': [
 | 
						|
		'modifyDate', 
 | 
						|
		'exif.DateTimeOriginal'],
 | 
						|
 | 
						|
	// IPCT...
 | 
						|
	'image.Artist': 'artist',
 | 
						|
	'image.Copyright': 'copyright',
 | 
						|
 | 
						|
	// XXX anything else???
 | 
						|
}
 | 
						|
 | 
						|
// NOTE: this only reads the .rating from xmp...
 | 
						|
var exifReader2exiftool = 
 | 
						|
module.exifReader2exiftool =
 | 
						|
function(exif, xmp){
 | 
						|
	return Object.entries(EXIF_FORMAT)
 | 
						|
		// handle exif...
 | 
						|
		.reduce(function(res, [path, to]){
 | 
						|
			var handler
 | 
						|
			;[to, handler] = to instanceof Array ?
 | 
						|
				to
 | 
						|
				: [to]
 | 
						|
			// resolve handler reference/alias...
 | 
						|
			while(typeof(handler) == typeof('str')){
 | 
						|
				handler = EXIF_FORMAT[handler][1] }
 | 
						|
			// resolve source path...
 | 
						|
			var value = path.split(/\./g)
 | 
						|
				.reduce(function(res, e){ 
 | 
						|
					return res && res[e] }, exif)
 | 
						|
			// set the value...
 | 
						|
			if(value !== undefined){
 | 
						|
				res[to] = handler ?
 | 
						|
					handler(value)
 | 
						|
					: value }
 | 
						|
			return res }, {})
 | 
						|
		// handle xmp...
 | 
						|
		.run(function(){
 | 
						|
			var rating = xmp 
 | 
						|
				// NOTE: we do not need the full XML 
 | 
						|
				// 		fluff here, just get some values...
 | 
						|
				&& parseInt(
 | 
						|
					(xmp.toString()
 | 
						|
							.match(/(?<match><(xmp:Rating)[^>]*>(?<value>.*)<\/\2>)/i) 
 | 
						|
						|| {groups: {}})
 | 
						|
					.groups.value)
 | 
						|
			rating
 | 
						|
				&& (this.rating = rating) }) }
 | 
						|
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/*********************************************************************/
 | 
						|
 | 
						|
var SharpActions = actions.Actions({
 | 
						|
	config: {
 | 
						|
		'preview-normalized': true,
 | 
						|
 | 
						|
		// can be:
 | 
						|
		// 	'gids'
 | 
						|
		// 	'files'
 | 
						|
		'preview-progress-mode': 'gids',
 | 
						|
 | 
						|
		'preview-generate-threshold': 2000,
 | 
						|
 | 
						|
		// NOTE: this uses 'preview-sizes' and 'preview-path-template' 
 | 
						|
		// 		from filesystem.IndexFormat...
 | 
						|
	},
 | 
						|
 | 
						|
	// XXX revise return values...
 | 
						|
	// XXX make backup name pattern configurable...
 | 
						|
	// XXX CROP ready for crop support...
 | 
						|
	makeResizedImage: ['- Image/',
 | 
						|
		core.doc`Make resized image(s)...
 | 
						|
 | 
						|
			.makeResizedImage(gid, size, path[, options])
 | 
						|
			.makeResizedImage(gids, size, path[, options])
 | 
						|
				-> promise
 | 
						|
 | 
						|
 | 
						|
		Image size formats:
 | 
						|
			500px		- resize to make image's *largest* dimension 500 pixels (default).
 | 
						|
			500p		- resize to make image's *smallest* dimension 500 pixels.
 | 
						|
			500			- same as 500px
 | 
						|
 | 
						|
 | 
						|
		options format:
 | 
						|
			{
 | 
						|
				// output image name / name pattern...
 | 
						|
				//
 | 
						|
				// NOTE: for multiple images this should be a pattern and not an
 | 
						|
				// 		explicit name...
 | 
						|
				// NOTE: if not given this defaults to: "%n"
 | 
						|
				name: null | <str>,
 | 
						|
 | 
						|
				// image name pattern data...
 | 
						|
				//
 | 
						|
				// NOTE: for more info on pattern see: .formatImageName(..)
 | 
						|
				data: null | { .. },
 | 
						|
 | 
						|
				// if true and image is smaller than size enlarge it...
 | 
						|
				// 
 | 
						|
				// default: null / false
 | 
						|
				enlarge: null | true,
 | 
						|
 | 
						|
				// overwrite, backup or skip (default) existing images...
 | 
						|
				//
 | 
						|
				// default: null / false
 | 
						|
				overwrite: null | true | 'backup',
 | 
						|
 | 
						|
				// if true do not write an image if it's smaller than size...
 | 
						|
				// 
 | 
						|
				// default: null / false
 | 
						|
				skipSmaller: null | true,
 | 
						|
 | 
						|
				// XXX not implemented...
 | 
						|
				transform: ...,
 | 
						|
				crop: ...,
 | 
						|
 | 
						|
				timestamp: ...,
 | 
						|
				logger: ...,
 | 
						|
,			}
 | 
						|
 | 
						|
 | 
						|
		NOTE: all options are optional.
 | 
						|
		NOTE: this will not overwrite existing images.
 | 
						|
		`,
 | 
						|
		core.queueHandler('Making resized image', 
 | 
						|
			// prepare the data for image resizing (session queue)...
 | 
						|
			core.sessionQueueHandler('Gathering image data for resizing', 
 | 
						|
				// prepare the input index-dependant data in a fast way...
 | 
						|
				function(queue, _, images, size, path, options){
 | 
						|
					var args = [...arguments].slice(2)
 | 
						|
					if(queue == 'sync'){
 | 
						|
						args.unshift(_)
 | 
						|
						var [images, size, path, options] = args }
 | 
						|
					// sanity check...
 | 
						|
					if(args.length < 3){
 | 
						|
						throw new Error('.makeResizedImage(..): '
 | 
						|
							+'need at least: images, size and path.') }
 | 
						|
					return [
 | 
						|
						(images == null || images == 'all') ? 
 | 
						|
								this.data.getImages('all')
 | 
						|
							: images == 'current' ? 
 | 
						|
								[this.current]
 | 
						|
							: images instanceof Array ? 
 | 
						|
								images 
 | 
						|
							: [images],
 | 
						|
						...args.slice(1),
 | 
						|
					]},
 | 
						|
				// prepare index independent data, this can be a tad slow...
 | 
						|
				function(gid, _, path, options={}){
 | 
						|
					// special case: we already got the paths...
 | 
						|
					if(gid instanceof Array){
 | 
						|
						return gid }
 | 
						|
 | 
						|
					var image = this.images[gid]
 | 
						|
					// options...
 | 
						|
					var {
 | 
						|
						name, 
 | 
						|
						data, 
 | 
						|
					} = options || {}
 | 
						|
					name = name || '%n'
 | 
						|
					// skip non-images...
 | 
						|
					if(!image || !['image', null, undefined]
 | 
						|
							.includes(image.type)){
 | 
						|
						return runner.SKIP }
 | 
						|
					return [
 | 
						|
						// source...
 | 
						|
						this.getImagePath(gid),
 | 
						|
						// target...
 | 
						|
						pathlib.resolve(
 | 
						|
							this.location.path,
 | 
						|
							pathlib.join(
 | 
						|
								path, 
 | 
						|
								// if name is not a pattern do not re-format it...
 | 
						|
								name.includes('%') ?
 | 
						|
									this.formatImageName(name, gid, data || {})
 | 
						|
									: name)),
 | 
						|
						// image data...
 | 
						|
						// note: we include only the stuff we need...
 | 
						|
						{
 | 
						|
							orientation: image.orientation,
 | 
						|
							flipped: image.flipped,
 | 
						|
							// crop...
 | 
						|
						},
 | 
						|
					] }),
 | 
						|
			// do the actual resizing (global queue)...
 | 
						|
			function([source, to, image={}], size, _, options={}){
 | 
						|
				// handle skipped items -- source, to and image are undefined...
 | 
						|
				if(source == null){
 | 
						|
					return undefined }
 | 
						|
 | 
						|
				// sizing...
 | 
						|
				var fit = 
 | 
						|
					typeof(size) == typeof('str') ?
 | 
						|
						(size.endsWith('px') ?
 | 
						|
							'inside'
 | 
						|
						: size.endsWith('p') ?
 | 
						|
							'outside'
 | 
						|
						: 'inside')
 | 
						|
					: 'inside'
 | 
						|
				size = parseInt(size)
 | 
						|
				// options...
 | 
						|
				var {
 | 
						|
					enlarge,
 | 
						|
					skipSmaller,
 | 
						|
					overwrite,
 | 
						|
					transform, 
 | 
						|
					timestamp,
 | 
						|
					backupImagePattern,
 | 
						|
					//logger, 
 | 
						|
				} = options
 | 
						|
				// defaults...
 | 
						|
				transform = transform === undefined ? 
 | 
						|
					true 
 | 
						|
					: transform
 | 
						|
				timestamp = timestamp || Date.timeStamp()
 | 
						|
				// backup by default...
 | 
						|
				overwrite = overwrite === undefined ? 
 | 
						|
					'backup' 
 | 
						|
					: overwrite
 | 
						|
				backupImagePattern = 
 | 
						|
					(backupImagePattern 
 | 
						|
						|| '${PATH}.${TIMESTAMP}${COUNT}.bak')
 | 
						|
					.replace(/\${PATH}|$PATH/, to)
 | 
						|
					.replace(/\${TIMESTAMP}|$TIMESTAMP/, timestamp)
 | 
						|
				// backup...
 | 
						|
				// NOTE: we are doing the check at the very last moment and 
 | 
						|
				// 		not here to avoid race conditions as much as practical...
 | 
						|
				var backupName = function(){
 | 
						|
					var i = 0
 | 
						|
					var n
 | 
						|
					do{
 | 
						|
						n = backupImagePattern
 | 
						|
							.replace(/\${COUNT}|$COUNT/, i++ ? '.'+i : i)
 | 
						|
					} while(fse.existsSync(n))
 | 
						|
					return n }
 | 
						|
 | 
						|
				var img = sharp(source)
 | 
						|
				return (skipSmaller ?
 | 
						|
						// skip if smaller than size...
 | 
						|
						img
 | 
						|
							.metadata()
 | 
						|
							.then(function(m){
 | 
						|
								// skip...
 | 
						|
								if((fit == 'inside'
 | 
						|
											&& Math.max(m.width, m.height) < size)
 | 
						|
										|| (fit == 'outside'
 | 
						|
											&& Math.min(m.width, m.height) < size)){
 | 
						|
									return }
 | 
						|
								// continue...
 | 
						|
								return img })
 | 
						|
						: Promise.resolve(img))
 | 
						|
					// prepare to write...
 | 
						|
					.then(function(img){
 | 
						|
						return img 
 | 
						|
							&& ensureDir(pathlib.dirname(to))
 | 
						|
								.then(function(){
 | 
						|
									// handle existing image...
 | 
						|
									if(fse.existsSync(to)){
 | 
						|
										// rename...
 | 
						|
										if(overwrite == 'backup'){
 | 
						|
											fse.renameSync(to, backupName(to))
 | 
						|
										// remove...
 | 
						|
										} else if(overwrite){
 | 
						|
											fse.removeSync(to)
 | 
						|
										// skip...
 | 
						|
										} else {
 | 
						|
											return Promise.reject('target exists') } }
 | 
						|
									// write...
 | 
						|
									return img
 | 
						|
										.clone()
 | 
						|
										// handle transform (.orientation / .flip) and .crop...
 | 
						|
										.run(function(){
 | 
						|
											if(transform && (image.orientation || image.flipped)){
 | 
						|
												image.orientation
 | 
						|
													&& this.rotate(image.orientation)
 | 
						|
												image.flipped
 | 
						|
													&& image.flipped.includes('horizontal')
 | 
						|
													&& this.flip() }
 | 
						|
												image.flipped
 | 
						|
													&& image.flipped.includes('vertical')
 | 
						|
													&& this.flop() 
 | 
						|
											// XXX CROP
 | 
						|
											//if(crop){
 | 
						|
											//	// XXX
 | 
						|
											//}
 | 
						|
										})
 | 
						|
										.resize({
 | 
						|
											width: size,
 | 
						|
											height: size,
 | 
						|
											fit: fit,
 | 
						|
											withoutEnlargement: !enlarge,
 | 
						|
										})
 | 
						|
										.withMetadata()
 | 
						|
										.toFile(to) 
 | 
						|
										.then(function(){
 | 
						|
											// XXX what should we return???
 | 
						|
											return to }) }) }) })],
 | 
						|
 | 
						|
	// XXX should we split this into a session and global versions 
 | 
						|
	// 		a-la .makeResizedImage(..)???
 | 
						|
	// XXX this does not update image.base_path -- is this correct???
 | 
						|
	// XXX make index dir hidden...
 | 
						|
	makePreviews: ['Sharp|File/Make image $previews',
 | 
						|
		core.doc`Make image previews
 | 
						|
 | 
						|
			Make previews for all images...
 | 
						|
			.makePreviews()
 | 
						|
			.makePreviews('all')
 | 
						|
				-> promise
 | 
						|
 | 
						|
			Make previews for current image...
 | 
						|
			.makePreviews('current')
 | 
						|
				-> promise
 | 
						|
 | 
						|
			Make previews for specific image(s)...
 | 
						|
			.makePreviews(gid)
 | 
						|
			.makePreviews([gid, gid, ..])
 | 
						|
				-> promise
 | 
						|
 | 
						|
 | 
						|
			Make previews of images, size and at base_path...
 | 
						|
			.makePreviews(images, sizes)
 | 
						|
			.makePreviews(images, sizes, base_path)
 | 
						|
				-> promise
 | 
						|
 | 
						|
	
 | 
						|
		NOTE: if base_path is given .images will not be updated with new 
 | 
						|
			preview paths...
 | 
						|
		NOTE: currently this is a core.sessionQueueHandler(..) and not a .queueHandler(..)
 | 
						|
			mainly because we need to add the preview refs back to the index and this
 | 
						|
			would need keeping the index in memory even if we loaded a different index,
 | 
						|
			this is possible but needs more thought.
 | 
						|
		`,
 | 
						|
		core.sessionQueueHandler('Make image previews', 
 | 
						|
			function(queue, images, ...args){
 | 
						|
				// get/normalize images...
 | 
						|
				return [
 | 
						|
					(images == null || images == 'all') ? 
 | 
						|
							this.data.getImages('all')
 | 
						|
						: images == 'current' ? 
 | 
						|
							[this.current]
 | 
						|
						: images instanceof Array ? 
 | 
						|
							images 
 | 
						|
						: [images],
 | 
						|
					...args,
 | 
						|
				] },
 | 
						|
			function(gid, sizes, base_path, logger){
 | 
						|
				var that = this
 | 
						|
 | 
						|
				var logger_mode = this.config['preview-progress-mode'] || 'gids'
 | 
						|
 | 
						|
				// get/normalize sizes....
 | 
						|
				var cfg_sizes = this.config['preview-sizes'].slice() || []
 | 
						|
				cfg_sizes
 | 
						|
					.sort()
 | 
						|
					.reverse()
 | 
						|
				// XXX revise...
 | 
						|
				if(sizes){
 | 
						|
					sizes = sizes instanceof Array ? sizes : [sizes]
 | 
						|
					// normalize to preview size...
 | 
						|
					sizes = (this.config['preview-normalized'] ? 
 | 
						|
						sizes
 | 
						|
							.map(function(s){ 
 | 
						|
								return cfg_sizes.filter(function(c){ return c >= s }).pop() || s })
 | 
						|
						: sizes)
 | 
						|
							.unique()
 | 
						|
				} else {
 | 
						|
					sizes = cfg_sizes }
 | 
						|
 | 
						|
				// partially fill in the template...
 | 
						|
				var index_dir = this.config['index-dir'] || '.ImageGrid'
 | 
						|
				var path_tpl = that.config['preview-path-template']
 | 
						|
					.replace(/\$INDEX|\$\{INDEX\}/g, index_dir)
 | 
						|
				var set_hidden_attrib = true
 | 
						|
 | 
						|
				var img = this.images[gid]
 | 
						|
				var base = base_path 
 | 
						|
					|| img.base_path 
 | 
						|
					|| this.location.path
 | 
						|
 | 
						|
				return Promise.all(
 | 
						|
					sizes
 | 
						|
						.map(function(size, i){
 | 
						|
							var name = path = path_tpl
 | 
						|
								.replace(/\$RESOLUTION|\$\{RESOLUTION\}/g, parseInt(size))
 | 
						|
								.replace(/\$GID|\$\{GID\}/g, gid) 
 | 
						|
								.replace(/\$NAME|\$\{NAME\}/g, img.name)
 | 
						|
 | 
						|
							// set the hidden flag on index dir...
 | 
						|
							// NOTE: this is done once per image...
 | 
						|
							// NOTE: we can't do this once per call as images can
 | 
						|
							// 		have different .base_path's...
 | 
						|
							set_hidden_attrib 
 | 
						|
								&& (process.platform == 'win32' 
 | 
						|
									|| process.platform == 'win64')
 | 
						|
								&& name.includes(index_dir)
 | 
						|
								&& cp.spawn('attrib', ['+h', 
 | 
						|
									pathlib.resolve(
 | 
						|
										base,
 | 
						|
										name.split(index_dir)[0], 
 | 
						|
										index_dir)]) 
 | 
						|
							set_hidden_attrib = false
 | 
						|
 | 
						|
							// NOTE: we are 'sync' here for several reasons, mainly because
 | 
						|
							// 		this is a small list and in this way we can take 
 | 
						|
							// 		advantage of OS file caching, and removing the queue
 | 
						|
							// 		overhead, though small makes this noticeably faster...
 | 
						|
							return that.makeResizedImage('sync', gid, size, base, { 
 | 
						|
									name, 
 | 
						|
									skipSmaller: true,
 | 
						|
									transform: false,
 | 
						|
									overwrite: false,
 | 
						|
									logger: logger_mode == 'gids' ? 
 | 
						|
										false 
 | 
						|
										: logger,
 | 
						|
								})
 | 
						|
								// XXX handle errors -- rejected because image exists...
 | 
						|
								.then(
 | 
						|
									function(res){
 | 
						|
										// update metadata...
 | 
						|
										if(!base_path){
 | 
						|
											var preview = img.preview = img.preview || {} 
 | 
						|
											preview[parseInt(size) + 'px'] = name
 | 
						|
											that.markChanged
 | 
						|
												&& that.markChanged('images', [gid]) }
 | 
						|
										return [gid, size, name] },
 | 
						|
									function(err){
 | 
						|
										// XXX erro
 | 
						|
										logger 
 | 
						|
											&& logger.emit('skipped', `${gid} / ${size}`)
 | 
						|
									}) })) })],
 | 
						|
	// XXX EXPERIMENTAL: need a way to update the index when preview is 
 | 
						|
	// 		created (if we did not navigate away)
 | 
						|
	// 			- we could abort the update if we go away...
 | 
						|
	// 			- we could clone the index and if index.gid does not 
 | 
						|
	// 				match the main index use the clone to save....
 | 
						|
	// 		...the cloning approach would be quite simple:
 | 
						|
	// 			ig.clone().makePreviews()
 | 
						|
	// 		or:
 | 
						|
	// 			ig.peer.clone().makePreviews() // hypothetical api...
 | 
						|
	// 		the only question here is how to manage this...
 | 
						|
	// XXX change base_path to target path...
 | 
						|
	_makePreviews: ['- Sharp|File/Make image $previews (experimental)',
 | 
						|
		core.queueHandler('Make image previews', 
 | 
						|
			core.sessionQueueHandler('Getting image data for previews', 
 | 
						|
				// prepare the static data...
 | 
						|
				function(queue, _, images, sizes){
 | 
						|
					// sync mode...
 | 
						|
					var args = [...arguments].slice(2)
 | 
						|
					if(queue == 'sync'){
 | 
						|
						args.unshift(_)
 | 
						|
						var [images, sizes, ...args] = args }
 | 
						|
 | 
						|
					// get/normalize sizes....
 | 
						|
					var cfg_sizes = this.config['preview-sizes'].slice() || []
 | 
						|
					cfg_sizes
 | 
						|
						.sort()
 | 
						|
						.reverse()
 | 
						|
					if(sizes){
 | 
						|
						sizes = sizes instanceof Array ? sizes : [sizes]
 | 
						|
						// normalize to preview size...
 | 
						|
						sizes = 
 | 
						|
							(this.config['preview-normalized'] ? 
 | 
						|
								sizes
 | 
						|
									.map(function(s){ 
 | 
						|
										return cfg_sizes
 | 
						|
											.filter(function(c){ 
 | 
						|
												return c >= s })
 | 
						|
											.pop() || s })
 | 
						|
								: sizes)
 | 
						|
							.unique()
 | 
						|
					} else {
 | 
						|
						sizes = cfg_sizes }
 | 
						|
 | 
						|
					// XXX we should cache this on a previous stage...
 | 
						|
					var index_dir = this.config['index-dir'] || '.ImageGrid'
 | 
						|
 | 
						|
					// get/normalize images...
 | 
						|
					return [
 | 
						|
						(images == null || images == 'all') ? 
 | 
						|
								this.data.getImages('all')
 | 
						|
							: images == 'current' ? 
 | 
						|
								[this.current]
 | 
						|
							: images instanceof Array ? 
 | 
						|
								images 
 | 
						|
							: [images],
 | 
						|
						sizes,
 | 
						|
						// name template -- partially filled...
 | 
						|
						this.config['preview-path-template']
 | 
						|
							.replace(/\$INDEX|\$\{INDEX\}/g, index_dir),
 | 
						|
						// NOTE: this is not the most elegant way to go but
 | 
						|
						// 		it's better than getting it once per image...
 | 
						|
						index_dir,
 | 
						|
						...args,
 | 
						|
					] },
 | 
						|
				// generate image paths...
 | 
						|
				function(gid, sizes, path_tpl, index_dir, base_path){
 | 
						|
					var that = this
 | 
						|
					var img = this.images[gid]
 | 
						|
					var base = base_path 
 | 
						|
						|| img.base_path 
 | 
						|
						|| this.location.path
 | 
						|
					return [
 | 
						|
						gid,
 | 
						|
						// source...
 | 
						|
						this.getImagePath(gid), 
 | 
						|
						// targets -- [[size, to], ...]...
 | 
						|
						sizes
 | 
						|
							.map(function(size){
 | 
						|
								var name = path_tpl
 | 
						|
									.replace(/\$RESOLUTION|\$\{RESOLUTION\}/g, parseInt(size))
 | 
						|
									.replace(/\$GID|\$\{GID\}/g, gid) 
 | 
						|
									.replace(/\$NAME|\$\{NAME\}/g, img.name)
 | 
						|
								return [
 | 
						|
									size,
 | 
						|
									pathlib.resolve(
 | 
						|
										that.location.path,
 | 
						|
										pathlib.join(base, name)),
 | 
						|
								] }),
 | 
						|
						index_dir,
 | 
						|
					]}),
 | 
						|
			// generate the previews...
 | 
						|
			// NOTE: this is competely isolated...
 | 
						|
			// XXX args/logger is wrong here...
 | 
						|
			function([gid, source, targets, index_dir], logger){
 | 
						|
				var that = this
 | 
						|
				//var logger_mode = this.config['preview-progress-mode'] || 'gids'
 | 
						|
				
 | 
						|
				// NOTE: if this is false attrib will not be called...
 | 
						|
				var set_hidden_attrib = true
 | 
						|
				return Promise.all(
 | 
						|
					targets
 | 
						|
						.map(function([size, target]){
 | 
						|
							// set the hidden flag on index dir...
 | 
						|
							// NOTE: this is done once per image...
 | 
						|
							// NOTE: we can't do this once per call as images can
 | 
						|
							// 		have different .base_path's...
 | 
						|
							set_hidden_attrib 
 | 
						|
								&& (process.platform == 'win32' 
 | 
						|
									|| process.platform == 'win64')
 | 
						|
								&& target.includes(index_dir)
 | 
						|
								&& cp.spawn('attrib', ['+h', 
 | 
						|
									pathlib.join(target.split(index_dir)[0], index_dir)]) 
 | 
						|
							set_hidden_attrib = false
 | 
						|
 | 
						|
							// NOTE: we are 'sync' here for several reasons, mainly because
 | 
						|
							// 		this is a small list and in this way we can take 
 | 
						|
							// 		advantage of OS file caching, and removing the queue
 | 
						|
							// 		overhead, though small makes this noticeably faster...
 | 
						|
							return that.makeResizedImage('sync', [[source, target]], size, null, { 
 | 
						|
									name, 
 | 
						|
									skipSmaller: true,
 | 
						|
									transform: false,
 | 
						|
									overwrite: false,
 | 
						|
									//logger: logger_mode == 'gids' ? 
 | 
						|
									//	false 
 | 
						|
									//	: logger,
 | 
						|
								})
 | 
						|
								.then(
 | 
						|
									function(res){
 | 
						|
										// update metadata...
 | 
						|
										// XXX do this only if we are in the same index......
 | 
						|
										// 		...might be fun to create a session 
 | 
						|
										// 		queue for this at the start and if it 
 | 
						|
										// 		survives till this point we use it...
 | 
						|
										/*
 | 
						|
										if(!base_path){
 | 
						|
											var preview = img.preview = img.preview || {} 
 | 
						|
											preview[parseInt(size) + 'px'] = name
 | 
						|
											that.markChanged
 | 
						|
												&& that.markChanged('images', [gid]) }
 | 
						|
										//*/
 | 
						|
										return [gid, size, name] },
 | 
						|
									function(err){
 | 
						|
										logger 
 | 
						|
											&& logger.emit('skipped', `${gid} / ${size}`)
 | 
						|
									}) })) })],
 | 
						|
 | 
						|
	// XXX add support for offloading the processing to a thread/worker...
 | 
						|
	// XXX revise logging and logger passing...
 | 
						|
	cacheMetadata: ['- Sharp|Image/',
 | 
						|
		core.doc`Cache metadata
 | 
						|
 | 
						|
			Cache metadata for current image...
 | 
						|
			.cacheMetadata()
 | 
						|
			.cacheMetadata('current')
 | 
						|
				-> promise([ gid | null ])
 | 
						|
 | 
						|
			Force cache metadata for current image...
 | 
						|
			.cacheMetadata(true)
 | 
						|
			.cacheMetadata('current', true)
 | 
						|
				-> promise([ gid | null ])
 | 
						|
 | 
						|
			Cache metadata for all images...
 | 
						|
			.cacheMetadata('all')
 | 
						|
				-> promise([ gid | null, .. ])
 | 
						|
 | 
						|
			Force cache metadata for all images...
 | 
						|
			.cacheMetadata('all', true)
 | 
						|
				-> promise([ gid | null, .. ])
 | 
						|
 | 
						|
			Cache metadata for specific images...
 | 
						|
			.cacheMetadata([ gid, .. ])
 | 
						|
				-> promise([ gid | null, .. ])
 | 
						|
 | 
						|
			Force cache metadata for specific images...
 | 
						|
			.cacheMetadata([ gid, .. ], true)
 | 
						|
				-> promise([ gid | null, .. ])
 | 
						|
 | 
						|
 | 
						|
		This will:
 | 
						|
			- quickly reads/caches essential (.orientation and .flipped) metadata
 | 
						|
			- quickly read some non-essential but already there values
 | 
						|
			- generate priority previews for very large images (only when in index)
 | 
						|
 | 
						|
 | 
						|
		This will overwrite/update if:
 | 
						|
			- .orientation and .flipped iff image .orientation AND .flipped 
 | 
						|
				are unset or force is true
 | 
						|
			- metadata if image .metadata is not set or 
 | 
						|
				.metadata.ImageGridMetadata is not set
 | 
						|
			- all metadata if force is set to true
 | 
						|
 | 
						|
 | 
						|
		NOTE: this will effectively update metadata format to the new spec...
 | 
						|
		NOTE: for info on full metadata format see: .readMetadata(..)
 | 
						|
		`,
 | 
						|
		core.sessionQueueHandler('Cache image metadata', 
 | 
						|
			// XXX timeouts still need tweaking...
 | 
						|
			{quiet: true, pool_size: 2, busy_timeout: 400}, 
 | 
						|
			//{quiet: true, pool_size: 2, busy_timeout_scale: 10}, 
 | 
						|
			// parse args...
 | 
						|
			function(queue, image, ...args){
 | 
						|
				var that = this
 | 
						|
				var force = args[0] == 'force'
 | 
						|
 | 
						|
				// expand images...
 | 
						|
				var images = image == 'all' ?
 | 
						|
						this.images.keys()
 | 
						|
					: image == 'loaded' ?
 | 
						|
						this.data.getImages('loaded')
 | 
						|
					: image instanceof Array ?
 | 
						|
						image
 | 
						|
					: [this.data.getImage(image || 'current')]
 | 
						|
				// narrow down the list...
 | 
						|
				images = force ? 
 | 
						|
					images 
 | 
						|
					: images
 | 
						|
						.filter(function(gid){
 | 
						|
							var img = that.images[gid]
 | 
						|
							return img
 | 
						|
								// high priority must be preset...
 | 
						|
								&& ((img.orientation == null
 | 
						|
										&& img.flipped == null)
 | 
						|
									// update metadata...
 | 
						|
									|| (img.metadata || {}).ImageGridMetadata == null) })
 | 
						|
 | 
						|
				return [
 | 
						|
					images,
 | 
						|
					...args,
 | 
						|
				] },
 | 
						|
			function(image, force, logger){
 | 
						|
				var that = this
 | 
						|
 | 
						|
				// XXX cache the image data???
 | 
						|
				var gid = this.data.getImage(image)
 | 
						|
				var img = this.images[gid]
 | 
						|
				var path = img && that.getImagePath(gid)
 | 
						|
 | 
						|
				// XXX
 | 
						|
				//var base_path = that.location.load == 'loadIndex' ?
 | 
						|
				//	null
 | 
						|
				//	: tmp
 | 
						|
				//var base_path = img && img.base_path
 | 
						|
				var base_path
 | 
						|
		
 | 
						|
				// skip...
 | 
						|
				if(!(img && path
 | 
						|
						&& (force
 | 
						|
							// high priority must be preset...
 | 
						|
							|| (img.orientation == null
 | 
						|
								&& img.flipped == null)
 | 
						|
							// update metadata...
 | 
						|
							|| (img.metadata || {}).ImageGridMetadata == null))){
 | 
						|
					return }
 | 
						|
 | 
						|
				// XXX handle/report errors...
 | 
						|
				return sharp(that.getImagePath(gid))
 | 
						|
					.metadata()
 | 
						|
					.then(function(metadata){
 | 
						|
						// no metadata...
 | 
						|
						if(metadata == null){
 | 
						|
							return }
 | 
						|
 | 
						|
						var o = normalizeOrientation(metadata.orientation)
 | 
						|
						;(force || img.orientation == null)
 | 
						|
							// NOTE: we need to set orientation to something
 | 
						|
							// 		or we'll check it again and again...
 | 
						|
							&& (img.orientation = o.orientation || 0)
 | 
						|
						;(force || img.flipped == null)
 | 
						|
							&& (img.flipped = o.flipped)
 | 
						|
 | 
						|
						// mark metadata as partially read...
 | 
						|
						// NOTE: this will intentionally overwrite the 
 | 
						|
						// 		previous reader mark/mode...
 | 
						|
						img.metadata =
 | 
						|
							Object.assign(
 | 
						|
								img.metadata || {}, 
 | 
						|
								{ 
 | 
						|
									ImageGridMetadataReader: 'sharp/exif-reader/ImageGrid',
 | 
						|
									// mark metadata as partial read...
 | 
						|
									// NOTE: partial metadata will get reread by 
 | 
						|
									// 		the metadata feature upon request...
 | 
						|
									ImageGridMetadata: 'partial', 
 | 
						|
								})
 | 
						|
 | 
						|
						// read the metadata...
 | 
						|
						var exif = metadata.exif 
 | 
						|
							&& exifReader(metadata.exif) 
 | 
						|
						exif
 | 
						|
							&& Object.assign(
 | 
						|
								img.metadata, 
 | 
						|
								exifReader2exiftool(exif, metadata.xmp))
 | 
						|
 | 
						|
						// if image too large, generate preview(s)...
 | 
						|
						// XXX EXPERIMENTAL...
 | 
						|
						var size_threshold = that.config['preview-generate-threshold']
 | 
						|
						if(size_threshold
 | 
						|
								&& img.preview == null
 | 
						|
								&& Math.max(metadata.width, metadata.height) > size_threshold){
 | 
						|
							logger && logger.emit('Image too large', gid)
 | 
						|
							// XXX make this more generic...
 | 
						|
							// 		...if 'loadImages' should create previews in tmp...
 | 
						|
							that.location.load == 'loadIndex'
 | 
						|
								&& that.makePreviews(gid, 
 | 
						|
									that.config['preview-sizes-priority'] || 1080,
 | 
						|
									base_path,
 | 
						|
									logger) }
 | 
						|
 | 
						|
						that.markChanged
 | 
						|
							&& that.markChanged('images', [gid])
 | 
						|
						that.ribbons
 | 
						|
							&& that.ribbons.updateImage(gid) 
 | 
						|
 | 
						|
						return gid }) })],
 | 
						|
	cacheAllMetadata: ['- Sharp/Image/',
 | 
						|
		'cacheMetadata: "all" ...'],
 | 
						|
 | 
						|
 | 
						|
	// XXX EXPERIMENTAL...
 | 
						|
	makePreviewsAndSave: ['- Sharp|File/',
 | 
						|
		function(){
 | 
						|
			var link = this.link()
 | 
						|
			return link
 | 
						|
				.makePreviews(...arguments)
 | 
						|
				.then(function(){
 | 
						|
					link.saveIndex
 | 
						|
						&& link.saveIndex() }) }],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
var Sharp = 
 | 
						|
module.Sharp = core.ImageGridFeatures.Feature({
 | 
						|
	title: '',
 | 
						|
	doc: '',
 | 
						|
 | 
						|
	tag: 'sharp',
 | 
						|
	depends: [
 | 
						|
		'location',
 | 
						|
		'index-format',
 | 
						|
	],
 | 
						|
 | 
						|
	actions: SharpActions, 
 | 
						|
 | 
						|
	isApplicable: function(){ return !!sharp },
 | 
						|
 | 
						|
	handlers: [
 | 
						|
		// NOTE: this is about as fast as filtering the images and 
 | 
						|
		// 		calling only on the ones needing caching...
 | 
						|
		// 		...but this is not a no-op, especially on very large 
 | 
						|
		// 		indexes...
 | 
						|
		// XXX this needs to be run in the background...
 | 
						|
		// XXX this is best done in a thread 
 | 
						|
		[['loadIndex',
 | 
						|
				'loadImages', 
 | 
						|
				'loadNewImages'],
 | 
						|
			'cacheMetadata: "all"'],
 | 
						|
		//*/
 | 
						|
 | 
						|
		// set orientation if not defined...
 | 
						|
		// NOTE: progress on this is not shown so as to avoid spamming 
 | 
						|
		// 		the UI...
 | 
						|
		['updateImage',
 | 
						|
			function(_, gid){
 | 
						|
				var that = this
 | 
						|
				// NOTE: as this directly affects the visible lag, this 
 | 
						|
				// 		must be as fast as possible...
 | 
						|
				// NOTE: running .cacheMetadata(..) in sync mode here forces
 | 
						|
				// 		the image to update before it gets a change to get
 | 
						|
				// 		drawn...
 | 
						|
				;((this.images[gid] || {}).metadata || {}).ImageGridMetadata
 | 
						|
					|| this.cacheMetadata('sync', gid, false) }],
 | 
						|
 | 
						|
		// XXX need to:
 | 
						|
		// 		- if image too large to set the preview to "loading..."
 | 
						|
		// 		- create previews...
 | 
						|
		// 		- update image...
 | 
						|
		/*
 | 
						|
		['updateImage.pre',
 | 
						|
			function(gid){
 | 
						|
				var that = this
 | 
						|
				if(this.images[gid].preview == null){
 | 
						|
					sharp(this.getImagePath(gid))
 | 
						|
						.metadata()
 | 
						|
						.then(function(metadata){
 | 
						|
							// current image is larger than any of the previews...
 | 
						|
							if(Math.max(metadata.width, metadata.height) 
 | 
						|
									> Math.max.apply(Math, that.config['preview-sizes'])){
 | 
						|
								// create the currently needed preview first...
 | 
						|
								that.makePreviews(gid, that.ribbons.getVisibleImageSize())
 | 
						|
									.then(function(){
 | 
						|
										// load the created preview...
 | 
						|
										that.ribbons.updateImage(gid)
 | 
						|
 | 
						|
										// create the rest...
 | 
						|
										that.makePreviews(gid) }) } }) } }]
 | 
						|
		//*/
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
 | 
						|
 | 
						|
/**********************************************************************
 | 
						|
* vim:set ts=4 sw=4 :                               */ return module })
 |