Skip to main content
m1b
Community Expert
Community Expert
January 12, 2021
Answered

Scripting Live Effects

  • January 12, 2021
  • 9 replies
  • 6030 views

I've made a suite of javascript convenience functions to make it much easier to apply Live Effects in your scripts.

 

Get them here: AI Live Effects Functions. (Note that some of the functions don't work as expected: see my notes.)

 

I've had a lot of help from others on this site. Most of the information here has been compiled from knowledge shared by several other forum users, especially @CarlosCanto (with initial help from Adobe's Sanjay K) and @Silly-V.

 

Let me know what you think. - Mark

 

Edit: My original post was explaining the underlying process to work out the structure of the applyEffect XML parameter, but now that I've made these functions, most people won't need to worry about all that detail and should find the functions much easier to use. (For anyone interested, I've pasted the original notes in later as a reply.)

This topic has been closed for replies.
Correct answer m1b

Get them here: AI Live Effects Functions. (Note that some of the functions don't work as expected: see my notes.)

9 replies

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
June 19, 2024

I share your frustration.. and the feeling that there must be a way. Alas, I couldn't find it.

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
June 14, 2024

I had the same hope as you, but I did not find a way to achieve it. Applying a graphic style or executing an Action are the only ways I know of. Neither are ideal.

- Mark

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
June 11, 2024

Hi @ Synder37927574fian, I have fixed the link now. The old one went dead when github moved gists. You can examine the tool, but bear in mind that it isn't a proper XML parser; I just use a bunch of RegExp to parse what I needed. - Mark

m1b
Community Expert
m1bCommunity ExpertAuthorCorrect answer
Community Expert
February 4, 2021

Get them here: AI Live Effects Functions. (Note that some of the functions don't work as expected: see my notes.)

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
February 4, 2021

For anyone interested in the workings out..

 

How to derive the XML string parameter:

1. Make a blank document (in the color space you want), draw a square and apply the live effect manually, and configure it the way you want for your script.

 

2. Save an .ai document without compression

 

 

 

 

 

 

 

 

 

 

3. Open that uncompressed .ai document in a text editor

It looks like an impossible mess... at first.

 

 

 

 

4. Search for words that might match the Live Effect name

 

eg, 'roughen' or, alternatively, you can search for "(Adobe".

Cycle through the found instances until you see the one most promising.

 

 

 

5. Select text from /BasicFilter down until the end of the property lines 

 

 

 

 

 

 

6. Use this quick-and-dirty helper tool I made.

7. Paste the text copied from step 4 into the entry field 

 

 

 

8. The helper tool will parse that text and display the result on the right. Click "Copy XML to clipboard".

 

9. Paste the xml into your script! Your script might look something like this:

 

 

 

 

 

 

var xml = '<yourPastedStringHere>';
var item = app.activeDocument.selection[0];
item.applyEffect(xml);

 

 

 

Done!


Further Info

Here are some cheat sheets graphics:

 

 

 

There's still A LOT we don't know, so please share if you discover something, or even better add to the github repository!

- Mark

 

EDIT 2024-06-11: updated dead link to old bl.ocks.org gist.

itsBenMason
Inspiring
January 25, 2021

Thank you for such a detailed explanation and repo for us to look at.

I have always wondered about the Live Effects but just pretended they did not exist for the most part!

 

Will have a read through the repo and see what I can learn 🙂

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
January 25, 2021

Thanks! I hope its useful. Really I'm just organising all the work others had previously done and shared to make it easier (I hope *much* easier) for people to use in their own scripts. If I've succeeded, it means I've pretty much made the topic of this post obsolete. I mean: I don't expect people to parse raw .ai file markup. Most people should try the functions first! So I'll need to edit the intial post.

 

(By the way, I've just added multi-item support.)

CarlosCanto
Community Expert
Community Expert
January 12, 2021

this is awesome Mark! thanks for putting together such comprehensive tutorial. Love the helper tool!

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
January 12, 2021

Thank you! I mean... thank you guys for passing on what you learned about this function. I'd honestly never come across it before. I really got into it! 🙂 Actually, I don't think I've got it out of my system yet either, lol. I'm talking to femkeblanco about making a set of functions to wrap all this xml stuff in, so no-one needs to reinvent the wheel.

CarlosCanto
Community Expert
Community Expert
January 13, 2021

that is a wonderful idea!!

Silly-V
Legend
January 12, 2021

Wow, what an in-depth tutorial! Great work, this is going to be one of the legendary posts.

Also, thank you for building that tool. It's super-useful and will help anyone get to using live effect strings in no-time!

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
January 12, 2021

Thanks! I did notice that when I searched the forum for Live effect script solutions there was quite a lot, but nowhere was a central topic, and it required careful reading of many threads to glean the knowledge. Yeah, I found the helper tool useful too. It isn't complete, as I'm sure there will be dictionary entries that it doesn't handle. Although it's already pretty good at parsing the basics. Let me know when it fails and I will update.

femkeblanco
Legend
January 12, 2021

Thanks.  This is great, particularly the helper tool.  I would like, when I have time, to go through the effects systematically and try each.  Thanks again.  

m1b
Community Expert
m1bCommunity ExpertAuthor
Community Expert
January 12, 2021

Here's a few example functions, what do you think? I like them. I'll make some more, when I next get time. Feel free to make some too if you'd like!

 

 

function LE_Feather(item, radius) {
    radius = radius || 10;
    var xml = '<LiveEffect name="Adobe Fuzzy Mask"><Dict data="R Radius #Radius# "/></LiveEffect>'
        .replace(/#Radius#/, radius);
    item.applyEffect(xml);
}

function LE_Offset(item, offset, joinType, miterlimit) {
    // joinTypes: 0 = Round, 1 = Bevel , 2 = Miter
    offset = offset || 10;
    joinType = joinType || 1;
    miterlimit = miterlimit || 2;
    var xml = '<LiveEffect name="Adobe Offset Path"><Dict data="R mlim #mLim# R ofst #ofst# I jntp #jntp# "/></LiveEffect>'
        .replace(/#ofst#/, offset)
        .replace(/#jntp#/, joinType)
        .replace(/#mLim#/, miterlimit);
    item.applyEffect(xml);
}

function LE_Rasterize(item, dpi, colorType, transparentBackground, paddingInPts, antialiasType, clipMask) {
    // colorTypes: 0 = RGB, 1 = CMYK, 2 = Grayscale, 3 = Bitmap
    // antialiasTypes: 0 = none, 1 = art optimizes, 2 = type optimized
    offset = dpi || 72;
    colorType = colorType || 0;
    transparentBackground = transparentBackground || true;
    paddingInPts = paddingInPts || 0;
    antialiasType = antialiasType || 0;
    clipMask = clipMask || 0;
    var xml = '<LiveEffect name="Adobe Rasterize"><Dict data="I colr #colr# B alis #alis# I dpi. #dpi# B mask #mask# R padd #padd# I optn #optn# "/></LiveEffect>'
        .replace(/#colr#/, colorType + (transparentBackground ? 4 : 0))
        .replace(/#alis#/, antialiasType > 0 ? 1 : 0)
        .replace(/#dpi#/, Math.round(dpi))
        .replace(/#mask#/, clipMask ? 1 : 0)
        .replace(/#padd#/, paddingInPts)
        .replace(/#optn#/, antialiasType == 2 ? 16 : 0);
    item.applyEffect(xml);
}

 

 

Silly-V
Legend
January 14, 2021

I've got the bit between my teeth! I'm thinking of doing one filter every morning before work.

 

I've made a 'final' tweak to the design of the functions. I wanted to make the default values neater (this problem goes away in newer versions of javascript) so I've made a helper function called defaults. It's a shame to have an extra function required, but after a bit of pondering I think it's nicer. Anyway the defaults function can be put inside the individual function if user prefers. Feel free to show me a better way of course.

 

Here's two examples (and defaults function at end):

 

 

function LE_Scribble(item, angle, pathOverlap, pathOverlapVariation, strokeWidth, curviness, curvinessVariation, spacing, spacingVariation) {
    try {
        eval(
            defaults([
                'angle', 30,
                'pathOverlap', 0,
                'pathOverlapVariation', 5,
                'strokeWidth', 3,
                'curviness', 5,
                'curvinessVariation', 1,
                'spacing', 5,
                'spacingVariation', 0.5
            ])
        );
        if (item == undefined) throw new Error('LE_Scribble: no item');
        xml = '<LiveEffect name="Adobe Scribble Fill"><Dict data="R Spacing #7 R Angle #1 R Scribbliness #5 R StrokeWidth #4 R EdgeOverlap #2 R ScribbleVariation #6 R SpacingVariation #8 R EdgeOverlapVariation #3"/></LiveEffect>'
            .replace(/#1/, angle)
            .replace(/#2/, pathOverlap)
            .replace(/#3/, pathOverlapVariation)
            .replace(/#4/, strokeWidth)
            .replace(/#5/, curviness / 100)
            .replace(/#6/, curvinessVariation / 100)
            .replace(/#7/, spacing)
            .replace(/#8/, spacingVariation);
        item.applyEffect(xml);
    } catch (error) {
        alert(error);
    }
}

function LE_Offset(item, offset, joinType, miterlimit) {
    // joinTypes: 0 = Round, 1 = Bevel , 2 = Miter
    try {
        eval(
            defaults([
                'offset', 10,
                'joinType', 2,
                'miterlimit', 4
            ])
        )
        if (item == undefined) throw new Error('LE_Offset: no item');
        var xml = '<LiveEffect name="Adobe Offset Path"><Dict data="R mlim #3 R ofst #1 I jntp #2 "/></LiveEffect>'
            .replace(/#1/, offset)
            .replace(/#2/, joinType)
            .replace(/#3/, miterlimit);
        item.applyEffect(xml);
    } catch (error) {
        alert(error);
    }
}

function defaults(arr) {
    var defaults = '';
    for (var i = 0; i < arr.length; i += 2) {
        defaults += (arr[i] + '=' + arr[i] + '!=undefined?' + arr[i] + ':' + arr[i + 1] + ';');
    }
    return defaults;
}

 

Edit: removed extraneous parts of code examples, and added a line I forgot.

 


Here is my take on the default function. Using some regex we can get the argument names and going through the arguments object we can get the values. If an object is supplied which has some arguments inside with default values and if the actual argument is not defined, the object is left alone - otherwise an object's default value is overriden by the value of the argument.

In the first function the whole default-setting procedure is just in the function.

In the 2nd function, it uses a helper function that takes in a default object as well as the parent function and its arguments as the next parameters. This is very much like your approach, but it uses no string-building or eval.

 

#target illustrator
function test () {

	function getCustomString (itemArg, userName, email, phone) {
		var paramObj = {
			userName : "Bob",
			email : "bob@bob.com",
			phone : null
		};
		var argNames = getCustomString.toString().match(/function\s[a-z0-9_]+\s?\((([a-z0-9_]\s?,?\s?)+)\)/i)[1].split(/\s?,\s?/g);
		
		var thisArg, thisArgName;
		for (var i = 0; i < arguments.length; i++) {
			thisArg = arguments[i];
			thisArgName = argNames[i];
			if (!(thisArgName in paramObj)) {
				continue;
			}
			if (typeof(thisArg) != "undefined") {
				paramObj[thisArgName] = arguments[i];
			}
		}
		var templateString = "User name: #userName#\nEmail: #email#\nPhone: #phone#";
		for (var all in paramObj) {
			templateString = templateString.replace("#" + all + "#", paramObj[all]);
		}
		return templateString;
	}

	alert(getCustomString("SomeItem", "Suzy", undefined, "000.222.2323"));

	//==============================================================================================//

	function setDefaults (defaultObj, func, args) {
		var argNames = func.toString().match(/function\s[a-z0-9_]+\s?\((([a-z0-9_]\s?,?\s?)+)\)/i)[1].split(/\s?,\s?/g);
		var thisArg, thisArgName;
		for (var i = 0; i < args.length; i++) {
			thisArg = args[i];
			thisArgName = argNames[i];
			if (!(thisArgName in defaultObj)) {
				continue;
			}
			if (typeof(thisArg) != "undefined") {
				defaultObj[thisArgName] = args[i];
			}
		}
		return defaultObj;
	}

	function getCustomString_2 (itemArg, userName, email, phone) {
		var paramObj = setDefaults(
			{
				userName : "Bob",
				email : "bob@bob.com",
				phone : null
			},
			arguments.callee,
			arguments
		);

		var templateString = "User name: #userName#\nEmail: #email#\nPhone: #phone#";
		for (var all in paramObj) {
			templateString = templateString.replace("#" + all + "#", paramObj[all]);
		}
		return templateString;
	}

	alert(getCustomString_2("SomeItem", "Suzy", undefined, "000.222.2323"));

};
test();