Skip to main content

Lesson 14 — Special Modules: Pixel Access, Brushes & More

This lesson surveys the SDK modules that extend the core API with powerful but less commonly discovered capabilities: direct pixel access, brush inspection, binary buffers, and document metadata. Each module is introduced with its key types, access patterns, and a practical example.

These modules unlock whole categories of scripts the core API alone can't support — pixel-level image processing, binary data handling, and document metadata inspection.

Each module is self-contained and solves a different problem, so this lesson works as a survey rather than a single narrative. Each section covers what the module contains, what problem it solves, the correct access pattern, and a minimal example. /pixelaccessor gets the full treatment in Lesson 14; font enumeration is covered in full in Lesson 15.

One module deserves special attention: /pixelaccessor. Its access pattern isn't obvious from the name — you don't instantiate a reader directly, you obtain one with the free functions createPixelReader(bitmap) / createPixelReaderWriter(bitmap), or with the typed PixelReaderWriterRGBA8.create(bitmap). Knowing that up front avoids the natural but wrong assumption that you're looking for a class constructor.

Use this lesson as a map. When a future script needs something you're not sure the SDK provides, this is the first place to look.

What this lesson covers​

This lesson documents the SDK's supporting modules that don't fit in the earlier lessons — direct pixel read/write, per-node metadata, document/drawing-scale properties, brushes, image resources, picture frames, timers, unit conversion, hatch patterns, system and environment modules, and the additional node types. Font enumeration is covered in Lesson 15.

  • /pixelaccessor — read and write individual pixels
  • /taginterface — per-node custom metadata
  • /documentproperties, /drawingscale, /marginsinterface — document properties
  • /rasterbrush, /pathbrush — brush property access
  • /imageresourceinterface — placed-image file metadata
  • /pictureframeinterface — picture-frame state
  • /rasterselection — pixel-selection state
  • /timers — Timer objects
  • /units — UnitValueConverter DPI conversions
  • /hatch — building hatch patterns
  • /os, /environment, /logging, /configuration, /inspect — system modules
  • DocumentCommand method reference, AddChildNodesCommandBuilder
  • Additional node types — TableTextNode, PatternRasterNode, EnclosureRasterNode, MeasurementNode, DevelopNode

Modules at a glance​

ModuleWhat it adds
/pixelaccessorRead and write individual pixels in any colour format
/fontsFont enumeration — covered in full in Lesson 15
/rasterbrushInspect and modify raster brush properties
/bufferBinary buffer creation and manipulation
/rasterobjectRaster object wrapper (used by brushes)
/documentpropertiesDocument metadata — DPI, size, colour space
/drawingscaleDrawing scale (map scale) access
/taginterfacePer-node custom key-value tag storage
/marginsinterfaceMargin information on spread nodes
/pageboxinterfacePage box (bleed, trim, slug) access
/artboardpropertiesArtboard metadata
/imageresourceinterfacePlaced-image source file metadata
/pictureframeinterfacePicture-frame state on vector nodes
/rasterselectionRead-only pixel-selection state
/timersTimer objects behind setTimeout/setInterval
/unitsUnitValueConverter DPI-based unit conversion
/hatchHatchPattern / HatchLine construction
/osHost OS info — platform, arch, version, line endings
/environmentRuntime permissions, filesystem roots, SDK version
/loggingFile-based logging via LogFile
/configurationParse JSON config into a typed tree
/inspectNode.js-style inspect for deep object introspection
/pathbrushVector path brush property access

/pixelaccessor — Direct pixel read/write​

Read and write individual pixels across all of Affinity's colour formats. The accessor class must match the bitmap's format exactly — mismatches throw INVALID_ARGS.

'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { PixelReaderWriterRGBA8 } = require('/pixelaccessor.js');

