Skip to main content

Lesson 09 — Export & File I/O

Getting content out of Affinity — as PNGs, PDFs, SVGs, or any of the 60+ supported formats — is one of the most valuable things a script can do. This lesson covers the full export pipeline: choosing a format, defining what to export, writing to disk, and optionally generating a JSON manifest. It also covers plain-text file I/O and HTTP requests.

Export automation is where scripts deliver the most time savings. A script that exports every artboard to a named PNG, writes a manifest, and uploads to an API can replace hours of manual work. The pieces are FileExportOptions, FileExportArea, doc.export(), the File class, and HttpRequest.

Exporting involves two independent choices: the format preset (PNG, PDF, SVG, etc.) and the export area (the whole document, a single artboard, the current spread, or just the selection). This lesson walks through the most common combinations.

Two things to remember:

  • fs writes files to the Desktop or folders you have granted access to (JSON manifests, config, any text output).
  • HTTP requests are executed with req.do().

By the end you'll have a complete export pipeline: format selection, area targeting, file writing, manifest generation, and external communication. These example scripts are among the most directly reusable in the tutorial.

What this lesson covers​

  • FileExportOptions — choosing and configuring export formats
  • FileExportArea — what part of the document to export
  • doc.export() — synchronous export
  • doc.promises.export() — async export
  • Checking export results
  • Per-node export settings — ExportConfig, ExportFormat, ExportScale
  • fs module — reading and writing files
  • network module — HTTP requests from scripts

Export overview​

Three things combine to describe an export operation:

ObjectWhat it defines
FileExportOptionsFormat (PNG, JPEG, PDF, SVG, etc.) and format-specific settings
FileExportAreaWhich part of the document (whole doc, current spread, artboard, selection)
Path stringWhere the file goes (Desktop + granted folders)

Getting available export presets​

FileExportOptions.allPresetNames lists every format preset available in the current Affinity session.

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

const presets = FileExportOptions.allPresetNames;
console.log('Available presets:');
for (const name of presets) {
console.log(' ', name);
}
// PNG, JPEG, PDF, SVG, EPS, TIFF, ...

FileExportOptions.createForCanvaExport(dpi) builds options tuned for Canva export at the given DPI.


Synchronous export​

Call doc.export() with a format preset, area, and output path — returns an array of result records.

'use strict';
const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { app } = require('/application.js');

const doc = Document.current;
const desktop = app.userDesktopPath;

if (!doc) { console.log('No document open.'); }
else {
const opts = FileExportOptions.createWithPresetName('PNG');
const area = FileExportArea.createForWholeDocument();
const outPath = `${desktop}/${doc.title}.png`;

const records = doc.export(outPath, opts, area, null);

for (const rec of records.all) {
if (rec.isSuccess) {
console.log('Exported to:', rec.path);
} else {
console.log('Export failed:', rec.errorMessage);
}
}
}

Export areas​

FileExportArea provides factory methods for targeting different parts of the document — whole doc, current spread, specific artboards, pages, or selected nodes.

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

const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
// Whole document
const wholeDoc = FileExportArea.createForWholeDocument();

// Current spread only
const spread = FileExportArea.createForCurrentSpread();

// Current page only
const page = FileExportArea.createForCurrentPage();

// Specific artboard — ai comes from spread.getArtboard(i)
// const ai = doc.spreads.first.getArtboard(0);
// const artboard = FileExportArea.createForArtboard(ai);

// Selection bounding box
const selArea = FileExportArea.createForSelectionArea(doc.selection);

// Specific pages by string (e.g. "1-3,5")
const pages = FileExportArea.createForPages('1-3');

// Specific spreads by string
const spreads = FileExportArea.createForSpreads('1-2');

// Exact selection bounding box (vs selectionArea which pads to page)
const sel = FileExportArea.createForSelection(doc.selection);

console.log('Export areas ready.');
}

Export every artboard​

Iterate spreads and artboards, exporting each one as a separate file named after the artboard.

'use strict';
const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { app } = require('/application.js');

const doc = Document.current;
const desktop = app.userDesktopPath;

if (!doc) { console.log('No document open.'); }
else {
const opts = FileExportOptions.createWithPresetName('PNG');

for (const spread of doc.spreads) {
for (let i = 0; i < spread.artboardCount; i++) {
const ai = spread.getArtboard(i); // returns ArtboardInterface directly
const name = ai.description ?? `artboard-${i}`;
const area = FileExportArea.createForArtboard(ai);
const path = `${desktop}/${doc.title}_${name}.png`;

const records = doc.export(path, opts, area, null);
const rec = records.all[0]; // DocumentExportRecords -> DocumentExportRecord
console.log(rec?.isSuccess ? `OK: ${path}` : `FAIL: ${name}`);
}
}
}

