---
name: acorn-javascript-api
description: Use when an agent needs to drive Acorn (the macOS image editor by Flying Meat) — opening, editing, exporting images, or working with documents, layers, shapes, selections, or filters — by sending JavaScript to a running Acorn over the command line.
---

# Driving Acorn from JavaScript

## Overview

Acorn is a macOS image editor with a JavaScript scripting API. An agent can drive Acorn by piping JavaScript into `Acorn -jsio`; the script runs inside the **already-running** Acorn process.

The JavaScript bridge exposes Acorn's public Objective-C interface (declared in `ACPlugin.h`). Calling these methods from JS uses a small set of bridging conventions described below.

## Safety

The JavaScript Objective-C bridge is not sandboxed in anyway. All of the frameworks on macOS are available for scripting. Files can be created and deleted. Be extra careful when performing io.

## When to Use

- Opening, editing, scaling, cropping, exporting, or batch-processing images via Acorn
- Inspecting or manipulating layers, graphics, selections, masks, blend modes, filters
- Automating Acorn from an MCP server, shell pipeline, or any agent harness
- Anywhere the user asks "make Acorn do X"

## Exporting, writing, or saving
- Acorn provides a number of APIs for exporting or saving a composite of all the layers or individual layers. Look to use these APIs instead of using NSImage or NSImageRep for writing images (PNG/JPEG/TIFF/HEIF/HEIC/etc).

## How to Invoke

```bash
echo 'acorn.orderedDocuments().count();' \
  | /Applications/Acorn.app/Contents/MacOS/Acorn -jsio
```

For multi-line scripts, prefer a heredoc or piping a file:

```bash
/Applications/Acorn.app/Contents/MacOS/Acorn -jsio <<'JS'
var doc = acorn.open("/Users/me/Pictures/MyImage.acorn");
doc.canvasSize().width + "x" + doc.canvasSize().height;
JS
```


There is also `-js <file.js>` (run a file) and `-javascript <file.js>` (alias). Use `-jsio` for piping from stdin.

## Bridging Conventions (Objective-C → JavaScript)

Acorn uses FMJS. A few rules cover almost every call:

| Obj-C selector | JavaScript call |
|---|---|
| `[doc cropToRect:r]` | `doc.cropToRect(r)` |
| `[doc setCanvasSize:s usingAnchor:a]` | `doc.setCanvasSize_usingAnchor(s, a)` |
| `[doc writeToFile:p withUTI:u]` | `doc.writeToFile_withUTI(p, u)` |
| `[arr objectAtIndex:0]` | `arr[0]` *or* `arr.objectAtIndex_(0)` |
| `[arr count]` | `arr.count()` |

Rule: **each `:` in the selector becomes `_`** in the JS method name; the trailing `:` is dropped. Arguments are passed in normal JS order.

Cocoa structs have constructor helpers:

```javascript
NSMakeRect(x, y, w, h)
NSMakeSize(w, h)
NSMakePoint(x, y)
```

NSArray bridges to something array-like: `arr[i]` indexing and `arr.count()` both work; iterate with a normal `for` loop.

Comparing strings using === will not work for bridged NSStrings. Wrap bridged NSStrings like so:
`String(layer.layerName()) === 'Foo'`

## The `acorn` Global

Top-level entry points for the running app:

| Call | Purpose |
|---|---|
| `acorn.open(path)` | Open a file, return its `ACDocument` |
| `acorn.makeDocumentWithSize(NSMakeSize(w, h))` | Create a new untitled document |
| `acorn.orderedDocuments()` | Array of open documents (front-most first) |
| `acorn.activate()` | Bring Acorn to the foreground |
| `acorn.setPreference_forKey(value, key)` | Set an Acorn preference |

## ACDocument (the open file)

Common methods on a document returned by `acorn.open(...)` or `acorn.orderedDocuments()[0]`:

