javascript: custom menu item to Mirror (Flip) annotations  SOLVED

Forum for the PDF-XChange Editor - Free and Licensed Versions

Moderators: TrackerSupp-Daniel, Tracker Support, Paul - Tracker Supp, Vasyl-Tracker Dev Team, Chris - Tracker Supp, Sean - Tracker, Ivan - Tracker Software, Tracker Supp-Stefan

Post Reply
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

javascript: custom menu item to Mirror (Flip) annotations  SOLVED

Post by Mathew »

I made a script to mirror selected annotations because PDF-XChange currently doesn't have that functionality. The source is pasted below, and I've attached a zip file with it made into a menu item. It adds to the "Edit" menu below "Duplicate", and also adds a toolbar button to the Add-Ins toolbar, but you can edit that in the menu file. Select all the annotations you want to mirror first, then run the script.
mirror annotations tool v2.3.zip
Unzip and place the javascript file into the Javascripts folder
(6.12 KiB) Downloaded 71 times
To add it, unzip the file and put the file "mirror annotations tool.js" into the Javascripts folder either in the application folder, or
C:\Users\[your user name]\AppData\Roaming\Tracker Software\PDFXEditor\3.0\Javascripts\
(If there's no Javascripts folder, make one)

The script also works if you copy and paste the source below into the JavaScript console and run it from there.

Abilities:
  • Mirrors Lines, Polylines, Polygons, Squares, Rectangles, Circles, and Pencil annotations
  • Repositions & rotates Text, Callouts, Stamps, Dimensions so that they match the rest of the mirrored markup
  • Can select which axis of the selected annotations to mirror about, and whether mirroring horizontally and/or vertically
  • Can mirror a copy of the annotations instead of mirroring the selected annotations; however, I prefer to copy and paste first, and then mirror that because then the mirrored annotations are selected already if you need to move them to the correct location.
Limitations: This is not an exhaustive list, just what I've run into while using it.
  • Cannot mirror stamps, pictures or other items that are an object that you can't edit in PDFX-Change. If you absolutely must mirror these items, you generally can by first flattening them, and then you can mirror them with the edit tool and the right click menu.
  • Doesn't mirror form elements.
  • Does not mirror text, but it repositions it and changes the left/right alignment for horizontal mirrors. Fixed as of build 370: It currently can't get the left or right alignment of the text correct (bug fix promised in version 10 of pdfx-change viewtopic.php?p=166841#p166841 )
  • Bullet or numbered lists disappear when mirrored (they become just indented rows of text). I suspect this property is not available to javascript.
  • Fixed as of build 367.0 Pencil tool annotations get completely messed up because of a bug in PDFX-Change.
  • I don't think it's possible to change the selection within a javascript, so I can't make it select the new annotations if doing a mirror copy.
  • Fixed as of build 370: If the view is rotated, "horizontal" and "vertical" in the dialog won't be correct - as of build 370 viewState.pageViewRotation gives view rotation so this is fixed.
  • Adds a long list of items to the undo, (one for each item flipped) so it takes quite a few "undo"s to undo the flip.
  • Add a comment if you see something else, I'll try to fix it.

Code: Select all

// tool data and custom icon
var iconSet_mirrorTool = { mirror:
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC0C0C00000000000000000FF91C8F6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC9C9C9FFB5B5B50000000000000000FF7BBFF6FFA1D0F50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB6B6B6FFB5B5B50000000000000000FF7BBFF7FF7DBFF60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF70BBF7FFBAD8F5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB4B4B4FFB1B1B1FFB5B5B50000000000000000FF7BBFF6FF71BBF7FF77BDF70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFBBBBBBFFB1B1B1FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF71BBF7FF71BBF7FF87C4F600000000000000000000000000000000000000000000000000000000000000000000000000000000FFAFAFAFFFB1B1B1FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF71BBF7FF71BBF7FF6FB9F6000000000000000000000000000000000000000000000000000000000000000000000000FFB6B6B6FF989898FFAAAAAAFFB1B1B1FFB6B6B60000000000000000FF7BBFF7FF71BBF7FF63B4F6FF42A5F5FF84BFF50000000000000000000000000000000000000000000000000000000000000000FF999999FF989898FF999999FFAAAAAAFFB6B6B60000000000000000FF7BBFF7FF63B4F6FF45A6F5FF42A5F5FF43A5F500000000000000000000000000000000000000000000000000000000FF9B9B9BFF989898FF989898FF989898FF999999FFAEAEAE0000000000000000FF6FB8F6FF43A6F5FF42A5F5FF42A5F5FF42A5F5FF7AB7F50000000000000000000000000000000000000000FFCCCCCCFF9A9A9AFF989898FF989898FF989898FF989898FFA2A2A20000000000000000FF58ADF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF45A6F5FFAED2F500000000000000000000000000000000FFA2A2A2FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF57ADF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF58AEF4000000000000000000000000FFB2B2B2FF989898FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF56ACF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF7CBCF50000000000000000FFA4A4A4FF989898FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF56ADF5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF5FAFF500000000FFCBCBCBFFAFAFAFFFAFAFAFFFAFAFAFFFB0B0B0FFAFAFAFFFAFAFAFFFB0B0B0FFB5B5B50000000000000000FF7EBEF5FF71B9F5FF71B9F5FF71B9F4FF71B9F5FF70B9F5FF71B9F5FF71B9F6FFA7D0F500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
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: "mirrorAnnotationsTool",
	cLabel: "Mirror",
	oIcon: iconSet_mirrorTool.iconStream("mirror"),
	cTooltext: "Flip/Mirror Comments…",
	cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
	cExec: "mirrorAnnotationsTool(this)" }
);
// This adds a menu item to the edit menu
app.addMenuItem( {
	cName: "mirrorAnnotations",
	cUser: "Flip/Mirror Comments…",
	oIcon: iconSet_mirrorTool.iconStream("mirror"),
	cParent: "Edit",
	nPos: 13,
	cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
	cExec: "mirrorAnnotationsTool(this)" }
);

