PixInsight 1.5.8 - New Script process / Improved scripting functionality

Juan Conejero

PixInsight Staff
Staff member
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 Object

In 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:

Script01.jpg

[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.isGlobalTarget

This property is true if the script is being executed in the global context, false otherwise.

Boolean Parameters.isViewTarget

This 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.targetView

When 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:

Script02.jpg

[Click here to enlarge]

Here is the result of the previous script execution:

Script03.jpg

[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:

Script04.jpg

[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 Interface

Script 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.

Script05.jpg

[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:

Script06.jpg

[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 Instantiable

So 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:

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> &mdash; 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.
" +
                "stretch > 100 draws wider (extended) characters.
" +
                "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.
" +
                "(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.
" +
                "(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 Context

An 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()

Code:
/*
 * 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 Parameters

How 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:

Code:
/**
 * 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 Parameters

Finally, 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:

Code:
   ...

   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 Considerations

To 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:

Code:
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).


Conclusion

PixInsight 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.


Acknowledgements

The 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).
 
Juan,

this sound just like what I was looking for. Can't wait to give it a try.

One thing though:
- When I drag a process icon from the history explorer to the desktop, I get a process instance there.
- When I now double click on it, the process GUI opens

- 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.

- I guess, this is not what happens in the case of scripts. If I understand your text right, the JScript Process GUI would open, not the GUI implemented in the script.

If possible, it would be nice to be able to determine which kind of GUI would open for a script instance. Maybe this could be the JScript Module GUI as a default, and if implemented, the GUI of the specific script with the current parameters.This way, scripts really would be able to behave almost like true processing modules.

Georg

 
Hi Georg,

this sound just like what I was looking for. Can't wait to give it a try.

Indeed, this means that JavaScript scripts can work as regular processes, so they can be applied through ImageContainer. The best part is that existing scripts only need, in general, minimal changes to become fully instantiable.

- When I drag a process icon from the history explorer to the desktop, I get a process instance there.
- When I now double click on it, the process GUI opens
- I guess, this is not what happens in the case of scripts. If I understand your text right, the JScript Process GUI would open, not the GUI implemented in the script.

Yes and yes. When you double click a Script icon, the new Script interface opens and loads the icon's transported instance.

If possible, it would be nice to be able to determine which kind of GUI would open for a script instance. Maybe this could be the JScript Module GUI as a default, and if implemented, the GUI of the specific script with the current parameters.This way, scripts really would be able to behave almost like true processing modules.

This is a very interesting point. The main problem, of course, is that a JavaScript script does not "exist" until it is executed. In other words, the core PixInsight application cannot communicate with a script that is not running because it is just a piece of plain text.

Actually, if we analyze the problem in depth, we see that what we want is to be able to run all scripts in both the global and view contexts. When a script runs in a view context it performs as I have described in my post, that is, without a GUI. This is necessary for obvious reasons; for example, imagine your banding remover script opening its GUI each time you execute it through an ImageContainer. When the same script is being run in the global context, however, it knows two things: (1) that it has been launched from an existing instance (usually a process icon), and (2) that the user expects the script to behave in the usual way, i.e. opening its GUI and providing all of its interactive functionality.

So all we need, as I see the problem, is to add a new option to the context menu that opens when the user right-clicks a Script instance: Execute in the Global Context. This will be implemented in 1.5.8.

Then we have three execution modes for common JavaScript scripts:

1. Direct context. This happens when a script is executed from the Script Editor (F9). The script should use a GUI, if appropriate, and a set of default working parameters (perhaps stored via Settings).

2. Global context. The script has been launched from an existing instance, but the user has chosen global execution. The script should behave exactly as in case 1, but it should import its working parameters from the instance, using the new Parameters object.

3. View context. The script is being executed on a target view. No GUI must be used and all output must be directed to the console.

Along with these options, the new Script interface is a generic tool that allows you to edit Script parameters for any script, irrespective of its execution capabilities and GUI features. This is great, especially for script development.

Of course, there are cases that don't fit in the scheme above. For example, the BatchFormatConversion script cannot be executed on a view, so option 3 does not apply in this case (and the script will refuse view execution by throwing an Error exception).

I guess this does exactly what you want. What do you think?
 
Sounds just great!

Of course, I would still need to select the context menue entry for the "global execute", and thus a script is still a bit different from a C++ module. I guess what I am asking for is the ability of a script to behave just like a C++ module, which may very well be impossible to implement. In my view, this would be the logical next step, given the difficulties that exist with C++ modules (steeper learning curve, need to recompile for every release, need to compile for all platforms).

Anyway: fantastic work  :)

Geirg
 
Good stuff ;).

I'm missing the possibility of creating Script instances directly from the editor window. As you have described the procedure, it seems that in order to get a Script process icon, the required flow is:

1.- Launch a script on an image.
2.- Access the process history of that image.
3.- DnD the icon from the history to the desktop.

Of course, the action of saving a process icon without even trying its effects on an image isn't very common, but all processes allow it if you want, and this is an aspect in which scripts are going to keep being different to processes. The key of the problem here is obtaining the parameters: that could be done from the script's default, or from Settings (which is my partly-forgotten idea in the other thread, something that you already mentioned here).



Juan Conejero said:
So all we need, as I see the problem, is to add a new option to the context menu that opens when the user right-clicks a Script instance: Execute in the Global Context. This will be implemented in 1.5.8.

But please, give it another name, suitable for all those people that aren't following this conversation! xD. I'd use something like "Show script's interface" -- yes, it has nothing to do with the actual way it works, but it means something to the average user.
 
OK guys, this is going to cost you a lot of new scripts.  >:D

Let's start by the most obvious. Here is the new context menu for process icons:

IntegratedScript01.jpg

Click to enlarge

Note the two new options to execute a process in the view and global contexts. Of course, these options appear disabled for processes that cannot be executed on views or globally, respectively.

Now when a Script icon is executed globally, the standard behavior of (well-designed) scripts is to load its parameters from the Script instance (using the Parameters PJSR interface) and open a GUI (a Dialog) to allow the user edit them:

IntegratedScript02.jpg

Click to enlarge

And here starts the real fun. Do you see that small blue triangle at the bottom left corner. Isn't it a nice decoration? Well, not quite a decorative item, actually:

IntegratedScript03.jpg

Click to enlarge

No, you aren't hallucinating. And yes, a script can now generate new instances, just as a regular interface does in PixInsight :D

On the screenshot above, I am dragging a new instance, after clicking the blue triangle, and am about to drop it on the workspace to create a new icon. Your dreams turn into reality!  8)

IntegratedScript04.jpg

Click to enlarge

There is *only one* limitation, though: a newly created instance cannot be dropped over an image directly from the script's Dialog (an error message is shown if you do so). This is because the JavaScript runtime does not allow reentrant execution while a modal Dialog object is blocking the core application. Other than this, a script's Dialog can generate new instances without limitations.

Of course, after closing the script's dialog, one can drop the newly created icon to an image. Then the script loads the instance parameters and executes on the target view, as we have discussed before, just as a regular process in PixInsight:

IntegratedScript05.jpg

Click to enlarge

The best part of this is that only a few lines are necessary to include instance generation capabilities in a Dialog. On the following screenshot:

IntegratedScript06.jpg

Click to enlarge

you can see the Script Editor interface with the modified DrawSignature.js script. Note the new Dialog.newInstance() method, and how we can create a blue triangle ToolButton that behaves just as common New Instance buttons on processing interfaces. Look at the newInstance_Button definition at the top of the editor's page.

8)
 
Could you change a bit the color of the triangle, to emphasize that is very much like ordinary drag&drop object, but slighty different (cannot apply directly over images).

BTW, one thing that annoys me is the behavior of those dialog windows.... is there a way to avoid "blocking the entire application" because of them?
 
Crickey I am thick  , haven't got a clue what you all are talking about  ??? ??? ??? ??? ??? ??? ???

Harry
 
Harry,

What Juan has achieved is to guarantee that PI cannot really EVER 'stagnate'. Now, even without his direct input, PI can be customised and tweaked by virtually anybody who can learn how to program - from the simplest level (via PJSR scripting) right up to the most complex level (via PI core modules).

PI is developing like a healthy child - and, to me anyway, it seems that the development is almost at the same rate ! Can you imagine PI in its 'teenager years' - unruly hooligan, or child prodigy? Thankfully we have the PI Police to keep things all in order  :police:  :police:  :police:
 
Juan Conejero said:
you can see the Script Editor interface with the modified DrawSignature.js script. Note the new Dialog.newInstance() method, and how we can create a blue triangle ToolButton that behaves just as common New Instance buttons on processing interfaces.
Hello, I trying to use a blue triangle and PixelMath in script.
Code:
targetView.beginProcess();
var mergeThem = new PixelMath;
...
executeOn ( targetView );
targetView.endProcess();
If push "OK" button I getting error: Invalid view update request: The image is already being processed.
But no error if I use New Instance triangle (via desktop) to image or to Image container.
What's wrong? How to use PixelMath and beginProcess() in script with NewInstans and with standard OK button?
 
Hi Nikolay,

That happens because you are attempting to execute a process on a view that is already being processed by your script. Always call Instance.executeOn() outside a View.beginProcess() ... View.endProcess() sequence.
 
I know I am bringing up an ancient thread, but there is no other documentation on the Script process.

All of the images on the first post are gone. Can they be restored?
 
Back
Top