- **Layers:** `layers()`, `currentLayer()`, `baseGroup()`, `firstLayerWithName(name)`, `setSelectedLayer(layer)`
- **Canvas:** `canvasSize()`, `setCanvasSize(size)`, `setCanvasSize_usingAnchor(size, anchor)`, `dpi()`, `setDpi(size)`
- **Resize/crop:** `scaleImageToSize(size)`, `scaleImageToWidth(w)`, `scaleImageToHeight(h)`, `scaleImageWithPercentage(p)`, `resizeImageToSize(size)`, `cropToRect(rect)`, `trim()`
- **Selection:** `selectAll()`, `deselect()`, `selectFrame(rect)`, `selectOval(rect)`, `invertSelection()`
- **Filters & adjustments:** `callFilterNamed(name)`, `autoLevels()`
- **Blend / merge:** `mergeVisibleLayers()`, `copyMerged()`, `flipCanvasWithDirection("horizontal" | "vertical")`
- **Export:** `writeToFile_withUTI(path, uti)`, `webExportWithOptions({uti, file, quality, progressive})`, `dataOfType(uti).writeToFile(path)`
- **History:** `undo()`, `redo()`
- **Pixel/colour info:** `bitsPerPixel()`, `setBitsPerPixel(n)` *(32 / 64 / 128 only)*, `colorSpace()`, `CIImage()`

The `anchor` strings used by `setCanvasSize_usingAnchor` are: `"top left"`, `"top"`, `"top right"`, `"left"`, `"middle"`, `"right"`, `"bottom left"`, `"bottom"`, `"bottom right"`.

## Layers

Every layer conforms to `ACLayer`. Inspect `layerType()` against these constants from `ACPlugin.h`:

```
ACBitmapLayer = 1
ACShapeLayer  = 2
ACGroupLayer  = 3
ACMaskLayer   = 5
```

`ACLayer` properties: `layerName()` / `setLayerName(s)`, `visible()` / `setVisible(b)`, `opacity()` / `setOpacity(f)`, `compositingMode()` / `setBlendMode(name)`, `CIImage()`, `renderedCIImage()`, `opaqueBounds()`, `addMask()`, `mask()`, `setLayerMaskWithImageAtPath(path)`, `maskIsLinked()`, `setMaskIsLinked(b)`.

`ACGroupLayer` (including `baseGroup()`): `layers()`, `addLayer_atIndex(layer, idx)`, `addBitmapLayer()`, `addShapeLayer()`, `addGroupLayer()`, `insertImageWithPath_atIndex_withName(path, idx, name)`, `insertCGImage_atIndex_withName(img, idx, name)`.

`ACBitmapLayer`: `frame()`, `frameOrigin()` / `setFrameOrigin(p)`, `applyCIImageFromFilter(img)`, `applyCIImageFromFilter_shouldClipToSelection(img, b)`, `fillWithColor_shouldClipToSelection(color, b)`, `setNeedsDisplayInRect(r)`.

`ACShapeLayer`: `graphics()`, `selectedGraphics()`, `addRectangleWithBounds(r)`, `addOvalWithBounds(r)`, `addTextWithBounds(r)`, `addBezierPath(path)`, `moveGraphic_toIndex(g, idx)`.

## Graphics (vector shapes on shape layers)

Inspect `graphicType()` against:

```
ACRectangleGraphic = 1
ACOvalGraphic      = 2
ACLineGraphic      = 3
ACTextGraphic      = 4
ACImageGraphic     = 5
ACBezierGraphic    = 6
```

`ACGraphic` methods: `bounds()`, `bezierPath()`, `drawsFill()` / `setDrawsFill(b)`, `fillColor()` / `setFillColor(c)`, `drawsStroke()` / `setDrawsStroke(b)`, `strokeColor()` / `setStrokeColor(c)`, `strokeWidth()` / `setStrokeWidth(w)`, `hasCornerRadius()` / `setHasCornerRadius(b)`, `cornerRadius()` / `setCornerRadius(r)`, `hasShadow()` / `setHasShadow(b)`, `shadowBlurRadius()` / `setShadowBlurRadius(r)`, `shadowOffset()` / `setShadowOffset(s)`, `scaleXBy_yBy(x, y)`.