/* 
   script to flip (mirror) selected annotations
  		v 2.3		date Jul 04, 2023
   only tested on PDF-Xchange Editor v10.0
 * history:
    v2.0 Mar 14 2023 initial release
  	v2.1 May 31 2023 clean up code, reduce number of undos when changing annotations, let new annotation name be auto-generated, add option to not try keeping dimension text upright
  	v2.2 Jun 23 2023 mirror the alignment of rich text when it flips horizontally, fix access to global variable, use corner points instead of bounds so that rotated rectangle edges are correct.
	v2.3 add .rotate to properties duplicated so that duplicated text doesn't get automatically rotated to view, try to improve how text is mirrored
*/

function mirrorAnnotationsTool(t) {
	const mUtil = {
		getPgRotation: function(t,page) {
			// try to figure out up or down based on page rotation
			let pgRotation = t.getPageRotation( {nPage:page} );
			// try to include the view rotation
			if (t.viewState.pageViewRotation) {
				pgRotation += t.viewState.pageViewRotation;
			}
			if (pgRotation < 0) {
				pgRotation = 360 + pgRotation;
			}
			while (pgRotation >= 360) {
				pgRotation -= 360;
			}
			return pgRotation;
		},
		doFlip: function (selAnnots) {
			// get bounding box of annotations
			let aRec = this.getBounds(selAnnots);
			// get flip point of annotations
			let aCtr = this.getAxisPt(aRec, FLIPAXIS); 
			// flip the annotations
			this.flipAnns(aCtr,selAnnots);		
		},
		getAxisPt: function ( bBox, axis) {
			// axis must be one of the keys for the array below. There's no error checking.
			// The bBox array consists of [x min, ymin, x max, y max]
			let b = [ bBox.slice(0,2),bBox.slice(2,4) ];
			// Array of which index to use for each position [x1,x2],[y1,y2]
			let axPick = {
				"AxTL": [[0,0],[1,1]],
				"AxTC": [[0,1],[1,1]],
				"AxTR": [[1,1],[1,1]],
				"AxML": [[0,0],[0,1]],
				"AxMC": [[0,1],[0,1]],
				"AxMR": [[1,1],[0,1]],
				"AxBL": [[0,0],[0,0]],
				"AxBC": [[0,1],[0,0]],
				"AxBR": [[1,1],[0,0]]
			};
			// use the flip axis as the key to get array
			// no error checking here
			let axTr = axPick[ axis ];
			// calculate the axis
			// [ (x1+x2)/2, (y1+y2)/2 ]
			let axCtr = [ (+b[axTr[0][0]][0]+b[axTr[0][1]][0])/2 , (+b[axTr[1][0]][1]+b[axTr[1][1]][1])/2 ];
			return axCtr;
		},
		// rotate a point
		rotatePoint (point, degrees, ctr) {
			let rads = degrees * Math.PI / 180;
			let x = point[0] || 0; // ||0 in case undefined
			let y = point[1] || 0;
			let x0 = ctr[0];
			let y0 = ctr[1];
			let newx = (x - x0) * Math.cos(rads) - (y - y0) * Math.sin(rads) + x0;
			let newy = (x - x0) * Math.sin(rads) + (y - y0) * Math.cos(rads) + y0;
			return [newx, newy];
		},
		// rotate a shape, coords are in form [x1,y1,x2,y2,...] or [[x1,y1],[x2,y2],...]
		rotateRect (coords, degrees, ctr) {
			if (degrees) {
				if (ctr === undefined) {
					// assume centerpoint
					ctr = [0,0];
				}
				let rotRec = [];
				let i=0;
				// step through coordinates
				while ( i<coords.length ) {
					if ( Array.isArray(coords[i]) ) {
						// keep as a pair
						rotRec = rotRec.concat( [this.rotateRect(coords[i], degrees, ctr)] );
						i++;
					} else { // assumes that i+1 contains a number
						rotRec = rotRec.concat( this.rotatePoint( [coords[i],coords[i+1]], degrees, ctr ));
						i+=2;
					}
				}
				return rotRec;
			} else { // didn't need to rotate
				return coords;
			}
		},
		// returns array containing [x min, ymin, x max, y max] of all the annotations
		getBounds: function (anns) {
			let theBounds = [];
			let aRec = [];
			let axPt = [];
			//var m = new Matrix2D;
			for (let tAnn in anns) {
				// loop through all selected annotations
				// outer bounds of this annotation in default user space
				// this is the outside of the border
				aRec = anns[tAnn].rect;
				// rotation is about the center of .rect
				axPt = this.getAxisPt(aRec,"AxMC");
				// bounding box is approximate for polyline, polygon, ink so grab their actual coordinates
				switch(anns[tAnn].type) {
				case "PolyLine":
				case "Polygon":
					// to do this correctly, need to find the center of all the points,
					// then rotate all points about center, then get the bounds. 
					aRec = anns[tAnn].vertices;
					break;
				case "Ink":
					// using all points in the gestures
					aRec = anns[tAnn].gestures;
					break;
				default:
					// need corner points - these are at the outside edge of the line
					aRec = this.getCorners( aRec );
					// need to include coordinates from leader arrow
					if (anns[tAnn].callout && anns[tAnn].callout.length > 0) {
						// include the arrow coordinates in the bounds
						aRec = aRec.concat( anns[tAnn].callout );
					}		
				}
				// the coordinates need to be rotated by .rotation and flattened
				aRec = this.rotateRect( aRec, anns[tAnn].rotation, axPt ).flat(Infinity);
				// aRec is [x1,y1,x2,y2,...]
				for (let i = 0; i<(aRec.length-1) ; i+=2) {
					// assign min x value
					if ( typeof theBounds[0] == "undefined" || aRec[i] < theBounds[0] ) {
						theBounds[0]=aRec[i];
					}
					// assign max x value
					if ( typeof theBounds[2] == "undefined" || aRec[i] > theBounds[2] ) {
						theBounds[2]=aRec[i];
					}
					// assign min y value
					if ( typeof theBounds[1] == "undefined" ||  aRec[i+1] < theBounds[1] ) {
						theBounds[1]=aRec[i+1];
					}
					// assign max y value
					if ( typeof theBounds[3] == "undefined" ||  aRec[i+1] > theBounds[3] ) {
						theBounds[3]=aRec[i+1];
					}
				}
			}
			
			return theBounds;
		},
		// get the rotated corner points of the bounds [x1,y1,x2,y2]
		getCorners( bounds, rotation=0, center=[0,0], linethk=0 ) {
			const seq = [[0,1], [2,1], [2,3], [0,3]];
			
			let corners = seq.map( ipt => [ bounds[ipt[0]], bounds[ipt[1]] ] );
			
			// move corner by line thickness
			if ( 0 != linethk )
				corners = this.moveCoords( corners, center, linethk );
			// return the rotated coordinates of the corners
			return this.rotateRect( corners, rotation, center );
		},
		// move coordinates by half line thickness
		moveCoords( coords, center, linethk ) {
			return coords.map( cpt => cpt.map( (e,i) =>  (e + Math.sign( center[i]-e )*linethk/2) ));
		},
		// flip a line
		flipLine: function ( flipPt, tPoints) { 
			return tPoints.map( p => this.moveLineArray( flipPt, p ));
		},
		// move points in line array
		moveLineArray ( flipPt, lPoints) {
			// step through array in form [x1,y1,x2,y2,...]
			let mPts = [];
			for (let j=0;j<lPoints.length;j+=2) {
				// flip in x direction
				mPts[j] = FLIPDIR.horiz ? (flipPt[0]*2-lPoints[j]) : lPoints[j];
				// flip in y direction
				mPts[j+1] = FLIPDIR.vert ? (flipPt[1]*2-lPoints[j+1]) : lPoints[j+1];
			}
			return mPts;
		},
		doRotation: function (r,a) {
			if ( undefined === a ) a = 0;
			if (FLIPDIR.vert) r = a - r;
			if (FLIPDIR.horiz) r = 2*a - r;
			return r;
		},
		// try to switch text alignment
		flipTextAlign: function (tObj) {
			let changes = {};
			let align = tObj.alignment;
			if ( FLIPDIR.horiz ) {
				// just change the alignment
				if ( align != 1)
					changes.alignment = 2 - align;
				if (tObj.richContents.length) {
					let mirRC = [];
					let changed = false;
					for ( let rc of tObj.richContents ) {
						// I'm changing the actual rich contents object, maybe bad?
						if ( "center" != rc.alignment ) {
							changed = true;
							rc.alignment = ("right" == rc.alignment ? "left" : "right");
						}
						mirRC.push( rc );
					}
					if (changed)
						changes.richContents = mirRC;
				}
			}
			return changes;
		},
		// return properties to duplicate an annotation
		duplicateAnn: function (an,revs,notIncl) {
			// exclude elements from a "not included" array.
			if (undefined == notIncl) notIncl = ["author", "name", "doc", "rect"];
			
			let aProps = an.getProps();
			for ( let p of notIncl ) {
				if ( undefined !== aProps[p] )
					delete aProps[p];
			}
			// workaround - bug in pdfxchange that overwrites contents with empty richContents
			if ( aProps.richContents && !aProps.richContents.length ) {
				delete aProps.richContents;
			}
			// plug in the revised values (if any)
			for (let i in revs) {
				aProps[i] = revs[i];
			}
			return aProps;
		},
		// flip the annotations
		flipAnns: function (flipPt, anns) {
			let tPoints = [];
			for (let tAnn in anns) {
				// work on properties, not the annotation itself (reduce number of undos)
				let cAnn =  anns[tAnn];
				let revProps={};
				//console.println(anns[tAnn].type);
				switch(cAnn.type) {
					case "Line":
						tPoints = this.flipLine(flipPt,cAnn.points);
						 
						// for dimension lines, need to change leaderlength
						if (cAnn.leaderLength && FLIPDIR.vert) {
							revProps.leaderLength = -cAnn.leaderLength;
						}
						// also, for dimension lines, the point order affects which way up the leader text is
						// assumption here is that for horizontal flip, we don't want the text upside down.
						if ("LineDimension"==cAnn.intent && FLIPDIR.horiz) {
							if (DIMTXDIR) {
								tPoints = tPoints.slice(1,2).concat(tPoints.slice(0,1));
							} else {
								revProps.leaderLength = -revProps.leaderLength || -cAnn.leaderLength; // in case it was already flipped above
							}
						}
						revProps.points = tPoints;
						// lines don't need to be rotated
						break;
					case "PolyLine":
					case "Polygon":
						revProps.vertices = this.flipLine(flipPt,cAnn.vertices);
						revProps.rotation = this.doRotation( cAnn.rotation );
						break;
					case "FreeText":
						// move callout arrow
						if (cAnn.callout && cAnn.callout.length > 0) {
							revProps.callout = this.moveLineArray( flipPt, cAnn.callout );
						}
						// try to set alignment
						if ( TXALIGN )
							Object.assign( revProps, this.flipTextAlign(cAnn) );
								
						// no break - need to also move center and rotate, same as other items
					case "Text":
						// text rotation - prevent it from getting aligned with view rotation
						// revProps.rotate = cAnn.rotate;
						// no break
					case "Circle":
					case "Stamp":
					case "Square":
					case "Squiggly":
						// move center and rotate
						revProps.rect = this.moveLineArray( flipPt, cAnn.rect );
						revProps.rotation = this.doRotation( cAnn.rotation, 180 );
						break;
					case "Ink":
						tPoints = cAnn.gestures.map( g => this.flipLine(flipPt,g) );
						revProps.gestures = tPoints;
						revProps.rotation = this.doRotation( cAnn.rotation );
						break;
				}
				// duplicate the annotation
				if (FLIPCOPY) {
					// add current user as author
					revProps.author = identity.name;
					t.addAnnot( this.duplicateAnn( cAnn, revProps) );
				} else {
					// mirror original annotation with single undo
					anns[tAnn].setProps( revProps );
				}
				
			}
		},
	};
	// dialog box to ask for mirror direction
	const flipDialog = {
		pageRotation: 0,
		initialize: function (dialog) {
			let dir = this.getUp(FLIPDIR.vert,FLIPDIR.horiz);
			let defLoad = { 
				"vDir": dir.v, 
				"hDir": dir.h, 
				"copy": FLIPCOPY,
				"dTxt": DIMTXDIR,
				"mTxt": TXALIGN
				};
			defLoad[this.getAxDir(FLIPAXIS,false)] = true;
			
			dialog.load(defLoad);
		},
		commit:function (dialog) { // called when OK pressed
			let results = dialog.store();
			// try to figure out which way is up
			let dir = this.getUp(results["vDir"],results["hDir"]);
			FLIPDIR.vert = dir.v;
			FLIPDIR.horiz = dir.h;
			// get axis
			const axes = ["AxTL","AxTC","AxTR","AxML","AxMC","AxMR","AxBL","AxBC","AxBR"];
			for (let i in axes) {
				if (results[axes[i]]) {
					FLIPAXIS = this.getAxDir(axes[i],true); // need to rotate the saved one based on current page rotation
				}
			}
			// Mirror and copy
			FLIPCOPY = results["copy"];
			// Dimtext
			DIMTXDIR = results["dTxt"];
			// Text alignment
			TXALIGN = results["mTxt"];
			
			// return "ok"
		},
		// try to figure out up or down based on document rotation
		getUp: function (v, h) {
			let pageDir = {v:v,h:h};
			if( 0 == (this.pageRotation-90)%180 ) {
					pageDir = {v:h,h:v};
			}
			return pageDir;
		},
		// try to figure out where the axis is based on document rotation
		// i'm sure there's a better way to do this with matrices
		getAxDir: function (FLIPAXIS,reverse) {
			const axesR = ["AxTL","AxTC","AxTR","AxMR","AxBR","AxBC","AxBL","AxML"]; // no AxMC
			
			let retAxis = axesR.indexOf(FLIPAXIS);
			if (retAxis > -1) {
				switch(this.pageRotation) {
					case 90:
						retAxis += 2 * (reverse ? -1 : 1);
						break;
					case 180:
						retAxis += 4 * (reverse ? -1 : 1);
						break;
					case 270:
						retAxis += 6 * (reverse ? -1 : 1);
				}

				retAxis += (retAxis > 7) ? -8 : 0 ;
				retAxis += (retAxis < 0) ? 8 : 0 ;

				return axesR[retAxis];
			}else{ // either there was no axis saved, or it's the middle center
				return "AxMC";
			}
		},
		description: {
			name: "Mirror Annotations", // Dialog box title
			align_children: "align_left",
			width: 350,
			//height: 200,
			elements:
			[
				{
					type: "cluster",
					name: "Mirror Direction",
					align_children: "align_left",
					elements:
					[
						{
							type: "check_box",
							name: "Mirror Horizontally",
							item_id: "hDir"
						},
						{
							type: "check_box",
							name: "Mirror Vertically",
							item_id: "vDir"
						},
					]
				},
				{ // mirror location radio button grid
					type: "cluster",
					name: "Mirror Location",
					align_children: "align_left",
					elements:
					[	{
						type: "view",
						align_children: "align_row",
						elements:
						[	{
							type: "radio",
							groupid: "Axes",
							item_id: "AxTL"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxTC"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxTR"
							},
						]
					},{
						type: "view",
						align_children: "align_row",
						elements:
						[	{
							type: "radio",
							groupid: "Axes",
							item_id: "AxML"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxMC"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxMR"
							},
						]
					},{
						type: "view",
						align_children: "align_row",
						elements:
						[	{
							type: "radio",
							groupid: "Axes",
							item_id: "AxBL"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxBC"
							},{
							type: "radio",
							groupid: "Axes",
							item_id: "AxBR"
							},
						]
					}
					]
				},
				{ // copy check box
					type: "check_box",
					name: "Copy and Mirror Selection",
					item_id: "copy"
								},
				{ // maintain text alignment
					type: "check_box",
					name: "Mirror right/left alignment of text",
					item_id: "mTxt"
								},
				{ // maintain dimension text
					type: "check_box",
					name: "Maintain dimension text direction",
					item_id: "dTxt"
								},
				{
					alignment: "align_right",
					type: "ok_cancel",
					ok_name: "Ok",
					cancel_name: "Cancel"
				}
			]
		}
	}
	// get all selected annotations
	let anns = t.selectedAnnots;
	let fA;
	if (anns && anns.length>0){
		// Load saved variables
		({FLIPDIR, FLIPAXIS, FLIPCOPY, DIMTXDIR, TXALIGN} = MIRRORTOOL_GLOBAL_VALS.get() || {FLIPDIR:FLIPDIR, FLIPAXIS:FLIPAXIS, FLIPCOPY:FLIPCOPY, DIMTXDIR:DIMTXDIR, TXALIGN:TXALIGN});
		
		// set pageRotation in dialog
		flipDialog.pageRotation = mUtil.getPgRotation(t,anns[0].page);
		
		fA = app.execDialog(flipDialog);
	
		if ("ok" ==  fA) {
			mUtil.doFlip(anns);
			// save globals
			MIRRORTOOL_GLOBAL_VALS.set({FLIPDIR:FLIPDIR, FLIPAXIS:FLIPAXIS, FLIPCOPY:FLIPCOPY, DIMTXDIR:DIMTXDIR, TXALIGN:TXALIGN});
		}		
	} else {
		fA = "No Annotations selected.";
	}

	return fA;
}
// to save dialog settings in case global isn't working
var FLIPDIR = { horiz: true, vert: false }, FLIPAXIS, FLIPCOPY, DIMTXDIR = true, TXALIGN = false;
// needs function to access global variables
const MIRRORTOOL_GLOBAL_VALS = new class {
	constructor(name) {
		this.get = app.trustedFunction(() => {
			app.beginPriv();
			try{
			if ( global[name] )
				return JSON.parse( global[name] );
			}catch{
				console.println("Error accessing global variable '"+name+"'. Try:\n either uncheck 'Enable global object security policy',\n or (preferably) edit the file 'GlobData': Delete the line that begins with /D after the line \n/"+name+" <<");
			}});
		this.set = app.trustedFunction( value => {
			app.beginPriv();
			try{
			global[name] = JSON.stringify( value );
			global.setPersistent( name, true);
			}catch{}});
	}
}("MirrorToolGlobalVals")
Last edited by Mathew on Tue Jul 04, 2023 10:20 pm, edited 14 times in total.
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