const doc = Document.current;
const node = doc?.selection.nodes.filter(n => n.isRasterNode || n.isImageNode).first;
if (!node) { console.log('Select a raster or image node.'); }
else {
const ri = node.rasterInterface;

// Always check format before creating an accessor — mismatch throws INVALID_ARGS
if (String(ri.format) !== 'RGBA8') {
console.log('Format is', ri.format, '— this example requires RGBA8.');
console.log('Open an RGBA8 image (File > New or a standard JPEG/PNG) and run again.');
} else {
const bitmap = ri.createCompatibleBitmap(true); // true = copy contents
const rw = PixelReaderWriterRGBA8.create(bitmap);

// Read a pixel — returns { r, g, b, alpha } all 0–255
// Note: the alpha key is 'alpha', not 'a'
const px = rw.readPixel(10, 10);
console.log('Pixel at (10,10):', px.r, px.g, px.b, px.alpha);

// Write a pixel
rw.writePixel(10, 10, { r: 255, g: 0, b: 0, alpha: 255 });

// Push changes back — see 14a-pixel-inverter.js for a full loop example
doc.executeCommand(DocumentCommand.createReplaceBitmap(doc.selection, bitmap));
console.log('Pixel written.');
}
}

All supported pixel formats​

ClassFormatBits/channelNotes
PixelReaderRGBA8 / PixelReaderWriterRGBA8RGBA8-bitStandard web/screen
PixelReaderRGBA16 / PixelReaderWriterRGBA16RGBA16-bitHigh precision
PixelReaderIA8 / PixelReaderWriterIA8Intensity+Alpha8-bitGreyscale
PixelReaderIA16 / PixelReaderWriterIA16Intensity+Alpha16-bitGreyscale HD
PixelReaderCMYKA8 / PixelReaderWriterCMYKA8CMYK+Alpha8-bitPrint
PixelReaderLABA16 / PixelReaderWriterLABA16Lab*+Alpha16-bitPerceptual
PixelReaderM8 / PixelReaderWriterM8Mask8-bitSingle channel
PixelReaderM16 / PixelReaderWriterM16Mask16-bitSingle channel HD
PixelReaderRGBAuf / PixelReaderWriterRGBAufRGBAFloatHDR
PixelReaderMf / PixelReaderWriterMfMaskFloatHDR single channel

PixelReaderWriterRGBA8.create(bitmap) throws INVALID_ARGS if the format isn't RGBA8. Always check node.rasterInterface.format before creating an accessor. Use String(ri.format) to compare — the format value is an enum object, not a plain string.

Open the image as a document (File > Open) rather than placing it on a page. A placed image has isImageNode: true and isRasterNode: undefined — createReplaceBitmap silently does nothing on it. If you need to process a placed image in-place, rasterise it first via DocumentCommand.createRasteriseObjects(doc.selection, false, false).


/taginterface — Per-node custom metadata​

TagInterface lets you write arbitrary string key-value pairs to any node via DocumentCommand. These persist inside the .af file. You can also read them back at runtime through node.tagInterface (hasKey, getValueForKey, hasPredefinedKey, getValueForPredefinedKey). See Lesson 03 — Per-node tag metadata for the full read/write pattern.

'use strict';
const { PredefinedTagKey } = require('/taginterface.js');
const { DocumentCommand } = require('/commands.js');
const { Document } = require('/document.js');

const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else if (doc.selection.nodes.isEmpty) { console.log('Select a node first.'); }
else {
const sel = doc.selection;
const node = sel.nodes.first;

// Write a custom tag (persists in the file)
doc.executeCommand(
DocumentCommand.createSetTagValueForKey(sel, 'my-script-data', 'hello world')
);

// Write a predefined tag
doc.executeCommand(
DocumentCommand.createSetTagValueForPredefinedKey(
sel, PredefinedTagKey.ScriptLabel, 'my-label')
);

// Read a tag back
const tagIface = node.tagInterface;
console.log('Has my-script-data:', tagIface.hasKey('my-script-data'));
console.log('Value:', tagIface.getValueForKey('my-script-data'));
}

/documentproperties — Document metadata​

'use strict';
const { Document } = require('/document.js');

// Document properties are accessed directly on the doc object
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
console.log('DPI: ', doc.dpi);
console.log('Width (px): ', doc.widthPixels);
console.log('Height (px): ', doc.heightPixels);
console.log('Format: ', doc.format);
console.log('Colour profile:', doc.colourProfile);
}

doc.documentProperties does not exist as a property. Document metadata is read directly from doc.dpi, doc.widthPixels, doc.format, etc. The DocumentProperties export from /commands is an internal type used by createSetDocumentProperties — it is not user-constructable.


