/* * Scriptographer * * This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator * http://scriptographer.org/ * * Copyright (c) 2002-2010, Juerg Lehni * http://scratchdisk.com/ * * All rights reserved. See LICENSE file for details. * * File created on 18.02.2005. */ package com.scriptographer.ai; import java.util.ArrayList; import com.scratchdisk.script.Callable; import com.scratchdisk.util.IntMap; import com.scratchdisk.util.IntegerEnumUtils; import com.scriptographer.CommitManager; import com.scriptographer.ScriptographerEngine; import com.scriptographer.ScriptographerException; import com.scriptographer.ui.MenuItem; /** * Wrapper for Illustrator's LiveEffects. Unfortunately, Illustrator is not * able to remove once created effects again until the next restart. They can be * removed from the menu but not from memory. So In order to recycle effects * with the same settings, e.g. during development, where the code often changes * but the initial settings maybe not, keep track of all existing effects and * match against those first before creating a new one. Also, when * Scriptographer is (re)loaded, the list of existing effects needs to be walked * through and added to the list of unusedEffects. This is done by calling * getUnusedEffects. * * @author lehni * * @jshide */ /* * Re: sdk: Illustrator AILiveEffect questions Datum: 23. Februar 2005 18:53:13 * GMT+01:00 * * In general, live effects should run on all the art it is given. In the first * example of running a post effect on a path, the input art is split into two * objects: one path that contains just the stroke attributes and another path * that contains just the fill color. Input art is typically split up this way * when effects are involved. * * If the effect is dragged before any of the fill/stroke layers in the * appearance palette, then the input path the effect will see will just be one * path, since it has not been split up yet. However, the input path will * contain no paint, since it has not gone through the fill/stroke layers yet. * * In the example of running a post effect on a group with two paths, the input * art will be split up again in order to go through the fill/stroke layers and * the "Contents" layer. Thus, you will see three copies of the group: one that * may just be filled, one that may just be stroked, and one that contains the * original group unchanged. * * Because of this redundancy, it is more optimal to register effects as pre * effects, if the effect does not care about the paint on the input path. For * example, the Roughen effect in Illustrator is registered as a pre effect * since it merely roughens the geometry, regardless of the paint. On the other * hand, drop shadow is registered as a post effect, since its results depend on * the paint applied to the input objects. * * While there is redundancy, effects really cannot make any assumptions about * the input art they are given and should thus attempt to operate on all of the * input art. At the end of executing an entire appearance, Illustrator will * attempt to "clean up" and remove any unnecessary nested groups and unpainted * paths. * * When creating output art for the go message, the output art must be a child * of the same parent as the input art. It also must be the only child of this * parent, so if you create a copy of the input art, work on it and attempt to * return the copy as the output art, you must make sure to dispose the original * input art first. It is not legal to create an item in an arbitrary place and * return that as the output art. * * Effects are limited in the kinds of attributes that they can attach to the * output art. Effects must restrict themselves to using "simple" attributes, * such as: - 1 fill and 1 stroke (AIPathStyle) - transparency options * (AIBlendStyle) It is actually not necessary to use the AIArtStyle suite when * generating output art, and effects should try to avoid it. Effects also * should avoid putting properties on output art that will generate more styled * art (ie. nested styled art is not allowed). * * I suggest playing around with the TwirlFilterProject in Illustrator and * expanding the appearance to get a better picture of the live effect * architecture. * * Hope that helps, -Frank */ public class LiveEffect extends NativeObject { // AIStyleFilterPreferredInputArtType protected static final int INPUT_DYNAMIC = 0, INPUT_GROUP = 1 << (Item.TYPE_GROUP - 1), INPUT_PATH = 1 << (Item.TYPE_PATH - 1), INPUT_COMPOUNDPATH = 1 << (Item.TYPE_COMPOUNDPATH - 1), INPUT_PLACED = 1 << (Item.TYPE_PLACED - 1), INPUT_MYSTERYPATH = 1 << (Item.TYPE_MYSTERYPATH - 1), INPUT_RASTER = 1 << (Item.TYPE_RASTER - 1), // If INPUT_PLUGIN is not specified, the filter will receive the result // group of a plugin group instead of the plugin group itself INPUT_PLUGIN = 1 << (Item.TYPE_PLUGIN - 1), INPUT_MESH = 1 << (Item.TYPE_MESH - 1), INPUT_TEXTFRAME = 1 << (Item.TYPE_TEXTFRAME - 1), INPUT_SYMBOL = 1 << (Item.TYPE_SYMBOL - 1), INPUT_FOREIGN = 1 << (Item.TYPE_FOREIGN - 1), INPUT_LEGACYTEXT = 1 << (Item.TYPE_LEGACYTEXT - 1), // Indicates that the effect can operate on any input art. */ INPUT_ANY = 0xfff, // Indicates that the effect can operate on any input art other than // plugin groups which are replaced by their result art. INPUT_ANY_BUT_PLUGIN = INPUT_ANY & ~INPUT_PLUGIN, // Special values that don't correspond to regular art types should be // in the high half word // Wants strokes to be converted to outlines before being filtered // (not currently implemented) INPUT_OUTLINED_STROKE = 0x10000, // Doesn't want to take objects that are clipping paths or clipping text // (because it destroys them, e.g. by rasterizing, or by splitting a // single path into multiple non-intersecting paths, or by turning it // into a plugin group, like the brush filter). // This flag is on for "Not OK" instead of on for "OK" because // destroying clipping paths is an exceptional condition and we don't // want to require normal filters to explicitly say they're OK. // Also, it is not necessary to turn this flag on if you can't take any // paths at all. INPUT_NO_CLIPMASKS = 0x20000; //AIStyleFilterFlags public static final int FLAG_NONE = 0, /* Parameters can be scaled. */ FLAG_HAS_SCALABLE_PARAMS = 1 << 17, /* Supports automatic rasterization. */ FLAG_USE_AUTO_RASTARIZE = 1 << 18, /* Supports the generation of an SVG filter. */ FLAG_CAN_GENERATE_SVG_FILTER = 1 << 19, /* Has parameters that can be modified by a \c * #kSelectorAILiveEffectAdjustColors message. */ FLAG_HAS_ADJUST_COLOR_HANDLER = 1 << 20, /* Handles \c #kSelectorAILiveEffectIsCompatible messages. * If this flag is not set the message will not be sent. */ FLAG_HAS_IS_COMPATIBLE_HANDLER = 1 << 21; private String name; private String title; private LiveEffectPosition position; private int preferredInput; private int flags; private int majorVersion; private int minorVersion; private MenuItem menuItem = null; /** * effects maps effectHandles to their wrappers. */ private static IntMap<LiveEffect> effects = null; /** * Called from the native environment. */ protected LiveEffect(int handle, String name, String title, int position, int preferredInput, int flags, int majorVersion, int minorVersion) { super(handle); this.name = name; this.title = title; this.position = IntegerEnumUtils.get(LiveEffectPosition.class, position); this.preferredInput = preferredInput; this.flags = flags; this.majorVersion = majorVersion; this.minorVersion = minorVersion; } /** * @param title preferred * @param preferred a combination of LiveEffect.INPUT_* * @param flags a combination of LiveEffect.FLAG_* * @param majorVersion * @param minorVersion */ public LiveEffect(String title, String category, LiveEffectPosition position, Class preferredInput, int flags, int majorVersion, int minorVersion) { this(0, title, title, position != null ? position.value : 0, getInputType(preferredInput), flags, majorVersion, minorVersion); IntMap<LiveEffect> effects = getEffects(); // Now see first whether there is an effect already that fits this // description. Reuse it, as we're probably re-executing a script // that produces the same effect again. Integer key = effects.keyOf(this); if (key != null) { // Found one, let's reuse it's handle and remove the old effect from // the list: LiveEffect effect = effects.get(key); effect.remove(); handle = effect.handle; effect.handle = 0; effects.remove(key); } else { // No previously existing effect found, create a new one: handle = nativeCreate(name, title, this.position.value, this.preferredInput, flags, majorVersion, minorVersion); } if (handle == 0) throw new ScriptographerException("Unable to create LifeEffect."); if (category != null) menuItem = nativeAddMenuItem(name, category, title + "..."); effects.put(handle, this); } public LiveEffect(String title, String category, LiveEffectPosition position, Class preferredInput, int flags) { this(title, category, position, preferredInput, flags, 1, 0); } public LiveEffect(String title, String category, LiveEffectPosition position, Class preferredInput) { this(title, category, position, preferredInput, FLAG_NONE, 1, 0); } public LiveEffect(String title, String category, LiveEffectPosition position) { this(title, category, position, null, FLAG_NONE, 1, 0); } private native int nativeCreate(String name, String title, int position, int flags, int preferredInput, int majorVersion, int minorVersion); private native MenuItem nativeAddMenuItem(String name, String category, String title); /** * "Removes" the effect. there is no real destroy for LiveEffects in * Illustrator, so all it really does is remove the effect's menu item, if * there is one. It keeps the effectHandle and puts itself in the list of * unused effects */ public boolean remove() { // See whether we're still linked: if (effects.get(handle) == this) { if (menuItem != null) menuItem.remove(); menuItem = null; return true; } return false; } public static void removeAll() { // As remove() modifies the map, using an iterator is not possible here: if (effects != null) for (Object effect : effects.values().toArray()) ((LiveEffect) effect).remove(); } public MenuItem getMenuItem() { return menuItem; } /* * used for unusedEffects.indexOf in the constructor above */ public boolean equals(Object obj) { if (obj instanceof LiveEffect) { LiveEffect effect = (LiveEffect) obj; return name.equals(effect.name) && title.equals(effect.title) && preferredInput == effect.preferredInput && position == effect.position && flags == effect.flags && majorVersion == effect.majorVersion && minorVersion == effect.minorVersion; } return false; } private static IntMap<LiveEffect> getEffects() { if (effects == null) { effects = new IntMap<LiveEffect>(); for (LiveEffect effect : nativeGetEffects()) effects.put(effect.handle, effect); } return effects; } private static native ArrayList<LiveEffect> nativeGetEffects(); // Getters: public LiveEffectPosition getPosition() { return position; } /** * @jshide */ public String getName() { return name; } /** * @jshide */ public String getTitle() { return title; } /** * @jshide */ public int getFlags() { return flags; } /** * @jshide */ public int getMajorVersion() { return majorVersion; } /** * @jshide */ public int getMinorVersion() { return minorVersion; } // Callback functions: private Callable onEditParameters = null; public Callable getOnEditParameters() { return onEditParameters; } public void setOnEditParameters(Callable onEditParameters) { this.onEditParameters = onEditParameters; } protected void onEditParameters(LiveEffectEvent event) { if (onEditParameters != null) ScriptographerEngine.invoke( onEditParameters, this, event); } private Callable onCalculate = null; public Callable getOnCalculate() { return onCalculate; } public void setOnCalculate(Callable onCalculate) { this.onCalculate = onCalculate; } protected void onCalculate(LiveEffectEvent event) { if (onCalculate != null) ScriptographerEngine.invoke(onCalculate, this, event); } private Callable onGetInputType = null; public Callable getOnGetInputType() { return onGetInputType; } public void setOnGetInputType(Callable onGetInputType) { this.onGetInputType = onGetInputType; } protected int onGetInputType(LiveEffectEvent event) { if (onGetInputType != null) { Object ret = ScriptographerEngine.invoke( onGetInputType, this, event); // Determine type from returned class if (ret instanceof Class) return getInputType((Class) ret); } // Default is INPUT_ANY_BUT_PLUGIN return INPUT_ANY_BUT_PLUGIN; } protected static int getInputType(Class cls) { // Default setting for effects that provide no input type is // INPUT_DYNAMIC, so the getInputType handler is asked instead. int type = INPUT_DYNAMIC; // Determine type from Item class if (cls != null && Item.class.isAssignableFrom(cls)) { type = Item.getItemType(cls); if (type == Item.TYPE_ANY) type = INPUT_ANY_BUT_PLUGIN; else if (type == Item.TYPE_UNKNOWN) type = INPUT_DYNAMIC; else { type = 1 << (type - 1); if (type == INPUT_ANY) type = INPUT_ANY_BUT_PLUGIN; } } return type; } /** * To be called from the native environment: */ private static void onEditParameters(int handle, int dataHandle) { LiveEffect effect = getEffect(handle); if (effect != null) { effect.onEditParameters(new LiveEffectEvent(0, dataHandle)); } } /** * To be called from the native environment: */ private static int onCalculate(int handle, Item item, int dataHandle) { LiveEffect effect = getEffect(handle); if (effect != null) { LiveEffectParameters parameters = LiveEffectParameters.wrapHandle(dataHandle, item.document); Item parent = item.getParent(); // Scriptographer's new item recording feature makes // processing effects extremely convenient. All new items // are automatically collected, and the right thing is // done with them at the end. Since doing the wrong // thing leads to endless crashes, this is the best // way to handle this anyway. Item.collectCreatedItems(); ItemList newItems = null; try { effect.onCalculate(new LiveEffectEvent(item, parameters)); } finally { newItems = Item.retreiveCreatedItems(); } if (newItems.size() > 0) { boolean changed = false; Item newItem; if (newItems.size() == 1) { newItem = newItems.getFirst(); } else { // More than one new item was produced. Group them, as // LiveEffects require one item only. newItem = new Group(newItems); changed = true; } // "When creating output art for the go message, the output art // must be a child of the same parent as the input art. It also // must be the only child of this parent, so if you create a // copy of the input art, work on it and attempt to return the // copy as the output art, you must make sure to dispose the // original input art first. It is not legal to create an item // in an arbitrary place and return that as the output art." if (newItem.getParent().equals(parent) || parent.appendTop(newItem)) { item.remove(); item = newItem; changed = true; } // Since we're outside of Scriptographer's script handling, we // need to take care of committing changes ourselves here before // returning. if (changed) CommitManager.commit(); } } // already return the handle to the native environment so it doesn't // need to access it there... return item.handle; } /** * To be called from the native environment: */ private static int onGetInputType(int handle, int itemHandle, int parametersHandle) { // For improved performance of onGetInputType, we do not wrap the handle // on the native side already, as often it is not even used. Instead // The LiveEffectEvent takes care of that on demand. LiveEffect effect = getEffect(handle); if (effect != null) return effect.onGetInputType(new LiveEffectEvent(itemHandle, parametersHandle)); return INPUT_ANY_BUT_PLUGIN; } private static LiveEffect getEffect(int handle) { return effects.get(handle); } }