From 9bdb742cbd8301b174c25a7407ac3bc11bb9d2a2 Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Wed, 27 May 2026 20:09:54 -0400 Subject: [PATCH] patch: use-cliche-data-in-docs, adobe-illustrator-scripting (#1855) --- .../use-cliche-data-in-docs.instructions.md | 51 +++ skills/adobe-illustrator-scripting/SKILL.md | 305 ++++++++++++++++++ 2 files changed, 356 insertions(+) diff --git a/instructions/use-cliche-data-in-docs.instructions.md b/instructions/use-cliche-data-in-docs.instructions.md index 56cf976f..4ec1e6f4 100644 --- a/instructions/use-cliche-data-in-docs.instructions.md +++ b/instructions/use-cliche-data-in-docs.instructions.md @@ -49,6 +49,57 @@ Use these generic, cliche substitutes in all documentation and examples: | **File paths** | `accounts/acme.mjs`, `config/reports.json` | | **Project names** | My Project, Sample App, Demo Tool | +## Match the Placeholder to the Context + +A placeholder is only correct if it is **plausible in the surrounding context**. A generic name that violates OS conventions, tooling norms, or the workflow being described is just as misleading as a real value. Pick substitutes that fit the platform, the tool, and the role the value plays. + +### Choose Paths That Match the Platform + +| OS / context | Use | Avoid | +| --- | --- | --- | +| Windows, per-user data | `C:\Users\\AppData\Local\AcmeApp\` | `/home/user/...`, `~/.config/...` | +| Windows, per-machine shared data | `C:\ProgramData\AcmeApp\` | `C:\Users\\...` | +| Windows, temporary | `%TEMP%\acme\` or `C:\Users\\AppData\Local\Temp\acme\` | `/tmp/acme/` | +| POSIX, per-user data | `~/.config/acme/`, `~/.local/share/acme/` | `C:\Users\\...` | +| POSIX, temporary | `/tmp/acme/` | `%TEMP%\acme\` | +| Cross-platform examples | Show both, or use `/acme/` | Picking one silently | + +When the surrounding text or code is OS-specific (a `.bat` file, a `.jsx` running on Windows, a `bash` snippet), the path placeholder must match that OS. When the docs are platform-neutral, either show both forms or use a clearly abstract token (``, ``). + +### Match the Scope to the Workflow + +The placeholder must sit in a location that makes sense for the kind of data it represents: + +| Data role | Plausible placeholder location | +| --- | --- | +| Per-user logs and runtime output | User-profile folder (`C:\Users\\AppData\Local\\logs\`, `~/.local/state//`) | +| Per-user settings | User config folder (`%APPDATA%\\`, `~/.config//`) | +| Machine-wide shared state | `C:\ProgramData\\`, `/var/lib//` | +| Project-local working files | Repository-relative paths (`./build/`, `./tmp/`) | +| Generated output artifacts | Project output folder (`./dist/`, `./out/`) | + +A user-driven script that writes a debug log should not place that log in `C:\ProgramData\…` (machine-shared); a service that maintains shared state should not place it in `~/.config/…` (per-user). Pick the location a real implementation of that role would pick. + +### Match the Identifier to the Domain + +When the example uses an identifier (account name, project name, dataset key), choose a placeholder consistent with the surrounding domain vocabulary. + +- A CRM example: `acme-corp`, `northwind-traders`. +- A geographic dataset example: `springfield`, `region-west`. +- A developer tooling example: `demo-app`, `sample-project`. + +Do not mix domains (`acme-corp` inside a geographic-data example reads as wrong even though both names are approved generically). + +### Self-Check + +Before committing a placeholder, ask: + +- Does the path syntax match the OS shown in the same code block? +- Does the location match the **role** of the data (user vs. machine, runtime vs. config, local vs. shared)? +- Does the identifier match the **domain** of the surrounding example? + +If any answer is no, swap the placeholder for one that fits. + ## How to Apply This Rule ### When Adding a Feature diff --git a/skills/adobe-illustrator-scripting/SKILL.md b/skills/adobe-illustrator-scripting/SKILL.md index b53ffcf7..103bf7ce 100644 --- a/skills/adobe-illustrator-scripting/SKILL.md +++ b/skills/adobe-illustrator-scripting/SKILL.md @@ -48,6 +48,7 @@ Expert guidance for automating Adobe Illustrator through ExtendScript (JavaScrip - **Startup Scripts**: Place scripts in the Startup Scripts folder to run automatically on launch - **Target directive**: Begin scripts with `#target illustrator` when running from ESTK or external tools - **`#targetengine` directive**: Use `#targetengine "session"` to persist variables across script executions +- **External invocation**: Scripts are frequently launched from outside Illustrator — by shell scripts, task runners, CI jobs, ExtendScript Toolkit (`ExtendScript Toolkit.exe -run script.jsx`), or `BridgeTalk` messages from other Adobe apps. See [External Invocation & Argument Passing](#external-invocation--argument-passing). ### Naming Conventions (JavaScript) @@ -561,6 +562,152 @@ When calling methods with multiple optional parameters, use `undefined` to skip item.rotate(30, undefined, undefined, true); ``` +## External Invocation & Argument Passing + +Illustrator scripts are routinely launched from outside the application — +shell scripts, schedulers, build pipelines, ExtendScript Toolkit, or +`BridgeTalk` messages from other Creative Cloud apps. The execution +environment under those launchers differs from the in-application *File > +Scripts* path in several ways that frequently break otherwise-correct code. + +### `arguments[]` Is Unreliable Under External Launchers + +ExtendScript Toolkit's `-run` invocation and `BridgeTalk.send()` do not +forward arbitrary launcher arguments into the script's top-level +`arguments[]` array. In many configurations the array contains a single +`[object BridgeTalk]` element instead of the values the caller passed, as +demonstrated below: + +```javascript +// At top of script +var passed = (typeof arguments !== "undefined") ? arguments : []; +for (var i = 0; i < passed.length; i++) { + $.writeln("arg[" + i + "] = " + passed[i]); + // Often prints: arg[0] = [object BridgeTalk] +} +``` + +**Do not rely on `arguments[]` for required inputs when the script is +launched externally.** Use one of the following more reliable channels. + +### Sidecar File for Parameters + +When a script fails under an external launcher and the source of the error +is not obvious, fall back to a sidecar file: have the caller write a small +text file at a known absolute path, and read it on startup. This works +regardless of launcher quirks and is easy to inspect after a failed run. + +```javascript +var SIDECAR_PATH = "C:/Users/userName/job.args.txt"; + +function readSidecar(path) { + var f = new File(path); + if (!f.exists || !f.open("r")) return null; + var lines = []; + while (!f.eof) { + var ln = f.readln(); + if (ln && !/^\s*$/.test(ln)) lines.push(ln); + } + f.close(); + return { + input: lines[0], + output: lines[1], + mode: lines[2] + }; +} +``` + +A `key=value` format is equally workable and avoids positional fragility: + +```text +input=C:/path/to/input.ai +output=C:/path/to/output.pdf +mode=preview +``` + +### Environment Variables + +`$.getenv("NAME")` returns environment variables visible to **Illustrator's +process**, not the launcher's. If the launcher needs Illustrator to see a +value, it must set the variable system-wide or in Illustrator's parent +environment before launching. For per-invocation values, prefer a sidecar +file. + +### `$.fileName` and `File($.fileName).parent` + +Under in-application execution, `$.fileName` is the absolute path of the +running script and `File($.fileName).parent` yields the script's folder. +Under some external launchers (notably ESTK `-run`) `$.fileName` can be +empty, causing relative path resolution to silently fail. + +```javascript +// Fragile: returns null under some launchers +var here = $.fileName ? File($.fileName).parent : null; +var sidecar = here ? new File(here.fsName + "/job.args.txt") : null; + +// Robust: hardcode a known absolute path or fall back to a stable location +var sidecar = new File("C:/Users/userName/job.args.txt"); +if (!sidecar.exists) sidecar = new File(Folder.temp.fsName + "/job.args.txt"); +``` + +### Diagnostic Logging to an Absolute Path + +Silent failures are common because dialogs are suppressed and the launcher +may not surface `$.writeln` output. Write a plain-text log to a known +absolute path so a run can be inspected after the fact. Create the parent +folder on demand so the first call cannot fail for a missing directory. + +```javascript +var LOG_PATH = "C:/Users/userName/logs/job.log"; + +function log(msg) { + try { + var f = new File(LOG_PATH); + try { if (!f.parent.exists) f.parent.create(); } catch (eDir) {} + if (f.open("a")) { + f.writeln("[" + new Date() + "] " + msg); + f.close(); + } + } catch (e) {} +} +``` + +### Wrap the Entry Point in `try { ... } catch` + +Externally launched scripts often fail without any visible indication. A +top-level `try`/`catch` that writes the error to the log file converts +silent failures into a single inspectable line. + +```javascript +try { + main(); +} catch (err) { + log("FATAL: " + err + (err && err.line ? " line=" + err.line : "")); +} +``` + +### Suppress User Interaction + +External callers cannot answer dialogs. Disable them before any DOM work +and avoid `alert()` / `confirm()` / `prompt()` entirely in scripts that may +be launched headlessly. + +```javascript +app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS; +``` + +### Save Explicitly + +Closing or letting Illustrator return to its idle state does not save the +working file. After all DOM edits, call `doc.saveAs(...)` (or `doc.save()`) +explicitly and log whether it succeeded. + +```javascript +var opts = new IllustratorSaveOptions(); +opts.compatibility = Compatibility.ILLUSTRATOR17; +doc.saveAs(new File(doc.fullName.fsName), opts); +``` + ## Common Patterns ### Iterate All Page Items in a Document @@ -585,6 +732,157 @@ function processAllItems(doc) { } ``` +### Recursively Unlock Layers and Groups Before Editing + +A locked layer or any locked ancestor (parent group, clip group, sublayer) +will cause edits to throw `Error: Target layer cannot be modified`. Walk the +full hierarchy and clear `locked` / `hidden` flags before performing DOM +modifications. + +```javascript +function unlockAll(doc) { + function visitLayers(layers) { + for (var i = 0; i < layers.length; i++) { + var lyr = layers[i]; + try { lyr.locked = false; lyr.visible = true; } catch (e) {} + visitItems(lyr); + if (lyr.layers && lyr.layers.length) visitLayers(lyr.layers); + } + } + function visitItems(container) { + var items = container.pageItems; + for (var j = 0; j < items.length; j++) { + var it = items[j]; + try { it.locked = false; it.hidden = false; } catch (e) {} + if (it.typename === "GroupItem") visitItems(it); + } + } + visitLayers(doc.layers); +} +``` + +### Replacing the File Behind a Linked Image (Relink) + +`PlacedItem.file = newFile` replaces a linked image while preserving the +parent, stacking order, and (after re-applying) the bounds. **`RasterItem` +does not expose a writable `file` property**, so when a placeholder is a +raster you must add a fresh `PlacedItem` in the same parent, copy the bounds, +then remove the original. + +```javascript +function relinkOrRebuild(item, newFile) { + var bounds = item.geometricBounds.slice(); + var parent = item.parent; + var name = item.name; + + if (item.typename === "PlacedItem") { + item.file = newFile; + item.geometricBounds = bounds; + return item; + } + + // RasterItem path: rebuild as a linked PlacedItem in the same parent. + var fresh = parent.placedItems.add(); + fresh.file = newFile; + fresh.geometricBounds = bounds; + if (name) try { fresh.name = name; } catch (e) {} + fresh.move(item, ElementPlacement.PLACEBEFORE); + item.remove(); + return fresh; +} +``` + +### Placing SVG Content (Copy/Paste Pattern) + +`PlacedItem.file` accepts raster formats and AI/PDF, **but not SVG**. Setting +it to an `.svg` File throws `Unable to set placed item's file, is the file +path provided valid?`. The reliable way to bring SVG artwork into a document +is to open the SVG as a separate document, select all, copy, close, and paste +into the working document. + +```javascript +function placeSVG(targetDoc, svgFile, targetLayer) { + var donor = app.open(svgFile); + app.executeMenuCommand("selectall"); + app.executeMenuCommand("copy"); + donor.close(SaveOptions.DONOTSAVECHANGES); + + app.activeDocument = targetDoc; + targetDoc.activeLayer = targetLayer; + app.executeMenuCommand("pasteFront"); + + var sel = targetDoc.selection; + if (!sel || sel.length === 0) return null; + if (sel.length === 1) return sel[0]; + + // Multiple pasted items: group them so callers get a single handle. + var group = targetLayer.groupItems.add(); + for (var i = sel.length - 1; i >= 0; i--) { + sel[i].move(group, ElementPlacement.PLACEATBEGINNING); + } + return group; +} +``` + +### Finding a Clipping Path Inside a Mask Group + +Clip groups expose their clipping shape as a child `PathItem` (or, less +commonly, a child of a `CompoundPathItem`) with `clipping === true`. The +clip's `geometricBounds` give the visible frame to size or center content +against. + +```javascript +function findClipPath(group) { + var items = group.pageItems; + for (var i = 0; i < items.length; i++) { + var it = items[i]; + try { + if (it.typename === "PathItem" && it.clipping) return it; + if (it.typename === "CompoundPathItem") { + for (var j = 0; j < it.pathItems.length; j++) { + if (it.pathItems[j].clipping) return it; + } + } + } catch (e) {} + } + return null; +} +``` + +### Cover-Fit and Contain-Fit Sizing + +To make an image fully cover a rectangle (any overflow hidden by a mask), use +the larger of the width/height ratios. To make it fit entirely inside, use +the smaller. A bleed factor (e.g. `1.10`) lets a cover image extend slightly +past the clip edge. + +```javascript +function fitItemToRect(item, rect, mode, bleed) { + // rect = [L, T, R, B] (Illustrator: T > B) + var rw = rect[2] - rect[0]; + var rh = rect[1] - rect[3]; + var ib = item.geometricBounds; + var iw = ib[2] - ib[0]; + var ih = ib[1] - ib[3]; + if (iw <= 0 || ih <= 0) return; + + var sx = rw / iw; + var sy = rh / ih; + var s = (mode === "cover" ? Math.max(sx, sy) : Math.min(sx, sy)) + * (bleed || 1); + item.resize(s * 100, s * 100); + + var cx = (rect[0] + rect[2]) / 2; + var cy = (rect[1] + rect[3]) / 2; + var b = item.geometricBounds; + var w = b[2] - b[0]; + var h = b[1] - b[3]; + item.position = [cx - w / 2, cy + h / 2]; +} +``` + + + ### Batch Process Files in a Folder ```javascript @@ -621,6 +919,13 @@ try { - **File paths on Windows**: Use forward slashes (`/`) or double backslashes (`\\`) in path strings, or use the `File` object constructor. - **Dialog boxes interrupting batch scripts**: Set `app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS` before batch operations. - **Collections use `getByName()`**: Many collection objects support `getByName("name")` which throws an error if not found; wrap in try/catch. +- **"Target layer cannot be modified"**: A locked layer, sublayer, or parent group (often a clip group like `Cover_Mask`) is blocking the edit. Recursively clear `locked` and `hidden` across the document before modifying. See [Recursively Unlock Layers and Groups](#recursively-unlock-layers-and-groups-before-editing). +- **"Unable to set placed item's file, is the file path provided valid?"**: The file exists and the path is correct, but `PlacedItem.file` does not accept the format. SVG is the most common cause — use the [open / copy / paste pattern](#placing-svg-content-copypaste-pattern) instead. +- **`RasterItem.file = newFile` does nothing or throws**: `RasterItem` does not expose a writable `file` property. Add a new `PlacedItem` to the same parent, restore the bounds and name, then `.remove()` the raster. +- **`arguments[0]` is `[object BridgeTalk]`** (or empty): The script was launched through ESTK `-run` or a `BridgeTalk` message; positional arguments are not forwarded. Use a sidecar file at a known absolute path. See [External Invocation & Argument Passing](#external-invocation--argument-passing). +- **`$.fileName` is empty**: Same external-launcher cause. Do not derive resource paths from `$.fileName` in scripts that may be invoked headlessly — use absolute paths or `Folder.temp`. +- **Script appears to do nothing**: Almost always either a locked ancestor, suppressed dialogs swallowing the error, or a missing explicit `saveAs` after edits. Add a top-level `try`/`catch` that logs to an absolute path to confirm execution and capture the error. +- **`item.resize(sx, sy)` recentered the artwork unexpectedly**: `resize` defaults to scaling around the item's center (`Transformation.CENTER`). Pass an explicit `scaleAbout` argument or follow with `translate(dx, dy)` to reposition. ## Scripting Constants Reference