Btw. what determines the order of the colors? It's unfortunate that there is no square with the color being referred to / the color produced.
I have some AutoHotKey experience, but JavaScript is very difficult, I don't see anything referring to item_id: "cCxx", .... So it's difficult to make the content of this match a new HeX field.
Code: Select all
// script to add a change colors tool
// tool data and custom icon
var iconSet_colorsTool = { button:
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F500000000FFFF7043FFFF7043FFFF7043FFFF7043FFFF704300000000FFD70C50FFD70C50FFD70C50FFD70C50FFD70C50000000000000000000000000FF84C0F6FF47A7F5FF42A5F5FF42A5F5FF42A5F500000000FFFF794FFFFF7043FFFF7043FFFF7043FFFF704300000000FFF73C68FFD70C50FFD70C50FFD70C50FFD70C50000000000000000000000000FF89C4F6FF66B6F6FF47A7F5FF3FA4F5FF42A5F500000000FFFF8F6DFFFF7A51FFFF7043FFFF7043FFFF704300000000FFFF597DFFF73C68FFD70C50FFD70C50FFD70C50000000000000000000000000FF8AC4F7FF71BBF7FF65B5F6FF44A6F5FF42A5F500000000FFFF9271FFFF906EFFFF7A51FFFF7043FFFF704300000000FFFF597DFFFF597DFFF73C68FFD70C50FFD70C50000000000000000000000000FF8CC5F6FF71BBF7FF71BBF7FF65B5F7FF42A5F500000000FFFF9271FFFF9372FFFF906EFFFF7A51FFFF704300000000FFFF597DFFFF597DFFFF597DFFF73C68FFD70C500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF7D55C2FF7D55C2FF7D55C2FF7D55C2FF7D55C200000000FFFFA726FFFFA726FFFFA726FFFFA726FFFFA72600000000FF000000FF000000FF9D9D9DFFD6D6D6FF9D9D9D000000000000000000000000FF8E6DC9FF7D55C2FF7D55C2FF7D55C2FF7D55C200000000FFFFB74DFFFFA92CFFFFA726FFFFA726FFFFA72600000000FF323232FF9D9D9DFFD6D6D6FFFBA98FFFFD815A80FD89640000000000000000FF9D80D1FF8E6DC9FF7D55C2FF7D55C2FF7D55C200000000FFFFBD5BFFFFB850FFFFA92CFFFFA726FFFFA72600000000FFA1A1A1FFD7D7D7FFFAE098FFFDAA83FFFF7043FFFD896480FD896400000000FFAD96D7FF9D80D1FF8E6DC9FF7D55C2FF7D55C200000000FFFFBC59FFFFBD5CFFFFB850FFFFA726FFFFA7265FF4F4F4FFD7D7D7FFFAD28EFFFCDE80FFFDDF84FFFC9D6FFFFF7043FFFD815A00000000FFAD96D7FFAD96D7FF9D80D1FF8E6DC9FF7D55C200000000FFFFBC59FFFFBC59FFFFBD5CFFFFB850FFFBC8A0BEF4F4F4FFFBC679FFFFBD5CFFFCCC79FFFDDB6BFFFDD55CFFFC8F5AFFFB9575000000000000000000000000000000000000000000000000000000000000000000000000000000005FF4F4F4BEF4F4F4FFFBC679FFFFBD5CFFFFBD5CFFFFB241FFFCBB4DFFFDD557FFFADA83BEF4F4F400000000FF85B850FF79B23CFF7BB23FFF79B23CFF79B23C00000000FFFDDF87FFFDDF87FFFAE7BAFFF6EFDFFFFBC679FFFFBD5CFFFFBD5CFFFFB241FFFFA726FFFFA726FFFAC975BEF4F4F45FF4F4F400000000FF9CC671FF85B850FF79B23CFF7BB23FFF79B23C00000000FFFEE392FFFAE7BAFFF6EFDFFFFBC679FFFFBD5CFFFFBD5CFFFFB241FFFFA726FFFFA726FFFCBA5BFFF7F7F75FF4F4F40000000000000000FFABCD8AFF9CC671FF85B850FF79B23CFF79B23C00000000FFFEE28DFFF7F0E0FFF9CB8AFFFFBD5CFFFFBD5CFFFFB241FFFFA726FFFFA726FFFCBA5BFFF7F7F7FFFBFBFB000000000000000000000000FFBBD5A2FFABCE8AFF9CC671FF85B850FF79B23C00000000FFFDE291FFF5EDE7FFF9DBC1FFFCC372FFFFB241FFFFA726FFFFA726FFFCBA5BEEF6F6F6FFFBFBFBFFFFFFFF000000000000000000000000FFBBD5A2FFBBD5A2FFABCE8AFF9CC671FF85B85000000000FFFDE291FFF6E4D4FFFAD9BCFFF8D6B6FFFCB044FFFFA726FFFCBA5BEEF6F6F6D6FAFAFABEFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000000000000000000000000000000000000FFCCC5BFFFF5D1AFFFF9CDA6FFF7D0ACFFFAC273BEF4F4F45FF4F4F400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FF000000FFC1B8B0FFF6DFCBFFF4ECE5BEF4F4F45FF4F4F4000000000000000000000000000000000000000000000000",
iconStream:function(val){var data=this[val];
return {count:0, width:20, height:20, read:function(nBytes){return data.slice(this.count,this.count+=2*nBytes)}}}
};
// This adds a button to the Add-on Tools toolbar
app.addToolButton( {
cName: "changeColorAllTool",
cLabel: "Colors…",
oIcon: iconSet_colorsTool.iconStream("button"),
cTooltext: "Change Colors of Selected Annotations…",
cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
cExec: "changeColorsAll(this)" }
);
// This adds a menu item
app.addMenuItem( {
cName: "changeColorAllMenu",
cUser: "Change Colors…",
oIcon: iconSet_colorsTool.iconStream("button"),
cParent: "Tools", // Change this to "Comments" to put in the Comment menu, etc
//nPos: 13,
cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
cExec: "changeColorsAll(this)" }
);
//Added HeX extra MB
//End of HeX code
/* Script to change color of all annotations with a dialog
v 1.2 date Sep 20, 2023
only tested on PDF-Xchange Editor v10.0
* History:
v1.0 Jun 20, 2023 Initial release
v1.1 Jul 04, 2023 Fix to global variable access, revise icon
v1.2 Sep 20, 2023 Look at rich contents in all annotations
*/
function changeColorsAll (t) {
const MAX_SAVED_COLORS = 8; // This is the number of custom colors that will be saved
// ************************* Begin Color Utility ********************************
const cUtil = {
defaultColors: {
"Black":color.black,
"Dark Gray 25%":color.dkGray,
"Gray 40%":["G",0.4],
"Gray":color.gray,
"Light Gray 75%":color.ltGray,
"White":color.white,
"Transparent":color.transparent, // only in here because selected items will have it
"Dark Red":["RGB",139/255,0,0],
"Red":color.red,
"Rose":["RGB",1,228/255,225/255],
"Light Orange":["RGB",1,173/255,91/255],
"Orange":["RGB",1,104/255,32/255],
"Gold":["RGB",1,215/255,0],
"Yellow":color.yellow,
"Light Yellow":["RGB",1,1,224/255],
"Lime":["RGB",50/255,205/255,50/255],
"Pale Green":["RGB",152/255,251/255,152/255],
"Bright Green":color.green,
"Sea Green":["RGB",60/255,179/255,113/255],
"Green":["RGB",0,147/255,0],
"Dark Green":["RGB",0,85/255,0],
"Aqua":["RGB",127/255,1,212/255],
"Teal":["RGB",56/255,142/255,142/255],
"Turquoise":["RGB",64/255,224/255,208/255],
"Sky Blue":["RGB",192/255,1,1],
"Light Blue":["RGB",125/255,158/255,192/255],
"Cyan":color.cyan,
"Blue":color.blue,
"Dark Blue":["RGB",0,0,139/255],
"Indigo":["RGB",75/255,0,130/255],
"Midnight Blue":["RGB",0,0,94/255],
"Plum":["RGB",72/255,0,72/255],
"Pink":["RGB",1,192/255,203/255],
"Violet":["RGB",128/255,0,128/255],
"Magenta":color.magenta,
"Blue-Grey":["RGB",123/255,123/255,192/255],
"Brown":["RGB",165/255,142/255,0],
"Tan":["RGB",210/255,180/255,140/255],
//custom colors added by MB
"CBlue":["RGB",69/255,163/255,255/255],
"CPink":["RGB",255/255,192/255,203/255],
"CViolet":["RGB",200/255,126/255,231/255],
"CGreen":["RGB",135/255,206/255,111/255],
"COrange":["RGB",245/255,188/255,82/255],
"CBrown":["RGB",210/255,180/255,140/255],
"CYellow":["RGB",255/255,238/255,88/255],
},
otherColorText: "Other - Enter:",
savedCls: {},
annotCls: {},
// inititalize saved and annotation colors
initialize: function(savedColors, annots) {
// add the saved colors (clobbers what's in there)
this.savedCls = savedColors || {};
// add the annotation colors
this.annotCls = this.getColors( annots, [] );
},
// returns object {colorName:color,...}
getSavedColors: function( withOtherClr=false, withAnnotCls=false ) {
let sc = {};
let gsc = [ this.defaultColors, this.savedCls ];
// get other colors text
if (withOtherClr)
sc[ this.otherColorText ] = "";
// build color associative array
// because the key is the color name, if a duplicate color has been saved with a different name, it will show up
gsc.forEach( cl => Object.assign(sc, cl) );
// add in annotation colors
// this checks that it's not already in the list
if (withAnnotCls) {
let selClrs = this.findColors( Object.values( this.annotCls ), Object.values( sc ) ); // array
// doesn't bother to keep the text of the color
Object.assign(sc, this.colObj(selClrs));
}
return sc;
},
// get list of stroke colors of annotations, and only return those that are not in colorList[]
// returns object {colorName:color,...}
getColors: function(annots, colorList){
let fL = [];
for (let i in annots) {
let ann = annots[i];
// get colors from richContents
if ( ann.richContents && ann.richContents.length )
fL = fL.concat( ann.richContents.map( rc => rc.textColor ));
// default:
if ( ann.strokeColor ) fL.push( ann.strokeColor );
if ( ann.fillColor ) fL.push( ann.fillColor );
};
fL = this.findColors(fL, colorList);
return this.colObj(fL);
},
// returns only the colors in findList[] that are not in colorList[]
// returns array of colors
findColors: function(findList, colorList) {
let addList =[];
for ( let fc of findList ) {
let ceq = colorList.some( c => cUtil.equal( fc, c ) );
if (!ceq) { // not in the list, so add it
addList.push( fc );
};
};
return addList;
},
// add a color to the saved colors list
// returns object {colorName:color,...}
addlClrs: function( addColor, currColors=this.savedCls ) {
// will not add if it's in the default colors list
let ac = this.findColors( [addColor], Object.values(this.defaultColors));
// need to see if it's already in the list
if (ac.length) {
ac = this.findColors(ac, Object.values(currColors));
}
// ac is null if no additional colors
if (ac.length) {
this.trimObj( currColors, MAX_SAVED_COLORS-1 );
// add ac to the currColors list and return
Object.assign( currColors, this.colObj(ac) );
}
return currColors;
},
// make a color:value list of colors
// returns object {colorName:color,...}
colObj: function(colList) {
let colOb = {};
// use the default colors to find actual color names
let cNames = Object.keys( this.defaultColors );
for ( let c of colList ) {
let cN = cNames.find( cc => cUtil.equal( this.defaultColors[cc], c ));
if ( undefined == cN ) cN = this.colArr256(c);
colOb[ cN ] = c;
};
return colOb;
},
// change the values to 256 based and return string
colArr256: function(colr) {
let c = [colr[0].toUpperCase()];
for (let i=1; i<colr.length; i++){
c[i] = Math.round( 255*colr[i] );
}
return c.join(", ");
},
// change 256 based string to color array
colFromStr: function(colStr) {
let sColor = colStr.split(",");
sColor[0] = sColor[0].toUpperCase();
// maybe should check if it's in the correct format?
for (let i=1; i<sColor.length; i++ ) {
sColor[i] = sColor[i] / 255;
}
return sColor;
},
trimObj: function (ob,maxL) {
// deletes from the front of ob.
let ol = Object.keys(ob).length - maxL;
for (let i in ob) {
// trim length
if ( ol > 0 ) {
delete ob[i];
ol--;
} else {
break;
}
}
//return ob;
},
// based on the color.equal, but added rounding
equal : function (c1, c2) {
if (c1[0] == "G") {
c1 = color.convert(c1, c2[0]);
}
else {
c2 = color.convert(c2, c1[0]);
}
if (c1[0] != c2[0]) {
return false;
}
let nComponents = 0;
switch (c1[0]) {
case "G":
nComponents = 1;
break;
case "RGB":
nComponents = 3;
break;
case "CMYK":
nComponents = 4;
break;
case "HeX": //MB added
nComponents = 3;
break;
default: ;
}
for (let i = 1; i <= nComponents; i++) {
if ( Math.round(c1[i]*255) != Math.round(c2[i]*255) ) {
return false;
}
}
return true;
}
}
// ************************* Begin Object Utility ********************************
const obUtil = {
// get object with prop == val
getObj: function ( obj, prop, val ) {
let found;
if ( obj[prop] == val ) {
return obj;
} else {
if (obj.elements) {
for (let e in obj.elements) {
found = this.getObj( obj.elements[e], prop, val );
if (found) break;
}
}
};
return found;
},
}
// ************************* Begin Dialog ********************************
const colorDialog = {
colors:[],
colorKeys:[], // to hold all the colors for the dropdowns (keys - text)
data:[], // dialog results
selColors:[], // selected color (index on colors or colorKeys)
nClrs: () => colorDialog.selColors.length , // number of selected colors
//nAnns: 0,
initialize: function (dialog) {
let dLoad = {}; //cUtil.updateObject( {}, this.data ); // don't change data
// need to deal with the popups
for (let i=0;i<this.nClrs();i++) {
let butn = util.printf("%02d", i);
dLoad[ "cE"+butn ] = this.colorKeys[ this.selColors[i] ];
dLoad[ "cC"+butn ] = cUtil.colArr256( this.colors[ this.selColors[i] ] );
dLoad[ "cP"+butn ] = this.getListboxArray( this.colorKeys, this.selColors[i] );
};
dialog.load( dLoad );
},
commit:function (dialog) { // called when OK pressed
this.data = dialog.store();
// need to deal with the popups
for (let i=0;i<this.nClrs();i++) {
let butn = util.printf("%02d", i);
this.data[ "cP"+butn ] = this.getIndex( this.data[ "cP"+butn ] );
};
// return "ok"
},
// returns the index number of the first item with positive number value
getIndex: function (elements) {
for (let i in elements) {
if ( elements[i] > 0 ) {
return elements[i]-1; //i ; the index is the text of the dropdown
}
}
},
// create object array suitable for the listbox. selItem is index
// returned array is {"Displayed option":-order,...}
getListboxArray: function(vals, selItem) {
let sub = {};
for (let i=0; i<vals.length; i++) {
// positive number if selected
sub[vals[i]] = ((selItem == i)?1:-1)*(i+1);
}
return sub;
},
// change to the custom color input box -- needs to be called from each box
// butn is formatted util.printf("String format: %02d", n)
clrInput: function (dialog,butn) {
let results = dialog.store();
let pickedIndx = this.colors.findIndex( c => cUtil.equal( c, cUtil.colFromStr( results["cC"+butn] ) ));
if ( pickedIndx < 0 ) pickedIndx = 0;
let dLoad={};
dLoad["cP"+butn] = this.getListboxArray( this.colorKeys, pickedIndx );
dialog.load( dLoad );
},
clrDropdn: function (dialog,butn) { // change to the dropdown
let results = dialog.store();
// update the custom color box
let pickedClr = this.colors[ this.getIndex( results["cP"+butn] ) ];
if ( pickedClr ) { // should be falsy "" for othercolor
let dLoad={};
dLoad["cC"+butn] = cUtil.colArr256(pickedClr);
dialog.load( dLoad );
}
//dialog.enable({"cClr": (results["hClr"][cUtil.otherColorText] > 0)});
},
description: {
name: "Replace Color", // Dialog box title
align_children: "align_left",
width: 380,
//height: 200,
elements:
[{ type: "cluster",
name: "Colors",
align_children: "align_left",
item_id: "Ctnr", // this name is used to get the container for the dropdowns
elements:
[{ type: "view",
align_children: "align_row",
name:"Headings",
//item_id: "Ctnr",
elements:
[
{ type: "static_text",
name: "Current Color RGB",
width: 100,
alignment: "align_center",
bold: true
},
{ type: "static_text",
name: "Current Color HeX",
width: 100,
alignment: "align_center",
bold: true
},
{ type: "static_text",
name: "|",
width: 5
},
{ type: "static_text",
name: "Select New Color",
width: 100,
bold: true
},
{ type: "gap",
width: 20 // for the dropdown arrow
},
{ type: "static_text",
name: "Enter Color RGB",
alignment: "align_fill",
//width: 120
bold:true
},
//MB added first row
{
type: "static_text",
name: "Enter Color Hex",
width: 120,
alignment: "align_center",
bold: true
},
]
}
]
},
{ type:"static_text",
name: "The first element in the comma separated list is a string denoting the color space \ntype. The subsequent elements are numbers that range between zero and 255 inclusive. \nFor example, the color red can be represented as [RGB, 255, 0, 0]. \nColor Space options are: \nG (Gray - single value 0 is black), RGB (3 values), CMYK (4 values).",
font: "palette",
alignment: "align_fill",
width: 380,
height:70,
},
{ alignment: "align_right",
type: "ok_cancel",
ok_name: "Ok",
cancel_name: "Cancel"
}
]
}
};
// menu items
class MenuItem {
baseMenu = { type:"view",
align_children:"align_row",
//item_id: "cTxx", // typical container //MB 2nd row and down current RGB color
elements:
[{ type:"static_text",
name: "Existing Color RGB", // for the existing color RGB : original "Existing Color"
width:100,
alignment: "align_center",
item_id: "cExx"
},
//MB added new HeX current color
{ type:"static_text",
name: "Existing Color HeX", // for the existing color HeX
width:100,
alignment: "align_center",
item_id: "cHxx"
},
{ type: "static_text",
name: "|",
width: 5
},
{ type: "popup",
alignment: "align_fill",
width: 100,
item_id: "cPxx"
},
{ type: "edit_text",
width:125,
alignment: "align_fill",
item_id: "cCxx",
},
//MB added HeX field 2nd row and others
{ type: "edit_text",
width: 60,
alignment: "align_fill",
item_id: "cHxx",
}
]
};
xx;
constructor ( num, dia ) {
let xx = this.xx = util.printf("%02d", num);
// relabel
this.baseMenu.elements.forEach( e =>
e.item_id = (e.item_id ? e.item_id.substring(0,2)+xx : null ));
};
// fns = { diaID:func,... }
setHandlers ( dia, fns ) {
let xx = this.xx;
for ( let f in fns ) {
dia[f+xx] = function(dialog) { dia[fns[f]]( dialog, xx ) };
};
};
get() {
return this.baseMenu;
}
};
// Logic:
// Get colors of all selected annotations
// Build menu > curr color > new color
// Check if any of the colors are changed & dialog "OK"
// Make a list of old color > new color
// For each annotation
// go through old color > new color list
// change all colors of that annotation with one .setProps()
// get all annotation objects - try to use selected annotations
let annArr = t.selectedAnnots;
if (!annArr || !annArr.length){
if ( t.getAnnots() ) { // ie there are some annotations in the document
var noAnSel = app.alert({
cMsg: "No Annotations selected, change all annotations in the current document?",
cTitle: "Changing ALL Annotations",
nIcon: 1, nType: 1 });
}
if ( noAnSel == 1 ) {
annArr = t.getAnnots();
} else {
return "Nothing Selected";
}
}
// try to get global variables
let savedPrefs = CHANGE_COLORS_PREFS.get();
if (!savedPrefs) {
// defaults
savedPrefs = {
savedColors:{}
//dialog:{},
};
};
// saved values to the dialog
//for (let d in savedPrefs.dialog)
// colorDialog.data[d] = savedPrefs.dialog[d]; // shallow copy
// get the annotation colors
cUtil.initialize( savedPrefs.savedColors, annArr );
// initialize dialog
//colorDialog.nAnns = annArr.length;
let allColors = cUtil.getSavedColors( true, true );
colorDialog.colors = Object.values( allColors );
colorDialog.colorKeys = Object.keys( allColors );
// build dialog dropdowns
let drContainer = obUtil.getObj ( colorDialog.description, "item_id", "Ctnr" );
drContainer.name = annArr.length + " Annotations Selected";
// array of the annotation color values
let annotColors = Object.values( cUtil.annotCls );
for (let x in annotColors) {
// get the annotation colors index to the colors list
colorDialog.selColors[x] = colorDialog.colors.findIndex( c => cUtil.equal( c, annotColors[x] ));
let dia = new MenuItem( x, colorDialog );
dia.setHandlers( colorDialog, { "cP":"clrDropdn", "cC":"clrInput" } );
drContainer.elements.push( dia.get() );
};
//MB array for rgb to HeX
function rgbToHex(rgbArray) {
// Ensure the input is in the correct format
if (rgbArray.length !== 3) {
throw new Error("RGB array must contain three values");
}
// Convert each RGB value to HEX
return "#" + rgbArray.map(value => {
const hexValue = Math.round(value * 255).toString(16);
return hexValue.length === 1 ? "0" + hexValue : hexValue;
}).join("");
}
// *** run the dialog ***
let result = app.execDialog(colorDialog);
if ("cancel" == result) return result;
// make list of changed colors RGB
let chCols = [];
for (let x in annotColors) {
// compare the new to existing colors
let xx = util.printf("%02d", x);
let newClr = cUtil.colFromStr( colorDialog.data["cC"+xx] );
if ( !cUtil.equal( annotColors[x], newClr )) {
chCols.push( {"oldCl":annotColors[x],"newCl":newClr} );
};
};
if ( !chCols.length ) return "No colors changed";
annArr.forEach( ann => {
let revs = {};
// get colors from richContents
if ( ann.richContents && ann.richContents.length ) {
let changed = false;
let spans = [];
for (let s in ann.richContents) {
spans[s] = ann.richContents[s];
for (let c of chCols) {
if ( cUtil.equal( spans[s].textColor, c.oldCl )) {
changed = true;
spans[s].textColor = c.newCl;
}
};
};
if (changed) revs["richContents"] = spans;
}
// get colors from array items props
let props = [ "strokeColor", "fillColor" ];
for (let prop of props) {
for (let c of chCols) {
// property may not exist for this annotation
if ( ann[prop] && cUtil.equal( ann[prop], c.oldCl ))
revs[ prop ] = c.newCl;
};
};
// apply the revisions
if (Object.keys(revs).length) ann.setProps( revs );
});
//MB make list of changed colors HeX
// save the custom colors
for ( let sc of chCols ) {
// only add if the strokeColor isn't part of the default and saved colors
Object.assign( savedPrefs, {"savedColors": cUtil.addlClrs( sc.newCl, savedPrefs.savedColors)} );
};
// update global variable
CHANGE_COLORS_PREFS.set( savedPrefs );
return result;
}
// need trusted function to store preferences
const CHANGE_COLORS_PREFS = new class {
constructor(name) {
this.get = app.trustedFunction(() => {
app.beginPriv();
if ( global[name] )
return JSON.parse( global[name] );
});
this.set = app.trustedFunction( value => {
app.beginPriv();
global[name] = JSON.stringify( value );
global.setPersistent( name, true);
});
}
}("CHANGE_COLORS_PREFS");