/drawingscale — Drawing/map scale​

For technical drawing and mapping workflows:

'use strict';
const { DrawingScale } = require('/drawingscale.js');

// Create a 1:100 scale
const scale = DrawingScale.createFromIntegers(1, 100, true);
console.log(scale.toString(false, true, false)); // "1 : 100"
console.log('Left:', scale.leftValue);
console.log('Right:', scale.rightValue);

// Get default scales for a unit
// IMPORTANT: pass a raw integer — NOT UnitType.* (those return undefined and cause INVALID_ARGS)
// 8 = Millimetres, 9 = Centimetres, 15 = Metres, 11 = Inches, 5 = Pixels
const defaults = DrawingScale.getDefaults(8); // 8 = Millimetres
for (const s of defaults) {
console.log(s.toString(true, true, false));
}

// Identity scale (1:1)
const identity = DrawingScale.identity;

DrawingScale.createFromString('1:50', …) parses a ratio string, and DrawingScale.enumerateDefaults(units, callback) enumerates the defaults from getDefaults via callback instead of returning an array.


/marginsinterface — Spread margins​

Current limitation: spread.marginsInterface returns undefined — the MarginsInterface class is defined with hasMargins and useMargins properties, but MarginsInterfaceApi has no exported methods, making it inaccessible at runtime.

Page geometry can be accessed via spread.physicalRootPropertiesInterface:

'use strict';
const { Document } = require('/document.js');

const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const spread = doc.currentSpread;

// Available page geometry
const prpi = spread.physicalRootPropertiesInterface;
const props = spread.physicalRootInterface?.physicalRootProperties;

console.log('Page count:', prpi?.pageCount);

// Page bounding box (in document pixels)
const pbi = prpi?.pageBoxInterface;
if (pbi) {
const box = pbi.getPageBoundingBox(0); // index 0 = first page
console.log('Page box:', box);
}
}

/rasterbrush — Brush property access​

RasterBrush objects are returned from other API calls (for example, from a node's brush state). The class is fully functional once you have a handle:

'use strict';
const { RasterBrush, BrushDynamic, BrushDynamicControllerType, RasterBrushTextureMode } = require('/rasterbrush.js');

// Once you have a brush handle, inspect its properties:
// console.log('Size value:', brush.size.value);
// console.log('Opacity:', brush.opacity);
// console.log('Spacing:', brush.spacing);
// console.log('Blend mode:', brush.blendMode);

/os — Host operating system info​

Read the host platform, architecture, OS version, and line-ending convention. Static properties only — no instance needed.

'use strict';
const { OS } = require('/os.js');

console.log('Platform:', OS.platform); // "Darwin", "Windows", "Linux"
console.log('Arch: ', OS.arch); // "arm64", "x64"
console.log('Machine: ', OS.machine);
console.log('Release: ', OS.release);
console.log('OS name: ', OS.osName, OS.osVersion);
console.log('EOL: ', JSON.stringify(OS.Eol)); // "\n" or "\r\n"

/environment — Runtime permissions & config​

Environment reports what the script is allowed to do (filesystem, network, AI) and which folders it can touch, plus the internal SDK version.

'use strict';
const { Environment, EnvironmentPermission } = require('/environment.js');

console.log('Permissions:', Environment.permissions); // { fileSystem, network, genAI }
console.log('Can use network:', Environment.hasPermission(EnvironmentPermission.Network));
console.log('FS roots:', Environment.fileSystemRoots); // ["/Users/you/Desktop"]
console.log('SDK ABI:', Environment.sdkVersionStr); // internal SDK ABI string, not the app version
console.log('V8 version:', Environment.v8VersionStr);
console.log('Heap:', Environment.getHeapStatistics());

// Run a task after the current synchronous turn
Environment.postTask(() => console.log('queued'));
// Environment.quit(); // exit the app

Environment.enumerateFileSystemRoots(callback) enumerates the same roots via callback instead of returning the array.

Environment.sdkVersionStr is the internal SDK ABI version, not the app version. Use app.shortVersion from /application for the Affinity build number.


/logging — File-based logging​

LogFile mirrors the console output to a file at a given verbosity level. Create it, start(), write logs via the normal console.* methods, then flush() and stop().

'use strict';
const { LogFile, LogLevel } = require('/logging.js');
const { app } = require('/application.js');

const log = LogFile.create(app.userDesktopPath + '/script.log', LogLevel.Info, true);
log.start();
console.info('This is captured');
console.debug('This is excluded (below Info level)');
log.flush();
log.logLevel = LogLevel.Error; // raise the threshold mid-run
log.stop();

LogLevel values: All, Trace, Debug, Info, Warn, Error, None.


/configuration — Typed JSON config​

ConfigurationItem.createFromJsonString(json, allowComments) parses JSON into a typed tree you can read with getProperty, getArrayItem, and typed accessors.

'use strict';
const { ConfigurationItem } = require('/configuration.js');

const cfg = ConfigurationItem.createFromJsonString(
'{ "width": 1200, "title": "Poster", "tags": ["a", "b"] }'
);
console.log(cfg.value); // { width: 1200, title: "Poster", ... }
console.log(cfg.getPropertyInt('width')); // 1200
console.log(cfg.getPropertyString('title')); // "Poster"
console.log(cfg.getArrayItem('tags')?.value); // ["a", "b"]
console.log(cfg.isObject, cfg.isArray);

/inspect — Node.js-style object inspector​

/inspect provides a Node.js-compatible util.inspect for the Affinity runtime. It produces readable string representations of any value — including HandleObject instances whose internal state lives behind prototype getters. Getters that throw render as <error: ...> rather than failing the whole inspect.

'use strict';
const { inspect } = require('/inspect.js');

// Basic usage
console.log(inspect(doc));
console.log(inspect(doc.selection.nodes.first, { depth: 3 }));

// Format string (printf-style: %s %d %i %f %j %o %O)
const { format } = require('/inspect.js');
console.log(format('Node: %s, Type: %s', name, type));

// Automatically format all console output
const { patchConsole } = require('/inspect.js');
patchConsole({ depth: 4, getters: true });
console.log(doc); // now formatted by inspect
// ... later, when done:
const { unpatchConsole } = require('/inspect.js');
unpatchConsole();

Key options: depth (default 2, null = unlimited), showHidden (non-enumerable), getters (true/'get'/'set'), sorted (lexicographic sort), enums ('brief' shows RGBA8, 'full' shows RasterFormat.RGBA8, 'raw' shows the enum object).


/pathbrush — Vector path brush access​

PathBrush exposes a vector brush's width, size/opacity variance, and corner behaviour. Like RasterBrush, there is no createDefault() — obtain a brush from an existing node's brush state.

'use strict';
const { PathBrush, CornerStrategy, PathBrushDynamicControllerType } = require('/pathbrush.js');

// Obtain a PathBrush from a node's brush state:
// const brush = node.pathBrushInterface?.pathBrush;
// console.log(brush.brushWidth, brush.sizeVariance, brush.opacityVariance);
// console.log(brush.cornerStrategy); // CornerStrategy.Pull/Fold/Overlap

PathBrush.createDefault() throws — same as RasterBrush. Brushes are read from node state, not constructed from scratch. CornerStrategy = Pull/Fold/Overlap; PathBrushDynamicControllerType = None/Pressure/Velocity/VelocityInverse.


/imageresourceinterface — Placed-image metadata​

node.imageResourceInterface exposes everything about the source file behind an image node: its path, byte size, file type, original DPI, ICC profile, placement mode, and thumbnails. Available on ImageNode and EmbeddedDocumentNode.

'use strict';
const { Document } = require('/document.js');

const doc = Document.current;
const node = doc?.selection.nodes.first;

if (!node) { console.log('Select a node.'); }
else {
const iri = node.imageResourceInterface;
if (!iri) { console.log('Not a placed image.'); }
else {
console.log('Path: ', iri.imageFilePath ?? '(embedded)');
console.log('Bytes: ', iri.imageFileSize);
console.log('Type: ', iri.fileTypeName);
console.log('Original DPI:', iri.originalDPI);
console.log('Placement: ', iri.imagePlacement);
console.log('Modified: ', iri.modifiedTime);
}
}

saveOriginalFile(filename) writes the original source file back out to Desktop, and getSmallThumbnail() / getLargeThumbnail() return preview bitmaps — handy for building asset-catalogue scripts.


/pictureframeinterface — Picture-frame state​

Vector nodes can act as picture frames — rectangles that clip content placed inside them. node.pictureFrameInterface reads that state; DocumentCommand.createPopulatePictureFrame(contentNode, selection) places a node into the frame programmatically.

// Snippet — assumes node is a vector node
const pfi = node.pictureFrameInterface;
if (pfi) {
console.log('Frame enabled: ', pfi.enabled);
console.log('Has contents: ', pfi.hasFrameContents);
if (pfi.hasFrameContents) {
console.log('Contents: ', pfi.frameContents?.descriptionInterface?.description);
console.log('Anchor: ', pfi.anchor);
}
}

calculateAnchor(node, hint) and calculateConstraints(node, hint) reproduce the fitting logic the UI uses when content is placed, letting you predict how an image will sit inside the frame before committing.


/rasterselection — Pixel-selection state​

doc.rasterSelection returns a read-only snapshot of the current pixel-selection state. Use it to avoid running selection-mutating commands pointlessly — for example, skipping rasterInvertSelection when nothing is selected.

// Snippet — assumes doc is defined
const rs = doc.rasterSelection;
console.log('Has pixel selection:', rs.isCurrentPixelSelection);
console.log('Is select-all/none: ', rs.isSelectAllOrNone);
console.log('Is select-none: ', rs.isSelectNone);

The state-changing commands themselves (createRasterSelectAll, createSetRasterSelectionFromPolygon, …) are covered in Lesson 14.


/timers — Timer objects​

Beyond the setTimeout/setInterval shims (covered in Lesson 12), the /timers module exposes the underlying Timer class for high-resolution scheduling.

'use strict';
const { Timer } = require('/timers.js');

const t = Timer.create();
t.expiryFromNow = 2.5; // seconds from now
t.waitAsync((err) => {
if (!err) console.log('2.5s elapsed:', Timer.now);
});

Timer.now / Timer.nowBigInt read the monotonic clock, Timer.cancelAll() clears every pending timer, and setTimeout(delay, callback) returns a Timer you can cancel() later. Remember the shim's argument order — delay first, callback second.


/units — UnitValue and conversion factors​

doc.unitValueConverter converts between unit types using the document DPI — essential whenever the UI shows points but the SDK works in document pixels.

'use strict';
const { Document } = require('/document.js');
const { UnitType } = require('/units.js');

const doc = Document.current;
if (doc) {
const converter = doc.unitValueConverter;
console.log('DPI:', converter.dpi, '| view DPI:', converter.viewDpi);

const pxPerPt = converter.getConversionFactor(UnitType.Point, UnitType.Pixel);
console.log('1 pt =', pxPerPt, 'px');

const mmPerInch = converter.getConversionFactor(UnitType.Inch, UnitType.Millimetre);
console.log('1 in =', mmPerInch, 'mm');
}

UnitValue itself (re-exported from /units) carries prototype helpers: getValueAsPixels(converter), getValueAsUnitType(converter, unitType), getValueAsDegrees(), getValueAsRadians(), and isFinite.


/hatch — Building hatch patterns​

Lesson 05 showed how to apply a hatch fill; the /hatch module is where the patterns themselves come from. A HatchPattern is a list of HatchLines, each with an origin, rotation, step distance, and optional dash pattern — and patterns are fully constructible, unlike brushes.

'use strict';
const { HatchPattern, HatchLine } = require('/hatch.js');

const pattern = HatchPattern.createEmpty();
const line = HatchLine.create(
{ x: 0, y: 0 }, // origin
Math.PI / 4, // rotation (radians)
8, // step between lines
[4, 2] // dash pattern (4 on, 2 off) — omit for solid
);
pattern.appendHatchLine(line);
console.log('Lines in pattern:', pattern.hatchLineCount);

Pass the finished pattern to HatchFill.create(pattern, units, penColour, brushColour, lineWeight) from Lesson 05. HatchPattern.createDefault() and .all (enumerating the built-in library patterns) are also available.


DocumentCommand — full method reference​

commands.js exposes 43 static factory methods across the following groups:

Macro recording/playback​

// Snippet
DocumentCommand.createStartRecordingMacro()
DocumentCommand.createStopRecordingMacro()
DocumentCommand.createReplayMacro()
DocumentCommand.createClearMacro()
DocumentCommand.createImportMacro(path)
DocumentCommand.createExportMacro(path)

Raster selection commands​

// Snippet
DocumentCommand.createRasterSelectAll()
DocumentCommand.createRasterDeselect()
DocumentCommand.createRasterInvertSelection()
DocumentCommand.createRasterReselect()
DocumentCommand.createSetRasterSelectionFromPolygon(polygon, operation, antialias, feather) // replaces createRasterSelectPolygon

Opacity control (separate from visibility)​

// Snippet
DocumentCommand.createSetOpacity(selection, opacity) // layer opacity
DocumentCommand.createSetBrushFillOpacity(selection, opacity, options) // fill opacity
DocumentCommand.createSetLineFillOpacity(selection, opacity, options) // stroke opacity

Fill anchor control​

// Snippet
DocumentCommand.createSetBrushFillIsAnchoredToSpread(selection, bool, options)
DocumentCommand.createSetPenFillIsAnchoredToSpread(selection, bool, options)
DocumentCommand.createSetTransparencyFillIsAnchoredToSpread(selection, bool, options)

Tag metadata​

// Snippet
DocumentCommand.createSetTagValueForKey(selection, key, value)
DocumentCommand.createSetTagValueForPredefinedKey(selection, predefinedKey, value)

Blend options​

// Snippet
DocumentCommand.createSetBlendGamma(selection, gamma)
DocumentCommand.createSetBlendRanges(selection, blendOptions)
DocumentCommand.createSetAntialiasingMode(selection, antialiasingMode)

// Read the current values via node.blendModeInterface.blendOptions:
// .gamma, and spline-controlled channel ranges
// (getChannelSourceLayerRanges / getChannelUnderlyingCompositionRanges)
// Build range splines with Spline.createFromProfile(profile) /
// Spline.createFromPoints(points), then insertPoint / replaceOrInsertPoint.

Document-level​

// Snippet
DocumentCommand.createSetDocumentProperties(documentProperties)
DocumentCommand.createConvertDocumentFormat(format)
DocumentCommand.createGroupTransform(selection, xDataOrNull, yDataOrNull)
DocumentCommand.createSetWindingMode(selection, windingOrder)
DocumentCommand.createSetExportConfig(selection, exportConfig)
DocumentCommand.createMoveNodes(selection, target, moveType, childListType)
DocumentCommand.createHideSelection(selection)
DocumentCommand.createFlatten()
DocumentCommand.createMergeVisible()
DocumentCommand.createSplitCurve(selection, parametricDistance)
DocumentCommand.createInsertGlyph(selection, glyph)

Snapshots​

// Snippet
DocumentCommand.createAddDocumentSnapshot(description)
DocumentCommand.createDeleteDocumentSnapshot(snapshot)
DocumentCommand.createRestoreDocumentSnapshot(snapshot)
DocumentCommand.createSetCurrentSnapshot(snapshot)
DocumentCommand.createSetCurrentSnapshotFromHistoryItem(historyItem)

// Open a snapshot as a new document: snapshot.createDocument(),
// or async via snapshot.createDocumentAsync(callback).

History navigation​

// Snippet
DocumentCommand.createSetHistoryIndex(index)
DocumentCommand.createCycleAlternateFutures(index)

createSetEditable — argument order​

Selection is the first argument:

// Snippet
// CORRECT: selection first, editable flag second
DocumentCommand.createSetEditable(selection, true) // unlock
DocumentCommand.createSetEditable(selection, false) // lock

AddChildNodesCommandBuilder — insertion options​

Setters for controlling insertion behaviour:

'use strict';
const { AddChildNodesCommandBuilder, InsertionMode } = require('/commands.js');
const builder = AddChildNodesCommandBuilder.create();
builder.autoName = true; // auto-assign layer name
builder.forcePixelAlignment = true; // snap to pixel grid
builder.clearCurrentSelection = false; // don't deselect before adding
builder.appendCurrentSelection = true; // add to existing selection
builder.setInsertionTarget(targetNode); // insert relative to a node
builder.setInsertionTargetSelection(targetSel); // insert relative to a selection
builder.setInsertionMode(InsertionMode.After); // Before / After / Into

addContainerNode adds a container node, and createCommandAndReset allows reusing the builder.


SDK native binding modules​

Two native C++ binding modules underpin the pixel and brush APIs:

ModuleContains
affinity:rasterAll PixelReader*Api and PixelReaderWriter*Api classes
affinity:brushesRasterBrushApi, BrushDynamicApi, brush enums


New node types​

Three new concrete node classes are available:

TableTextNode​

A grid-based text node with cells separated by HardBreakType.TableCell glyphs. See Lesson 07 for the full pattern.

// Snippet — assumes doc is defined
const { TableTextNodeDefinition } = require('/nodes.js');
const def = TableTextNodeDefinition.create({ x: 0, y: 0, width: 300, height: 200 }, { width: 3, height: 3 });
doc.addNode(def);

PatternRasterNode​

A raster node that tiles a repeating pattern. Useful for procedural fill effects.

// Snippet — assumes doc is defined
const { PatternRasterNodeDefinition } = require('/nodes.js');
const def = PatternRasterNodeDefinition.createDefault();

EnclosureRasterNode​

A raster node that wraps raster content in an enclosure. Less commonly used directly — typically created through other SDK operations.

// Snippet — assumes doc is defined
const { EnclosureRasterNodeDefinition } = require('/nodes.js');
const def = EnclosureRasterNodeDefinition.createDefault();

All three use standard node patterns: doc.addNode(def), AddChildNodesCommandBuilder for insertion targeting, and access to rasterInterface / storyInterface where applicable.

MeasurementNode​

A measurement annotation — a dimension line between two points. Create it with MeasurementNodeDefinition.create(start, end, factor, unitType) and add it via builder.addMeasurementNode().

'use strict';
const { MeasurementNodeDefinition } = require('/nodes.js');
const { AddChildNodesCommandBuilder } = require('/commands.js');
const { UnitType } = require('/units.js');
const { Document } = require('/document.js');

const doc = Document.current;
if (doc) {
const def = MeasurementNodeDefinition.create(
{ x: 100, y: 100 }, // start point
{ x: 400, y: 100 }, // end point
1.0, // scale factor
UnitType.Pixel // measurement unit
);
const builder = AddChildNodesCommandBuilder.create();
builder.addMeasurementNode(def);
doc.executeCommand(builder.createCommand(true));
console.log('Measurement added.');
}

Companion DocumentCommand factories configure measurement display globally: createSetMeasurementUnits(unitType), createSetMeasurementPrecision(useDocumentPrecision, decimalPlaces), createSetMeasurementAnnotationOffset(node, offset), and createSetMeasurementShowEndpointMarkers(show). Note there is no isMeasurementNode type guard.

DevelopNode​

Represents a RAW image in the Develop persona. Read its state via isDevelopNode, .parameters (a DevelopParameters), and .sourceIsRaw; push changes back with createSetDevelopParameters(sel, developParameters). See Lesson 16 for the full develop workflow.


Exercises​

Exercise 14.1 — Pixel inverter​

Select a raster node. Read every pixel and write back its inverted colour (255-r, 255-g, 255-b). Use PixelReaderWriterRGBA8.

Exercise 14.2 — Tag all selected nodes​

Build a dialog asking for a key and a value. Apply that tag to every selected node using createSetTagValueForKey.

Exercise 14.3 — Macro recorder UI​

Build a dialog with buttons to start recording, stop recording, replay, and clear a macro. Wire each to the corresponding DocumentCommand.

Exercise 14.4 — Raster selection from polygon​

Create a rectangular raster selection programmatically using createSetRasterSelectionFromPolygon (replaces createRasterSelectPolygon), then invert it with createRasterInvertSelection.

Exercise 14.5 — Image asset catalogue​

Walk every ImageNode in the document and log its layer name, source file path, original DPI, and file size using imageResourceInterface. Flag any image below 150 DPI with a [LOW-RES] marker.


SDK modules used in this lesson​

  • /pixelaccessor — All PixelReader* and PixelReaderWriter* classes
  • /taginterface — TagInterface, PredefinedTagKey
  • /rasterbrush — RasterBrush, BrushDynamic
  • /imageresourceinterface — ImageResourceInterface
  • /pictureframeinterface — PictureFrameInterface
  • /rasterselection — RasterSelection
  • /timers — Timer
  • /units — UnitType, UnitValue, UnitValueConverter
  • /hatch — HatchPattern, HatchLine
  • /drawingscale — DrawingScale
  • /marginsinterface — MarginsInterface
  • /documentproperties — DocumentProperties
  • /commands — updated DocumentCommand, AddChildNodesCommandBuilder
  • /nodes — TableTextNodeDefinition, PatternRasterNodeDefinition, EnclosureRasterNodeDefinition, MeasurementNodeDefinition, DevelopNode
  • /os — OS
  • /environment — Environment, EnvironmentPermission
  • /logging — LogFile, LogLevel
  • /configuration — ConfigurationItem, ConfigurationValueType
  • /inspect — inspect, format, patchConsole, unpatchConsole
  • /pathbrush — PathBrush, CornerStrategy, PathBrushDynamicControllerType

See also​


Example scripts​

14a-pixel-inverter.js​

Download 14a-pixel-inverter.js

14a-pixel-inverter.js
'use strict';

// Example 13a — Pixel inverter
// Lesson 13: New modules
//
// Inverts every pixel in the selected raster node.
// Demonstrates the PixelReaderWriterRGBA8 API.
//
// Open the image as a document (File > Open), not as a placed image on a page.
// Select the layer and run.

const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { PixelReaderWriterRGBA8 } = require('/pixelaccessor.js');

const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const node = doc.selection.nodes.filter(n => n.isRasterNode || n.isImageNode).first;
if (!node) {
console.log('Select a raster or image node and run again.');
} else {
const ri = node.rasterInterface;
const w = ri.width;
const h = ri.height;
if (String(ri.format) !== 'RGBA8') {
console.log('Format is', ri.format, '— this script requires RGBA8. Open an RGBA8 image and run again.');
} else {
const bitmap = ri.createCompatibleBitmap(true); // copy contents
const rw = PixelReaderWriterRGBA8.create(bitmap);

console.log(`Inverting ${w}×${h} pixels...`);

for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const px = rw.readPixel(x, y);
rw.writePixel(x, y, {
r: 255 - px.r,
g: 255 - px.g,
b: 255 - px.b,
alpha: px.alpha
});
}
}

doc.executeCommand(DocumentCommand.createReplaceBitmap(doc.selection, bitmap));
console.log('Done — pixels inverted.');
}
}
}