Hello, Mathew

Thank you for creating this, I am sure that it will help some users who have been eagerly awaiting this function. I do have one note though, here:
Mathew wrote: Fri Feb 10, 2023 5:45 pm Does not mirror text, but it repositions it. Because of a bug in PDFX-Change it currently can't get the left or right alignment of the text right either
You mention a bug in our software, but I do not believe this is a bug, moreso, a limitation in how comment "containers" and the "text" contained within them are separate items. You cannot modify the text content of a comment while only the container is selected, and so trying to do this goes outside the scope of what can be achieved with JS alone.

Kind regards,
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

Hi Daniel,
I didn't have time to post crosslinks to the forum items. The text alignment issue is mentioned in this post, with a way to reproduce it:
viewtopic.php?p=166808#p166808
- Mathew.
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

Hello, Mathew

I see, thank you for clarifying and providing that.

Kind regards,
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

With build 367.0 this tool also works for Ink annotations.

I've made a slightly more snazzy version below that adds a button to the Add-in toolbar. As before, copy the script and save as a text file with .js suffix to this folder:
C:\Users\[your user name]\AppData\Roaming\Tracker Software\PDFXEditor\3.0\Javascripts\mirror annotations tool.js

or you can run it from the javascript window but the toolbar button will go away next time you run PDFxchange.

