// illustrator-batch-svg-to-ai-with-clipping-mask.jsx // Free script from LinearAndLoom Icons: https://linearandloomicons.netlify.app/scripts/ // Provided as is. Try it on a copy of your files first. /* SVG to AI Converter for Adobe Illustrator (with artboard clipping mask) ------------------------------------------------------------ HOW TO RUN 1. If your SVG files contain live text, install the fonts they use first, then restart Illustrator. 2. In Illustrator: File > Scripts > Other Script... and choose this file. 3. Pick the ROOT folder when asked (the folder that holds your SVG files, or any parent folder). WHAT IT DOES, FOR EVERY .svg IN THE FOLDER AND ITS SUBFOLDERS 1. Opens the SVG. 2. Adds a clipping mask the exact size of the artboard to every layer, so anything that sticks out past the artboard edge is hidden. Layers, text and shapes stay editable (each layer gets one clip group named "Artboard_Clip"). 3. Saves the SVG again WITH the clipping mask (text stays live text, fonts are not outlined). 4. Saves an .ai file with the same name into a folder named "Illustrator" next to the SVGs. Saved as PDF-compatible .ai (needed so the files can also be imported into Canva). NOTES - At the start you can choose whether existing .ai files should be replaced (choose YES if you already converted files with an older version of this script). - Layers that are already clipped ("Artboard_Clip") are not clipped twice, so it is safe to run again. - Shows a summary when finished. */ #target illustrator (function () { if (app.name.indexOf("Illustrator") < 0) { alert("Please run this script in Adobe Illustrator."); return; } var root = Folder.selectDialog("Select the ROOT folder that contains your SVG files"); if (!root) { return; } var overwriteAi = confirm("Replace .ai files that already exist?\n\nYES = replace them (recommended if you converted before, so they get the clipping mask)\nNO = skip them"); var updateSvg = confirm("Also save the clipping mask into the original SVG files?\n\nYES = SVG files are updated with the clipping mask (recommended)\nNO = only the .ai files get the clipping mask"); var previousLevel = app.userInteractionLevel; app.userInteractionLevel = UserInteractionLevel.DONTDISPLAYALERTS; var CLIP_NAME = "Artboard_Clip"; var converted = 0, skipped = 0, foldersMade = 0, svgUpdated = 0, failed = []; function isSvg(f) { return (f instanceof File) && /\.svg$/i.test(f.name) && f.name.charAt(0) !== "."; } function aiOptions() { var o = new IllustratorSaveOptions(); o.pdfCompatible = true; // required for Canva import and for previews in other apps o.embedLinkedFiles = true; o.embedICCProfile = false; o.compressed = true; o.fontSubsetThreshold = 100; // embed the fonts used (as subsets) inside the PDF-compatible part return o; } function svgOptions() { var o = new ExportOptionsSVG(); o.DTD = SVGDTDVersion.SVG1_1; o.fontType = SVGFontType.SVGFONT; // keep text as real, editable text (not outlines) o.fontSubsetting = SVGFontSubsetting.None; // use the installed fonts, do not embed glyphs o.embedRasterImages = true; o.cssProperties = SVGCSSPropertyLocation.PRESENTATIONATTRIBUTES; o.documentEncoding = SVGDocumentEncoding.UTF8; o.coordinatePrecision = 3; o.preserveEditability = false; o.includeFileInfo = false; o.slices = false; o.sVGAutoKerning = false; o.sVGTextOnPath = false; try { o.saveMultipleArtboards = false; } catch (e) {} return o; } function alreadyClipped(layer) { for (var i = 0; i < layer.groupItems.length; i++) { var g = layer.groupItems[i]; if (g.name === CLIP_NAME && g.clipped && g.parent === layer) { return true; } } return false; } // Puts all items of one layer into a group clipped to the artboard rectangle. function clipLayer(layer, ab) { for (var s = 0; s < layer.layers.length; s++) { clipLayer(layer.layers[s], ab); } // sublayers get their own clip var wasLocked = layer.locked, wasVisible = layer.visible; layer.locked = false; layer.visible = true; try { var n = layer.pageItems.length; if (n === 0 || alreadyClipped(layer)) { return; } var items = []; for (var i = 0; i < n; i++) { items.push(layer.pageItems[i]); } // index 0 = top of the stack var grp = layer.groupItems.add(); grp.name = CLIP_NAME; grp.move(layer, ElementPlacement.PLACEATBEGINNING); // move bottom-first, each to the top of the group, so the original stacking order is kept for (var k = items.length - 1; k >= 0; k--) { var it = items[k]; if (it === grp) { continue; } var wasItLocked = false, wasHidden = false; try { wasItLocked = it.locked; it.locked = false; } catch (e1) {} try { wasHidden = it.hidden; it.hidden = false; } catch (e2) {} it.move(grp, ElementPlacement.PLACEATBEGINNING); try { if (wasHidden) { it.hidden = true; } } catch (e3) {} try { if (wasItLocked) { it.locked = true; } } catch (e4) {} } // artboardRect = [left, top, right, bottom] var r = grp.pathItems.rectangle(ab[1], ab[0], ab[2] - ab[0], ab[1] - ab[3]); r.filled = false; r.stroked = false; r.name = "Artboard_Clip_Path"; r.move(grp, ElementPlacement.PLACEATBEGINNING); // clipping path must be the top item grp.clipped = true; } finally { layer.visible = wasVisible; layer.locked = wasLocked; } } function addClipping(doc) { var ab = doc.artboards[doc.artboards.getActiveArtboardIndex()].artboardRect; for (var i = 0; i < doc.layers.length; i++) { clipLayer(doc.layers[i], ab); } } function walk(folder) { var items = folder.getFiles(); var svgs = []; var i; for (i = 0; i < items.length; i++) { var it = items[i]; if (it instanceof Folder) { var n = it.name.toLowerCase(); if (n !== "illustrator" && n !== "__macosx" && n.charAt(0) !== ".") { walk(it); } } else if (isSvg(it)) { svgs.push(it); } } if (svgs.length === 0) { return; } var out = new Folder(folder.fsName + "/Illustrator"); if (!out.exists) { out.create(); foldersMade++; } svgs.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); }); for (i = 0; i < svgs.length; i++) { var svg = svgs[i]; var target = new File(out.fsName + "/" + decodeURI(svg.name).replace(/\.svg$/i, ".ai")); if (target.exists && !overwriteAi) { skipped++; continue; } var doc = null; try { doc = app.open(svg); addClipping(doc); if (updateSvg) { // export to a temporary file first, then replace the original only if the export worked var tmp = new File(folder.fsName + "/~clip_tmp_" + decodeURI(svg.name)); doc.exportFile(tmp, ExportType.SVG, svgOptions()); if (tmp.exists && tmp.length > 0) { var path = svg.fsName, name = svg.name; svg.remove(); tmp.rename(decodeURI(name)); svg = new File(path); svgUpdated++; } } if (target.exists) { target.remove(); } doc.saveAs(target, aiOptions()); doc.close(SaveOptions.DONOTSAVECHANGES); converted++; } catch (e) { failed.push(decodeURI(svg.name) + " (" + e + ")"); try { if (doc) { doc.close(SaveOptions.DONOTSAVECHANGES); } } catch (e2) {} } } } try { walk(root); } finally { app.userInteractionLevel = previousLevel; } var msg = "SVG to AI - finished\n\n" + "Converted to .ai (with clipping mask): " + converted + " file(s)\n" + "SVG files updated with clipping mask: " + svgUpdated + "\n" + "Already existed (skipped): " + skipped + "\n" + "New 'Illustrator' folders: " + foldersMade + "\n"; if (failed.length) { msg += "\nCould not convert " + failed.length + " file(s):\n" + failed.slice(0, 15).join("\n"); } alert(msg); })();