`ACTextGraphic` adds: `setString(s)`, `setHTMLString(html)`.

`ACBezierGraphic` adds: `ACBezierPath()`, `setACBezierPath(path)`, `anchorCount()`, `anchorAtIndex(i)`. Each `ACBezierAnchor` has `location()` / `setLocation(p)`, `leftCP()` / `rightCP()` (control points), `translateBy(p)`, and segment-type accessors.

## Common UTIs

For `writeToFile_withUTI` and `webExportWithOptions`:

| Format | UTI |
|---|---|
| PNG | `public.png` |
| JPEG | `public.jpeg` |
| JPEG 2000 | `public.jpeg-2000` |
| GIF | `com.compuserve.gif` |
| TIFF | `public.tiff` |
| HEIC | `public.heic` |
| Acorn native | `com.flyingmeat.acorn.image` |

## Minimal Examples

**Open and save as PNG:**
```javascript
var doc = acorn.open("/Users/me/Pictures/MyImage.acorn");
doc.dataOfType("public.png").writeToFile("/tmp/MyImage.png");
```

**Make a new 200×100 document:**
```javascript
var doc = acorn.makeDocumentWithSize(NSMakeSize(200, 100));
```

**Print the names of the top-level layers:**
```javascript
var doc = acorn.orderedDocuments()[0];
for (var i = 0; i < doc.layers().count(); i++) {
    print(doc.layers()[i].layerName());
}
```

**Crop, then undo:**
```javascript
var doc = acorn.orderedDocuments()[0];
doc.cropToRect(NSMakeRect(100, 100, 400, 400));
doc.undo();
```

**Apply a Core Image filter to the active layer:**
```javascript
var doc = acorn.orderedDocuments()[0];
doc.callFilterNamed("CIColorInvert");
```

**Web-export to JPEG at quality 25:**
```javascript
var doc = acorn.orderedDocuments()[0];
doc.webExportWithOptions({"uti": "public.jpeg", "quality": 25, "file": "/tmp/foo.jpeg"});
```


**Get the open documents as a JSON string:**
```javascript
function getOpenPaths() {
    var documentPaths = []
    
    for (var doc of acorn.allDocuments()) {
        // doc is a NSDocument subclass
        // fileURL will be null if the file is not saved yet.
        if (doc.fileURL() != null) {
            documentPaths.push(doc.fileURL().path())
        }
    }
    
    return documentPaths;
}

print(JSON.stringify(getOpenPaths(), null, 2));
```

**Take a layered screenshot:**
```javascript
var arrayOfDocuments = acorn.takeLayeredScreenshot();
```







## Common Mistakes

- **Forgetting to bridge selectors with underscores.** `setCanvasSize:usingAnchor:` is `setCanvasSize_usingAnchor`, not `setCanvasSizeUsingAnchor`.
- **Treating NSArray length like JS.** Use `arr.count()`, not `arr.length`. Indexing with `arr[i]` does work.
- **App Store sandboxing.** The Mac App Store build of Acorn cannot read/write arbitrary paths. Direct-from-flyingmeat.com builds have no such limitation; if a script works in one and not the other, this is the likely cause.
- **Quoting heredocs.** Use `<<'JS' ... JS` (single-quoted delimiter) to stop the shell from interpreting `$` and backticks inside the script.

## Reference

- More worked examples (AppleScript + JavaScript side-by-side): Flying Meat docs, "Example AppleScript and JavaScript Scripts"

## Finding Acorn's Install Path.

- If Acorn is not installed in the /Applications folder, the following shell command will find it for you:
```bash
mdfind "kMDItemCFBundleIdentifier == 'com.flyingmeat.Acorn8'" | head -1
```