Code: Select all

// script minified using http://jscompress.com/ also need to escape all backslashes
// tool data and custom icon
var iconSet_mirrorTool = { mirror:
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC0C0C00000000000000000FF91C8F6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFC9C9C9FFB5B5B50000000000000000FF7BBFF6FFA1D0F50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB6B6B6FFB5B5B50000000000000000FF7BBFF7FF7DBFF60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF70BBF7FFBAD8F5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFB4B4B4FFB1B1B1FFB5B5B50000000000000000FF7BBFF6FF71BBF7FF77BDF70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFBBBBBBFFB1B1B1FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF71BBF7FF71BBF7FF87C4F600000000000000000000000000000000000000000000000000000000000000000000000000000000FFAFAFAFFFB1B1B1FFB1B1B1FFB5B5B50000000000000000FF7BBFF7FF71BBF7FF71BBF7FF6FB9F6000000000000000000000000000000000000000000000000000000000000000000000000FFB6B6B6FF989898FFAAAAAAFFB1B1B1FFB6B6B60000000000000000FF7BBFF7FF71BBF7FF63B4F6FF42A5F5FF84BFF50000000000000000000000000000000000000000000000000000000000000000FF999999FF989898FF999999FFAAAAAAFFB6B6B60000000000000000FF7BBFF7FF63B4F6FF45A6F5FF42A5F5FF43A5F500000000000000000000000000000000000000000000000000000000FF9B9B9BFF989898FF989898FF989898FF999999FFAEAEAE0000000000000000FF6FB8F6FF43A6F5FF42A5F5FF42A5F5FF42A5F5FF7AB7F50000000000000000000000000000000000000000FFCCCCCCFF9A9A9AFF989898FF989898FF989898FF989898FFA2A2A20000000000000000FF58ADF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF45A6F5FFAED2F500000000000000000000000000000000FFA2A2A2FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF57ADF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF58AEF4000000000000000000000000FFB2B2B2FF989898FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF56ACF5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF42A5F5FF7CBCF50000000000000000FFA4A4A4FF989898FF989898FF989898FF989898FF989898FF989898FFA1A1A10000000000000000FF56ADF5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF41A5F5FF5FAFF500000000FFCBCBCBFFAFAFAFFFAFAFAFFFAFAFAFFFB0B0B0FFAFAFAFFFAFAFAFFFB0B0B0FFB5B5B50000000000000000FF7EBEF5FF71B9F5FF71B9F5FF71B9F4FF71B9F5FF70B9F5FF71B9F5FF71B9F6FFA7D0F500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
script: 'var flipDir,flipAxis,flipCopy;(function(a){var b={selAnnots:[],initialize:function(b){if(b&&0<b.length){this.selAnnots=b,null==flipDir&&(flipDir={horiz:!0,vert:!1});var c=a.getPageRotation({nPage:this.selAnnots[0].page});for(a.viewState.pageViewRotation&&(c+=a.viewState.pageViewRotation),0>c&&(c=360-c);360<=c;)c-=360;return this.flipDialog.pageRotation=c,app.execDialog(this.flipDialog)}return"No Annotations selected."},doFlip:function(){var a=this.getBounds(this.selAnnots),b=this.getAxisPt(a,flipAxis);this.flipAnns(b,this.selAnnots)},getAxisPt:function(a,c){var d=[a.slice(0,2),a.slice(2,4)],b={AxTL:[[0,0],[1,1]],AxTC:[[0,1],[1,1]],AxTR:[[1,1],[1,1]],AxML:[[0,0],[0,1]],AxMC:[[0,1],[0,1]],AxMR:[[1,1],[0,1]],AxBL:[[0,0],[0,0]],AxBC:[[0,1],[0,0]],AxBR:[[1,1],[0,0]]}[c],e=[(+d[b[0][0]][0]+d[b[0][1]][0])/2,(+d[b[1][0]][1]+d[b[1][1]][1])/2];return e},rotatePoint:function(a,b,c){var d=c*Math.PI/180,e=a[0],f=a[1],g=b[0],h=b[1],i=(e-g)*Math.cos(d)-(f-h)*Math.sin(d)+g,j=(e-g)*Math.sin(d)+(f-h)*Math.cos(d)+h;return[i,j]},rotateRect:function(a,b,c){if(b){void 0===c&&(c=[(a[0]+a[2])/2,(a[1]+a[3])/2]);var d=[];for(let e=0;e<a.length-1;e+=2)d=d.concat(this.rotatePoint([a[e],a[e+1]],c,b));return d}return a},toFlatArray:function(b){var a,c=[];for(let d in b)a=b[d],"object"==typeof a[0]&&(a=this.toFlatArray(a)),c=c.concat(a);return c},getBounds:function(a){var b=[],c=[],d=[];for(let e in a){switch(c=a[e].rect,d=this.getAxisPt(c,"AxMC"),a[e].type){case"PolyLine":case"Polygon":c=this.toFlatArray(a[e].vertices);break;case"Ink":c=this.toFlatArray(a[e].gestures);break;default:a[e].callout&&0<a[e].callout.length&&(c=c.concat(a[e].callout));}c=this.rotateRect(c,a[e].rotation,d);for(let a=0;a<c.length-1;a+=2)("undefined"==typeof b[0]||c[a]<b[0])&&(b[0]=c[a]),("undefined"==typeof b[2]||c[a]>b[2])&&(b[2]=c[a]),("undefined"==typeof b[1]||c[a+1]<b[1])&&(b[1]=c[a+1]),("undefined"==typeof b[3]||c[a+1]>b[3])&&(b[3]=c[a+1])}return b},flipLine:function(a,b){for(let c=0;c<b.length;c++)b[c]=this.moveLineArray(a,b[c]);return b},moveLineArray(a,b){for(let c=0;c<b.length;c+=2)flipDir.horiz&&(b[c]=2*a[0]-b[c]),flipDir.vert&&(b[c+1]=2*a[1]-b[c+1]);return b},doRotation:function(a){return flipDir.horiz&&(a=-a),flipDir.vert&&(a=-a),a},flipTextAlign:function(a){var b=a.alignment;return flipDir.horiz&&1!=b&&(b=2-b),b},flipAnns:function(b,c){var d=[],e={};for(let g in c)switch(flipCopy?(e=c[g].getProps(),!e.richContents.length&&delete e.richContents,e=a.addAnnot(e)):e=c[g],e.type){case"Line":d=this.flipLine(b,e.points),e.leaderLength&&flipDir.vert&&(e.leaderLength=-e.leaderLength),"LineDimension"==e.intent&&flipDir.horiz&&(d=d.slice(1,2).concat(d.slice(0,1))),e.points=d;break;case"PolyLine":case"Polygon":e.setProps({vertices:this.flipLine(b,e.vertices),rotation:this.doRotation(e.rotation)});break;case"FreeText":e.callout&&0<e.callout.length&&(e.callout=this.moveLineArray(b,e.callout)),e.alignment=this.flipTextAlign(e);case"Circle":case"Stamp":case"Text":case"Square":case"Squiggly":e.setProps({rect:this.moveLineArray(b,e.rect),rotation:this.doRotation(e.rotation)});break;case"Ink":for(var f in d=e.gestures,d)d[f]=this.flipLine(b,d[f]);e.setProps({gestures:d,rotation:this.doRotation(e.rotation)});}},flipDialog:{pageRotation:0,getUp:function(a,b){var c={v:a,h:b};switch(this.pageRotation){case 90:case 270:c={v:b,h:a};}return c},getAxDir:function(a,b){var c=["AxTL","AxTC","AxTR","AxMR","AxBR","AxBC","AxBL","AxML"],d=c.indexOf(a);if(-1<d){switch(this.pageRotation){case 90:d+=2*(b?-1:1);break;case 180:d+=4*(b?-1:1);break;case 270:d+=6*(b?-1:1);}return d+=7<d?-8:0,d+=0>d?8:0,c[d]}return"AxMC"},initialize:function(a){var b=this.getUp(flipDir.vert,flipDir.horiz),c={vDir:b.v,hDir:b.h,copy:flipCopy};c[this.getAxDir(flipAxis,!1)]=!0,a.load(c)},commit:function(a){var b=a.store(),c=this.getUp(b.vDir,b.hDir);flipDir.vert=c.v,flipDir.horiz=c.h;var d=["AxTL","AxTC","AxTR","AxML","AxMC","AxMR","AxBL","AxBC","AxBR"];for(let c in d)b[d[c]]&&(flipAxis=this.getAxDir(d[c],!0));flipCopy=b.copy},description:{name:"Mirror Annotations",align_children:"align_left",width:350,elements:[{type:"cluster",name:"Mirror Direction",align_children:"align_left",elements:[{type:"check_box",name:"Mirror Horizontally",item_id:"hDir"},{type:"check_box",name:"Mirror Vertically",item_id:"vDir"}]},{type:"cluster",name:"Mirror Location",align_children:"align_left",elements:[{type:"view",align_children:"align_row",elements:[{type:"radio",groupid:"Axes",item_id:"AxTL"},{type:"radio",groupid:"Axes",item_id:"AxTC"},{type:"radio",groupid:"Axes",item_id:"AxTR"}]},{type:"view",align_children:"align_row",elements:[{type:"radio",groupid:"Axes",item_id:"AxML"},{type:"radio",groupid:"Axes",item_id:"AxMC"},{type:"radio",groupid:"Axes",item_id:"AxMR"}]},{type:"view",align_children:"align_row",elements:[{type:"radio",groupid:"Axes",item_id:"AxBL"},{type:"radio",groupid:"Axes",item_id:"AxBC"},{type:"radio",groupid:"Axes",item_id:"AxBR"}]}]},{type:"check_box",name:"Copy and Mirror Selection",item_id:"copy"},{alignment:"align_right",type:"ok_cancel",ok_name:"Ok",cancel_name:"Cancel"}]}}},c=b.initialize(a.selectedAnnots);return"ok"==c&&b.doFlip(),c})(this);',
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: "mirrorAnnotationsTool",
	cLabel: "Mirror",
	oIcon: iconSet_mirrorTool.iconStream("mirror"),
	cTooltext: "Flip/Mirror Comments…",
	cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
	cExec: iconSet_mirrorTool.script }
);
// This adds a menu item to the edit menu
app.addMenuItem( {
	cName: "mirrorAnnotations",
	cUser: "Flip/Mirror Comments…",
	oIcon: iconSet_mirrorTool.iconStream("mirror"),
	cParent: "Edit",
	nPos: 13,
	cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
	cExec: iconSet_mirrorTool.script }
);
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

