New version 1.1 of the BatchFormatConversion script

Juan Conejero

PixInsight Staff
Staff member
Hi!

As per Nikolay Volkov's request, here is a new version (1.1) of the standard BatchFormatConversion script. The new version allows you to specify an output sample format (bit depth and integer/float data type) as well as the output file format. Source code follows.

Code:
/*
   BatchFormatConversion v1.1

   A batch image format conversion utility.

   Copyright (C) 2009, 2010 Pleiades Astrophoto S.L.

   This program is free software: you can redistribute it and/or modify it
   under the terms of the GNU General Public License as published by the
   Free Software Foundation, version 3 of the License.

   This program is distributed in the hope that it will be useful, but WITHOUT
   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
   more details.

   You should have received a copy of the GNU General Public License along with
   this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#feature-id    Utilities > BatchFormatConversion

#feature-info  A batch image format conversion utility.
\
   
 \
   This script allows you to define a set of input image files, an optional output \
   directory, and output file and sample formats. The script then iterates reading \
   each input file, converting it to the selected sample format if necessary, and \
   saving it on the output directory with the specified output file format.
\
   
\
   This script is very useful when you have to convert several images &mdash;no \
   matter if dozens or hundreds&mdash; from one format to another automatically. \
   It saves you the work of opening and saving each file manually one at a time.
\
   
 \
   Written by Juan Conejero (PTeam)
\
   Copyright &copy; 2009, 2010 Pleiades Astrophoto S.L.

#feature-icon  BatchFormatConversion.xpm

#include <pjsr/Sizer.jsh>
#include <pjsr/FrameStyle.jsh>
#include <pjsr/TextAlign.jsh>
#include <pjsr/StdButton.jsh>
#include <pjsr/StdIcon.jsh>

#define DEFAULT_EXTENSION  ".jpg"

#define WARN_ON_NO_OUTPUT_DIRECTORY 1

#define VERSION "1.1"
#define TITLE   "BatchFormatConversion"

/**
 * Batch Conversion Engine
 */
function BatchFormatConversionEngine()
{
   this.inputFiles = new Array;
   this.outputDirectory = "";
   this.outputExtension = DEFAULT_EXTENSION;
   this.forceSampleFormat = false;
   this.bitsPerSample = 16;
   this.floatSample = false;
   this.overwriteExisting = false;

   this.convertFiles = function()
   {
      for ( var i = 0; i < this.inputFiles.length; ++i )
      {
         var w = ImageWindow.open( this.inputFiles[i] );

         if ( w.length == 0 && i+1 < this.inputFiles.length )
         {
            var msg = new MessageBox( "<p>Unable to load input image file:</p>" +
                                      "<p>" + this.inputFiles[i] + "</p>" +
                                      "<p><b>Continue batch format conversion ?</b></p>",
                                      TITLE, StdIcon_Error, StdButton_Yes, StdButton_No );
            if ( msg.execute() == StdButton_No )
               break;
         }

         /*
            Build the output file path from its separate components:
            drive+directory+name+extension
            Of course, drive is always an empty string under Linux/UNIX/OSX
          */
         var fileDir = (this.outputDirectory.length != 0) ?
            this.outputDirectory : File.extractDrive( this.inputFiles[i] ) +
                                   File.extractDirectory( this.inputFiles[i] );

         // Ensure that our directory string ends with a slash separator.
         if ( fileDir.length != 0 && fileDir.charAt( fileDir.length-1 ) != '/' )
            fileDir += '/';

         var fileName = File.extractName( this.inputFiles[i] );

         var outputFilePath = fileDir + fileName + this.outputExtension;

         if ( !this.overwriteExisting && File.exists( outputFilePath ) )
         {
            /*
               Obtain a nonexisting file name by appending an underscore and a
               growing integer to the output file name.
             */
            for ( var u = 1; ; ++u )
            {
               var tryFilePath = File.appendToName( outputFilePath, '_' + u.toString() );
               if ( !File.exists( tryFilePath ) )
               {
                  outputFilePath = tryFilePath;
                  break;
               }
            }
         }

         for ( var j = 0; j < w.length; ++j )
         {
            /*
               Force the specified sample format, if requested and different
               from the current one.
             */
            if ( this.forceSampleFormat )
               if ( this.bitsPerSample != w[j].bitsPerSample || this.floatSample != w[j].isFloatSample )
                  w[j].setSampleFormat( this.bitsPerSample, this.floatSample );

            /*
               Write the output image.

               Force ImageWindow to disable all format and security features:

               * Query format-specific options
               * Warning messages on missing format features (icc profiles, etc)
               * Strict image writing mode (ignore lossy image generation)
               * Overwrite verification/protection
             */
            w[j].saveAs( outputFilePath, false, false, false, false );

            /*
               Unconditionally close the image window.

               ImageWindow.forceClose() will always close the image, without
               asking any questions in the event that the modified state of
               the image hasn't been cleared at this point.

               Also note that we haven't called w.show(), so it is hidden.
             */
            w[j].forceClose();
         }
      }
   }
}

