Mis à jour le 26/01/2015
  • 4 heures
  • Moyenne
Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Introduction du cours

Introduction

L’objectif de ce cours est de vous apprendre à écrire des scripts pour les logiciels de la Creative Suite.

Nous apprendrons par exemple à automatiser des tâches dans InDesign et Photoshop. Lorsqu’on parle de scripts en PAO (Publication Assistée par Ordinateur) on sous-entend régulièrement l’enregistrement d’étapes effectuées par un utilisateur. Ici, nous écrirons nos scripts dans un langage de programmation, ce qui nous permettra d’aller plus loin qu’avec un simple script Photoshop.

Prérequis

ExtendScript est un langage de programmation d’Adobe System. Ce langage est basé sur le célèbre langage de programmation JavaScript. Le but de ce cours n’est pas d’apprendre à programmer en JavaScript, aussi, si vous n’avez pas de bases en programmation, je vous invite à suivre le cours Dynamisez vos sites web avec JavaScript sur OpenClassrooms.

Contrairement à la croyance populaire, l’écriture de scripts simples est à la portée de chacun pour autant qu’on suive quelques tutoriels et que l’on s’intéresse à cette technologie.

Rappel JavaScript et notation

Les bonnes pratiques JavaScript présentées dans cette section sont issues de l’excellent livre de Douglas Crowford «JavaScript, Les bons éléments» aux éditions Pearson.

Syntaxe

  • Chaque ligne se termine par un ;

  • On évite l’usage des raccourcis dans l’utilisation des blocs.

    if (test) {
        // your code here
    }
  • Chaque variable est déclarée avec le mot réservé var.

Tableaux

  • On préfère l’usage de la forme courte.

    // juste
    var my_array = [];
    // faux
    var wrong_array = new Array("Saab", "Volvo", "BMW");

Objets

  • On restreint l'usage du mot clé new  même si l'on aime bien l'orienté objet !

  • var my_object = {};

Opérateurs d'égalité

  • On utilise les opérateurs d'égalité sans conversion de type.

    // il faut utiliser les opérateurs sans conversion de type
    // ===, !==
    if (0 == '') {
        alert('ces deux instructions ne sont pas égales et pourtant?');
    }
    
    if (0 === '') {
        alert('cette alert ne sera pas affichée');
    }
    else {
        alert('cette fois-ci, opérateur fonctionne bien');
    }

Valeurs fausses

Si on veut tester une valeur équivalente à faux, on peut utiliser l'instruction if(!boolean).

  • /**
      * Les valeurs JavaScript équivalentes à faux:
      * 0, NaN, '', false, null, undefined
      *
      */
    
    var my_object = {};
    // test si l'objet contient la propriété name
    // la propriété n'existe pas donc le test renvoie undefined
    // en utilisant == il y a une conversion de type donc undefined vaut null
    // ce genre de comportement est risqué et nuisible pour une bonne lecture du code
    if (my_object.name == null) {
        alert('object name vaut undefined -> null');
    }
    if (my_object.name === undefined) {
        alert("a propriété n'existe pas");
    }
    
    // on propose donc d'utiliser ! pour tester si la propriété n'existe pas
    if (!my_object.name) {
        alert("la propriété n'existe pas, la valeur faux est retournée");
    }

Documentation et ressources nécessaires

Comme vous l'aurez certainement constaté, le principal problème avec les scripts en ExtendScript est qu'il y a peu de documentation.
Pour augmenter la difficulté, on a presque l'impression que le développement du IDE (Integrated Development Environment) ExtendScript Toolkit est à moitié terminé…
Bon, trêves de mauvaises nouvelles, je vais essayer de vous aider en vous donnant quelques liens utiles :
• Pour poser des questions, le forum d'Adobe.
• Il existe des SDK (Software Development Kit) pour InDesign et Photoshop. 
• Vous pouvez trouver de la documentation HTML sur le site de Jongware
• Si l'allemand ne vous effraie pas, ce livre est intéressant: InDesign Automatisieren.

Environnement de développement

ExtendScript Toolkit

Adobe propose un utilitaire inclus dans la Creative Suite pour écrire et exécuter des scripts ExtendScript (JavaScript). Pour commencer, ouvrez le logiciel ExtendScript Tookit.

Utilitaire Extendscript Toolkit
Utilitaire Extendscript Toolkit

Notre premier script

// remplacez par photoshop pour tester dans photoshop

#target "indesign"

alert('hello indesign');

InDesign

Manipuler les liens

Les scripts peuvent être très pratiques si l'on souhaite manipuler les liens d'un fichier InDesign.
Pour cela, il faut avoir en tête l'object model des liens. Si vous vous rendez sur jongware.mit.edu, vous pouvez parcourir facilement la structure des objets JavaScript liés à InDesign.

Link object model
Link object model

Si par exemple vous voulez effectuer une action sur toutes les images d'un document, vous pouvez faire un script comme ceci :

#target "indesign"

var doc = app.activeDocument;
var collection_of_links =  doc.links;

for(var i = 0; i < collection_of_links.length; i++) {
    var current_link = collection_of_links[i];
    if (current_link.parent.constructor.name === 'Image') {
        alert('je suis une image');
    }
}

Ou encore mieux, vous pouvez directement sélectionner les «graphics» d'un document, c'est-à-dire les fichiers EPS, Image, ImportedPage, PDF, PICT, WMF sans les Movie, Sound et Story.

#target "indesign"

var doc = app.activeDocument;
var collection_of_graphics=  doc.allGraphics;

for(var i = 0; i < collection_of_graphics.length; i++) {
    var current_graphic = collection_of_graphics[i];
    if (current_graphic.constructor.name === 'Image') {
        alert('je suis une image');
    }
}

Travailler avec un Document InDesign

Si l'on veut par exemple ouvrir un document InDesign et copier les liens et le document dans un dossier (faire un assemblage).

var SAVE_FOLDER_PATH = '~/Desktop/crop';
var doc = app.activeDocument;

var new_doc_path = save_with_package(doc, SAVE_FOLDER_PATH);
var new_doc = app.open(new File(new_doc_path));

doc.close(SaveOptions.NO);

/**
 * Function to format a date
 * @return {String} the date as string in this format yyyy-mm-dd
 */
Date.prototype.year_month_day = function () {
    
    var year = this.getFullYear();
    var month = this.getMonth() + 1;
    
    if (month.toString().length === 1) {
        month = '0' + month;
    }
    
    var day = this.getDate();
    if (day.toString().length === 1) {
        day = '0' + day;
    }
    return year + '-' + month + '-' + day;
}


/**
 * Function to format time
 * @return {String} the date as string in this format hh-mm-ss
 */
Date.prototype.hours_minutes_seconds = function () {
    var hours = this.getHours();
    
    if (hours.toString().length === 1) {
        hours = '0' + hours;    
    } 
    
    var minutes = this.getMinutes();
    
    if (minutes.toString().length === 1) {
        minutes = '0' + minutes;
    }
    
    var seconds = this.getSeconds();
    
    if (seconds.toString().length === 1) {
        seconds = '0' + seconds;
    }
    return hours + '-' + minutes + '-' + seconds;
}

/**
 * Function who package a document and its links
 * @param {Document} document the indesign document
 * @param {String} directories_path_str the path where to store the new document
 * @returns {String} the path of the new document
 * @throws {Error} if an error occurred during the package for print
 */
function save_with_package (doc, folder_path) {
    
    var current_date = new Date();
    var folder_package = new Folder(folder_path);
    
    if (!folder_package.exists) {
        folder_package.create();
    }
    
    var doc_file = document.fullName;
    var current_folder = new Folder(folder_package + '/' + current_date.year_month_day() + '_' + current_date.hours_minutes_seconds());
    current_folder.create();
    /**
     *  bool packageForPrint (to: File, copyingFonts: bool, copyingLinkedGraphics: bool, copyingProfiles: bool, updatingGraphics: bool,
     *  includingHiddenLayers: bool, ignorePreflightErrors: bool, creatingReport: bool[, versionComments: string][, forceSave: bool=false])
     **/
    var result_package = document.packageForPrint(current_folder, false, true, false, true, false, true, false);
    if (!result_package) {
        throw new Error("il y a eu un problème lors de l'assemblage");
    }
    return current_folder + '/' + document.name;
}

TP : renommer tous les liens d'un document

Pour ce petit exercice pratique, on va écrire un script qui permet d'enlever tous les caractères spéciaux dans le nom de fichiers des liens du document InDesign.

Donc on va en premier devoir récupérer tous les liens présents dans le document. Ensuite, on va les renommer et les mettre à jour pour informer InDesign du changement de nom. Afin d'éviter de perdre des informations dans le document source, on va tout d'abord faire un assemblage sur le Bureau.

//@target "indesign"

var doc = app.activeDocument;

var collection_of_links = doc.links;

for (var i = 0; i < collection_of_links.length; i++) {

    var current_link = collection_of_links[i];
    var file_path = current_link.filePath;
    //  edit the file name
    var my_file = new File(file_path);
    var file_name = rename(current_link.name);
    // replace the file name
    my_file.rename(file_name);
    current_link.relink(my_file);
    current_link.update();
}

/**
 * Function to replace all space by _
 * @param str
 * @returns {string}
 */
function rename (str) {
    return str.replace(/\s/g, '_');
}

Photoshop

Exécuter un script dans Photoshop

Si l'on souhaite faire un script pour Photoshop au lieu d'InDesign, il faut commencer par changer la première ligne du script.

#target "photoshop"
/** 
 *  remarque : parfois j'ai écrit //@target au lieu de #target
 *  ceci est utile si vous souhaitez écrire vos scripts 
 *  dans un autre programme que ExtendScript Toolkit
 *  ainsi l'instruction est prise en compte 
 *  mais n'est pas détectée comme une erreur
 */

Un exemple pratique

Je propose de commencer directement par un exemple pratique ! :magicien:

Admettons que vous souhaitiez changer le texte d'un calque Photoshop d'après une structure de données (tableau Excel, base de données, etc.).

Pour commencer, créez un document Photoshop avec un calque de texte comme l'image ci-dessous. Ensuite, enregistrer ce document sur votre bureau en le nommant test.psd.

créer un document indesign avec un calque de texte
Créer un document InDesign avec un calque de texte

Une fois ceci fait, nous pouvons écrire un script qui ira changer le contenu du calque de texte. Pour chaque numéro présent dans le tableau "data", on créera un nouveau document PSD.

//@target "photoshop"

/**
 * img_file_path the image path
 * @type {string}
 */
var source_file_path = '~/Desktop/test.psd';

/**
 * data the number to change in photoshop
 * @type {string[]}
 */
var data = ['No  28', 'No  29', 'No  30', 'No  31'];


/**
 * open the image test.psd on the desktop
 * for each number in the array data change the content of the text layer
 * then save results on the desktop as PSD
 */

try{
    app.displayDialogs = DialogModes.NO;

    for(var i = 0; i < data.length; i++){
        var doc = open_doc(source_file_path);
        edit_text_layer(doc, data[i]);
        save_to_PSD('~/Desktop/test_' + i + '.psd');
    }

}
catch (ex){
    alert('error: ' + ex.message + '\n line' + ex.line);
}
finally{
    app.displayDialogs = DialogModes.ALL;
}

/**
 * Function to edit the text layer
 * @param doc
 * @param text
 */
function edit_text_layer(doc, text) {
    var text_layer = doc.artLayers[0]; // the second last layer in photoshop
    text_layer.name = text;
    text_item_ref = text_layer.textItem;
    text_item_ref.contents = text;
}


/**
 * Function to open an image in photoshop
 * @param file_path
 * @returns {Photoshop document}
 */
function open_doc(file_path) {
    var img_file = new File(file_path);
    var ps_doc = app.open(img_file);
    return ps_doc;
}

/**
 * Function to save a document in PSD
 * @param {String} file_path
 */
function save_to_PSD(file_path) {
    psdSaveOption = new PhotoshopSaveOptions();
    psdSaveOption.embedColorProfile = true;
    app.activeDocument.saveAs(new File(file_path), psdSaveOption, true, Extension.LOWERCASE);
    app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}

ExtendScript-library

Il existe un certain nombre de projets de librairie ExtendScript sur le web. Le but de cette partie n'est pas de dresser une liste exhaustive de tous ces projets. Néanmoins, on note que ces différents projets sont une très bonne source d'inspiration pour quiconque souhaite écrire des scripts :

  • La librairie Extendable qui fournit un certain nombre de fonctionnalités pour InDesign. Malheureusement, cette librairie n'est plus maintenue à jour.

  • La librairie Basil.js qui s'adresse plus à des designers qu'à des développeurs. Cette librairie fournit des méthodes utiles pour traiter le contenu des documents InDesign.

  • La librairie Xtools qui fournit des fonctions pour Photoshop. 

Problématique

Il existe quelques librairies avec chacune leurs forces et faiblessses cependant aucun projet ne s'est vraiment imposé sur le Web. Il serait très intéressant de pouvoir mettre à disposition des utilisateurs de l'ExtendScript une librairie complète et maintenue régulièrement. On peut prendre comme exemple la librairie jQuery qui s'est imposée comme l'un des standards pour ceux qui souhaitent ajouter des fonctionnalités JavaScript à un site web.

En partant de ce constat, les contributeurs ExtendScript-library souhaitent lancer un projet de librairie pour les logiciels de la Creative Suite d'Adobe. Ainsi, ce projet regrouperait aussi bien des fonctions pour InDesign, Photoshop, Illustrator…

Ce projet est encore très jeune puisque les premiers "commits" datent de septembre 2014. On retient que l'objectif final est de créer une communauté active pour promouvoir cette technologie.

Présentation de la librairie

La librairie ExtendScript-library comprend un module différent pour chaque application de la Creative Suite. Ainsi, le module InDesign regroupe toutes les fonctions qui peuvent être exécutées dans le logiciel InDesign.

Les fonctionnalités qui sont communes à toute les applications comme l'écriture de logs, les fonctions liées aux tableaux et aux chaînes de caractères sont incluses dans le module Helper.

organisation des modules
organisation des modules

Le but de cette librairie est de se focaliser sur l'automatisation dans le milieu du prépresse. Les fonctions liées à Photoshop et au XML seront également approfondies.

Exemples

En premier, téléchargez la librairie sur le dépôt GitHub. Vous trouverez dans le répertoire public-scripts un exemple de script qui permet de recadrer toutes les images d'un document InDesign selon la box du lien.

/**
 * This script allow you to crop all pictures of an InDesign Document
 * All images are cropped with the same dimensions of the indesign block
 * These images are set to 100% in the layout, the resize is done without resampling
 * All modification are done on a new document save on the desktop crop/timestamp/document
 * @author Bastien Eichenberger
 * @requires {@link IN}
 * @requires {@link PS}
 * @throws {Error} if a link is used more than 1 times
 * @throws {Error} if a link is not proportional
 */

//@target "indesign"
//@include "../../lib/indesign/indesign-lib.jsx"

try {
    IN.Config.init();
    /**
     * The script folder
     * @type {Folder}
     */
    var SCRIPT_FOLDER = new File($.fileName).parent;

    /**
     * The log folder
     * @type {Folder}
     */
    var LOG_FOLDER = new Folder(SCRIPT_FOLDER + '/log');

    /**
     * The result folder path
     * @type {string}
     */
    var SAVE_FOLDER_PATH = '~/Desktop/crop';

    H.Log.init(LOG_FOLDER, 0);

    // make a package, then all modification are done on the new document
    var doc = app.activeDocument;
    var new_doc_path = IN.Document.save_with_package(doc, SAVE_FOLDER_PATH);
    var new_doc = app.open(new File(new_doc_path));
    doc.close(SaveOptions.NO);

    var links_collection = new_doc.links;


    for (var i = 0; i < links_collection.length; i++) {

        var current_link = links_collection[i];

        if (is_link_valid(current_link, new_doc)) {

            // link is [EPS, Image, ImportedPage | PDF | PICT | WMF]
            // link.parent = graphic
            // graphic.parent = PageItem
            var page_item = current_link.parent.parent;
            var graphic = current_link.parent;

            // get the resolution
            var resolution = graphic.effectivePpi[0];

            // The bounds of the Graphic excluding the stroke width, in the format [y1, x1, y2, x2], which give the coordinates of the top-left and bottom-right corners of the bounding box.
            var top_left_corner = {};
            top_left_corner.x = page_item.geometricBounds[1] - graphic.geometricBounds[1];
            top_left_corner.y = page_item.geometricBounds[0] - graphic.geometricBounds[0];

            // The bounds of the Graphic excluding the stroke width, in the format [y1, x1, y2, x2], which give the coordinates of the top-left and bottom-right corners of the bounding box.
            var bottom_right_corner = {};
            bottom_right_corner.x = top_left_corner.x + (page_item.geometricBounds[3] - page_item.geometricBounds[1]);
            bottom_right_corner.y = top_left_corner.y + (page_item.geometricBounds[2] - page_item.geometricBounds[0]);

            // crop this link
            H.Gateway.call_app(
                'photoshop',
                crop_in_photoshop,
                [SCRIPT_FOLDER, current_link.filePath, top_left_corner, bottom_right_corner, graphic.absoluteHorizontalScale, graphic.absoluteVerticalScale, resolution, 'ResampleMethod.NONE'],
                100
            );

            set_proportional_link_to_100(current_link);
            H.Log.info('the link ' + current_link.name + 'was correctly cropped');
        }
    }
    new_doc.save();
    alert('The script is finished');
}
catch (ex) {
    H.Log.errorAlert(
            'file: ' + ex.fileName +
            '\n name: ' + ex.name +
            '\n message: ' + ex.message +
            '\n line: ' + ex.line
    );
}
finally {
    IN.Application.restore();
}


/**
 * Function to check if a link is valid, only [TIFF, JPEG, PNG, PSD] are edited
 * @param {Link} link_item
 * @param {Document} new_doc
 * @returns {boolean} true if the link is valid
 * @throws {Error} if a link is used more than 1 times
 * @throws {Error} if a link is not proportional
 */
function is_link_valid (link_item, new_doc) {

    // check if a link is used more than 1 times
    if (IN.Link.count(link_item, new_doc) > 1) {
        throw {
            name: 'Error',
            message: 'A link cannot been used more than 1 times',
            fileName: $.fileName,
            line: $.line
        };
    }

    // check if a link is not proportional
    if (!IN.Link.is_proportional(link_item)) {
        throw {
            name: 'Error',
            message: 'A link is not proportional',
            fileName: $.fileName,
            line: $.line
        };
    }
    // only TIFF, JPEG, PNG AND PSD are edited
    if (link_item.linkType === 'TIFF' || link_item.linkType === 'JPEG'
        || link_item.linkType === 'Portable Network Graphics (PNG)' || link_item.linkType === 'Photoshop') {
        return true;
    }

    return false;
}


/**
 * Function to crop an image in Photoshop
 * @param file_path
 * @param top_left_corner
 * @param bottom_right_corner
 * @param horizontal_scale
 * @param vertical_scale
 * @param resolution
 * @param resample_method
 * @requires {@link PS}
 */
function crop_in_photoshop (SCRIPT_FOLDER, file_path, top_left_corner, bottom_right_corner, horizontal_scale, vertical_scale, resolution, resample_method) {
    $.evalFile(SCRIPT_FOLDER + "/../lib/photoshop/photoshop-lib.jsx");

    try {
        PS.Config.init();
        PS.Application.open(file_path);
        var ps_doc = app.activeDocument;

        PS.Document.Resize.resampling(ps_doc, file_path, horizontal_scale, vertical_scale, resolution, resample_method);

        bounds = [top_left_corner.x, top_left_corner.y, bottom_right_corner.x, bottom_right_corner.y];
        ps_doc.crop(bounds);
        ps_doc.save();
        ps_doc.close();
    }
    catch (ex) {
        throw {
            name: 'Error',
            message: ex.message,
            fileName: $.fileName,
            lineNumber: $.line
        };
    }
    finally{
        PS.Application.restore();
    }
}


/**
 * Function to set a link to 100% in indesign and put the x y coordinates to 0,0
 * @param link_item
 */
function set_proportional_link_to_100 (link_item) {

    var status = link_item.status;

    if (status === LinkStatus.LINK_OUT_OF_DATE) {
        link_item.update();
    }

    if (link_item.status != LinkStatus.NORMAL) {
        throw {
            name: 'Error',
            message: 'The link ' + link_item.name + ' as an wrong status',
            fileName: $.fileName,
            line: $.line
        };
    }

    var graphic = link_item.parent;
    var page_item = current_link.parent.parent;
    // [y1, x1, y2, x2], get the x,y coordinate of the block then put the graphic to 0,0
    var x = page_item.geometricBounds[1];
    var y = page_item.geometricBounds[0];
    graphic.move([x, y]);

    // set to 100%
    graphic.absoluteHorizontalScale = 100;
    graphic.absoluteVerticalScale = 100;

}

Tests

ExtendScript-library dispose d'un module Node.js qui permet d'exécuter des scripts JavaScript dans les différents logiciels. Pour utiliser ce module, il faut installer Node.js. Pour plus de détails, lisez la documentation sur la page GitHub au chapitre «Install Node.js».

Le module de test fonctionne avec Grunt.js qui permet d'exécuter chaque test à la suite. Les tests sont envoyés vers les différents logiciels via AppleScript (Mac) et Visual Basic (Windows).

Si le résultat est une valeur, celle-ci est écrite dans le fichier results/tests.xml et s'il s'agit d'un document PSD par exemple, celui-ci est ajouté directement dans le dossier results.

Pour finir, les différents tests sont faits avec node-unit qui permet de valider l'existence des différents fichiers ou valeurs.

schéma du module de tests
schéma du module de tests

Le mot de la fin

Comme vous l'aurez compris, ce cours d'introduction sur l'ExtendScript touche à sa fin. Une version plus complète sera mise à disposition des utilisateurs lorsque la librairie sera plus aboutie.

Le projet ExtendScript-library cherche activement des contributeurs donc n'hésitez à prendre contact si vous souhaitez apporter quelque chose au projet ! :D

Exemple de certificat de réussite
Exemple de certificat de réussite