:)
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

I made a few updates to the script and attached it below. I'll also update the one in the first post.
mirror annotations tool v2.1.zip
Unzip and place the javascript file into the Javascripts folder
(5.25 KiB) Downloaded 79 times
  • cleaned up code a bit
  • reduce number of undos that show up when the script mirrors annotations
  • annotation name is auto-generated when mirror and copy is used
  • add option to not try keeping dimension text upright - sometimes I want to flip the direction of the text on distance tool measurements because it's facing the wrong way
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

Hello, Mathew

Thank you once again for the fantastic update!

Kind regards,
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

One more update on this.
mirror annotations tool v2.2.zip
Unzip and place the javascript file into the Javascripts folder
(6.29 KiB) Downloaded 60 times
  • Mirror the alignment of rich text when it flips horizontally
  • fix access to global variable so settings save between sessions
  • use corner points instead of bounds on rectangles so that rotated rectangle edges are correct
Limitation:
  • Bullet or numbered lists disappear when mirrored (they become just indented rows of text). I suspect this property is not available to javascript.
User avatar
Tracker Supp-Stefan
Site Admin
Posts: 17824
Joined: Mon Jan 12, 2009 8:07 am
Location: London
Contact:

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Tracker Supp-Stefan »

Hello Mathew,

Once again, many thanks for the update!