14b-resource-inspector.js​

Download 14b-resource-inspector.js

14b-resource-inspector.js
'use strict';

// Example 13b — Resource inspector
// Lesson 13: Special Modules
//
// Reports image resource metadata, picture-frame state, blend options,
// and DPI-aware unit conversions for the selected node.

const { Document } = require('/document.js');
const { UnitType } = require('/units.js');

const doc = Document.current;
const node = doc?.selection.nodes.first;

if (!node) { console.log('Select a node.'); }
else {
const iri = node.imageResourceInterface;
if (iri) {
console.log('Image file: ', iri.imageFilePath ?? '(embedded)');
console.log('File size: ', iri.imageFileSize, 'bytes');
console.log('Original DPI: ', iri.originalDPI);
console.log('File type: ', iri.fileTypeName);
} else {
console.log('Not a placed image (no imageResourceInterface).');
}

const pfi = node.pictureFrameInterface;
if (pfi) {
console.log('Picture frame enabled:', pfi.enabled);
console.log('Has frame contents: ', pfi.hasFrameContents);
if (pfi.hasFrameContents) {
console.log('Contents:', pfi.frameContents?.descriptionInterface?.description ?? '(unnamed)');
}
}

const opts = node.blendModeInterface?.blendOptions;
if (opts) {
console.log('Blend gamma:', opts.gamma);
}

const pxPerPt = doc.unitValueConverter.getConversionFactor(UnitType.Point, UnitType.Pixel);
console.log('1 pt =', pxPerPt, 'px at', doc.dpi, 'dpi');
}