Per-node export settings (ExportConfig)​

Beyond scripting a one-off doc.export() call, each node can carry its own export configuration — the same settings the Export persona writes. These live on node.exportableInterface.exportConfig and follow a three-level hierarchy: config → formats → sizes, where each size wraps an ExportScale (multiplier, fixed width/height, square, or preset).

'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { ExportScale, ExportSize } = require('/exportconfig.js');

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

if (!node) { console.log('Select a node.'); }
else {
const config = node.exportableInterface?.exportConfig;
if (!config) {
console.log('No export config on this node yet.');
} else {
for (const format of config.formats) {
for (const size of format.sizes) {
const s = size.exportScale;
console.log('size — multiplier:', s.hasMultiplier ? s.multiplier : `${s.width}x${s.height}`);
}
}

if (config.formatCount > 0) {
const twoX = ExportSize.createWithExportScale(ExportScale.createWithMultiplier(2));
config.formats[0].appendSize(twoX);
doc.executeCommand(DocumentCommand.createSetExportConfig(doc.selection, config));
console.log('2x size appended.');
}
}
}

ExportScale factories: createWithMultiplier(m), createWithWidth(w), createWithHeight(h), createWithSquare(size), createWithWidthHeight(w, h), createWithMultiplierSquare(m, size), and createWithPreset(preset) for the named presets. ExportFormat.createWithFileExportOptions(options) derives a format from the same FileExportOptions used by doc.export(). Read scales back via sizeType / width / height (companions to hasMultiplier / multiplier / size).

ExportConfig has no constructor. It can only be obtained from a node that already has one — set it up once via the UI Export persona, then read, modify (appendSize / replaceSize / deleteSize / enumerateSizes / sizes on formats; appendFormat / replaceFormat / deleteFormat / enumerateFormats / formats on the config; setExportScale / createWithExportScale on sizes), and re-apply with DocumentCommand.createSetExportConfig(selection, config). For one-off scripted exports without per-node settings, plain doc.export() is simpler.


Async export with doc.promises​

Use DocumentPromises.getCurrent() and docP.export() for async export workflows that don't block the UI.

'use strict';
async function exportAsync() {
const { DocumentPromises, FileExportOptions, FileExportArea } = require('/document.js');
const { app } = require('/application.js');

const docP = await DocumentPromises.getCurrent();
if (!docP) { console.log('No doc.'); return; }

const opts = FileExportOptions.createWithPresetName('PDF');
const area = FileExportArea.createForWholeDocument();
const desktop = app.userDesktopPath;
const path = `${desktop}/output.pdf`;

try {
const records = await docP.export(path, opts, area, null);
for (const rec of records.all) {
console.log(rec.isSuccess ? 'Exported: ' + rec.path : 'Failed.');
}
} catch (err) {
console.log('Export error:', err);
}
}

exportAsync();

Saving the document​

doc.save() writes in place; doc.saveAs(path) writes to a new location. New documents require saveAs() first.

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

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

// Save in place (fails if mustSaveAs is true)
doc.save();

// Save to a new path
doc.saveAs(`${desktop}/my-file.af`);

// Async versions
// doc.promises.save();
// doc.promises.saveAs(`${desktop}/my-file.af`);
}

File system — reading and writing text​

Use File from /fs to read and write text files. Only the Desktop and explicitly granted folders are accessible.

'use strict';
const { File } = require('/fs.js');
const { Buffer } = require('/buffer.js');
const { app } = require('/application.js');

const desktop = app.userDesktopPath;

// Write a text file
const wf = File.create(`${desktop}/output.txt`, 'w');
wf.writeString('Hello from Affinity SDK');
wf.close();
console.log('File written.');

// Read a text file
const rf = File.create(`${desktop}/output.txt`, 'r');
const buf = Buffer.create(rf.length);
rf.read(buf, buf.length);
rf.close();
console.log(buf.toString());

Desktop and explicitly granted folders. Paths outside the Desktop or a folder you have granted access to return PERMISSION_DENIED.


JSON sidecar pattern​

A common pattern — export assets and write a JSON manifest alongside them:

'use strict';
const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { app } = require('/application.js');
const { File } = require('/fs.js');

const doc = Document.current;
const desktop = app.userDesktopPath;