Kind regards,
Stefan
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

Just a few small changes here - mostly to change how text is handled when mirrored:
  • add .rotate to properties duplicated so that duplicated text doesn't get automatically rotated to view
  • try to improve how text is mirrored - vertically mirrored text stays upright
mirror annotations tool v2.3.zip
Unzip and place the javascript file into the Javascripts folder
(6.12 KiB) Downloaded 73 times
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

:)
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
User avatar
David.P
User
Posts: 1510
Joined: Thu Feb 28, 2008 8:16 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by David.P »

I'm just now getting around to mentioning (having just installed the Mirror Comments JavaScript) what a fantastic tool this is, even with its own gorgeous GUI.

The underlying JavaScript code is a piece of art!

I even managed to move the button to launch the Mirror Comments tool from the menu to the titlebar. Unfortunately, it doesn't stay there after restarting PDF-XChange Editor.

image.png

Since I'd like to use JavaScript to implement and use this and other custom features in PDF-XChange Editor more often in the future: is it possible to assign a keyboard shortcut to a JavaScript button, or generally to a JavaScript in the folder C:\Users\[Username]\AppData\Roaming\Tracker Software\PDFXEditor\3.0\JavaScripts, in order to call this particular button, or JavaScript?

In this context, what would be the best way to execute a sequence of several (menu) commands in succession via JavaScript, should this be possible?

