// illustrator-batch-ai-to-svg-eps.jsx // Free script from LinearAndLoom Icons: https://linearandloomicons.netlify.app/scripts/ // Provided as is. Try it on a copy of your files first. /* AI Convert - Adobe Illustrator script ------------------------------------------------------------ Converts Adobe Illustrator (.ai) files to SVG, EPS (Illustrator 10), or BOTH. You choose the mode when the script runs. EPS files are resized so the artboard is as large as possible while staying UNDER 25 megapixels (at 72 ppi, 1 pt = 1 px). The scale is uniform, so proportions, layout, strokes, patterns and gradients stay exactly as drawn. Small files are scaled up and oversized files are scaled down. SVG files are exported at the original size (before any resizing). Output files are saved in "SVG" and/or "EPS" folders next to the source .ai files. Original .ai files are never modified. Existing output files are skipped, so it is safe to run again. (Delete old EPS files if you want them regenerated at the new size.) HOW TO RUN Illustrator: File > Scripts > Other Script... > choose this file. 1. A dialog asks: SVG only / EPS only / Both 2. Pick the folder that contains your .ai files (or any parent folder). Done. OUTPUT FOLDER STRUCTURE YourFolder/ yourfile.ai <- never touched SVG/ yourfile.svg <- created if SVG or Both chosen EPS/ yourfile.eps <- created if EPS or Both chosen (resized to < 25 MP) */ #target illustrator (function () { // ── Settings ────────────────────────────────────────────────────────────── var MAX_PIXELS = 25000000; // must stay strictly below this (25 MP) var SAFETY = 0.9995; // tiny margin so rounding never reaches 25 MP var MAX_SIDE = 16383; // Illustrator canvas limit in points // ── 1. Ask the user which conversion to run ────────────────────────────── var dlg = new Window("dialog", "AI Convert — Choose Output"); dlg.orientation = "column"; dlg.alignChildren = ["fill", "top"]; dlg.margins = 24; dlg.spacing = 14; var lbl = dlg.add("statictext", undefined, "Convert .ai files to:"); lbl.graphics.font = ScriptUI.newFont("dialog", ScriptUI.FontStyle.BOLD, 13); var grp = dlg.add("group"); grp.orientation = "column"; grp.alignChildren = ["left", "center"]; grp.spacing = 10; var r1 = grp.add("radiobutton", undefined, "SVG only"); var r2 = grp.add("radiobutton", undefined, "EPS only (Illustrator 10, resized to max under 25 MP)"); var r3 = grp.add("radiobutton", undefined, "Both SVG and EPS"); r3.value = true; // default: both var btnGrp = dlg.add("group"); btnGrp.alignment = "right"; btnGrp.spacing = 10; var btnCancel = btnGrp.add("button", undefined, "Cancel", { name: "cancel" }); var btnOK = btnGrp.add("button", undefined, "Next: Choose Folder »", { name: "ok" }); btnOK.onClick = function () { dlg.close(1); }; btnCancel.onClick = function () { dlg.close(0); }; var result = dlg.show(); if (result !== 1) { return; } var doSVG = r1.value || r3.value; var doEPS = r2.value || r3.value; // ── 2. Pick folder ──────────────────────────────────────────────────────── var root = Folder.selectDialog("Select the folder with the .ai files"); if (!root) { return; } var previousLevel = app.userInteractionLevel; app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS; var svgConverted = 0, svgSkipped = 0; var epsConverted = 0, epsSkipped = 0; var failed = []; var sizeLog = []; // ── SVG export options ──────────────────────────────────────────────────── function svgOptions() { var o = new ExportOptionsSVG(); o.DTD = SVGDTDVersion.SVG1_1; o.fontType = SVGFontType.OUTLINEFONT; // text as outlines o.embedRasterImages = true; o.preserveEditability = false; o.includeFileInfo = false; o.includeUnusedStyles = false; o.coordinatePrecision = 4; o.documentEncoding = SVGDocumentEncoding.UTF8; o.cssProperties = SVGCSSPropertyLocation.STYLEATTRIBUTES; return o; } // ── EPS save options (Illustrator 10, Shutterstock spec) ────────────────── function epsOptions() { var o = new EPSSaveOptions(); o.compatibility = Compatibility.ILLUSTRATOR10; o.preview = EPSPreview.COLORTIFF; o.embedAllFonts = true; o.embedLinkedFiles = true; o.includeDocumentThumbnails = true; o.cmykPostScript = false; o.postScript = EPSPostScriptLevelEnum.LEVEL2; return o; } // ── Unlock / show all layers (recursively), remembering their state ────── function openLayers(layers, states) { for (var i = 0; i < layers.length; i++) { var L = layers[i]; states.push({ layer: L, locked: L.locked, visible: L.visible }); if (!L.visible) { L.visible = true; } if (L.locked) { L.locked = false; } if (L.layers.length) { openLayers(L.layers, states); } } } function restoreLayers(states) { // reverse order: sublayers are restored before their parents for (var i = states.length - 1; i >= 0; i--) { var s = states[i]; try { s.layer.locked = s.locked; } catch (e) {} try { s.layer.visible = s.visible; } catch (e) {} } } // ── Resize artwork so artboard 1 is the largest size under 25 MP ───────── // Uniform scale about the artboard centre; every artboard and every // top-level object is scaled by the same factor, so nothing shifts // relative to anything else. function fitUnder25MP(doc) { var prevCS = app.coordinateSystem; app.coordinateSystem = CoordinateSystem.DOCUMENTCOORDINATESYSTEM; var prevScaleStrokes = null; try { prevScaleStrokes = app.preferences.getBooleanPreference("scaleLineWeight"); app.preferences.setBooleanPreference("scaleLineWeight", true); } catch (e) { prevScaleStrokes = null; } var layerStates = []; var info = null; try { var r = doc.artboards[0].artboardRect; // [left, top, right, bottom] var w = Math.abs(r[2] - r[0]); var h = Math.abs(r[1] - r[3]); if (w <= 0 || h <= 0) { return null; } // Largest uniform scale that keeps w*h under 25 MP var s = Math.sqrt((MAX_PIXELS * SAFETY) / (w * h)); if (w * s > MAX_SIDE) { s = MAX_SIDE / w; } if (h * s > MAX_SIDE) { s = MAX_SIDE / h; } while (Math.ceil(w * s) * Math.ceil(h * s) >= MAX_PIXELS) { s *= 0.9999; } info = { w: Math.round(w * s), h: Math.round(h * s), mp: (Math.ceil(w * s) * Math.ceil(h * s) / 1000000).toFixed(2), capped: (w * s >= MAX_SIDE - 0.5 || h * s >= MAX_SIDE - 0.5) }; if (Math.abs(s - 1) < 0.000001) { return info; } // already the right size var cx = (r[0] + r[2]) / 2; var cy = (r[1] + r[3]) / 2; var pct = s * 100; openLayers(doc.layers, layerStates); // Collect top-level objects only (children move with their parent) var all = doc.pageItems, list = [], i; for (i = 0; i < all.length; i++) { if (all[i].parent.typename === "Layer") { list.push(all[i]); } } for (i = 0; i < list.length; i++) { var it = list[i]; var wasLocked = it.locked, wasHidden = it.hidden; if (wasLocked) { it.locked = false; } if (wasHidden) { it.hidden = false; } var b0 = it.geometricBounds; var ox = (b0[0] + b0[2]) / 2, oy = (b0[1] + b0[3]) / 2; // scale positions, fill patterns, gradients, stroke patterns, line widths it.resize(pct, pct, true, true, true, true, pct, Transformation.CENTER); // move the object so its centre lands at the scaled position var b1 = it.geometricBounds; var nx = (b1[0] + b1[2]) / 2, ny = (b1[1] + b1[3]) / 2; it.translate((cx + (ox - cx) * s) - nx, (cy + (oy - cy) * s) - ny); if (wasHidden) { it.hidden = true; } if (wasLocked) { it.locked = true; } } // Scale every artboard about the same centre for (i = 0; i < doc.artboards.length; i++) { var a = doc.artboards[i].artboardRect; doc.artboards[i].artboardRect = [ cx + (a[0] - cx) * s, cy + (a[1] - cy) * s, cx + (a[2] - cx) * s, cy + (a[3] - cy) * s ]; } } finally { restoreLayers(layerStates); if (prevScaleStrokes !== null) { try { app.preferences.setBooleanPreference("scaleLineWeight", prevScaleStrokes); } catch (e) {} } app.coordinateSystem = prevCS; } return info; } // ── Recursive folder walker ─────────────────────────────────────────────── function walk(folder) { var items = folder.getFiles(), ais = [], i; for (i = 0; i < items.length; i++) { var it = items[i]; if (it instanceof Folder) { var n = it.name.toLowerCase(); if (n !== "eps" && n !== "svg" && n !== "illustrator" && n !== "__macosx" && n.charAt(0) !== ".") { walk(it); } } else if (/\.ai$/i.test(it.name) && it.name.charAt(0) !== ".") { ais.push(it); } } if (!ais.length) { return; } // Create output folders only if needed var svgOut = doSVG ? new Folder(folder.fsName + "/SVG") : null; var epsOut = doEPS ? new Folder(folder.fsName + "/EPS") : null; if (svgOut && !svgOut.exists) { svgOut.create(); } if (epsOut && !epsOut.exists) { epsOut.create(); } for (i = 0; i < ais.length; i++) { var baseName = decodeURI(ais[i].name).replace(/\.ai$/i, ""); var svgTarget = doSVG ? new File(svgOut.fsName + "/" + baseName + ".svg") : null; var epsTarget = doEPS ? new File(epsOut.fsName + "/" + baseName + ".eps") : null; // Check what actually needs doing for this file var needSVG = doSVG && !svgTarget.exists; var needEPS = doEPS && !epsTarget.exists; if (doSVG && !needSVG) { svgSkipped++; } if (doEPS && !needEPS) { epsSkipped++; } if (!needSVG && !needEPS) { continue; } // both already exist var doc = null; try { doc = app.open(ais[i]); // SVG first, at the original size if (needSVG) { doc.exportFile(svgTarget, ExportType.SVG, svgOptions()); svgConverted++; } // Then resize to max under 25 MP and save EPS if (needEPS) { var info = fitUnder25MP(doc); doc.saveAs(epsTarget, epsOptions()); epsConverted++; if (info) { sizeLog.push(baseName + ".eps " + info.w + " x " + info.h + " px (" + info.mp + " MP" + (info.capped ? ", limited by canvas size" : "") + ")"); } } doc.close(SaveOptions.DONOTSAVECHANGES); } catch (e) { failed.push(baseName + ".ai (" + e + ")"); try { if (doc) { doc.close(SaveOptions.DONOTSAVECHANGES); } } catch (e2) {} } } } // ── Run ─────────────────────────────────────────────────────────────────── try { walk(root); } finally { app.userInteractionLevel = previousLevel; } // ── Summary ─────────────────────────────────────────────────────────────── var msg = "AI Convert — finished\n"; if (doSVG) { msg += "\nSVG\n Converted : " + svgConverted + "\n Skipped : " + svgSkipped; } if (doEPS) { msg += "\nEPS\n Converted : " + epsConverted + "\n Skipped : " + epsSkipped; if (sizeLog.length) { msg += "\n\nEPS sizes:\n" + sizeLog.slice(0, 15).join("\n"); if (sizeLog.length > 15) { msg += "\n…and " + (sizeLog.length - 15) + " more"; } } } if (failed.length) { msg += "\n\nFailed (" + failed.length + "):\n" + failed.slice(0, 15).join("\n"); } alert(msg); })();