if (!doc) { console.log('No document open.'); }
else {
const opts = FileExportOptions.createWithPresetName('PNG');
const manifest = [];

for (const spread of doc.spreads) {
for (let i = 0; i < spread.artboardCount; i++) {
const ai = spread.getArtboard(i); // returns ArtboardInterface directly
const name = ai.description ?? `artboard-${i}`;
const path = `${desktop}/${name}.png`;

const area = FileExportArea.createForArtboard(ai);
const records = doc.export(path, opts, area, null);
if (records.all[0]?.isSuccess) {
manifest.push({ name, path, exported: new Date().toISOString() });
}
}
}

if (manifest.length === 0) {
console.log('No artboards found — add artboards to your document first.');
} else {
const manifestPath = `${desktop}/manifest.json`;
const mf = File.create(manifestPath, 'w');
mf.writeString(JSON.stringify(manifest, null, 2));
mf.close();
console.log(`Manifest written: ${manifest.length} artboard(s).`);
}
}

Promise-based file reads and disk space​

file.promises.read() / .write() take an optional position argument and resolve to result objects rather than byte counts. FileSystemPromises.getSpace() resolves to { available, free, capacity } and accepts an optional useBigInts flag for large volumes:

// Snippet — assumes file is an open File, buf is a Buffer, FileSystemPromises is required from /fs.js, desktop is a path string
const { bytesRead, buffer } = await file.promises.read(buf, 0, buf.length, 0);
const space = await FileSystemPromises.getSpace(desktop);
console.log(`Read ${bytesRead} bytes; ${space.free} free.`);

Network requests​

Network access requires permission. In Affinity Settings, the user must enable network access for scripts. If not enabled, HttpRequest.create() throws PERMISSION_DENIED.

'use strict';
const { HttpRequest, RequestMethod } = require('/network.js');

// GET request — uses httpbin.org, a public HTTP testing service
try {
const req = HttpRequest.create('https://httpbin.org/get', RequestMethod.Get);
const result = req.do();
const response = result.response;
console.log('Status:', response.statusCode); // statusCode is an enum object — use == 200 not === 200
console.log('Body: ', response.content);
} catch(e) {
console.log('Network error:', e.message);
// PERMISSION_DENIED → enable network access in Affinity Settings
// INVALID_HANDLE → the server could not be reached (bad URL or no connectivity)
}

// POST with JSON body
try {
const req = HttpRequest.create('https://httpbin.org/post', RequestMethod.Post);
req.setHeaderValue('Content-Type', 'application/json');
const result = req.do();
console.log('POST status:', result.response.statusCode); // statusCode is an enum object — use == 200 not === 200
} catch(e) {
console.log('Network error:', e.message);
}

req.do() returns an HttpRequestResult — read result.response for the HttpResponse (statusCode, content) and result.reason on failure. req.getHeaderValue(name) reads back a previously set header. Further tuning setters cover the user-agent header, RFC 2047 header encoding, metered-network use, and chunked-transfer avoidance.


Key gotchas​

response.statusCode == 200 not === 200. statusCode is an enum object, not a number or string. Strict equality always returns false. Use loose equality (==) or statusCode.equals(200).

Paths outside the Desktop or a granted folder fail with PERMISSION_DENIED.

mustSaveAs on new documents. Call doc.saveAs() first before doc.save() or export — a fresh document has no path.

Check rec.isSuccess on every record. Export can partially succeed with warnings. Always iterate records.all and check each record.

Export area is optional — pass null to export the whole document by default.


Exercises​

Exercise 9.1 — Export current spread​

Export the current spread as a PNG to the Desktop, named after the document title.

Exercise 9.2 — Export all artboards​

Export every artboard in the document as separate PNG files. Name each file after the artboard.

Exercise 9.3 — JSON manifest​

After exporting all artboards, write a manifest.json to the Desktop listing each exported file path, artboard name, and timestamp.

Exercise 9.4 — Export with dialog​

Combine Lesson 08 (Dialog) with this lesson: show a dialog to choose the export format and output path suffix, then export.

Exercise 9.5 — Export config inspector​

Log the export configuration of every selected node (formats, sizes, scales). Where a config exists, append a 3x size and re-apply it with createSetExportConfig.


SDK modules used in this lesson​

  • /document — Document, FileExportOptions, FileExportArea, DocumentPromises
  • /exportconfig — ExportConfig, ExportFormat, ExportScale, ExportSize, ExportScalePreset
  • /application — app
  • /fs — File, Buffer, Directory, FileSystemPromises
  • /network — HttpRequest, RequestMethod, HttpStatusCode

Further examples​

ExampleWhat it shows
bitmapWriter.jsExportConfig, ExportSize, pixel-level bitmap writing via /buffer and /rasterobject
logToFile.js/fs file writing with LogFile, Directory, FileSystemPromises

See also​


Example scripts​

09a-export-artboards-with-manifest.js​

Download 09a-export-artboards-with-manifest.js

09a-export-artboards-with-manifest.js
'use strict';