var engine = new BatchFormatConversionEngine;

function trim( s )
{
   return s.replace( /^\s*|\s*$/g, '' );
}

/**
 * Batch Conversion Dialog
 */
function BatchFormatConversionDialog()
{
   // Add all properties and methods of the core Dialog object to this object.
   this.__base__ = Dialog;
   this.__base__();

   //

   var emWidth = this.font.width( 'M' );
   var labelWidth1 = this.font.width( "Output extension:" ) + emWidth;

   //

   this.helpLabel = new Label( this );
   this.helpLabel.frameStyle = FrameStyle_Box;
   this.helpLabel.margin = 4;
   this.helpLabel.wordWrapping = true;
   this.helpLabel.useRichText = true;
   this.helpLabel.text = "<p><b>" + TITLE + " v" + VERSION + "</b> &mdash; " +
                         "A batch image format conversion utility.</p>" +
                         "<p>Copyright &copy; 2009, 2010 Pleiades Astrophoto</p>";
   //

   this.files_TreeBox = new TreeBox( this );
   this.files_TreeBox.multipleSelection = true;
   this.files_TreeBox.rootDecoration = false;
   this.files_TreeBox.alternateRowColor = true;
   this.files_TreeBox.setMinSize( 500, 200 );
   this.files_TreeBox.numberOfColumns = 1;
   this.files_TreeBox.headerVisible = false;

   for ( var i = 0; i < engine.inputFiles.length; ++i )
   {
      var node = new TreeBoxNode( this.files_TreeBox );
      node.setText( 0, engine.inputFiles[i] );
   }

   this.filesAdd_Button = new PushButton( this );
   this.filesAdd_Button.text = " Add ";
   this.filesAdd_Button.toolTip = "<p>Add image files to the input images list.</p>";

   this.filesAdd_Button.onClick = function()
   {
      var ofd = new OpenFileDialog;
      ofd.multipleSelections = true;
      ofd.caption = "Select Images";
      ofd.loadImageFilters();

      if ( ofd.execute() )
      {
         this.dialog.files_TreeBox.canUpdate = false;
         for ( var i = 0; i < ofd.fileNames.length; ++i )
         {
            var node = new TreeBoxNode( this.dialog.files_TreeBox );
            node.setText( 0, ofd.fileNames[i] );
            engine.inputFiles.push( ofd.fileNames[i] );
         }
         this.dialog.files_TreeBox.canUpdate = true;
      }
   };

   this.filesClear_Button = new PushButton( this );
   this.filesClear_Button.text = " Clear ";
   this.filesClear_Button.toolTip = "<p>Clear the list of input images.</p>";

   this.filesClear_Button.onClick = function()
   {
      this.dialog.files_TreeBox.clear();
      engine.inputFiles.length = 0;
   };

   this.filesInvert_Button = new PushButton( this );
   this.filesInvert_Button.text = " Invert Selection ";
   this.filesInvert_Button.toolTip = "<p>Invert the current selection of input images.</p>";

   this.filesInvert_Button.onClick = function()
   {
      for ( var i = 0; i < this.dialog.files_TreeBox.numberOfChildren; ++i )
         this.dialog.files_TreeBox.child( i ).selected =
               !this.dialog.files_TreeBox.child( i ).selected;
   };

   this.filesRemove_Button = new PushButton( this );
   this.filesRemove_Button.text = " Remove Selected ";
   this.filesRemove_Button.toolTip = "<p>Remove all selected images from the input images list.</p>";

   this.filesRemove_Button.onClick = function()
   {
      engine.inputFiles.length = 0;
      for ( var i = 0; i < this.dialog.files_TreeBox.numberOfChildren; ++i )
         if ( !this.dialog.files_TreeBox.child( i ).selected )
            engine.inputFiles.push( this.dialog.files_TreeBox.child( i ).text( 0 ) );
      for ( var i = this.dialog.files_TreeBox.numberOfChildren; --i >= 0; )
         if ( this.dialog.files_TreeBox.child( i ).selected )
            this.dialog.files_TreeBox.remove( i );
   };

   this.filesButtons_Sizer = new HorizontalSizer;
   this.filesButtons_Sizer.spacing = 4;
   this.filesButtons_Sizer.add( this.filesAdd_Button );
   this.filesButtons_Sizer.addStretch();
   this.filesButtons_Sizer.add( this.filesClear_Button );
   this.filesButtons_Sizer.addStretch();
   this.filesButtons_Sizer.add( this.filesInvert_Button );
   this.filesButtons_Sizer.add( this.filesRemove_Button );

   this.files_GroupBox = new GroupBox( this );
   this.files_GroupBox.title = "Input Images";
   this.files_GroupBox.sizer = new VerticalSizer;
   this.files_GroupBox.sizer.margin = 6;
   this.files_GroupBox.sizer.spacing = 4;
   this.files_GroupBox.sizer.add( this.files_TreeBox, 100 );
   this.files_GroupBox.sizer.add( this.filesButtons_Sizer );

   //

   this.outputDir_Edit = new Edit( this );
   this.outputDir_Edit.readOnly = true;
   this.outputDir_Edit.text = engine.outputDirectory;
   this.outputDir_Edit.toolTip =
      "<p>If specified, all converted images will be written to the output directory.</p>" +
      "<p>If not specified, converted images will be written to the same directories " +
      "of their corresponding input images.</p>";

   this.outputDirSelect_Button = new PushButton( this );
   this.outputDirSelect_Button.text = " Select ";
   this.outputDirSelect_Button.toolTip = "<p>Select the output directory.</p>";

   this.outputDirSelect_Button.onClick = function()
   {
      var gdd = new GetDirectoryDialog;
      gdd.initialPath = engine.outputDirectory;
      gdd.caption = "Select Output Directory";

      if ( gdd.execute() )
      {
         engine.outputDirectory = gdd.directory;
         this.dialog.outputDir_Edit.text = engine.outputDirectory;
      }
   };

   this.outputDir_GroupBox = new GroupBox( this );
   this.outputDir_GroupBox.title = "Output Directory";
   this.outputDir_GroupBox.sizer = new HorizontalSizer;
   this.outputDir_GroupBox.sizer.margin = 6;
   this.outputDir_GroupBox.sizer.spacing = 4;
   this.outputDir_GroupBox.sizer.add( this.outputDir_Edit, 100 );
   this.outputDir_GroupBox.sizer.add( this.outputDirSelect_Button );

   //

   var outExtToolTip = "<p>Specify a file extension to identify the output file format.</p>" +
      "<p>Be sure the selected output format is able to write images, or the batch conversion " +
      "process will fail upon attempting to write the first output image.</p>" +
      "<p>Also be sure that the output format can generate images with the specified output " +
      "sample format (see below), if you change the default setting.</p>";

   this.outputExt_Label = new Label( this );
   this.outputExt_Label.text = "Output extension:";
   this.outputExt_Label.minWidth = labelWidth1;
   this.outputExt_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
   this.outputExt_Label.toolTip = outExtToolTip;

   this.outputExt_Edit = new Edit( this );
   this.outputExt_Edit.text = engine.outputExtension;
   this.outputExt_Edit.setFixedWidth( this.font.width( "MMMMMM" ) );
   this.outputExt_Edit.toolTip = outExtToolTip;

   this.outputExt_Edit.onEditCompleted = function()
   {
      // Image extensions are always lowercase in PI/PCL.
      var ext = trim( this.text ).toLowerCase();

      // Use the default extension if empty.
      // Ensure that ext begins with a dot character.
      if ( ext.length == 0 || ext == '.' )
         ext = DEFAULT_EXTENSION;
      else if ( ext.charAt( 0 ) != '.' )
         ext = '.' + ext;

      this.text = engine.outputExtension = ext;
   };

   this.options_Sizer = new HorizontalSizer;
   this.options_Sizer.spacing = 4;
   this.options_Sizer.add( this.outputExt_Label );
   this.options_Sizer.add( this.outputExt_Edit );
   this.options_Sizer.addStretch();

   //

   var bpsToolTip = "<p>Sample format for output images.</p>" +
      "<p>Note that these settings are just a <i>hint</i>. The script will convert all " +
      "input images to the specified sample format, if necessary, but it can be ignored " +
      "by the output format if it is unable to write images with the specified bit depth " +
      "and sample type.</p>";

   this.sampleFormat_Label = new Label( this );
   this.sampleFormat_Label.text = "Sample format:";
   this.sampleFormat_Label.textAlignment = TextAlign_Right|TextAlign_VertCenter;
   this.sampleFormat_Label.minWidth = labelWidth1;
   this.sampleFormat_Label.toolTip = bpsToolTip;

   this.sampleFormat_ComboBox = new ComboBox( this );
   this.sampleFormat_ComboBox.addItem( "Same as input images" );
   this.sampleFormat_ComboBox.addItem( "8-bit integer" );
   this.sampleFormat_ComboBox.addItem( "16-bit integer" );
   this.sampleFormat_ComboBox.addItem( "32-bit integer" );
   this.sampleFormat_ComboBox.addItem( "32-bit IEEE 754 floating point" );
   this.sampleFormat_ComboBox.addItem( "64-bit IEEE 754 floating point" );
   this.sampleFormat_ComboBox.toolTip = bpsToolTip;

   this.sampleFormat_ComboBox.onItemSelected = function( index )
   {
      if ( (engine.forceSampleFormat = index > 0) != false )
         switch ( index )
         {
         case 1:
            engine.bitsPerSample = 8;
            engine.floatSample = false;
            break;
         case 2:
            engine.bitsPerSample = 16;
            engine.floatSample = false;
            break;
         case 3:
            engine.bitsPerSample = 32;
            engine.floatSample = false;
            break;
         case 4:
            engine.bitsPerSample = 32;
            engine.floatSample = true;
            break;
         case 5:
            engine.bitsPerSample = 64;
            engine.floatSample = true;
            break;
         default: // ?
            break;
         }
   };

   this.sampleFormat_Sizer = new HorizontalSizer;
   this.sampleFormat_Sizer.spacing = 4;
   this.sampleFormat_Sizer.add( this.sampleFormat_Label );
   this.sampleFormat_Sizer.add( this.sampleFormat_ComboBox );
   this.sampleFormat_Sizer.addStretch();

   //

   this.overwriteExisting_CheckBox = new CheckBox( this );
   this.overwriteExisting_CheckBox.text = "Overwrite existing files";
   this.overwriteExisting_CheckBox.checked = engine.overwriteExisting;
   this.overwriteExisting_CheckBox.toolTip =
      "<p>Allow overwriting of existing image files.</p>" +
      "<p><b>* Warning *</b> Enabling this option may lead to irreversible data loss.</p>";

   this.overwriteExisting_CheckBox.onClick = function( checked )
   {
      engine.overwriteExisting = checked;
   };

   this.overwriteExisting_Sizer = new HorizontalSizer;
   this.overwriteExisting_Sizer.addSpacing( labelWidth1 + 4 );
   this.overwriteExisting_Sizer.add( this.overwriteExisting_CheckBox );
   this.overwriteExisting_Sizer.addStretch();

   //

   this.outputOptions_GroupBox = new GroupBox( this );
   this.outputOptions_GroupBox.title = "Output File Options";
   this.outputOptions_GroupBox.sizer = new VerticalSizer;
   this.outputOptions_GroupBox.sizer.margin = 6;
   this.outputOptions_GroupBox.sizer.spacing = 4;
   this.outputOptions_GroupBox.sizer.add( this.options_Sizer );
   this.outputOptions_GroupBox.sizer.add( this.sampleFormat_Sizer );
   this.outputOptions_GroupBox.sizer.add( this.overwriteExisting_Sizer );

   //

   this.ok_Button = new PushButton( this );
   this.ok_Button.text = "OK";

   this.ok_Button.onClick = function()
   {
      this.dialog.ok();
   };

   this.cancel_Button = new PushButton( this );
   this.cancel_Button.text = "Cancel";

   this.cancel_Button.onClick = function()
   {
      this.dialog.cancel();
   };

   this.buttons_Sizer = new HorizontalSizer;
   this.buttons_Sizer.spacing = 6;
   this.buttons_Sizer.addStretch();
   this.buttons_Sizer.add( this.ok_Button );
   this.buttons_Sizer.add( this.cancel_Button );

   //

   this.sizer = new VerticalSizer;
   this.sizer.margin = 6;
   this.sizer.spacing = 6;
   this.sizer.add( this.helpLabel );
   this.sizer.addSpacing( 4 );
   this.sizer.add( this.files_GroupBox, 100 );
   this.sizer.add( this.outputDir_GroupBox );
   this.sizer.add( this.outputOptions_GroupBox );
   this.sizer.add( this.buttons_Sizer );

   this.windowTitle = TITLE + " Script";
   this.userResizable = true;
   this.adjustToContents();
}

