Hi everybody,
The upcoming version 1.5.8 of PixInsight introduces important changes to the PixInsight JavaScript Runtime (PJSR) that improve the way scripts integrate with the platform. In this post I want to describe these changes and put some examples that demonstrate the new scripting features.
PixInsight's JavaScript runtime was first implemented in 2006, when the platform was still in beta development stages. The initial purpose was just to automate relatively simple tasks; the JavaScript runtime was not intended as a true development tool in PixInsight, but only as a helper feature to simplify batch processing.
Surpassing all of my initial expectations, and compelled by the contributions and growing interest of many users and developers, PixInsight's JavaScript runtime has since then evolved into an amazingly powerful development framework. We are seeing these days JavaScript implementations of extremely useful and efficient tools in PixInsight. Many of these scripts compete with PCL/C++ modules in portability and ease of development.
So far, however, JavaScript scripts have been
strangers in the PixInsight platform. This is because they actually have not integrated with PixInsight's object-oriented architecture. For example, before version 1.5.8, a script could be executed to modify an image in some useful ways, but unlike a regular process in PixInsight (i.e., a process implemented by an external module that has been developed with the PCL C++ framework), a script violated some essential design principles:
- Scripts violated the
isolation principle. One of PixInsight's design principles is that processes are independent from images. This means that a process can always be defined as a self-contained, independent object, without knowing anything about any particular image. This is why PixInsight is so different from other applications (and why it is more powerful). With the exception of some pure batch-processing tasks (as BatchFormatConversion for example), a script could only be used in the context of a particular image.
- Scripts couldn't be
encapsulated. For example, there was no way to generate a process icon to encapsulate and transport a script instance and its working parameters.
- Mainly as a consequence of the previous limitations, a script instance couldn't be
reused. To apply the same script to another image, one had to select the image in question, run the script, enter working parameters again, then click OK. Doesn't this sound similar to other applications/environments? Of course, and this is not the
PixInsight way of doing things.
Starting from version 1.5.8, JavaScript scripts are first-class PixInsight citizens, thanks to a number of key changes that I'll describe briefly now.
The Parameters JavaScript ObjectIn order to be
encapsulable, a script must be able to generate
instances. An instance defines a set of working parameters and control data that completely determine the behavior and identity of a process. The new Parameters JavaScript object allows a script to import and export a set of working parameters.
Scripts can now be executed in three different
contexts:
Global context. We say that a process instance runs in the global context when it is executed without an explicit reference to a particular image. For example, the Preferences process always runs globally.
View context. This refers to an instance being executed with an explicit reference to a
target view. Most processes run in this way in PixInsight; for example, HistogramTransformation and ATrousWaveletTransform always run in a view context.
Direct context. This context is exclusive to script instances. It refers to a script that is being executed without an instance. For example, a script is executed directly by selecting the
Execute > Compile and Run menu item from the Script Editor window, or by selecting the
Script > Run Script and
Script > Execute Script File main menu items. Regular processes (module-defined processes) can be executed in a similar way through the command-line process interface.
Here is the Script Editor window with a modified,
instantiable version of the DrawSignature script:
[
Click here to enlarge]
The new Parameters object is shown on the Object Browser interface, to the left of the editor. Parameters has the following properties:
Boolean Parameters.isGlobalTargetThis property is true if the script is being executed in the global context, false otherwise.
Boolean Parameters.isViewTargetThis property is true if the script is being executed in the context of a view, false otherwise. When this property is true, the targetView property (see below) holds a reference to the view this script is being executed on.
View Parameters.targetViewWhen this script is being run on a view, this property is a reference to that view. Otherwise --either because the script runs in the global or direct contexts--, this property is null.
Parameters has the following methods:
Boolean Parameters.has( String id )Returns true if a script parameter whose identifier is
id has been defined for the running instance; false otherwise.
String Parameters.getString( String id )
String Parameters.get( String id )Both methods are synonyms. Returns a String parameter whose identifier is
id. If an
id parameter has not been defined, or if it cannot be legally converted to a String object, an Error exception is thrown.
Boolean Parameters.getBoolean( String id )Returns the Boolean value of a parameter whose identifier is
id. If an
id parameter has not been defined, or if it cannot be legally converted to a Boolean object, an Error exception is thrown. Boolean parameters can be the strings "false" and "true". Any valid string representation of an integer number can also be acquired as a Boolean parameter; in this case the return value is true if the corresponding integer is nonzero.
Number Parameters.getInteger( String id[, int radix=0] )Returns the 32-bit signed integer value of a parameter whose identifier is
id.
radix is an integer radix (zero, or in the range from 2 to 26) for numeric conversion. If
radix is zero, which is the default value, the actual radix of the value string is detected from standard C prefixes (0x for hexadecimal, 0 for octal, decimal otherwise). If an
id parameter has not been defined, or if it cannot be legally converted to a 32-bit signed integer value, an Error exception is thrown.
Number Parameters.getUInt( String id[, int radix=0] )Returns the 32-bit unsigned integer value of a parameter whose identifier is
id. The
radix argument has the same meaning as for getInteger() (see above). If an
id parameter has not been defined, or if it cannot be legally converted to a 32-bit unsigned integer value, an Error exception is thrown.
Number Parameters.getReal( String id )Returns the 64-bit IEEE floating point value of a parameter whose identifier is
id. If an
id parameter has not been defined, or if it cannot be legally converted to a
double value, an Error exception is thrown.
void Parameters.set( String id, Object value )Exports a script parameter with the specified
id identifier id and
value. The specified identifier must be a valid JavaScript identifier and the specified value must be convertible into a String object; otherwise an Error exception will be thrown.
I'll put a practical example to show instantiable scripts in action. The DrawSignature example script is included in all standard distributions of PixInsight. It is a relatively simple script that draws a single line of text over an image, providing customizable font, style, and colors. This is a typical example of DrawSignature execution:
[
Click here to enlarge]
Here is the result of the previous script execution:
[
Click here to enlarge]
I have modified DrawSignature to convert it into a fully instantiable script. As shown above, we can select the
Load History Explorer option to open the History Explorer window with the view's processing history selected:
[
Click here to enlarge]
On the screenshot above, note how the Script process integrates with processing histories. As the automatically generated script shows, on the right panel of the History Explorer window, a Script instance has the following process parameters:
filePath. This is a string parameter which stores the file path of the executed script file, usually a file with the .js suffix.
md5sum. This is a string parameter representing a cryptographic hash value (to be precise, a hexadecimal representation of a MD5 checksum) calculated for the entire source code executed. Note that the checksum is computed for all source code lines, including all #include files as well as fully preprocessed code, with all macro substitutions performed, and all leading/trailing whitespace and comments removed. This parameter ensures that the same source code runs on successive instance executions. If this parameter is an empty string, however, no checksum verification is performed.
parameters. This is a table parameter. Each table row is a pair identifier/value that defines a script parameter. Both columns --id and value-- are stored as strings.
In addition to these process parameters, a Script instance can also store the identifier of the target view, when appropriate, for informative purposes.
Script Instance Management: The Script InterfaceScript instances can be manipulated exactly in the same way as regular process instances. For example, we can create Script process icons and store them in .psm files, as usual. A new process interface, namely the Script interface, allows us to edit Script instance parameters, as shown on the next screenshot.
[
Click here to enlarge]
On the Script interface, along with the filePath and md5sum parameters, the user can add, edit and delete script parameters arbitrarily. Remember that all script parameters are stored as plain string objects, so there is no type check at all. This is fully coherent with the dynamic nature of the JavaScript language. Contrarily, regular process parameters (module-defined) are subject to strict type checking in PixInsight, as corresponds to the static nature of PCL/C++.
The Script interface allows execution of Script instances in the view and global contexts. In the following figure you can see an example of view execution:
[
Click here to enlarge]
On the above screenshot, we have applied the Script instance to the left image by dragging the blue triangle from the Script interface. This is a huge step forward: JavaScript scripts fully integrated with the PixInsight platform.
The Client Side: Making JavaScript Scripts InstantiableSo far we have described the
server side of the new Script process in PixInsight. However, just as happens with regular processes developed around the PCL/C++ framework, JavaScript scripts must implement a number of
client side resources to become instantiable objects. This is better described through an example.
Let's take our DrawSignature script. This script is a good candidate because it acts on a unique target image, and it's simple enough to serve our demonstration purposes. This is the script's source code:
/*
DrawSignature v1.1
An example script to draw an arbitrary text on a corner of the selected
image. This script provides a graphical user interface where the user can
select the text, font, size and colors of the text to be drawn, among other
parameters.
Written by Juan Conejero (PTeam)
Copyright (C) 2009 Pleiades Astrophoto S.L.
*/
#feature-id Sample Scripts > DrawSignature
#feature-info An example script to draw an arbitrary text on a corner of the \
selected image. This script provides a graphical user interface \
where the user can select the text, font, size and colors of the \
text to be drawn, among other parameters.
#feature-icon DrawSignature.xpm
#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>
#define VERSION 1.1
#define TITLE DrawSignature
/**
* The DrawSignatureEngine object defines and implements the DrawSignature
* routine and its functional parameters.
*/
function DrawSignatureEngine()
{
this.initialize = function()
{
if ( Parameters.isGlobalTarget )
throw new Error( "The " + #TITLE + " script cannot be run in the global context." );
// Default parameters
this.isInstance = false;
this.text = "PixInsight";
this.fontFace = "Helvetica";
this.fontSize = 128; // px
this.bold = false;
this.italic = false;
this.stretch = 100;
this.textColor = 0xffff7f00;
this.bkgColor = 0x80000000;
this.margin = 8;
this.softEdges = false;
if ( Parameters.isViewTarget )
{
// Our script is being executed as a Script instance.
this.isInstance = true;
this.targetView = Parameters.targetView;
// Retrieve instance parameters
if ( Parameters.has( "text" ) )
this.text = Parameters.getString( "text" );
if ( Parameters.has( "fontFace" ) )
this.fontFace = Parameters.getString( "fontFace" );
if ( Parameters.has( "fontSize" ) )
this.fontSize = Parameters.getUInt( "fontSize" );
if ( Parameters.has( "bold" ) )
this.bold = Parameters.getBoolean( "bold" );
if ( Parameters.has( "italic" ) )
this.italic = Parameters.getBoolean( "italic" );
if ( Parameters.has( "stretch" ) )
this.stretch = Parameters.getUInt( "stretch" );
if ( Parameters.has( "textColor" ) )
this.textColor = Parameters.getUInt( "textColor" );
if ( Parameters.has( "bkgColor" ) )
this.bkgColor = Parameters.getUInt( "bkgColor" );
if ( Parameters.has( "margin" ) )
this.margin = Parameters.getInteger( "margin" );
if ( Parameters.has( "softEdges" ) )
this.softEdges = Parameters.getBoolean( "softEdges" );
}
else
{
// Our script is being executed directly.
// Get access to the active image window
var window = ImageWindow.activeWindow;
if ( !window.isNull )
this.targetView = window.currentView;
}
};
this.apply = function()
{
with ( this )
{
// Export script parameters. We must carry out this here, *before* applying
// our routine to targetView, so that a newly created Script instance will
// encapsulate our current set of working parameters.
exportParameters();
// Tell the core application that we are going to change this view.
// Without doing this, we'd have just read-only access to the view's image.
targetView.beginProcess();
// Perform our drawing routine.
draw();
// Done with view.
targetView.endProcess();
}
};
this.exportParameters = function()
{
Parameters.set( "text", this.text );
Parameters.set( "fontFace", this.fontFace );
Parameters.set( "fontSize", this.fontSize );
Parameters.set( "bold", this.bold );
Parameters.set( "italic", this.italic );
Parameters.set( "stretch", this.stretch );
Parameters.set( "textColor", format( "0x%x", this.textColor ) );
Parameters.set( "bkgColor", format( "0x%x", this.bkgColor ) );
Parameters.set( "margin", this.margin );
Parameters.set( "softEdges", this.softEdges );
};
/**
* A routine to draw an arbitrary text at the lower-left corner of an image.
*
* The data argument provides operating parameters:
*
* targetView Image to draw the text over.
*
* text The text to draw.
*
* fontFace The font to draw with.
*
* pointSize The font size in points.
*
* bold Whether the text will be drawn with a bold font.
*
* italic Whether the text will be drawn with an italic font.
*
* stretch The font stretch factor. A stretch factor of 100 draws
* characters with their normal widths. stretch > 100 draws
* wider (extended) characters, and stretch < 100 draws
* compressed characters.
*
* textColor The text color. Encoded as a 32-bit integer: AARRGGBB, where
* AA is the 8-bit alpha (transparency) value, and RR, GG, BB
* are the red, green and blue 8-bit values, respectively.
*
* bgColor The background color, encoded as explained above.
*
* margin The outer margin in pixels.
*
* softEdges If true, the text will be drawn with extra soft edges;
* normal edges otherwise.
*/
this.draw = function()
{
// To execute with diagnostics messages, #define the __DEBUG__ macro; e.g.:
// run -D=__DEBUG__ signature.js
#ifdef __DEBUG__
console.writeln( "text : ", this.text );
console.writeln( "font : ", this.fontFace );
console.writeln( "fontSize : ", this.fontSize );
console.writeln( "stretch : ", this.stretch );
console.writeln( "bold : ", this.bold );
console.writeln( "italic : ", this.italic );
console.writeln( format( "textColor : %X", this.textColor ) );
console.writeln( format( "bgColor : %X", this.bkgColor ) );
console.writeln( "margin : ", this.margin );
console.writeln( "soft : ", this.softEdges );
#endif
var image = this.targetView.image;
// Create the font
var font = new Font( this.fontFace );
font.pixelSize = this.fontSize;
if ( this.bold )
font.bold = true;
if ( this.italic )
font.italic = true;
font.stretchFactor = this.stretch;
#ifdef __DEBUG__
console.writeln( "Exact font match : ", font.isExactMatch );
console.writeln( "Font point size : ", font.pointSize );
#endif
// Calculate a reasonable inner margin in pixels
var innerMargin = Math.round( font.pixelSize/5 );
// Calculate the sizes of our drawing box
var width = font.width( this.text ) + 2*innerMargin;
var height = font.ascent + font.descent + 2*innerMargin;
#ifdef __DEBUG__
console.writeln( "Drawing box sizes : w=", width, ", h=", height );
#endif
// Create a bitmap where we'll perform all of our drawing work
var bmp = new Bitmap( width, height );
// Fill the bitmap with the background color
bmp.fill( this.bkgColor );
// Create a graphics context for the working bitmap
var G = new Graphics( bmp );
// Select the required drawing tools: font and pen.
G.font = font;
G.pen = new Pen( this.textColor );
G.transparentBackground = true; // draw text with transparent bkg
G.textAntialiasing = true;
// Now draw the signature
G.drawText( innerMargin, height - font.descent - innerMargin, this.text );
// Finished drawing
G.end();
// If soft text has been requested, we apply a convolution with a mild
// low-pass filter to soften text edges.
if ( this.softEdges )
{
// Create a RGB image with an alpha channel. The alpha channel is
// necessary to preserve bitmap transparency.
var simg = new Image( width, height, 4, 1 );
// Select all channels, including alpha.
simg.firstSelectedChannel = 0;
simg.lastSelectedChannel = 3;
// Fill the whole image with transparent black
simg.fill( 0 );
// Blend the bitmap
simg.blend( bmp );
// Apply the low-pass filter (feel free to try out other kernel values)
simg.convolve( [0.05, 0.15, 0.05,
0.15, 1.00, 0.15,
0.05, 0.15, 0.05] );
// Render the resulting image back to our working bitmap
bmp.assign( simg.render() );
}
// Blend our bitmap at the lower left corner of the image
image.selectedPoint = new Point( this.margin,
image.height - this.margin - height );
image.blend( bmp );
};
this.initialize();
}
// Global DrawSignature parameters.
var engine = new DrawSignatureEngine;
/**
* DrawSignatureDialog is a graphical user interface to define
* DrawSignature parameters.
*/
function DrawSignatureDialog()
{
this.__base__ = Dialog;
this.__base__();
//
var emWidth = this.font.width( 'M' );
var labelWidth1 = this.font.width( "Target image:" );
//
this.helpLabel = new Label( this );
with ( this.helpLabel )
{
frameStyle = FrameStyle_Box;
margin = 4;
wordWrapping = true;
useRichText = true;
text = "<p><b>" + #TITLE + " v" + #VERSION +
"</b> — This script draws an arbitrary text at the lower-left corner of " +
"an image. You can enter the text to draw and select the font, along with a " +
"number of operating parameters below.</p>" +
"<p>To apply the script, click the OK button. To close this dialog without " +
"making any changes, click the Cancel button.</p>";
}
//
this.targetImage_Label = new Label( this );
with ( this.targetImage_Label )
{
text = "Target image:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1;
}
this.targetImage_ViewList = new ViewList( this );
with ( this.targetImage_ViewList )
{
getAll();
currentView = engine.targetView;
toolTip = "Select the image to draw the text over";
onViewSelected = function( view )
{
engine.targetView = view;
};
}
this.targetImage_Sizer = new HorizontalSizer;
with ( this.targetImage_Sizer )
{
spacing = 4;
add( this.targetImage_Label );
add( this.targetImage_ViewList, 100 );
}
//
this.text_Label = new Label( this );
with ( this.text_Label )
{
text = "Text:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1;
}
this.text_Edit = new Edit( this );
with ( this.text_Edit )
{
text = engine.text;
minWidth = 42*emWidth;
toolTip = "Enter the text to draw";
onEditCompleted = function()
{
engine.text = this.text;
};
}
this.text_Sizer = new HorizontalSizer;
with ( this.text_Sizer )
{
spacing = 4;
add( this.text_Label );
add( this.text_Edit );
}
//
this.fontFace_Label = new Label( this );
with ( this.fontFace_Label )
{
text = "Face:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1 - 4-1;
}
this.fontFace_ComboBox = new ComboBox( this );
with ( this.fontFace_ComboBox )
{
addItem( "Helvetica" );
addItem( "Times" );
addItem( "Courier" );
addItem( "SansSerif" );
addItem( "Serif" );
addItem( "Monospace" );
editEnabled = true;
editText = engine.fontFace;
toolTip = "Type a font face to draw with, or select a standard font family.";
onEditTextUpdated = function()
{
engine.fontFace = this.editText;
};
onItemSelected = function( index )
{
engine.fontFace = this.itemText( index );
};
}
this.fontFace_Sizer = new HorizontalSizer;
with ( this.fontFace_Sizer )
{
spacing = 4;
add( this.fontFace_Label );
add( this.fontFace_ComboBox, 100 );
}
//
this.fontSize_Label = new Label( this );
with ( this.fontSize_Label )
{
text = "Size (px):";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1 - 4-1;
}
this.fontSize_SpinBox = new SpinBox( this );
with ( this.fontSize_SpinBox )
{
minValue = 8;
maxValue = 4000;
value = engine.fontSize;
toolTip = "Font size in pixels.";
onValueUpdated = function( value )
{
engine.fontSize = value;
};
}
this.bold_CheckBox = new CheckBox( this );
with ( this.bold_CheckBox )
{
text = "Bold";
checked = engine.bold;
toolTip = "Check to draw with a bold typeface.";
onCheck = function( checked )
{
engine.bold = checked;
};
}
this.italic_CheckBox = new CheckBox( this );
with ( this.italic_CheckBox )
{
text = "Italic";
checked = engine.italic;
toolTip = "Check to draw with an italics typeface.";
onCheck = function( checked )
{
engine.italic = checked;
};
}
this.stretch_Label = new Label( this );
with ( this.stretch_Label )
{
text = "Stretch:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
}
this.stretch_SpinBox = new SpinBox( this );
with ( this.stretch_SpinBox )
{
minValue = 50;
maxValue = 200;
value = engine.stretch;
toolTip = "<p>Font stretch factor:</p>" +
"<p>stretch = 100 draws characters with their normal widths.<br/>" +
"stretch > 100 draws wider (extended) characters.<br/>" +
"stretch < 100 draws compressed characters.</p>";
onValueUpdated = function( value )
{
engine.stretch = value;
};
}
this.fontStyle_Sizer = new HorizontalSizer;
with ( this.fontStyle_Sizer )
{
spacing = 4;
add( this.fontSize_Label );
add( this.fontSize_SpinBox );
addSpacing( 12 );
add( this.bold_CheckBox );
add( this.italic_CheckBox );
addSpacing( 12 );
add( this.stretch_Label );
add( this.stretch_SpinBox );
addStretch();
}
//
this.textColor_Label = new Label( this );
with ( this.textColor_Label )
{
text = "Text color:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1 - 4-1;
}
this.textColor_Edit = new Edit( this );
with ( this.textColor_Edit )
{
text = format( "%X", engine.textColor );
minWidth = 14*emWidth;
toolTip = "<p>The text color encoded as a 32-bit hexadecimal integer.<br/>" +
"(AARRGGBB format: AA=alpha (transparency), RR=red, GG=green, BB=blue)</p>";
onEditCompleted = function()
{
engine.textColor = parseInt( this.text, 16 );
this.text = format( '%X', engine.textColor );
};
}
this.bkgColor_Label = new Label( this );
with ( this.bkgColor_Label )
{
text = "Background:";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
}
this.bkgColor_Edit = new Edit( this );
with ( this.bkgColor_Edit )
{
text = format( "%X", engine.bkgColor );
minWidth = 14*emWidth;
toolTip = "<p>The background color encoded as a 32-bit hexadecimal integer.<br/>" +
"(AARRGGBB format: AA=alpha (transparency), RR=red, GG=green, BB=blue)</p>";
onEditCompleted = function()
{
engine.bkgColor = parseInt( this.text, 16 );
this.text = format( '%X', engine.bkgColor );
};
}
this.textColor_Sizer = new HorizontalSizer;
with ( this.textColor_Sizer )
{
spacing = 4;
add( this.textColor_Label );
add( this.textColor_Edit );
addStretch();
add( this.bkgColor_Label );
add( this.bkgColor_Edit );
}
//
this.font_Sizer = new VerticalSizer;
with ( this.font_Sizer )
{
margin = 4;
spacing = 4;
add( this.fontFace_Sizer );
add( this.fontStyle_Sizer );
add( this.textColor_Sizer );
}
this.font_GroupBox = new GroupBox( this );
with ( this.font_GroupBox )
{
title = "Font";
sizer = this.font_Sizer;
}
//
this.margin_Label = new Label( this );
with ( this.margin_Label )
{
text = "Margin (px):";
textAlignment = TextAlign_Right|TextAlign_VertCenter;
minWidth = labelWidth1;
}
this.margin_SpinBox = new SpinBox( this );
with ( this.margin_SpinBox )
{
minValue = 0;
maxValue = 250;
value = engine.margin;
toolTip = "The margin in pixels between the drawing rectangle and the borders of the image.";
onValueUpdated = function( value )
{
engine.margin = value;
};
}
this.softEdges_CheckBox = new CheckBox( this );
with ( this.softEdges_CheckBox )
{
text = "Soft edges";
checked = engine.softEdges;
toolTip = "If checked, the text will be drawn with extra soft edges";
onCheck = function( checked )
{
engine.softEdges = checked;
};
}
this.renderOptions_Sizer = new HorizontalSizer;
with ( this.renderOptions_Sizer )
{
spacing = 4;
add( this.margin_Label );
add( this.margin_SpinBox );
addSpacing( 12 );
add( this.softEdges_CheckBox );
addStretch();
}
//
this.ok_Button = new PushButton( this );
with ( this.ok_Button )
{
text = "OK";
onClick = function()
{
this.dialog.ok();
};
}
this.cancel_Button = new PushButton( this );
with ( this.cancel_Button )
{
text = "Cancel";
onClick = function()
{
this.dialog.cancel();
};
}
this.buttons_Sizer = new HorizontalSizer;
with ( this.buttons_Sizer )
{
spacing = 6;
addStretch();
add( this.ok_Button );
add( this.cancel_Button );
}
//
this.sizer = new VerticalSizer;
with ( this.sizer )
{
margin = 6;
spacing = 6;
add( this.helpLabel );
addSpacing( 4 );
add( this.targetImage_Sizer );
add( this.text_Sizer );
add( this.font_GroupBox );
add( this.renderOptions_Sizer );
add( this.buttons_Sizer );
}
with ( this )
{
windowTitle = #TITLE + " Script";
adjustToContents();
setFixedSize();
}
}
// Our dialog inherits all properties and methods from the core Dialog object.
DrawSignatureDialog.prototype = new Dialog;
/*
* Script entry point.
*/
function main()
{
// If the script is being executed as a Script instance, retrieve
// parameters, apply them, and exit. This allows us to run a script just as
// a regular (module-defined) process instance.
if ( engine.isInstance )
{
engine.apply();
return;
}
#ifndef __DEBUG__
console.hide();
#endif
// If the script is being executed directly, we need a target view, so an
// image window must be available.
if ( !engine.targetView )
{
var msg = new MessageBox( "There is no active image window!",
(#TITLE + " Script"), StdIcon_Error, StdButton_Ok );
msg.execute();
return;
}
var dialog = new DrawSignatureDialog();
for ( ;; )
{
// Execute the DrawSignature dialog.
if ( !dialog.execute() )
break;
// A view must be selected.
if ( engine.targetView.isNull )
{
var msg = new MessageBox( "You must select a view to apply this script.",
(#TITLE + " Script"), StdIcon_Error, StdButton_Ok );
msg.execute();
continue;
}
// Perform the DrawSignature routine.
engine.apply();
// Quit after successful execution.
break;
}
}
main();
Don't try to run this script in PixInsight 1.5.7. It requires the new version 1.5.8 because it depends on the new Script instance functionality.
Detecting the Execution ContextAn instantiable script must be prepared to work in at least two different scenarios, depending on the way it is invoked:
- In the direct context, that is, executed directly from the Script Editor or from the Script main menu item. This is the way scripts have been working so far. In this mode, a script usually presents a graphical user interface in the form of a dialog window.
- In the view or global contexts. This is the new
instance mode of scripts. When a script is working as an instance, it
must not create any graphical interface, besides (perhaps) a message box to inform the user about some critical condition. Instead, the script must perform "silently", according to the parameters it receives, just as all regular processes do in PixInsight. In instance mode, all script output should directed to the console exclusively.
Let's take a look at the way DrawSignature implements this. First, in line number 675, we have function main(), which is the script's entry point. These are the first lines in main()
/*
* Script entry point.
*/
function main()
{
// If the script is being executed as a Script instance, retrieve
// parameters, apply them, and exit. This allows us to run a script just as
// a regular (module-defined) process instance.
if ( engine.isInstance )
{
engine.apply();
return;
}
...
As you see, the script runs as we have described before when it detects it's being executed as a Script instance. The rest of main() creates and opens a dialog window when the script runs in direct mode.
Retrieving Instance ParametersHow does the script know it's running as an instance? In line #36 we have the DrawSignatureEngine object, which is the script's workbench. DrawSignatureEngine's constructor starts as follows:
/**
* The DrawSignatureEngine object defines and implements the DrawSignature
* routine and its functional parameters.
*/
function DrawSignatureEngine()
{
this.initialize = function()
{
if ( Parameters.isGlobalTarget )
throw new Error( "The " + #TITLE + " script cannot be run in the global context." );
// Default parameters
this.isInstance = false;
this.text = "PixInsight";
this.fontFace = "Helvetica";
this.fontSize = 128; // px
this.bold = false;
this.italic = false;
this.stretch = 100;
this.textColor = 0xffff7f00;
this.bkgColor = 0x80000000;
this.margin = 8;
this.softEdges = false;
if ( Parameters.isViewTarget )
{
// Our script is being executed as a Script instance.
this.isInstance = true;
this.targetView = Parameters.targetView;
// Retrieve instance parameters
if ( Parameters.has( "text" ) )
this.text = Parameters.getString( "text" );
if ( Parameters.has( "fontFace" ) )
this.fontFace = Parameters.getString( "fontFace" );
if ( Parameters.has( "fontSize" ) )
this.fontSize = Parameters.getUInt( "fontSize" );
if ( Parameters.has( "bold" ) )
this.bold = Parameters.getBoolean( "bold" );
if ( Parameters.has( "italic" ) )
this.italic = Parameters.getBoolean( "italic" );
if ( Parameters.has( "stretch" ) )
this.stretch = Parameters.getUInt( "stretch" );
if ( Parameters.has( "textColor" ) )
this.textColor = Parameters.getUInt( "textColor" );
if ( Parameters.has( "bkgColor" ) )
this.bkgColor = Parameters.getUInt( "bkgColor" );
if ( Parameters.has( "margin" ) )
this.margin = Parameters.getInteger( "margin" );
if ( Parameters.has( "softEdges" ) )
this.softEdges = Parameters.getBoolean( "softEdges" );
}
else
{
// Our script is being executed directly.
// Get access to the active image window
var window = ImageWindow.activeWindow;
if ( !window.isNull )
this.targetView = window.currentView;
}
};
...
This section performs the following actions:
- If the script is being executed in the global context, an Error exception is thrown. This is the way a script prevents global execution.
- A default set of parameters is defined (as properties of DrawSignatureEngine). This is necessary in all cases: if the script is working directly, obviously all parameters must be defined with starting values; if the script runs as an instance (in a view context), perhaps not all working parameters have been defined in the instance (e.g., the user has deleted some parameters with the Script interface).
- If the script is working as an instance, the Parameters object is used to retrieve working parameters. Note the calls to Parameters.has(), in order to verify that a given parameter has effectively been defined.
- Finally, if the script is being executed directly, the current active view is taken as the target view.
Exporting Instance ParametersFinally, an instantiable script must
export its working parameters each time it is executed. This completes the necessary bidirectional handshake between the script and the core PixInsight application. This is implemented starting at line number 96:
...
this.apply = function()
{
with ( this )
{
// Export script parameters. We must carry out this here, *before* applying
// our routine to targetView, so that a newly created Script instance will
// encapsulate our current set of working parameters.
exportParameters();
// Tell the core application that we are going to change this view.
// Without doing this, we'd have just read-only access to the view's image.
targetView.beginProcess();
// Perform our drawing routine.
draw();
// Done with view.
targetView.endProcess();
}
};
this.exportParameters = function()
{
Parameters.set( "text", this.text );
Parameters.set( "fontFace", this.fontFace );
Parameters.set( "fontSize", this.fontSize );
Parameters.set( "bold", this.bold );
Parameters.set( "italic", this.italic );
Parameters.set( "stretch", this.stretch );
Parameters.set( "textColor", format( "0x%x", this.textColor ) );
Parameters.set( "bkgColor", format( "0x%x", this.bkgColor ) );
Parameters.set( "margin", this.margin );
Parameters.set( "softEdges", this.softEdges );
};
...
Security ConsiderationsTo complete this description of the new scripting features, I'll elaborate on some important aspects of my implementation regarding security. A Script instantiable process is potentially dangerous. If our implementation doesn't prevent it, a Script instance could be used to inject malicious JavaScript code that could be executed without conscious user intervention. Of course, this cannot happen in PixInsight, for the following reasons:
- Script parameters are plain String objects which are never executed or evaluated. The Parameters core runtime object is the only interface a script can use to retrieve script parameters, and Parameters won't evaluate any source code, neither directly nor indirectly, because the eval() function is never called.
- The Script process is exposed to the PJSR as the Script external JavaScript object, which inherits from Instance. However, the Script JavaScript object is
sealed by default. This means that, under default security settings, a script cannot create new Script instances, because all properties of Script are read-only properties. In other words, code as the following snippet:
var p = new Script;
with ( p )
{
filePath = "$PCLDIR/dist/x86_64/PixInsight/src/scripts/DrawSignature.js";
md5sum = "3d81804876dd9547e4248d6db73f5678";
parameters = // id, value
[
["text", "PixInsight"],
["fontFace", "SansSerif"],
["fontSize", "128"],
["bold", "false"],
["italic", "false"],
["stretch", "100"],
["textColor", "0xffff7f00"],
["bkgColor", "0x80000000"],
["margin", "8"],
["softEdges", "false"]];
}
cannot be executed by a script because the Script object is sealed by default. The user can disable Script sealing via
Edit > Security Settings, but he/she will do that on his/her own risk (and the Global Security Options dialog informs about the potential risks of doing so).
ConclusionPixInsight 1.5.8 introduces significant improvements to the PixInsight JavaScript Runtime. In particular, the new instantiable Script process is an important step forward that boosts the power and versatility of JavaScript programming on the PixInsight platform.
In my opinion, these improvements open the door to exciting development projects that will be extremely beneficial to all PixInsight users, including both developers and end users.
As always, I'll be glad to hear about any comments, suggestions and criticisms about the new implementation.
AcknowledgementsThe work and encouragement from some PixInsight users and PTeam members have been essential during these years to achieve the state of development that the PixInsight JavaScript Runtime currently has. In particular, I want to say thanks to David Serrano, Georg Viehoever, Niall Saunders, Oriol Lehmkuhl, Ivette Rodríguez, Carlos Sonnenstein, Carlos Milovic, Vicent Peris, Sander Pool, Andrés Pozo, Juan Manuel Gómez, and everybody who has contributed with code and ideas. Sorry if I forget someone (please let me know).