What is it?
This script keeps selected portions of the image in the image viewer and blurs the rest. It is both a proof-of-concept and boilerplate for your own implementations. It supports multiple selections and if you navigate to a new image after adding areas to another images, the areas are automatically cleared.
Quick recap: In this thread @David talked about recompressing certain areas of images in different qualities while retaining other areas untouched. This brought me to the idea of using selective blurring instead but with adjustments this could also be used for compressing different portions in different qualities (I'd suggest converting everything to PNG first, reconverting back to JPG in last step). And Jon kindly added the new selection property to Viewer object I requested which was the last missing ingredient, which I tested today, and voila, this is the result.
Setup
There are 3 commands:
-
cuImageOpsClearCache: Clears the cache which keeps the selections; cache is necessary since the script might have been unloaded between adding areas
-
cuImageOpsAddToRect: Adds the selection to the list of areas to be processed
-
cuImageOpsBlurUnselectedPortions: Sample implementation - keeps the selected areas sharp while blurring the rest.
Add these buttons to your image viewer toolbar or assign keyboard shortcuts, etc. Use clear cache command if you want to start anew.
You need ImageMagick (e.g. Portable Win64 static at 8 bits-per-pixel) and need to set the path to magick.exe in script settings.
Usage
- Select an area and add it to the list (cuImageOpsAddToRect)
- Repeat as necessary...
- Click 8-ball (cuImageOpsBlurUnselectedPortions)
Adjust the generated file name in method getNewFileName()
as you see fit.
At the moment, I am not planning to expand further on this idea, or fix bugs, or accept feature requests, but the code should be self-explanatory and you could put DOpus script dialogs on top and annotate areas with user input, draw borders around them, magnify them, export each area individually, etc. The sky is your limit.
Demo
Step 1:
Step 2:
Output:
Script:
// @ts-check
/* eslint quotes: ['error', 'single'] */
/* eslint-disable no-inner-declarations */
/* global ActiveXObject DOpus Script */
///<reference path="./_DOpusDefinitions.d.ts" />
var DEBUG = true;
function $dbg(/**@type {any}*/str) { if (DEBUG) DOpus.output(str); }
function $dbgClear() { if (DEBUG) DOpus.clearOutput(); }
$dbgClear();
var CACHE = 'CACHE';
var IMPATH = 'Magick.exe path';
function OnInit(/** @type {DOpusScriptInitData} */ initData) { // eslint-disable-line no-unused-vars
initData.name = 'cuImageOps';
initData.desc = 'Perform image operations on the image using selected portions in the DOpus viewer';
initData.default_enable = true;
initData.group = 'cy';
initData.config[IMPATH] = 'full path to magick.exe';
}
function OnAddColumns(/** @type {DOpusAddColData} */ colData) { // eslint-disable-line no-unused-vars
// nothing yet
}
function OnAddCommands(/** @type {DOpusAddCmdData} */ cmdData) { // eslint-disable-line no-unused-vars
var cmd;
cmd = cmdData.addCommand();
cmd.name = 'cuImageOpsClearCache';
cmd.method = 'cuImageOpsClearCache';
cmd.icon = 'clearfilters';
cmd.desc = 'clears the internal cache for image change detection, use only for debugging purposes';
cmd = cmdData.addCommand();
cmd.method = 'cuImageOpsAddToRect';
cmd.name = 'cuImageOpsAddToRect';
cmd.icon = 'recursivefilter';
cmd.desc = 'adds the selected area to internal buffer, so you can perform operations on multiple areas';
cmd = cmdData.addCommand();
cmd.method = 'cuImageOpsBlurUnselectedPortions';
cmd.name = 'cuImageOpsBlurUnselectedPortions';
cmd.icon = '8ball';
cmd.desc = 'performs the image operation - in this case blurring the unselected portions';
}
/** @returns void */
function cuImageOpsClearCache() { // eslint-disable-line no-unused-vars
Script.vars.set(CACHE, '');
$dbg('cleared cache');
}
/** @returns {string} */
function _cuImageSetCache(/** @type {DOpusViewerImage} */ img) {
var _tmp = JSON.stringify(img, null, 4);
Script.vars.set(CACHE, _tmp);
return _tmp;
}
/** @returns {string} */
function _cuImageGetCache() {
if (!Script.vars.exists(CACHE)) cuImageOpsClearCache();
return Script.vars.get(CACHE);
}
function cuImageOpsAddToRect(/** @type {DOpusScriptCommandData} */ cmdData) { // eslint-disable-line no-unused-vars
var v = cmdData.func.viewer;
if (!v || !v.selection || !v.selection.width || !v.selection.height) return;
// read the image from cache if any
var img = getDOpusViewerImage('' + v.current.realpath, v.imageSize);
// if the viewed image has been changed reset cache
if (img && typeof img.path !== 'undefined' && img.path !== '' + v.current.realpath) {
$dbg('image change detected, resetting image');
cuImageOpsClearCache();
img = new DOpusViewerImage('' + v.current.realpath, v.imageSize, v.selection);
}
// add the selection and set cache
_cuImageSetCache(img.addToRects(v.selection));
$dbg('current cache: ' + _cuImageGetCache());
}
function getNewFileName(/** @type {DOpusItem} */ item) {
return '' + item.path + '\\' + item.name_stem + '-' + getISODateTime();
}
function cuImageOpsBlurUnselectedPortions(/** @type {DOpusScriptCommandData} */ cmdData) { // eslint-disable-line no-unused-vars
var v = cmdData.func.viewer;
if (!v) return;
var img = getDOpusViewerImage('' + v.current.realpath, v.imageSize);
$dbg(pmsprintf('performing operation -- path: %s, size: %s', img.path, JSON.stringify(img.size)));
var magick_bin = Script.config[IMPATH];
var cmdline, shell = new ActiveXObject('WScript.shell');
var original_file = '' + v.current.realpath;
var _outbase = getNewFileName(v.current);
var output_file = _outbase + v.current.ext;
var selected_areas = '';
for (var rkey in img.rects) {
/** @type {DOpusViewerRect}*/
var r = img.rects[rkey];
selected_areas += pmsprintf(' ( "%s" -crop %sx%s+%s+%s +repage ) -geometry +%s+%s -composite ', original_file, r.width, r.height, r.left, r.top, r.left, r.top);
}
cmdline = pmsprintf(
'"%s" "%s" -blur 0x8 %s "%s"',
magick_bin,
original_file,
selected_areas,
output_file
);
$dbg(cmdline); shell.Run(cmdline, 0, true); // 0: hidden, true: wait
}
/** poor man's sprintf, only %s */
function pmsprintf(/** @type {String}*/ template) {
var idx = 0, args = Array.prototype.slice.call(arguments, 1);
return template.replace(/%s/g, function () {
return idx < args.length ? args[idx++] : '%s';
});
}
/** @returns {DOpusViewerImage} */
function getDOpusViewerImage(/** @type {string} */ path, /** @type {DOpusRect} */ size) {
/** @type {DOpusViewerImage|undefined} */
var img;
var cache = _cuImageGetCache();
if (cache) {
try {
$dbg('read from cache: ' + cache);
var _tmp = JSON.parse(cache);
img = new DOpusViewerImage(_tmp.path, _tmp.size, _tmp.rects);
} catch (e) {
$dbg('JSON.parse error: ' + e.toString());
}
}
// if the image could not be parsed from cache (e.g. first time this script is called), create a new one
if (!img) {
$dbg('initializing new image');
img = new DOpusViewerImage(path, size);
}
$dbg('getDOpusViewerImage() -- img: ' + JSON.stringify(img, null, 4));
return img;
}
function DOpusViewerRect(/** @type {DOpusRect} */ rect) {
this.left = rect.left;
this.right = rect.right;
this.top = rect.top;
this.bottom = rect.bottom;
this.width = rect.width;
this.height = rect.height;
}
DOpusViewerRect.prototype.getID = function () {
return pmsprintf('%s.%s.%s.%s', this.left, this.top, this.right, this.bottom);
};
function DOpusViewerImage(/** @type {string} */ path, /** @type {DOpusRect} */ size, /** @type {any} */ rects) {
this.path = path;
this.size = new DOpusViewerRect(size);
if (rects && Object.keys(rects)) {
for (var r in rects) this.addToRects(rects[r]);
}
}
DOpusViewerImage.prototype.addToRects = function (/** @type {DOpusRect} */ rect) {
if (!this.rects) this.rects = {};
var _tmp = new DOpusViewerRect(rect);
this.rects[_tmp.getID()] = _tmp;
return this;
};
function getISODateTime() {
var oDate = new Date();
// sample call:
// ISODateTime = new Date(getNewNameData.item.modify).toISODateTime();
var vYear = oDate.getFullYear();
var vMonth = '' + (1 + oDate.getMonth()); if (vMonth.length == 1) { vMonth = '0' + vMonth; }
var vDay = oDate.getDate().toString(); if (vDay.length == 1) { vDay = '0' + vDay; }
var vHours = oDate.getHours().toString(); if (vHours.length == 1) { vHours = '0' + vHours; }
var vMinutes = oDate.getMinutes().toString(); if (vMinutes.length == 1) { vMinutes = '0' + vMinutes; }
var vSeconds = oDate.getSeconds().toString(); if (vSeconds.length == 1) { vSeconds = '0' + vSeconds; }
var ISODateTime = '' + vYear + vMonth + vDay + '-' + vHours + vMinutes + vSeconds;
return ISODateTime;
}
/** {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys */
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}