// Our dialog inherits all properties and methods from the core Dialog object.
BatchFormatConversionDialog.prototype = new Dialog;

/*
 * Script entry point.
 */
function main()
{
   console.hide();

   // Show our dialog box, quit if cancelled.
   var dialog = new BatchFormatConversionDialog();
   for ( ;; )
   {
      if ( dialog.execute() )
      {
         if ( engine.inputFiles.length == 0 )
         {
            var msg = new MessageBox( "No input files have been specified!",
                                      TITLE, StdIcon_Error, StdButton_Ok );
            msg.execute()
            continue;
         }

#ifneq WARN_ON_NO_OUTPUT_DIRECTORY 0
         if ( engine.outputDirectory.length == 0 )
         {
            var msg = new MessageBox(
                  "<p>No output directory has been specified.</p>" +
                  "<p>Each converted image will be written to the directory of " +
                  "its corresponding input file.
" +
                  "<b>Are you sure ?</b></p>",
                  TITLE, StdIcon_Warning, StdButton_Yes, StdButton_No );
            if ( msg.execute() != StdButton_Yes )
               continue;
         }
#endif
         // Perform batch file format conversion and quit.
         console.show();
         console.abortEnabled = true;
         engine.convertFiles();

         var msg = new MessageBox(
            "Do you want to perform another format conversion ?",
            TITLE, StdIcon_Question, StdButton_Yes, StdButton_No );
         if ( msg.execute() == StdButton_Yes )
            continue;
      }

      break;
   }
}

main();

Feel free to change the value of the DEFAULT_EXTENSION macro (line #45) from ".jpg" to the file extension that best suits your needs (for example, ".fit").

Enjoy!
 
Juan, lines 193-197 in the script, why should they? It's mistake or magic?
Code:
   for ( var i = 0; i < engine.inputFiles.length; ++i )
   {
      var node = new TreeBoxNode( this.files_TreeBox );
      node.setText( 0, engine.inputFiles[i] );
   }
 
Hi Nikolay,

Smart finding! ;)

Well, actually those lines do nothing because when the dialog is constructed the engine.inputFiles array is empty. It's there just in case somebody wants to preload engine.inputFiles in some way at the very beginning of the script. That's unlikely to happen, but just in case :)
 
Back
Top