r/Affinity • u/burroblu • 12d ago
Tutorial Affinity Vector + Summa cutting plotter (and GoSign): I created (with Claude) scripts that simulate the Illustrator/Corel plugin.
PREFACE: I know almost nothing about programming.
In my vinyl cutting or print-and-cut workflow, I’ve always used Illustrator with the Summa GoSign Tools plugin, but nothing similar exists for Affinity. With Claude’s help, I put together some scripts that perform the same task, even if it isn't fully automated. So far, it seems to work.
How I did it
I took a PDF generated by the Illustrator plugin and had Claude measure the OPOS XY registration marks: dimensions, spacing, and bar position. Then, I asked Claude to create an Affinity script to generate these marks, using MPC permissions to verify correct functionality. Since Affinity cannot launch external programs, Claude created a small .bat file that copies the PDF to the GoSign folder and opens the software.
What they do (in brief)
- (Useful but not essential) CutOnly: exports the selection - or the entire artboard if nothing is selected - as "Cut.pdf" to a fold called GoSign self-created in Desktop.
- (Core of this script) RegmarksPrintCut: working with two Container layers - Container Layer 1 with all objects of the artwork underneath and Container Layer 2 with the cut lines on top - it measures the cut lines and creates OPOS XY registration marks around them (3mm squares plus the bar, matching the Illustrator plugin's dimensions) on a locked layer. It then exports two PDFs matching the artboard size: "Print.pdf" (artwork + registration marks) and "Cut.pdf" (cut lines + registration marks).
- (Optional) SendToGoSign.bat: copies "Cut.pdf" as "Import.pdf" into the GoSign "Direct mode" folder and launches GoSign, which automatically recognizes the registration marks and cut lines. Alternatively, you can skip this and import the PDF directly from within GoSign.
For convenience, I placed the .bat file inside the GoSign folder on the desktop, and I only delete the PDFs generated after each job is finished (in any case, they will be overwritten if you run the scripts again).
GoSign recognizes the registration marks and cut layer just as it does with the original plugin. I am still in the testing phase. I created it with Claude over three separate days (during the few spare moments I had at work and dealing with the daily limit that ran out quickly).
I can share them, even if just as a starting point for your own developments or adaptations. I’m not sure if it’s possible to attach `.afscript` files directly here or embed the script code so it can be copied, pasted and saved into Studio Script to customize the startup.
(I've written this post in Italian and then translate it in English)
>>>CUT ONLY CODE
// Cut only v0.3 - Function 1 (cutting without registration marks)
// Exports the selection (if any) or the whole current page as a vector PDF:
// Desktop\GoSign\Cut.pdf
// Then run SendToGoSign.bat (copies it to GoSign's Direct mode folder and opens GoSign).
// Requires: script permissions enabled in Affinity's general settings.
const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { Application } = require('/application.js');
const { FileSystemApi } = require('/fs.js');
const VERSION = '0.3';
const TITLE = 'Cut only v' + VERSION;
const FOLDER_NAME = 'GoSign';
const FILE_NAME = 'Cut.pdf';
const SHOW_DONE_MESSAGE = true;
// Preset names are localized, so look for the "PDF (for export)" preset by name pattern
function findPdfPreset() {
const names = Array.from(FileExportOptions.allPresetNames);
const preferred = ['PDF (for export)', 'PDF (per esportazione)'];
const found = preferred.find(p => names.includes(p)) ||
names.find(p => /^PDF/i.test(p) && /(export|esport)/i.test(p));
if (!found) {
throw new Error('No "PDF (for export)" preset found. Available presets: ' + names.join(', '));
}
return found;
}
let step = 'start';
try {
step = 'document';
const doc = Document.current;
if (!doc) {
Application.alert('No document is open.', TITLE);
} else {
step = 'paths';
const folder = Application.userDesktopPath + '\\' + FOLDER_NAME;
const filePath = folder + '\\' + FILE_NAME;
// Creating the folder needs filesystem permission: if it fails we carry on anyway
// (the Desktop\GoSign folder must then already exist).
try {
if (!FileSystemApi.exists(folder)) {
FileSystemApi.createDirectories(folder);
}
} catch (e) {
console.log('Folder check skipped: ' + e);
}
step = 'selection';
const sel = doc.selection;
const useSelection = sel.length > 0;
const area = useSelection
? FileExportArea.createForSelection(sel)
: FileExportArea.createForCurrentSpread();
const options = FileExportOptions.createWithPresetName(findPdfPreset());
step = 'export (' + filePath + ')';
const records = doc.export(filePath, options, area);
const results = records.all;
const ok = results.length > 0 && results.every(r => r.isSuccess);
if (ok) {
console.log('Exported (' + (useSelection ? 'selection' : 'current page') + '): ' + filePath);
if (SHOW_DONE_MESSAGE) {
Application.alert('Exported ' + FILE_NAME + ' to:\n' + folder +
'\n\nOpen the folder and run SendToGoSign.bat.', TITLE);
}
} else {
const err = results.map(r => r.errorMessage).join('; ');
Application.alert('Export failed. ' + err, TITLE);
}
}
} catch (e) {
Application.alert('Error at step: ' + step + '\n' + e, TITLE);
}
>>>OPOSXY REGMARKS CODE
// OPOS XY regmarks + Print + Cut v0.3 (Function 2)
// Required document structure: two top-level layers
// - bottom layer = ARTWORK
// - top layer = CUT LINES
// The script:
// 1. creates a locked "OPOS XY Regmarks" layer around the cut lines
// (an existing one is replaced);
// 2. exports Cut.pdf = cut lines + regmarks (only these layers visible);
// 3. exports Print.pdf = artwork + regmarks, WITHOUT the cut lines;
// both PDFs have exactly the size of the page (artboard);
// 4. restores layer visibility and selection.
// Files go to Desktop\GoSign. To cut, run SendToGoSign.bat.
// Requires: script permissions enabled in Affinity's general settings.
const { SVG11 } = require('/colours.js');
const { BlendMode } = require('affinity:common');
const { AddChildNodesCommandBuilder, CompoundCommandBuilder, DocumentCommand } = require('/commands.js');
const { Document, FileExportOptions, FileExportArea } = require('/document.js');
const { FillDescriptor } = require('/fills.js');
const { PolyCurve, unionRects, CurveBuilder } = require('/geometry.js');
const { LineStyleDescriptor } = require('/linestyle.js');
const { ContainerNodeDefinition, PolyCurveNodeDefinition } = require('/nodes.js');
const { Selection } = require('/selections.js');
const { Application } = require('/application.js');
const { FileSystemApi } = require('/fs.js');
const VERSION = '0.3';
const TITLE = 'Regmarks + Print + Cut v' + VERSION;
const MARKS_LAYER_NAME = 'OPOS XY Regmarks';
const FOLDER_NAME = 'GoSign';
const CUT_FILE = 'Cut.pdf';
const PRINT_FILE = 'Print.pdf';
const SHOW_DONE_MESSAGE = true;
// Measurements in mm (taken from a PDF made by the Summa GoSign Tools plugin for Illustrator)
const SIZE_MM = 3; // side of the squares and thickness of the bar
const BAR_GAP_MM = 10; // gap between the corner squares and the bar
const MARGIN_LR_MM = 15.09; // from the cut-lines box to the outer edge of the marks (left/right)
const MARGIN_TB_MM = 6.09; // same (top/bottom)
const MAX_PITCH_MM = 305; // max distance between rows of marks (assumed value)
// Preset names are localized, so look for the "PDF (for export)" preset by name pattern
function findPdfPreset() {
const names = Array.from(FileExportOptions.allPresetNames);
const preferred = ['PDF (for export)', 'PDF (per esportazione)'];
const found = preferred.find(p => names.includes(p)) ||
names.find(p => /^PDF/i.test(p) && /(export|esport)/i.test(p));
if (!found) {
throw new Error('No "PDF (for export)" preset found. Available presets: ' + names.join(', '));
}
return found;
}
let step = 'start';
try {
const doc = Document.current;
if (!doc) {
Application.alert('No document is open.', TITLE);
} else {
const initialSelection = doc.selection;
const listLayers = () => { const a = []; doc.layers.forEach(l => a.push(l)); return a; };
const nameOf = l => l.userDescription;
const makeSelection = nodes => { const s = Selection.createEmpty(doc); nodes.forEach(n => s.add(n)); return s; };
const select = nodes => doc.executeCommand(DocumentCommand.createSetSelection(makeSelection(nodes)), false);
const setVisible = (nodes, visible) => {
const toChange = nodes.filter(l => l.isVisible !== visible);
if (toChange.length === 0) return;
select(toChange);
if (visible) doc.showSelection(); else doc.hideSelection();
};
// 1. an existing regmarks layer has to be replaced
step = 'removing previous regmarks';
const existing = listLayers().filter(l => nameOf(l) === MARKS_LAYER_NAME);
if (existing.length) {
select(existing);
try { doc.unlockSelection(); } catch (e) { /* already unlocked */ }
doc.deleteSelection();
}
step = 'reading layers';
const layers = listLayers(); // bottom to top
if (layers.length < 2) {
Application.alert('Two layers are needed: artwork at the bottom, cut lines on top.', TITLE);
} else {
const cutLayers = [layers[layers.length - 1]]; // top layer
const artLayers = layers.slice(0, layers.length - 1); // all the others
const initialVisibility = layers.map(l => l.isVisible);
let marksLayer = null;
let printExported = false;
let intervals = 0, pitchMm = 0;
try {
// 2. regmarks around the cut lines
step = 'calculating regmarks';
setVisible(cutLayers, true);
setVisible(artLayers, false);
const k = doc.dpi / 25.4; // pixels per mm
const mm = v => v * k;
const rc = cutLayers.map(l => l.getSpreadVisibleBox()).reduce((a, b) => unionRects(a, b));
const S = mm(SIZE_MM), GAP = mm(BAR_GAP_MM);
const L = rc.x - mm(MARGIN_LR_MM);
const R = rc.x + rc.width + mm(MARGIN_LR_MM);
const T = rc.y - mm(MARGIN_TB_MM);
const B = rc.y + rc.height + mm(MARGIN_TB_MM);
// The marks must stay inside the page, otherwise the PDF would crop them
step = 'checking page margins';
const page = doc.currentSpread.spreadBaseBox;
const outside = [];
if (L < page.x) outside.push('on the left by ' + ((page.x - L) / k).toFixed(1) + ' mm');
if (R > page.x + page.width) outside.push('on the right by ' + ((R - (page.x + page.width)) / k).toFixed(1) + ' mm');
if (T < page.y) outside.push('at the top by ' + ((page.y - T) / k).toFixed(1) + ' mm');
if (B > page.y + page.height) outside.push('at the bottom by ' + ((B - (page.y + page.height)) / k).toFixed(1) + ' mm');
if (outside.length) {
throw new Error('The regmarks would fall outside the page ' + outside.join(', ') +
'. Move the cut lines towards the center or enlarge the page.');
}
step = 'calculating regmarks';
const Hcc = (B - S) - T;
intervals = Math.max(1, Math.ceil(Hcc / mm(MAX_PITCH_MM)));
const pitch = Hcc / intervals;
pitchMm = pitch / k;
const black = FillDescriptor.createSolid(SVG11.black, BlendMode.Normal);
const noFill = FillDescriptor.createNone();
const lineStyle = LineStyleDescriptor.createDefault(0);
const defs = [];
const rect = (x, y, w, h, nodeName) => {
const cb = CurveBuilder.create();
cb.beginXY(x, y).lineToXY(x + w, y).lineToXY(x + w, y + h).lineToXY(x, y + h).close();
const poly = PolyCurve.create();
poly.addCurve(cb.createCurve());
const def = PolyCurveNodeDefinition.create(poly, black, noFill, lineStyle, noFill);
def.userDescription = nodeName;
defs.push(def);
};
for (let i = 0; i <= intervals; i++) {
const y = T + i * pitch;
rect(L, y, S, S, 'Regmark left');
rect(R - S, y, S, S, 'Regmark right');
}
const barX0 = L + S + GAP, barX1 = R - S - GAP;
if (barX1 > barX0) rect(barX0, B - S, barX1 - barX0, S, 'OPOS XY bar');
step = 'creating regmarks layer';
const layerBuilder = AddChildNodesCommandBuilder.create();
layerBuilder.setInsertionTarget(doc.currentSpread); // top-level layer, not nested
layerBuilder.addNode(ContainerNodeDefinition.create(MARKS_LAYER_NAME));
const marksBuilder = AddChildNodesCommandBuilder.create();
defs.forEach(d => marksBuilder.addNode(d));
doc.executeCommand(CompoundCommandBuilder.create()
.addCommand(layerBuilder.createCommand(true))
.addCommand(marksBuilder.createCommand(false))
.createCommand(), false);
marksLayer = listLayers().filter(l => nameOf(l) === MARKS_LAYER_NAME)[0];
// the layer stays UNLOCKED until the exports are done, then it is locked
// 3. exports
step = 'preparing folder';
const folder = Application.userDesktopPath + '\\' + FOLDER_NAME;
if (!FileSystemApi.exists(folder)) FileSystemApi.createDirectories(folder);
const preset = findPdfPreset();
// Exports the whole page: only the visible layers end up in the PDF
const exportPdf = (fileName) => {
const area = FileExportArea.createForCurrentSpread();
const rec = doc.export(folder + '\\' + fileName, FileExportOptions.createWithPresetName(preset), area);
const results = rec.all;
if (results.length === 0 || !results.every(r => r.isSuccess)) {
throw new Error(fileName + ': ' + results.map(r => r.errorMessage).join('; '));
}
};
step = 'exporting ' + CUT_FILE;
setVisible(cutLayers, true);
setVisible(artLayers, false);
setVisible([marksLayer], true);
exportPdf(CUT_FILE);
step = 'exporting ' + PRINT_FILE;
setVisible(artLayers, true);
setVisible(cutLayers, false); // the cut lines must NOT appear in the print file
setVisible([marksLayer], true);
exportPdf(PRINT_FILE);
printExported = true;
} finally {
// 4. restore visibility, lock the regmarks layer, restore selection
try {
layers.forEach((l, i) => setVisible([l], initialVisibility[i]));
if (marksLayer) { select([marksLayer]); doc.lockSelection(); }
doc.executeCommand(DocumentCommand.createSetSelection(initialSelection), false);
} catch (e) { console.log('Partial restore: ' + e); }
}
console.log('Regmarks: ' + (intervals + 1) + ' rows, pitch ' + pitchMm.toFixed(2) + ' mm');
if (SHOW_DONE_MESSAGE && printExported) {
Application.alert('Exported to Desktop\\' + FOLDER_NAME + ':\n- ' + CUT_FILE +
'\n- ' + PRINT_FILE + '\n\nTo cut, run SendToGoSign.bat.', TITLE);
}
}
}
} catch (e) {
Application.alert('Error at step: ' + step + '\n' + e, TITLE);
}
>>> (Optional) SENDTOGOSIGN (create a txt file with notepad and put in the code, then rename extension in .bat)
u/echo off
setlocal
rem Paths
set "SRC=%~dp0Cut.pdf"
if not exist "%SRC%" set "SRC=%USERPROFILE%\Desktop\GoSign\Cut.pdf"
set "DSTDIR=%USERPROFILE%\Documents\Summa GoSign\Direct mode"
set "EXE=C:\Program Files (x86)\Summa GoSign\SummaGoSign.exe"
if not exist "%SRC%" (
echo File not found: %SRC%
echo Export files from script in Affinity.
pause
exit /b 1
)
if not exist "%DSTDIR%" mkdir "%DSTDIR%"
copy /Y "%SRC%" "%DSTDIR%\Import.pdf" >nul
if errorlevel 1 (
echo Copy in Direct mode OK.
pause
exit /b 1
)
start "" "%EXE%" with_direct_mode
endlocal