Thanks very much
Best regards
David
Last edited by David.P on Mon Jan 22, 2024 7:48 pm, edited 1 time in total.
David.P
PDF-XChange Pro
User avatar
Paul - Tracker Supp
Site Admin
Posts: 6835
Joined: Wed Mar 25, 2009 10:37 pm
Location: Chemainus, Canada
Contact:

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Paul - Tracker Supp »

Hi, David.P

there is no option to specify a ribbon on which to put the Tool. If you are using the Classic menu yo can then tell it what menu it should be on:
5th8pk8C1NZgk9Av.png

Code: Select all

// 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
        cParent: "Comments", // Change this to "Tools" to put in the Tools menu, etc
        //nPos: 13,
        cEnable: "event.rc = (this.selectedAnnots && this.selectedAnnots.length)",
        cExec: "changeColorsAll(this)" }
    );
cParent is what you want to use. 

Adding them to Toolbars (not menus) is not supported by the API.

Because Menu Items CAN be modified, it is possible to drag a menu item to a Toolbar but it will bot be retained as you observed, it will not survive an application restart. I am afraid that is due to the ISO specifications and will not change.

I am waiting to hear back about the Keyboard Shortcut - but I do not believe it possible. We will see...

Kind regards,
Paul - Tracker Supp
Best regards

Paul O'Rorke
Tracker Support North America
http://www.tracker-software.com
Mathew
User
Posts: 204
Joined: Thu Jun 19, 2014 7:30 pm

Re: javascript: custom menu item to Mirror (Flip) annotations

Post by Mathew »

That's one reason I've stuck with the Classic UI.

I did put in a feature request to try to get Tracker devs to add some way to add buttons on the ribbon UI... viewtopic.php?t=40887

For now, I'm relegated to adding buttons in the "Add-On" toolbar (it's floating on the left in @David's screenshot). I think PDF XChange may accept bigger icons than the tiny 20x20 icons I've been using, but I've not tried it.
User avatar
TrackerSupp-Daniel
Site Admin
Posts: 8436
Joined: Wed Jan 03, 2018 6:52 pm

javascript: custom menu item to Mirror (Flip) annotations

Post by TrackerSupp-Daniel »

:)
Dan McIntyre - Support Technician
Tracker Software Products (Canada) LTD

+++++++++++++++++++++++++++++++++++
Our Web site domain and email address has changed as of 26/10/2023.
https://www.pdf-xchange.com
Support@pdf-xchange.com
Post Reply