AIntrospector 1.0.11 – 30 May 2018
This project has multiple goals.
Short Term
The short term goal is to provide ExtendScript access to Illustrator data that is currently inaccessible to ExtendScript scripts.
There is a discrepancy between what can be accessed using the C++ API and what can be accessed from ExtendScript.
This plug-in attempts to bridge that gap.
The plug-in currently provides an additional DOM (Document Object Model), unrelated to the ExtendScript DOM.
The AIntrospector DOM is more low-level than the ExtendScript DOM.
This alternative DOM can be used from ExtendScript alongside the ExtendScript DOM and offers access to areas that are inaccessible to the ExtendScript DOM.
At this time, the AIntrospector DOM is still very sparse: it is populated with new attributes and new features on an ‘as-needed’ basis for some real-life projects. For example, the AIntrospector DOM gives you access to information about ‘type on a path’ that is not available in the ExtendScript DOM (nor in the C++ API).
The current focal areas, where the DOM is being extended, are determined by the needs of current customers who have a solid commitment to licensing AIntrospector.
Low-level DOM: optimizing for asynchronous use
The low-level AIntrospector DOM can be accessed from CEP/JavaScript as well as from ExtendScript.
When accessing the DOM from CEP/JavaScript, there is no ‘intermediate’ use of ExtendScript: JavaScript calls are handled by the plug-in without any use of ExtendScript. This allows the creation of CEP panels that are totally free of any ExtendScript.
PARM/MRAP errors
The hope/assumption is that this can put an end to PARM/MRAP errors which seem to be caused by some ‘disconnect’ between the ExtendScript engine and Illustrator.
When PARM/MRAP happens, Illustrator remains fully responsive and alive to the user, and at the same time, ExtendScript scripts can still be run, but somehow, ExtendScript seems to lose its grip on Illustrator, and anything you try causes a PARM/MRAP error, which can only be cured by restarting Illustrator.
Async DOM
One of the underlying principles is to support asynchronous access. The idea is that there are two realms: one ‘inside’ Illustrator, and one ‘outside’ Illustrator.
The AIntrospector API bridges these two realms.
When calling from ‘outside’ to ‘inside’ there is a substantial cost in time and performance, especially when calling from JavaScript/CEP: we’re switching between two very different contexts.
The AIntrospector DOM is being designed to reduce the number of such context switches as much as possible.
The ‘outside’ code can prepare a request object, which is a bit like a shopping list or ‘to do’ list.
The request object can express complex queries into the Illustrator data structures, and the idea is to shoehorn as much as possible into a single request.
The request is then fired off, the context switches, and the ‘inside’ resolves and execute the request, without needing to call back to the outside during processing.
If you have choice between formulating three small requests, or instead an equivalent single complex requests, you would choose the latter because you will only ‘pay’ for one context switch between ‘outside’ and ‘inside’.
Long Term
Once the low-level DOM nears completion, the mid term goal is to attempt to build a second, higher level DOM on top of the low-level AIntrospector DOM. This new DOM would be as close to the ExtendScript DOM as possible, and I would try and make the asynchronicity as invisible as possible, so existing ExtendScript scripts might be able to run in a JavaScript/CEP environment (as opposed to the ExtendScript engine) against the new DOM.
GPU features
Another aspect of the plug-in is to provide ‘GPU-like’ features. Some tasks are too massive to be handled in ExtendScript or JavaScript, but are feasible in C++. The plug-in will gradually provide functions to handle such requests. I’ve added one already as a proof of concept: simplifyPath.
Focus on CEP/JavaScript
Gradually, the focus will shift from ExtendScript to providing more features to CEP/JavaScript.
API
The API provided by the AIntrospectorUI extension works both in ExtendScript and in JavaScript.
From JavaScript, all calls into AIntrospector are asynchronous.
From ExtendScript, calls can be either synchronous or asynchronous.
To run stuff from ExtendScript-only setups
Make sure to start the script with something like
#include "<some path>/standalone.jsx"
or equivalent.
This loads and initializes the ExtendScript infrastructure.
XUID
In order to reference objects in Illustrator (page items, colors, documents, artboards…) the plug-in uses XUID (eXtended UID).
For many object types, XUID are just plain GUID.
But some objects in Illustrator do not have the internal data structures to retain their GUID. There is no way for Illustrator to ‘remember’ what the GUID for those objects would be.
In order to handle such objects (e.g. Artboards), the plug-in uses the GUID of the ‘owner’ of the object, suffixed with a short string. Part of this string will be a four-letter code:
ArBd -> Artboard
ArSt -> Art Style
LiEf -> Live Effect
Layr -> Layer
Artboards
For example, if a document has GUID 706c0a02-32fd-4fae-b745-778ba6995328, then its artboards will have the following XUID:
706c0a02-32fd-4fae-b745-778ba6995328ArBd0
706c0a02-32fd-4fae-b745-778ba6995328ArBd1
706c0a02-32fd-4fae-b745-778ba6995328ArBd2
... and so on
For artboards, the owner (document) GUID is suffixed with a type code (ArBd = Artboard) and an index 0..n-1 where n = number of artboards.
By nature, artboards do not have ‘stable’ XUID: if the order of the artboards in a document changes, the XUID associated with any of the artboards will change.
Layers
Any group in an Illustrator document will have its own GUID.
Illustrator layers are always associated with a special group (a layer group), and becaused of that, they don’t have their own GUID.
Instead they have an XUID which is derived from the GUID of the underlying layer group.
So, if we have a layer ‘ABC’, and the associated layer group has a GUID of
1715b7bb-0b79-486f-9cbd-c56846e73400
then the layer will have XUID
1715b7bb-0b79-486f-9cbd-c56846e73400Layr
Live Effects
Live effects are owned by the application, not by a document.
The Illustrator application has a fixed GUID 21862fc3-f16b-42b6-acb3-00165cedd121, and the live effects have an XUID based on their name derived from that.
21862fc3-f16b-42b6-acb3-00165cedd121LiEfLive Effect Name
Art Styles
Art styles are owned by a document.
Art styles have an XUID based on their name derived from the document GUID.
1715b7bb-0b79-486f-9cbd-c56846e73400ArStArt Style Name
Synchronous vs Asynchronous
Asynchronous use of the API is compatible with both JavaScript and ExtendScript.
Asynchronous API call:
AIn.sendAIntrospectorQuery(query, function(result) {...});
Synchronous API call (ExtendScript only):
result = AIn.sendAIntrospectorQuery(query);
Synchronous call with ‘implicit reference’ (ExtendScript only):
In ExtendScript you can omit the reference attribute from a query object and instead use
result = AIn.sendPageItemIntrospectorQuery(pageItem, query);
This allows you to reference objects directly, rather than via the reference mechanism.
Symbolic constants
A lot of Illustrator calls in the C++ API take enumeration constants.
These are numerical (integer) constants that each represent a particular state or condition.
For example, in the C++ API, you’ll find
/** Type constants for drawing modes. */
enum AIDrawingMode
{
kAIDrawNormalMode = 0,
kAIDrawBehindMode,
kAIDrawInsideMode
};
To keep things readable in C++ code, developers will rather use ‘kAIDrawNormalMode’ instead of a ‘0’. These two things are equivalent as far as the C++ compiler is concerned, but writing kAIDrawNormalMode makes it much easier to understand the code for the human reader of the C++ code.
The AIntrospector plug-in uses a similar mechanism, and will allow symbolic strings for nearly all parameters for API calls that need such integer values.
Any call to AIntrospector that has a JSON construct with a drawing mode parameter will accept either an integer value, as well as a string value.
You can either use
{
...
drawingMode: 1
...
}
or
{
...
drawingMode: "kAIDrawBehindMode"
...
}
and both will work the same.
It will also return these symbolic strings rather than the ‘naked’ integer values, so the returned data from a ‘getinfo’ becomes is easier to understand.
Example:
{
"uid": "460cec74-8915-477b-8baa-bc527d0cc094",
"name": "my group",
"type": "art",
"artType": "kTextFrameArt",
"artBounds:top": "-293.2734375",
"filePath": "",
"pathObject": "3c336f5b-ccf4-4d20-9f6a-389a3af3d30f",
"typeOnPath": {
"typeOnPathEffect": "kTypeOnPathRainbow",
"typeOnPathFlipped": "1"
}
}
artType is returned as ‘kTextFrameArt’, which is equivalent with the integer constant ’12’. Seeing ‘kTextFrameArt’ is a lot easier to understand than seeing ’12’.
Reference
To refer to an object in Illustrator you can use a reference object.
There are four types of references: symbolic, uid, indexed and relative
A single reference object can refer to multiple Illustrator objects (e.g. referring to the current selection)
Symbolic
To refer to the current document:
{
type: "symbolic",
symbolType: "curDoc"
}
To refer to the current selection (which might refer to multiple elements):
{
type: "symbolic",
symbolType: "selected"
}
To refer to the application object (this should always return the pre-determined GUID AIn.C.APPLICATION_UID = “21862fc3-f16b-42b6-acb3-00165cedd121”):
{
type: "symbolic",
symbolType: "application"
}
uid
To refer to an object by its XUID:
{
type: "uid",
uid: "706c0a02-32fd-4fae-b745-778ba6995328"
}
indexed
This is a recursive definition: you need a nested reference object to use the index with.
To refer to an object ‘below’ another object by its index:
{
type: "indexed",
index: anIdx,
reference: {
... a reference object for the parent object ...
}
}
relative
This is a recursive definition. you need a nested reference object as the base for the relation.
The available relations are:
ancestor
child
descendant
next
parent
previous
ancestor = recursive parent descendant = recursive child
The ancestor and descendant references track down via parent or child relations.
To refer to an object relative to another object:
{
type: "relative",
relation: "parent",
reference: {
... a reference object for the child object ...
}
}
{
type: "relative",
relation: "child",
reference: {
... a reference object for the parent object ...
}
}
{
type: "relative",
relation: "next",
reference: {
... a reference object for the sibling object ...
}
}
{
type: "relative",
relation: "previous",
reference: {
... a reference object for the sibling object ...
}
}
{
type: "relative",
relation: "ancestor",
includeRoots: true/false, // optional, default = false
reference: {
... a reference object for the descendant object ..
},
filter: ... a filter ...,
visitFilter: ... a filter ...
}
{
type: "relative",
relation: "descendant",
includeRoots: true/false, // optional, default = false
reference: {
... a reference object for the ancestor object ...
},
filter: ... a filter ...,
visitFilter: ... a filter ...
}
filter
The filter determines which elements are to be selected by the reference
visitFilter
The visitFilter determines how to recurse up or down: if an element does not match the visitFilter the recursion will not descend or ascend into it.
includeRoots
If includeRoots is true, then the referenced object(s) themselves are also tested for matching against the filter and can be included in the result.
By default only objects ‘one step removed’ – i.e. children or parents – of the referenced object(s) are tested.
filter objects
A basic filter object will compares attribute values:
{
attr1: value1,
attr2: value2...
}
This will filter all elements that have attr1 == value1 and attr2 == value2.
Example:
{
isArtClipping: "1"
}
will filter all elements that have isArtClipping = 1.
To change the filter comparison operation from ‘equals’ to something else (<, <=, >, >=), you need to nest the filter into a ‘cmp’ object.
{
cmp: "<",
filter : {
... a basic filter object ...
}
}
This causes the comparison to compare ‘attr < value’ instead of ‘attr == value’.
Example:
{
cmp: "<=",
filter : {
artType: "kCompoundPathArt"
}
}
will filter all art elements that have an art type <= kCompoundPathArt. kCompoundPathArt is a symbolic representation of the integer value 3.
Filters can be inverted or combined.
To invert a filter:
{
invert: true
filter : {
... a filter object ...
}
}
To combine two or more filters with ‘and’:
{
and:
[
{
... filter object 1 ...
},
{
... filter object 2 ...
}
...
]
}
To combine two or more filters with ‘or’:
{
or:
[
{
... filter object 1 ...
},
{
... filter object 2 ...
}
...
]
}
Complex references
It is allowed to have multiple nested reference objects.
For example, this works:
{
opcode: "getinfo",
attr: [ "uid", "name" ],
reference: {
type: "relative",
relation: "child",
reference: {
type: "relative",
relation: "child",
reference: {
type: "uid",
uid: AIn.C.APPLICATION_UID
}
}
}
}
It will retrieve all the children (e.g. layers, art styles) for all open documents: documents are children of the application, and layers and art styles are children of the documents.
Request
Requests allow us to ‘do’ something (i.e. extract data, or make changes to the document).
Asynchronous calls will always need to use a request that contains a reference object, so the plug-in can know what objects to target with the request.
Synchronous call with ‘implicit reference’ can omit the reference object from the request. They target the object that is provided in the synchronous call.
Each request needs at least an opcode (OPeration CODE).
The following opcodes are currently supported (work in progress):
disposeArt
getinfo
newArt
reorderArt
select
setinfo
simplifyPath
disposeArt
{
opcode: "disposeArt",
reference: {
.... a reference object ...
}
}
This will remove the referenced art from the document.
getinfo
This request retrieves information about one or more objects and returns it.
{
opcode: "getinfo",
alwaysReturnArray: true/false, // optional, defaults to false
attr: ... a single attr or an array of attr ...
reference: {
.... a reference object ...
}
}
When alwaysReturnArray is true, the returned value is always an array (which could be empty or have only a single element).
If alwaysReturnArray is false the returned value is either a single item if there is only a single result, or an array if there are multiple results.
There is some support for function-like calls. Instead of just an attribute name, they are specified by 2-element arrays [ <<attr>>, <<param>> ]
Supported function-like attr:
attr = allArt, param = { artType: ..., whichAttr:..., matchAttr:...}
each of those either a symbolic string,
e.g. 'kArtSelected' or an integer
attr = artOrder, param = string with artUID to compare with
attr = artUserAttr, param = whichAttr (either a symbolic string,
e.g. 'kArtSelected' or an integer)
attr = pathSegments, param = { segNum: ..., segCount: .... }.
pathSegments can also be used as a normal attribute
attr = pathLength, param = AIReal flatness
attr = validArt, param = bool searchAllLayerLists
To call these, you need to provide a two-element array: first element is the attr name, second element is a JSON-encoded parameter construct.
attr can currently be one or more selected from (work in progress):
artBounds
artBoundsOptions
artDrawingMode
artHasFill
artHasStroke
artIsGraph
artStrokeParams
artStyle
artStyleName
artStyleScaleFactor
artSubType
artTargettingAction
artTimeStampOptions
artTransferAttrsOptions
artType
artXMP
ascent
autoKernType
autoLeading
baseLineShift
characterRotation
childCount
contents
descent
dictionary
distanceToBaseLine
entryType
extractedText
filePath
font
fontBaselineOption
fontCapsOption
fontSize
frameIndex
glyphOrientation
hasNote
horizontalScale
isArtClipping
isArtStyledArt
isEnvelope
layerGroup
leading
lineOrientation
matrix
maxCapHeight
minCapHeight
name
next
note
onPathTextTRange
origins
paintOrder
parent
parsedStyle
partOfLinkedText
pathArea
pathClosed
pathGuide
pathHasLength
pathIsClip
pathObject
pathPolarity
pathSegmentCount
pathSegments (can also be used as a function)
placedType
pointTextAnchor
prepArt
previous
spaceGlyphWidth
storyIndex
styledArt
text
textAntialias
textFrameColumnCount
textFrameColumnGutter
textFrameMatrix
textFrameOpticalAlignment
textFrameRowCount
textFrameRowGutter
textFrameRowMajorOrder
textFrameSpacing
textFrameStoryDirection
textFrameType
textOrientation
tracking
type
typeOnPath
typeOnPathEffect
typeOnPathFlipped
uid
underlinePosition
underlineThickness
verticalScale
Not all attributes make sense for all object types. If the attribute is not defined, an empty string will be returned.
If the reference resolves to multiple objects, an array of replies will be returned.
The reply object will contain an entry for each requested attr.
Example:
{
opcode: "getinfo",
attr: "uid"
reference: {
type: "symbolic",
symbolType: "root"
}
}
will return a single object
{
uid: "706c0a02-32fd-4fae-b745-778ba6995328"
}
single uid, the uid of the document.
Example:
{
opcode: "getinfo",
attr: "name"
reference: {
type: "uid",
uid: ["706c0a02-32fd-4fae-b745-778ba6995328",
"807c0a02-32fd-4fae-b745-778ba6995439"]
}
}
will return an array of two objects, the names of these objects:
[
{
name: "name 1"
},
{
name: "name 2"
}
]
Example:
{
opcode: "getinfo",
attr: [ "name", "childCount" ]
reference: {
type: "uid",
uid: ["706c0a02-32fd-4fae-b745-778ba6995328",
"807c0a02-32fd-4fae-b745-778ba6995439"]
}
}
will return an array of two objects:
[
{
name: "name 1",
childCount: 5
},
{
name: "name 2"
childCount: 0
}
]
Example:
{
opcode: "getinfo",
attr: [ "allArt", { "artType" : "kAnyArt" } ],
reference: {
.... a reference to a document ...
}
}
will call allArt({ “artType” : “kAnyArt” }) and return all UIDS of all art in the referenced document.
newArt
{
opcode: "newArt",
params: {
artType: ... art type...,
paintOrder: ... paint order...,
prepArt: ... a reference to a preparatory art object ...
}
}
This will create a new, empty art object.
It will execute the C++ AIArtSuite call:
AIAPI AIErr(*NewArt)(ai::int16 type, ai::int16 paintOrder,
AIArtHandle prep, AIArtHandle *newArt)
reorderArt
{
opcode: "reorderArt",
reference: {
... an art object...
}
params: {
paintOrder: ... paint order...,
prepArt: ... a reference to a preparatory art object ...
}
}
This will reorder the targeted art relative to the prep art.
It will execute the C++ AIArtSuite call:
AIAPI AIErr (*ReorderArt) (AIArtHandle thisArt, ai::int16 paintOrder,
AIArtHandle prep)
select
Will attempt to select the referenced items
setinfo
This request will change some attributes or related data on the referenced objects.
simplifyPath
Will attempt to simplify the path. Smoothing uses a sliding average of smoothAveragePointCount to average out points along the path as long as they are no more than smoothToleranceInPoints apart.
If a point between two other points is less than toleranceInPoints away from the other two, it will be removed.
If a compound path subsegment has less than minSegmentCount segments, it will be deleted
{
opcode: "simplifyPath",
smoothToleranceInPoints: 10,
smoothAveragePointCount: 10,
toleranceInPoints: 0.4,
minSegmentCount: 4,
reference: ...
}
This seems to work well on bitmaps that have been converted to paths and zig-zag all over the place. I’ve been able to reduce a sample compound path from > 200,000 path segments down to a few 10,000 while retaining something visually very close to the original. It also helps in making a path more manageable for manual editing.