// Example 09a — Export all artboards with JSON manifest
// Lesson 09: Export & Files
//
// Exports every artboard in the document as PNG files to the Desktop.
// Writes a manifest.json alongside them.
//
// NOTE: spread.getArtboard(i) returns the ArtboardInterface directly.
// Pass it straight to FileExportArea.createForArtboard().
// The artboard name is in .description, not .name.

const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { app } = require('/application.js');
const { File } = require('/fs.js');

const doc = Document.current;
const desktop = app.userDesktopPath;

if (!doc) { console.log('No document open.'); }
else {
const opts = FileExportOptions.createWithPresetName('PNG');
const manifest = [];
let exported = 0;
let failed = 0;
let total = 0;

for (const spread of doc.spreads) total += spread.artboardCount;

if (total === 0) {
console.log('No artboards found. Add artboards to your document first.');
} else {
for (const spread of doc.spreads) {
for (let i = 0; i < spread.artboardCount; i++) {
const ai = spread.getArtboard(i); // returns ArtboardInterface directly
const name = (ai.description ?? `artboard-${exported + failed + 1}`)
.replace(/[^a-zA-Z0-9_-]/g, '_');
const path = `${desktop}/${doc.title}_${name}.png`;

const area = FileExportArea.createForArtboard(ai);
const records = doc.export(path, opts, area, null);
const rec = records.all[0];

if (rec?.isSuccess) {
console.log('OK:', path);
manifest.push({
artboard: ai.description ?? name,
file: path,
timestamp: new Date().toISOString(),
});
exported++;
} else {
console.log('FAIL:', name, rec?.errorMessage ?? '');
failed++;
}
}
}

const manifestPath = `${desktop}/${doc.title}_manifest.json`;
const mf = File.create(manifestPath, 'w');
mf.writeString(JSON.stringify(manifest, null, 2));
mf.close();

console.log(`\nDone. ${exported} exported, ${failed} failed.`);
console.log('Manifest:', manifestPath);
}
}

09b-network-request.js​

Download 09b-network-request.js

09b-network-request.js
'use strict';

// Example 09b — Network requests
// Lesson 09: Export & Files
//
// Demonstrates GET and POST requests using the /network module.
// Uses httpbin.org — a public HTTP testing service that echoes requests back.
//
// Before running: enable network access in Affinity Settings > Scripting.

const { HttpRequest, RequestMethod } = require('/network.js');

// GET request
try {
const req = HttpRequest.create('https://httpbin.org/get', RequestMethod.Get);
const result = req.do();
const response = result.response;
console.log('GET status:', response.statusCode); // statusCode is an enum object — use == 200 not === 200
console.log('GET body: ', response.content);
} catch (e) {
console.log('Network error:', e.message);
// PERMISSION_DENIED → enable network access in Affinity Settings
// INVALID_HANDLE → server could not be reached (bad URL or no connectivity)
}

// POST with JSON body
try {
const req = HttpRequest.create('https://httpbin.org/post', RequestMethod.Post);
req.setHeaderValue('Content-Type', 'application/json');
const result = req.do();
console.log('POST status:', result.response.statusCode); // statusCode is an enum object — use == 200 not === 200
console.log('POST body: ', result.response.content);
} catch (e) {
console.log('Network error:', e.message);
}

09c-node-export-config.js​

Download 09c-node-export-config.js

09c-node-export-config.js
'use strict';

// Example 09c — Node export config
// Lesson 09: Export & Files
//
// Reads the per-node export settings on a selected node, appends a 2x
// PNG size, and re-applies the config. Note: ExportConfig has no
// constructor — it can only be read from a node that already has one
// (set via the UI Export persona first).

const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const { ExportScale, ExportSize } = require('/exportconfig.js');

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

if (!node) { console.log('Select a node.'); }
else {
const config = node.exportableInterface?.exportConfig;
if (!config) {
console.log('This node has no export config yet.');
console.log('Set one up once via the UI (Export persona), then re-run.');
} else {
console.log('Existing formats:', config.formatCount);
for (const format of config.formats) {
console.log(' format sizes:', format.sizeCount);
for (const size of format.sizes) {
const s = size.exportScale;
console.log(' multiplier:', s.hasMultiplier ? s.multiplier : `${s.width}x${s.height}`);
}
}

if (config.formatCount > 0) {
const twoX = ExportSize.createWithExportScale(ExportScale.createWithMultiplier(2));
config.formats[0].appendSize(twoX);
doc.executeCommand(DocumentCommand.createSetExportConfig(doc.selection, config));
console.log('Appended 2x size — format now has', config.formats[0].sizeCount, 'size(s).');
}
}
}