diff --git a/Viewer/features/filesystem.js b/Viewer/features/filesystem.js index 1ac58c46..643db672 100755 --- a/Viewer/features/filesystem.js +++ b/Viewer/features/filesystem.js @@ -2082,8 +2082,10 @@ var FileSystemWriterActions = actions.Actions({ // XXX handle .image.path and other stack files... // XXX local collections??? // - // XXX BUG: seems to ignore max_size... - // ...'preview-size' does not affect the base image size... + // XXX BUG: max_size is measured by preview size and ignores main + // image size... + // ...this results in exported images being previews ONLY IF + // they have previews larger than max_size... // XXX BUG: this does not remove previews correctly... // to reproduce: // open: L:\media\img\my\2019 @@ -2123,10 +2125,14 @@ var FileSystemWriterActions = actions.Actions({ path = path || './exported' path = util.normalizePath(path) - max_size = parseInt(max_size || settings['preview-size-limit']) || null + max_size = parseInt(max_size + || settings['preview-size-limit']) + || null // XXX make this dependant on max_size.... include_orig = include_orig || true + var resize = max_size && this.makeResizedImage + // clear/backup target... clean_target_dir = clean_target_dir === undefined ? settings['clean-target'] @@ -2258,8 +2264,12 @@ var FileSystemWriterActions = actions.Actions({ // we are using as the primary image to // save space... : null }) - // add primary image... - .concat(include_orig && img.path ? + // add primary image (copy)... + // XXX check if any of the previews/main images + // matches the size and copy instead of resize... + .concat((!resize + && include_orig + && img.path) ? [[ (replace_orig && max != null) ? // replace the base image with the @@ -2306,18 +2316,25 @@ var FileSystemWriterActions = actions.Actions({ // XXX do we queue these or let the OS handle it??? // ...needs testing, if node's fs queues the io // internally then we do not need to bother... - queue.push(copy(from, to) - .then(function(){ - logger && logger.emit('done', to) }) - .catch(function(err){ - logger && logger.emit('error', err) })) } }) } }) + queue + .push(copy(from, to) + .then(function(){ + logger && logger.emit('done', to) }) + .catch(function(err){ + logger && logger.emit('error', err) })) } }) } }) - // prep the index... + // primary image (resize)... + resize + && include_orig + && queue + .push(this.makeResizedImage(gids, max_size, path, { logger })) + + // index... var index = this.prepareIndexForWrite(json, true) - // NOTE: if we are to use .saveIndex(..) here, do not forget // to reset .changes - queue.push( + queue + .push( file.writeIndex( index.index, index_path, @@ -2335,8 +2352,7 @@ var FileSystemWriterActions = actions.Actions({ return Promise.all(queue) }], - // XXX BUG: seems to ignore max_size... - // ...'preview-size' does not affect the base image size... + // XXX ASAP add option to control copy/resize -> .makeResizedImage(..)... // XXX might also be good to save/load the export options to .ImageGrid-export.json // XXX resolve env variables in path... (???) // XXX make custom previews (option)... @@ -2458,8 +2474,14 @@ var FileSystemWriterActions = actions.Actions({ && !fse.existsSync(to) && outputFile(to, img.text || '') - // normal images... + // normal images (resize)... + } else if(that.makeResizedImage){ + // XXX can we make this batch... + return that.makeResizedImage(gid, size, img_dir, { name, logger }) + + // normal images (copy)... } else { + //*/ // NOTE: we are intentionally losing image dir // name here -- we do not need to preserve // topology when exporting... @@ -2470,10 +2492,6 @@ var FileSystemWriterActions = actions.Actions({ +'/' + that.images.getBestPreview(gid, size).url - - // XXX see if we need to make a preview (sharp) - // XXX - var to = img_dir +'/'+ name logger && logger.emit('queued', to) diff --git a/Viewer/features/sharp.js b/Viewer/features/sharp.js index e12a14c1..e717c8df 100755 --- a/Viewer/features/sharp.js +++ b/Viewer/features/sharp.js @@ -34,6 +34,7 @@ if(typeof(process) != 'undefined'){ /*********************************************************************/ if(typeof(process) != 'undefined'){ + var copy = file.denodeify(fse.copy) var ensureDir = file.denodeify(fse.ensureDir) } @@ -112,15 +113,184 @@ var SharpActions = actions.Actions({ } }) - this.previewConstructorWorker.__post_handlers = {} - }], + this.previewConstructorWorker.__post_handlers = {} }], stopPreviewWorker: ['- Sharp/', function(){ this.previewConstructorWorker && this.previewConstructorWorker.kill() - delete this.previewConstructorWorker - }], + delete this.previewConstructorWorker }], + + // XXX should this resize up??? ...option??? + // XXX add transform/crop support... + // XXX revise logging... + 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... + // + // Used if processing a single image, ignored otherwise. + name: , + + // image name pattern and data... + // + // NOTE: for more info on pattern see: .formatImageName(..) + pattern: , + data: { .. }, + + // overwrite, backup or skip (default) existing images... + // + // default: null / false + overwrite: true | 'backup' | false, + + // XXX not implemented... + transform: ..., + crop: ..., + + logger: ... +, } + + + NOTE: this will not overwrite existing images. + `, + function(images, size, path, options={}){ + var that = this + + // sanity check... + if(arguments.length < 3){ + throw new Error('.makeResizedImage(..): ' + +'need at least images, size and path.') } + // get/normalize images... + //images = images || this.current + images = images + || 'all' + // keywords... + images = images == 'all' ? + this.data.getImages('all') + : images == 'current' ? + this.current + : images + images = images instanceof Array ? + images + : [images] + // sizing... + var fit = + typeof(size) == typeof('str') ? + (size.endsWith('px') ? + 'inside' + : size.endsWith('p') ? + 'outside' + : 'inside') + : 'inside' + size = parseInt(size) + // options... + var { + // naming... + name, + pattern, + data, + // file handling... + overwrite, + // transformations... + // XXX not implemented... + transform, + // XXX not implemented... + crop, + + logger, + } = options + // defaults... + pattern = pattern || '%n' + /* XXX + transform = transform === undefined ? + true + : transform + //*/ + logger = logger || this.logger + logger = logger && logger.push('Resize') + + var timestamp = Date.timeStamp() + + return Promise.all(images + .map(function(gid){ + // skip non-images... + if(that.images[gid].type != undefined){ + return } + + // paths... + var source = that.getImagePath(gid) + var to = pathlib.join( + path, + (images.length == 1 && name) ? + name + : that.formatImageName(pattern, gid, data || {})) + + logger && logger.emit('queued', to) + + // existing image... + if(fse.existsSync(to)){ + // rename... + if(overwrite == 'backup'){ + var i = 0 + while(fse.existsSync(`${to}.${timestamp}'-bak`+ (i || ''))){ + i++ } + fse.renameSync( + to, + fse.existsSync(`${to}.${timestamp}'-bak`+ (i || ''))) + // remove... + } else if(overwrite){ + fse.removeSync(to) + // skip... + } else { + logger && logger.emit('skipping', to) + return } } + + return ensureDir(pathlib.dirname(to)) + .then(function(){ + return sharp(source) + .clone() + // handle transform (.orientation / .flip) and .crop... + .run(function(){ + // XXX + if(transform || crop){ + throw new Error('.makeResizedImage(..): ' + +[ + transform ? 'transform' : [], + crop ? 'crop' : [], + ].flat().join(' and ') + +' not implemented...') } + // XXX need clear spec defining what + // order transforms are applied + // and in which coordinates we + // crop (i.e. pre/post transform)... + if(transform){ + } + if(crop){ + } + }) + .resize({ + width: size, + height: size, + fit: fit, + }) + .withMetadata() + .toFile(to) + .then(function(){ + logger + && logger.emit('done', to) })}) })) }], + + // XXX use .makeResizedImage(..) // XXX should this account for non-jpeg images??? // XXX BUG?: this breaks on PNG images... // XXX log: count gids and not specific images... @@ -150,12 +320,17 @@ var SharpActions = actions.Actions({ // get/normalize images... //images = images || this.current - images = images || 'all' + images = images + || 'all' // keywords... - images = images == 'all' ? this.data.getImages('all') - : images == 'current' ? this.current + images = images == 'all' ? + this.data.getImages('all') + : images == 'current' ? + this.current : images - images = images instanceof Array ? images : [images] + images = images instanceof Array ? + images + : [images] // // Format: @@ -203,8 +378,7 @@ var SharpActions = actions.Actions({ .unique() } else { - sizes = cfg_sizes - } + sizes = cfg_sizes } var path_tpl = that.config['preview-path-template'] .replace(/\$INDEX|\$\{INDEX\}/g, that.config['index-dir'] || '.ImageGrid') @@ -280,8 +454,7 @@ var SharpActions = actions.Actions({ sizes, base_path, path_tpl, - post_handler) }))} - }], + post_handler) }))} }], }) diff --git a/Viewer/lib/preview.js b/Viewer/lib/preview.js index 536bc86f..c82af739 100755 --- a/Viewer/lib/preview.js +++ b/Viewer/lib/preview.js @@ -80,8 +80,7 @@ function(images, sizes, base_path, target_tpl, callback){ .map(function(res){ // skip if image is smaller than res... if(res >= orig_res){ - return - } + return } var rel = target .replace(/\$RESOLUTION|\$\{RESOLUTION\}/g, res)