Created
July 28, 2026 14:38
-
-
Save muttoni/32ae1eab254a0f0e666952cece6ee583 to your computer and use it in GitHub Desktop.
Google Slides Batch Formatter — bulk-change text color, font size, and font family across all slides by matching criteria
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Google Slides — Batch Formatter | |
| * --------------------------------- | |
| * Finds text runs across ALL slides that match a criterion | |
| * (foreground color, font size, or font family) and rewrites | |
| * that attribute in bulk. | |
| * | |
| * HOW TO USE | |
| * 1. Open your presentation → Extensions → Apps Script. | |
| * 2. Paste this whole file in, Save. | |
| * 3. Reload the presentation. A "Batch Formatter" menu appears. | |
| * 4. Edit the CONFIG block below, then run from the menu | |
| * (or run a function directly from the editor). | |
| * | |
| * Colors are hex strings like "#FF0000" (red). Comparison is | |
| * case-insensitive. Font sizes are points (numbers). | |
| */ | |
| // ============================================================ | |
| // CONFIG — edit these, then run "Run With Config" from the menu | |
| // ============================================================ | |
| var CONFIG = { | |
| // Which attribute to match on: 'color' | 'size' | 'font' | |
| matchBy: 'color', | |
| // --- match values (only the one for matchBy is used) --- | |
| matchColor: '#FF0000', // e.g. find all red text | |
| matchSize: 18, // e.g. find all 18pt text | |
| matchFont: 'Arial', // e.g. find all Arial text | |
| // Optional tolerance for size matching (points). 0 = exact. | |
| sizeTolerance: 0, | |
| // --- what to change (set to null to leave that attribute alone) --- | |
| setColor: '#1155CC', // new color, or null | |
| setSize: null, // new size in pt, or null | |
| setFont: null, // new font family, or null | |
| // Restrict to a subset of slides (1-based indices). Empty = all slides. | |
| onlySlides: [], // e.g. [1, 2, 5] | |
| dryRun: false // true = only log matches, change nothing | |
| }; | |
| // ============================================================ | |
| // MENU | |
| // ============================================================ | |
| function onOpen() { | |
| SlidesApp.getUi() | |
| .createMenu('Batch Formatter') | |
| .addItem('Run With Config', 'runWithConfig') | |
| .addSeparator() | |
| .addItem('Recolor by color (prompt)', 'promptRecolorByColor') | |
| .addItem('Resize by size (prompt)', 'promptResizeBySize') | |
| .addItem('Change font by font (prompt)', 'promptChangeFontByFont') | |
| .addSeparator() | |
| .addItem('List all colors used', 'listColorsUsed') | |
| .addItem('List all fonts used', 'listFontsUsed') | |
| .addToUi(); | |
| } | |
| // ============================================================ | |
| // CORE | |
| // ============================================================ | |
| /** | |
| * Walk every text run in scope and apply `fn(textRange, style)`. | |
| * Returns count of runs touched. | |
| */ | |
| function forEachTextRun(fn, onlySlides) { | |
| var slides = SlidesApp.getActivePresentation().getSlides(); | |
| var scope = (onlySlides && onlySlides.length) | |
| ? onlySlides.map(function (i) { return slides[i - 1]; }).filter(Boolean) | |
| : slides; | |
| var touched = 0; | |
| scope.forEach(function (slide) { | |
| slide.getPageElements().forEach(function (el) { | |
| touched += processElement(el, fn); | |
| }); | |
| }); | |
| return touched; | |
| } | |
| function processElement(el, fn) { | |
| var count = 0; | |
| var type = el.getPageElementType(); | |
| if (type === SlidesApp.PageElementType.SHAPE) { | |
| count += processTextRange(el.asShape().getText(), fn); | |
| } else if (type === SlidesApp.PageElementType.TABLE) { | |
| var table = el.asTable(); | |
| for (var r = 0; r < table.getNumRows(); r++) { | |
| for (var c = 0; c < table.getNumColumns(); c++) { | |
| count += processTextRange(table.getCell(r, c).getText(), fn); | |
| } | |
| } | |
| } else if (type === SlidesApp.PageElementType.GROUP) { | |
| el.asGroup().getChildren().forEach(function (child) { | |
| count += processElement(child, fn); | |
| }); | |
| } | |
| return count; | |
| } | |
| /** | |
| * Split a TextRange into homogeneous style runs and hand each to fn. | |
| */ | |
| function processTextRange(textRange, fn) { | |
| if (!textRange) return 0; | |
| var runs = textRange.getRuns(); | |
| var count = 0; | |
| runs.forEach(function (run) { | |
| if (run.asString().length === 0) return; | |
| var style = run.getTextStyle(); | |
| if (fn(run, style)) count++; | |
| }); | |
| return count; | |
| } | |
| // --- helpers to read/compare styles --- | |
| function getHexColor(style) { | |
| try { | |
| var fg = style.getForegroundColor(); | |
| if (!fg) return null; | |
| if (fg.getColorType() === SlidesApp.ColorType.RGB) { | |
| return fg.asRgbColor().asHexString().toUpperCase(); | |
| } | |
| return null; // themed color — skip (can't reliably hex-compare) | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| function normHex(h) { | |
| if (!h) return null; | |
| h = h.trim().toUpperCase(); | |
| if (h[0] !== '#') h = '#' + h; | |
| return h; | |
| } | |
| // ============================================================ | |
| // MAIN ENTRY POINT | |
| // ============================================================ | |
| function runWithConfig() { | |
| var c = CONFIG; | |
| var matchFn; | |
| if (c.matchBy === 'color') { | |
| var want = normHex(c.matchColor); | |
| matchFn = function (style) { return getHexColor(style) === want; }; | |
| } else if (c.matchBy === 'size') { | |
| matchFn = function (style) { | |
| var s = style.getFontSize(); | |
| return s != null && Math.abs(s - c.matchSize) <= (c.sizeTolerance || 0); | |
| }; | |
| } else if (c.matchBy === 'font') { | |
| matchFn = function (style) { | |
| var f = style.getFontFamily(); | |
| return f && f.toLowerCase() === String(c.matchFont).toLowerCase(); | |
| }; | |
| } else { | |
| throw new Error('CONFIG.matchBy must be color | size | font'); | |
| } | |
| var changed = 0; | |
| var matched = forEachTextRun(function (run, style) { | |
| if (!matchFn(style)) return false; | |
| if (c.dryRun) { | |
| Logger.log('MATCH: "%s" [color=%s size=%s font=%s]', | |
| run.asString().slice(0, 40), | |
| getHexColor(style), style.getFontSize(), style.getFontFamily()); | |
| } else { | |
| if (c.setColor) style.setForegroundColor(normHex(c.setColor)); | |
| if (c.setSize != null) style.setFontSize(c.setSize); | |
| if (c.setFont) style.setFontFamily(c.setFont); | |
| changed++; | |
| } | |
| return true; | |
| }, c.onlySlides); | |
| var msg = c.dryRun | |
| ? 'Dry run: ' + matched + ' matching run(s). See Executions log.' | |
| : 'Done. ' + changed + ' run(s) updated (of ' + matched + ' matched).'; | |
| try { SlidesApp.getUi().alert(msg); } catch (e) { Logger.log(msg); } | |
| } | |
| // ============================================================ | |
| // PROMPT-DRIVEN SHORTCUTS (no config editing needed) | |
| // ============================================================ | |
| function promptRecolorByColor() { | |
| var ui = SlidesApp.getUi(); | |
| var from = ui.prompt('Recolor', 'Find text of color (hex, e.g. #FF0000):', ui.ButtonSet.OK_CANCEL); | |
| if (from.getSelectedButton() !== ui.Button.OK) return; | |
| var to = ui.prompt('Recolor', 'Change it to (hex, e.g. #1155CC):', ui.ButtonSet.OK_CANCEL); | |
| if (to.getSelectedButton() !== ui.Button.OK) return; | |
| var want = normHex(from.getResponseText()); | |
| var next = normHex(to.getResponseText()); | |
| var n = forEachTextRun(function (run, style) { | |
| if (getHexColor(style) === want) { style.setForegroundColor(next); return true; } | |
| return false; | |
| }); | |
| ui.alert('Recolored ' + n + ' run(s) from ' + want + ' to ' + next + '.'); | |
| } | |
| function promptResizeBySize() { | |
| var ui = SlidesApp.getUi(); | |
| var from = ui.prompt('Resize', 'Find text of size (pt, e.g. 18):', ui.ButtonSet.OK_CANCEL); | |
| if (from.getSelectedButton() !== ui.Button.OK) return; | |
| var to = ui.prompt('Resize', 'Change size to (pt):', ui.ButtonSet.OK_CANCEL); | |
| if (to.getSelectedButton() !== ui.Button.OK) return; | |
| var want = parseFloat(from.getResponseText()); | |
| var next = parseFloat(to.getResponseText()); | |
| var n = forEachTextRun(function (run, style) { | |
| var s = style.getFontSize(); | |
| if (s != null && s === want) { style.setFontSize(next); return true; } | |
| return false; | |
| }); | |
| ui.alert('Resized ' + n + ' run(s) from ' + want + 'pt to ' + next + 'pt.'); | |
| } | |
| function promptChangeFontByFont() { | |
| var ui = SlidesApp.getUi(); | |
| var from = ui.prompt('Change font', 'Find text using font family (e.g. Arial):', ui.ButtonSet.OK_CANCEL); | |
| if (from.getSelectedButton() !== ui.Button.OK) return; | |
| var to = ui.prompt('Change font', 'Change font family to (e.g. Roboto):', ui.ButtonSet.OK_CANCEL); | |
| if (to.getSelectedButton() !== ui.Button.OK) return; | |
| var want = from.getResponseText().trim().toLowerCase(); | |
| var next = to.getResponseText().trim(); | |
| var n = forEachTextRun(function (run, style) { | |
| var f = style.getFontFamily(); | |
| if (f && f.toLowerCase() === want) { style.setFontFamily(next); return true; } | |
| return false; | |
| }); | |
| ui.alert('Changed font on ' + n + ' run(s) to ' + next + '.'); | |
| } | |
| // ============================================================ | |
| // DISCOVERY HELPERS — see what's actually in the deck | |
| // ============================================================ | |
| function listColorsUsed() { | |
| var seen = {}; | |
| forEachTextRun(function (run, style) { | |
| var h = getHexColor(style); | |
| if (h) seen[h] = (seen[h] || 0) + 1; | |
| return false; | |
| }); | |
| var lines = Object.keys(seen).sort().map(function (k) { return k + ' ×' + seen[k]; }); | |
| var msg = lines.length ? lines.join('\n') : 'No RGB colors found (may be themed).'; | |
| Logger.log(msg); | |
| try { SlidesApp.getUi().alert('Colors used (RGB only):\n\n' + msg); } catch (e) {} | |
| } | |
| function listFontsUsed() { | |
| var seen = {}; | |
| forEachTextRun(function (run, style) { | |
| var f = style.getFontFamily(); | |
| if (f) seen[f] = (seen[f] || 0) + 1; | |
| return false; | |
| }); | |
| var lines = Object.keys(seen).sort().map(function (k) { return k + ' ×' + seen[k]; }); | |
| var msg = lines.length ? lines.join('\n') : 'No fonts detected.'; | |
| Logger.log(msg); | |
| try { SlidesApp.getUi().alert('Fonts used:\n\n' + msg); } catch (e) {} | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment