Skip to content


Viewing US only content while traveling abroad

Living in the US there are certain digital comforts we take for granted. Being disconnected from our digital lives can lead to internet withdrawal, mild depression, stomach aches, heart palpitations, headaches, etc etc.. When traveling abroad, I always recommend getting a prepaid sim card for your smart phone and/or tablet. Mobile maps and GPS are amazing when you really have no idea where you are going! Then at your home base, be it a friend’s home, hotel, hostel, rental condo, etc, always make sure you have some type of high speed internet for your digital upkeep on Facebook, Twitter, Instagram, Pinterest, _____ (fill in your favorite here).

However, even with all this effort, you may find yourself unable to access certain creature comforts like Netflix, Amazon Video, CrunchyRoll, etc.. Because of the complex world of content licensing, the high availability and affordability of online video we are used to in the USA is very lacking in the rest of the world. Luckily, there are technological ways around this. If you wish to access USA only sites you will need to use a proxy or proxy server. VPN providers like StrongVPN and Astrill, bypass these region limitations by routing all your traffic through a US server. This is the same basic idea typically used in most home internet routers. The website you are looking at will see a computer IP address originating from within the US, and you will be able to enjoy all your video creature comforts from your home away from home!

For the tech savvy, it can be more cost effective to roll your own. I found this easy tutorial on setting up your own personal VPN server on Amazon EC2. Very well laid out tutorial, so there isn’t much for me to add. I run my VPN only when I need it. I then used this tutorial to setup the VPN client correctly on my MacBook Pro. Once setup, its pretty straight forward to use and maintain. I turn mine on when I need it to maximize security and minimize costs. Also, I didn’t use the Dynamic DNS service, and I am connecting from a dynamic IP, so I have to modify my client settings and also the EC2 firewall settings (as well as start/stop the server each time). This is a little more trouble each time, but well within reason.

If rolling your own sounds complicated, sign up for a VPN service. They are only $5-10 (think 3 month min contract). Its great to be in the USA.. Even if its only virtually!

Posted in Mac.

Dynamic update of Yii CGridView

I’ve been exploring the Yii framework for about a week now and I have been very pleased with how extensible the framework and supporting libraries are. My most recent task was to add dynamic AJAX update functionality to a table of information. In Yii, a tabular arrangement of data is constructed using zii.widgets.grid.CGridView. For example:

$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
    'id' => 'item-grid',
    'columns'=>array(
        'title',
        'category.name',
        'content:html',
        array(           
            'name'=>'create_time',
            'value'=>'date("M j, Y", $data->create_time)',
        ),
        array(
            'name'=>'authorName',
            'value'=>'$data->author->username',
        ),
        array(
            'class'=>'CButtonColumn',
        ),
    ),
));

Yii supports dynamic AJAX updates using the yiiGridView api.

$('#item-grid a.submi').live('click',function(){
  $.fn.yiiGridView.update('item-grid', {
           type:'POST',
           url:$(this).attr('href'),
           data: {update:"now"},
           success:function() {
             $.fn.yiiGridView.update('item-grid');
           }
  });
  return false;
})

The update() call issues a new server request passing the extra data variables. The returned results are then parsed and the table item-grid is updated without too much fuss.

The interesting thing about this request, is there are actually two update() calls. The outer call uses POST, and then the inner update() call uses an implicit GET. After monitoring my server logs, it turns out this AJAX call is actually hitting my server with a total of three calls. A POST request, then two GET request, and one POST. After I noticed the duplication, I reduced this to the following:

$('#item-grid a.submi').live('click',function(){
  $.fn.yiiGridView.update('item-grid', {
           type:'POST',
           url:$(this).attr('href'),
           data: {update:"now"},
  });
  return false;
})

I posted a message here on the Yii forums regarding this issue. Time to wait and see if there is a rhyme or reason behind the seemingly duplicate calls..

Posted in PHP. Tagged with .

Out of the Stone Age: Crypt for password storage

The stone age in the Web 2.0 era really is anything more than 6 months old. While going through the Yii blog tutorial, I was caught off guard reading the following line of code (names changed for clarity):

if ($passwordHash === crypt($password, $passwordHash))...}

My last foray into passwords relied on comparing 32 character md5 hashes using:

md5($password)

After a quick look at the php manual on crypt, I found it a little odd that the hash of the password was both (a) the salt used for the password, but then also (b) the resulting output of the hash! Why does this work? As discussed here, the return value of crypt is the string concatenation of the salt value and the hashed string. This concatenated value can then be stored in the database, without compromising password integrity. The benefit of a salted hash of course, is the added complexity of cracking the password. These days there are md5 hash dictionaries online that you can quickly lookup the value of any md5 hashed string. A dictionary attack for these salted hashes are probably few and far between. A password hacker would have to rely on brute force methods to crack a password… And this would only provide them with one password, since each password relies on a different salt..

crypt('EgzamplPassword', '<strong>$2a$10$1qAz2wSx3eDc4rFv5tGb5t</strong>')
    >> '<strong>$2a$10$1qAz2wSx3eDc4rFv5t</strong>Gb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
crypt('EgzamplPassword', '<strong>$2a$10$1qAz2wSx3eDc4rFv5t</strong>Gb5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi')
    >> '<strong>$2a$10$1qAz2wSx3eDc4rFv5t</strong>Gb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'

If you look at the output of the crypt function, the inputed salt is included in the output. Since crypt ignores the excess characters in the salt string, using crypt to store and compare password values is pretty sweet. If you are having problems with crypt (or any function for that matter), always remember to RTM (Read the Manual). If the output for crypt() is less than 13 characters, you have an error. It could be a bad salt or an unsupported php/hash library. Make sure you are running php 5.3.0 or newer and use a hash salt generator.

Posted in PHP. Tagged with , , , .

Yii Framework

I’ve decided to take the plunge and start learning how to use a PHP MVC framework. Based on my years of experience in php I am very familiar with Zend, and its complete family of products (including AWS AMIs). I had originally planned on using Zend as the basis for my project, but as always I did a quick google search to see if anything else out there seems interesting. I found this article comparing Zend, Code Ignitor, and a framework I had never heard about called Yii Framework. The authors discussion regarding Yii definitely perked my interest:

Finally, I come to Yii.  I guess the thing with Yii is that it fits in nicely right between Zend Framework and Code Igniter.  It’s probably not the simplest to learn for a PHP or OO novice, but for an experienced OO developer it’s a breeze.  It has great documentation and if need be you can always inspect the code yourself.  I actually spend quite some time developing in ASP.NET and Yii feels very very similar.  It has used some of the best concepts from ASP.NET and from many other frameworks and because it’s strict PHP5 and strict OO (without crazy hacks that are typical of inexperienced PHP developers) it’s very very easy to learn and understand as long as you understand the basic principles of OOP even if you have little experience in PHP.  The thing with Yii is that it’s more comprehensive than CodeIgniter and better structured, yet smaller and more robust than Zend Framework, requiring a hell of a lot less code to get stuff done, while giving you all the same features and more (e.g. Code generation with Gii).

I also found an article titled The Case for Yii vs Zend which provided the following tidbit:

We have worked with both Yii and Zend in the past, while Zend has been an excellent framework, it has strong commercial ties which increase the cost of application development due to licensed IDE’s, server testing environments and licensed developers. Yii on the other hand has the same features but is completely open in nature and as a result has some very talented people working for it – passionately.

As a big fan of FOSS software, this tidbit was enough reason for me to give Yii a try.

Fastforward 3 days later, and I have completed the quickstart and the build a blog walkthrough. From my days working on Java MVC, I definitely feel right at home using Yii. The separation of the view and model are exactly what I have been wanting to use in php. I still need to setup a proper IDE and set of reference materials, but I believeI have a pretty good start now. I also took a look at Yii extensions looking for a user manager. I was surprised to see 3+ different user managers… All of which haven’t been updated in the last year. We will see how they look this weekend..

Posted in PHP. Tagged with , , .

Dropbox Annoyances

A while back I was caught in the dilemma of finding one cloud storage to store them all. On my list of potential services were Dropbox, SugarSync, CrashPlan, and SpiderOak. In the end I decided to sign up for SugarSync for my day to day collaboration needs and CrashPlan for my long term backup solution. It seemed the goals of collaboration and sharing versus long term storage and archival were not adequate addressed by a single solution.

Fast forward to today, and I have been using Dropbox heavily for day to day uses and have a background CrashPlan running here and there storing that emergency backup just in case all else fails. While I originally turned away from Dropbox because of (1) its single folder setup (2) Lack of selective syncing and (3) less than ideal encryption and privacy policies, I came back because of its amazingly simple and straight forward installation, use, and ability to share.

Yet, today was the day all that changed.

Cloud based storage systems like Dropbox and SugarSync follow a single user model. Sharing and collaboration is done through sharing folders. While at first this struck me as limiting for collaboration, I now consider this functionality simple and ingenious. Individual users have their own private data storage, their own private password, etc etc.. A shared folder is pushed from the author/maintainer/controller, and can be easily monitored and controlled as such.

The biggest problem with Dropbox for collaboration is that Shared Folders count against the quota for each person involved. So if you share a 1 gig folder to a friend w/a free DropBox account, you are taking 1 gig out of his 2 gigs. Dropbox says this is to prevent people gaming the system for free space. So, lets say I go Pro and pay for the space. I still won’t be able to share any folder that exceeds the free space/quota of who I want to share with. The only way to have communal space is through Dropbox for Teams— Which start at $795!

A reasonable solutions to this issue would for Dropbox to have Shared Folders not count against the quotas of the recipients ASSUMING you are Pro. You are paying for the space after all. Whats wrong with letting others access your files? Short of just sharing a file by URL. Think about it this way.. Let say you want to have cloud storage for your small business. You have 200MB of confidential data and 20GB of communal data. Its not possible to have a master account that contains both confidential and communal data with administrator rights. Yes it is possible to put the larger communal data in shared account, and share it with a more private confidential account— but security controls is bottom up rather than top down in this case! Until I can afford the jump from $100 a year to $795 a year for Dropbox for Teams, it looks like SugarSync has made a convert out of me.

 

Posted in Ramblings. Tagged with , , , , , .

Netsuite Process Integration for Product Images

I have been actively using and developing on the NetSuite platform for over a year now, yet no matter how much I accomplish, I  am still continually disappointed at how unnecessarily complicated tasks can be in the Netsuite ERP/CRM framework. Features we take for granted in consumer level products, and the simplicity and relative ease they can be done, are relegated to the realm of additional customizations and third party integrators. My most recent task has been to streamline the process to load product images in NetSuite.

The standard process follows two steps:

  1. Upload Images into the NetSuite File Cabinet
    1. Individually upload full size image and thumb nail image
    2. Bulk upload using a zip file
  2. Attach these Images to the Item record.
    1. Display Image
    2. Thumbnail Image

In our NetSuite deployment, we had SuiteCommerce provide us a MultiImage bundle for our website. In our negotiations with SuiteCommerce it seemed this product was complete, but in actuality, the implementation and process flow leaves much to be desired. Specifically, for each image, it is necessary to have three images:

  • Small – Mini thumbnail on item detail page
  • Medium – standard image displayed on page
  • Large – Zoomed Image

In the simplest cases, to attach a single image to an item, there are a total of 4 image files to link (The three for MultiImage and the thumbnail.) The entire process uploading and linking the images takes approximately 1-2 minutes. While this sounds reasonable if you only have several hundred images, it is grossly inefficient since we are dealing with roughly 10,000 products.

Technology should simplify life and provide the simplest and fastest way of getting this job done. To understand the role technology should play in this process, I examined our complete process for this task.

Step 0: Product is photographed and edited.
Step 1: Image is prepared for upload

  • a. Image sizes
  • b. watermarks

Step 2: Image is uploaded to Amazon S3 Server
Step 3:Amazon S3 Image Link is loaded as a NetSuite File
Step 4: Images are attached to the item record.

Looking at this list, only Step 0 and part of Step 1 really requires human intervention. The rest should be automated. An ideal process for this task should be:

Step 0: Product is photographed and edited. (User)
Step 1: Image is marked for upload. (User)
Step 2: Image is processed and linked. (Automated)

This process flow makes sense. It is the way it should be. To achieve this process I developed a set of scripts coupled with standard operating guidelines.

Step 0: Image is photographed and edited.

This step is really outside of our scope. For our discussions, this is considered a required step with minimal room for improvement in this scope. To facilitate future steps, files our assumed to be saved using the 5 digit product SKU we use in NetSuite.

Step 1: Image is marked for upload.

The fastest and simplistic way of doing this is simply moving a file from one folder to another on your computer. To achieve this idealistic goal, I turned to DropBox, with its ease of installation and use. In my model, the DropBox folder is an interim repository that can be easily interfaced in the automation process. Processing an image becomes simply copying the file to the DropBox folder (drag and drop).

Step 2. Image is Processed and Linked.

Everything in this step is handled but our company’s Amazon EC2 Server.

  1. Server retrieves the file out of DropBox
  2. Image is processed, creating the appropriate sizes with watermarks.
  3. Processed images are uploaded to Amazon S3 which is then replicated onto Amazon CloudFront
  4. Suing SuiteTalk, Images are added into the NetSuite file cabinet (External URL).
  5. Images are linked to the Item record based on their 5 digit SKU and image suffix.

Following this process flow, my completed solution to this task takes roughly 5 seconds to drag and drop the image into the DropBox folder. The images are processed and uploaded into NetSuite every 5 minutes. Most importantly the level of complexity for the end user is significantly reduced, with the only real potential place for error being an incorrectly named image.

Stay tuned and I will provide some implementation details.

Posted in NetSuite. Tagged with , , , , , .

InDesign Tip 3. Find/Grep after a Find/Grep (Relative searching) in JavaScript

My mission over the past few days was to take a row of tabular data in our catalog of the form:

[year]\t[part#]\t[description]\t[uom]\t[price]

then retrieve the part# and price. My eventual goal was then to execute a foreign script that checks/updates the price. So my first inclination was to do a Grep search for the part # (which is of the form “[0-9]{5}”), then from this point in the story/textframe, do a search for the price. I found this discussion on Find text from current insertion point – indesign cs3 javascript in the Adobe forums related to this issue.

As a baseline, I am using the following code for my initial find/grep:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myDoc = app.activeDocument;
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
 
//Set the find options. 
app.findChangeGrepOptions.includeFootnotes = false; 
app.findChangeGrepOptions.includeHiddenLayers = false; 
app.findChangeGrepOptions.includeLockedLayersForFind = false; 
app.findChangeGrepOptions.includeLockedStoriesForFind = false; 
app.findChangeGrepOptions.includeMasterPages = false;
 
app.findGrepPreferences.findWhat =  "[0-9]{5}";
app.findGrepPreferences.appliedParagraphStyle = myDoc.paragraphStyles.item("MyParts");
var myResults = myDoc.findGrep();
for (var i = 0; i < myResults.length; i++)
{
    var result= myResults[i];
}

In the discussion there is a proposed inner find/grep in the for loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var myDoc = app.activeDocument;
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
 
//Set the find options. 
app.findChangeGrepOptions.includeFootnotes = false; 
app.findChangeGrepOptions.includeHiddenLayers = false; 
app.findChangeGrepOptions.includeLockedLayersForFind = false; 
app.findChangeGrepOptions.includeLockedStoriesForFind = false; 
app.findChangeGrepOptions.includeMasterPages = false;
 
app.findGrepPreferences.findWhat =  "[0-9]{5}";
app.findGrepPreferences.appliedParagraphStyle = myDoc.paragraphStyles.item("MyParts");
var myResults = myDoc.findGrep();
for (var i = 0; i < myResults.length; i++)
{
    var result= myResults[i];
    app.findGrepPreferences = NothingEnum.nothing; 
    app.changeGrepPreferences = NothingEnum.nothing;
 
    app.findGrepPreferences.findWhat =  "[0-9]+.[0-9]{2}";
    app.findGrepPreferences.appliedParagraphStyle = myDoc.paragraphStyles.item("MyParts");
    var myResults2 = myDoc.findGrep();
}

The problem with this is that find/grep when executed in javascript is always executed absolutely in the document, meaning from the top/beginning and not with respect to the cursor or selection. So the inner find/grep will start from the top of the document. A proposed suggestion in the discussion is to ignore all results in the second find/grep that occur before our current index. The value we are looking for would probably the smallest match with a index greater than our current insertion point.

Sounds reasonable in theory, but that is a lot of computation, O(n * n).. So I figure there has to be an easier way. Keep in mind I have only been programming in InDesign for coming a week here, and all of my google searches are coming up sparse and dry. Given the format of my data,

[year]\t[part#]\t[description]\t[uom]\t[price]

it would be ideal if I could simply parse the entire row. My initial dilemma concerned the cases where the description could be multiline (with the price on a later line). These cases can be rather convoluted so I was hoping that a double find/grep would take me where I needed to go.

The key to my solution I realized is to have a single grep that gets me everything I need. If my result contains all the data I need, it would eliminate the need for nested find/grep. To achieve this model, I decided to break my find/grep searches based on the number of rows of data entry. For example, a single line entry of the form:

[year]\t[part#]\t[description]\t[uom]\t[price]

while a multiline would be:

[year]\t[part#]\t[description]
\t\t[description]\t\t[price]

where [description] is basically the regex [^\r\t]*. With this crucial insight in hand, my solution simplifies to the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myDoc = app.activeDocument;
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
//Set the find options. 
app.findChangeGrepOptions.includeFootnotes = false; 
app.findChangeGrepOptions.includeHiddenLayers = false; 
app.findChangeGrepOptions.includeLockedLayersForFind = false; 
app.findChangeGrepOptions.includeLockedStoriesForFind = false; 
app.findChangeGrepOptions.includeMasterPages = false;
 
app.findGrepPreferences.findWhat =  ".*\t[0-9]{5}\t[^\t\r]*\t[^\t\r]*\t[0-9]+\.([0-9]{2})";
app.findGrepPreferences.appliedParagraphStyle = myDoc.paragraphStyles.item("MyParts");
var myResults = myDoc.findGrep();
for (var i = 0; i < myResults.length; i++)
{
    var myResult = myResults[i];
}

At this point I’m still working on what the correct way to retrieve the (part #, price) pairs I am working on. Since I still have not completely understood the object model and how to use insertion points, I am converting the data to plain text and extracting the values I need.

   var num = myResults[i].contents.match(/[^\t\r]*\t[0-9]{5}\t/)[1];
   var len = myResults[i].words.length;
   var priceWord = myResults[i].words[len-1];
   var price = priceWord.contents;

My goal is to perform manipulations on the price (priceWord). So with this work around I have the algorithm template for exactly what I am looking to do. Of course I have to repeat the find/grep for the multiline cases, but that is the same same.

Posted in InDesign. Tagged with , , .

InDesign Tip 2. Removing Empty Frames

Perusing an InDesign discussion I found recommendations for a better InDesign scripting API: http://jongware.mit.edu/idcs6js/ For my style of development, the documentation feels cleaner and more organized. One thing I really do like is that the documentation prominently displays the class hierarchy.

With a better understand I was able to take a code snippet that removes empty frames from this discussion by a user named Harbs, and update it to fit my needs for Adobe CS6. The main difference from the original script is the sequence of API calls you use to obtain all the page items (line #6). The other thing I did was to extend the code to include all other immediate children of the class PageItem. While my focus right now is to remove empty text frames, I want to be prepared to extend the code if I encounter other types of empty frames while I’m using this script. Here is the script in its full entirety. Enjoy!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
    * Remove Empty Frames
    **/
var count = 0;
var doc = app.documents.item(0);
var pageitems = doc.allPageItems;
 
while (frame = pageitems.pop()) {
    if (frame instanceof EPSText)          {continue; }
    else if (frame instanceof FormField)   {continue; }
    else if (frame instanceof Graphic)     {continue; }
    else if (frame instanceof Group)       {continue; }
    else if (frame instanceof HtmlItem)    {continue; }
    else if (frame instanceof MediaItem)   {continue; }
    else if (frame instanceof SplineItem)  {continue; }
    else if (frame instanceof TextFrame){
        if (frame.contents == "") {
            count++;
            frame.remove();
        }
        continue;
    }
}
alert( 'Removed empty Frames: ' + count + '\n');

Posted in InDesign. Tagged with , .

SuiteScript Tip 2. Deleting Cancelled/Voided Bills

One aspect of NetSuite I find bothersome is the treatment of Cancelled/Voided Bills. A Bill can be Voided by clicking the Void button in the Bill edit mode. Similarly, a Bill with the status Pending Approval can be Cancelled if its not approved by a supervisor. Canceling/Voiding a bill zeros out the amount and prevents the bill from any further modification. For strict accounting this is a good system that always leaves a paper trail.

The problem is that sometimes we don’t always want a paper trail. Usually for us this occurs when there is an error in the original Purchase Order that trickles into the Bill. Rather than having to change two records, I find it easier to go back to the original Purchase Order and re-bill it out. To do this, you would click ‘Delete’ when editing the Bill. However, if you clicked the wrong button or reserve the permission to delete bills to supervisors, you may end up with a Cancelled/Voided bill that you can not longer modify.

For these situations, I found that a quick and dirty SuiteScript edit will easily un-cancel/void the bill. To do this, I opened a JavaScript debug window when in the view screen, and entered the following quick snippet:

1
2
3
4
var rec = nlapiLoadRecord('vendorbill', [internal id# of Bill]);
//rec.setFieldValue('total', 0);
rec.setFieldValue('status', 'Open');
nlapiSubmitField(rec);

You can find the internal id of the bill in the address bar by looking for the id=

https://system.netsuite.com/app/accounting/transactions/vendbill.nl?id=100000&whence=&e=T

While I’m not quite sure what exactly you have to do to un-Cancel/Void the bill, I have tried setting the total to 0, and also setting the Status to Open. Both of these allow the Bill to be edited again. With this technique I am able to properly remove the erroneous and either duplicated or unnecessary, and then recreate the correct Bill. The biggest benefit of Deleting the Bill is to simplify the already verbose transactions history for my Vendors.

Posted in InDesign.

Intro to InDesign Scripting: Crash & Burn Approach

One of my yearly responsibilities is to help maintain and update the catalog at John’s Mustang. Like most companies, our product catalog is designed in Adobe InDesign. I have been an avid user and advocate of InDesign since it has come out, but it wasn’t until today that I took a serious looked into Scripting in the Adobe platform. My goal this afternoon has been to investigate how to simplify and automate some of the mundane and time consuming tasks that we (usually I), end up doing each and every year.

Like more programming experts (reads idiots who don’t read manuals and guides, and prefer to skips steps), I am looking to get started and get what I want done as quickly as possible.

A quick google search for InDesign scripting brings up three links that ended up helping me out.

Adobe InDesign CS3 Scripting Tutorial: A getting started guide by Adobe. All the examples in the guide are provided with full code.

Indesign Scripting Reference: While there is not a CS6 version listed, this is the only one I could obviously find, and the material on the site has proved reasonably useful. Still open to better and more organized suggestions though.

Adobe Forums: Forum: InDesign Scripting: User groups and forums are great places to find practical applications.

Step 1. Getting Started

To begin, I opened up Adobe InDesign CS6 on my MacBook Pro and found the Scripting pane:

Windows > Utilities > Scripts

From here I was able to find the folder by right clicking on the User folder.

 

The full path is:

~/Library/Preferences/Adobe InDesign/Version 8.0/en_US/Scripts/Scripts Panel/

or more abstractly as:

/Users/<username>/Library/Preferences/Adobe InDesign/Version 8.0/<locale>/Scripts 

All InDesign scripts must be placed in this folder.

InDesign CS6 on the Mac allows users to create scripts in either AppleScript or JavaScript. For cross platform compatibility, I chose JavaScript. JavaScript files must end in the suffix .js or .jsx.

Step 2. Creating a Script

Like every Unix/Linux programmer, I have a strong preference toward console based editors, in my case Emacs or Vim. However, I eventually settled on using ExtendScript Toolkit for this project because of its tight integration with the Adobe platform. The obvious benefits of using EST are:

  • Syntax Highlighting (standard JS highlighting)
  • Code Completion
  • Integrated Debugger

To create a new JavaScript file,  go to the ExtendScript Tolkit menu and choose:

File > New JavaScript

After creating a new file, I went ahead and saved it.

File > Save As

Yes, it is kind of weird to save a file before you have done anything, but I wasn’t quite sure how the script would magically appear in InDesign. Luckily, it was as easy as it seemed. If you save the script in the appropriate directory (as discussed in Step 1.) the script will automatically appear in the User scripts in the Scripting pane.

Step 3. Creating a simple script

As cliche as it may seem, the easiest and best script to start with is Hello World. So I went back into EST and put the following typical JavaScript code in:

alert('Hello World');

After saving, I went back into InDesign and executed my script from the Scripting pane. Sure enough, the alert box popped up!

I was able to add a debug breakpoint into my script (at the alert of course) and immediately saw the debugger in action. After a bunch of tinkering, I found myself attempting to start the execution of my script from within ExtendScript Toolkit. The question arose, How exactly does EST know which Adobe application to start? One post I found provided me the answer I was looking for, namely the #target directive. So, my complete, albeit simple script is now:

1
2
#target indesign 
alert('hello World');

Step 4. Doing Something Useful

Up to this point, I’m sure everything discussed seems a bit remedial. While true, as a programmer of many languages and master of few, I find that understanding the software development environment and knowing how to do things can make all the difference.

Two years I dabbled a little in javascript for InDesign. At the time I coded everything in emacs, and without the integrated debugging facilities found in EST and reasonable API documentation, I found it very obscure trying to get anything to work right.

I have a couple target goals for learning scripting for InDesign. Most notably I would like to be able to hyperlink all 5 digit product codes in our catalog so that PDF versions of our catalog can take users directly to our website. My first step towards this goal is understanding the find/replace (and grep) model of InDesign CS6. For a simple starter script, I am looking to remove the trailing white space at the end of a text frame. After searching the web and the InDesign JavaScript guide, I can across a code sample that provided a good basis for what I needed. The only missing item was how do you reference the white space at the end of a text frame. I found one site that provided the regex

\s+\Z

where \s+ denotes 1 or more white space characters, and \Z denotes the end of the text frame. To be sure, I tested this regex using the Find/Replace GREP in InDesign. It worked like a charm! I had to stop and ask myself if it was this easy, was it really necessary to continue… And the answer was of course, Yes! I have a big fan of automation. There is simply too many small tasks to always remember to have to do. So as a software designer and engineer, simplicity is a good thing.

With the regex and script template in hand I was able to create a complete script that successfully removed all the white space at the end of all my text frames. The complete script is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
*  #1. remove trailing White space in Stories
**/
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
 
//Set the find options. 
app.findChangeGrepOptions.includeFootnotes = false; 
app.findChangeGrepOptions.includeHiddenLayers = false; 
app.findChangeGrepOptions.includeLockedLayersForFind = false; 
app.findChangeGrepOptions.includeLockedStoriesForFind = false; 
app.findChangeGrepOptions.includeMasterPages = false;
 
//Regular expression for finding an email address. 
app.findGrepPreferences.findWhat =  "\\s+\\Z";
 
app.changeGrepPreferences.changeTo = "";
var count = app.documents.item(0).findGrep().length;
app.documents.item(0).changeGrep();
 
//Clear the find/change preferences after the search. 
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
 
alert("Removing Trailing Whitespaces: " + count + "\n");

The final alert lets me know not only that the script was completed, but tells me exactly how many matches I got.

Step 5. Rinse and Repeat: A Second Script!

With this small but very useful script, I moved on to the next problem: How do I find product listings with too many columns (delineated by tabs). Unlink the previous script, there is no systematic algorithm for what should be done if the data is incorrectly formatted. For the time being, it is sufficient to know if the tabular data has an error or not. The complete code I have is as following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
    * #2. Search For columns with more than 5 columns
    **/
 
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
//Set the find options. 
app.findChangeGrepOptions.includeFootnotes = false; 
app.findChangeGrepOptions.includeHiddenLayers = false; 
app.findChangeGrepOptions.includeLockedLayersForFind = false; 
app.findChangeGrepOptions.includeLockedStoriesForFind = false; 
app.findChangeGrepOptions.includeMasterPages = false;
 
//Regular expression for finding an email address. 
app.findGrepPreferences.findWhat =  "([^\\t\\r]+\\t){3}[^\\t\\r]+";
 
//app.changeGrepPreferences.changeTo = "";
var results = app.documents.item(0).findGrep();
var count = results.length;
//Clear the find/change preferences after the search. 
app.findGrepPreferences = NothingEnum.nothing; 
app.changeGrepPreferences = NothingEnum.nothing;
 
alert("Finished Searching for Lines w/excesive tabs: " + count + '\n');

as you can see the code is nearly identical as the previous. The two differences are:

  1. The regex used: “([^\\t\\r]+\\t){3}”
  2. findGrep() was used while changeGrep() was not

The regex I’m using searches for tabular data of the format:

[data]\t[data]\t[data]\t

where [data] is considered to be any set of characters that are not a tab or new line. The {3} is syntax to denote the expression to the left will be repeated three times. They key part of this regex is the final trailing tab ‘\t’. All three column tabular data of the following format is considered correctly formatted.

[data]\t[data]\t[data]

As a test, I changed the regex to:

"([^\\t\\r]+\\t){2}"

to verify that a two column version of this regex would match the three column tabular data in the document. The scripts performed as expected.

My foray into InDesign scripting today started off slow, but has been steadily progressing both in terms of my understanding of the language and framework, but also in terms of creating productive scripts. I’m not sure what took me so long to look into all this, but I am very impressed with what I am seeing and believe I have unlocked a set of tools that can greatly improve efficiency, decrease errors, and allows our business a quicker turn around time in producing our catalog.

Posted in InDesign.