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:
fswrites 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 formatsFileExportArea— what part of the document to exportdoc.export()— synchronous exportdoc.promises.export()— async export- Checking export results
- Per-node export settings —
ExportConfig,ExportFormat,ExportScale fsmodule — reading and writing filesnetworkmodule — HTTP requests from scripts
Export overview
Three things combine to describe an export operation:
| Object | What it defines |
|---|---|
FileExportOptions | Format (PNG, JPEG, PDF, SVG, etc.) and format-specific settings |
FileExportArea | Which part of the document (whole doc, current spread, artboard, selection) |
| Path string | Where 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).
ExportConfighas 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/sizeson formats;appendFormat/replaceFormat/deleteFormat/enumerateFormats/formatson the config;setExportScale/createWithExportScaleon sizes), and re-apply withDocumentCommand.createSetExportConfig(selection, config). For one-off scripted exports without per-node settings, plaindoc.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()throwsPERMISSION_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
nullto 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
| Example | What it shows |
|---|---|
bitmapWriter.js | ExportConfig, 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
'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
'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
'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).');
}
}
}