Gemini Canvasで豪華なスライドを簡単に作れるようになりました。ですが、Googleスライドにエクスポートすると「テーマの編集」やマスター(テンプレート)が反映されず、社内ルールのロゴやフッターを毎回手で整える必要があります(2025年11月現在)。
本記事では、Apps Scriptを使いワンクリックで、Gemini製スライドを“社内仕様”に変換する方法を紹介します。ロゴとフッターを全ページに一括挿入し、再実行しても重複しない(上書き更新)のがポイントです。
まず使ってみる(コピーしてすぐ試す)
お使いのGoogle Workspaceの設定によって、試し方が変わります。
Step1 ロゴ画像をGoogleドライブにアップロード
ロゴ画像をGoogleドライブにアップロードします。他の方がこのツールを使う場合、ロゴ画像を共有しておきましょう。
お試し用に次のロゴを準備したので、ご自由にお使いください。
https://drive.google.com/file/d/1-OoaA6odpf8LAgCvnmZfopnff-CxfWhO/view?usp=sharing
Step2-a スプレッドシートをコピー
個人のGoogle Driveで試したり、組織のGoogle Workspaceが組織外のリソースへのアクセスを許可している場合、設定を管理するスプレッドシートをこちらからコピーします:
このシートにアクセスできない場合は、後述のStep2-b 手作業でコピーをお試しください。コピーに成功したら、後述するStep3 デプロイと実行へ進みます。
Step2-b 手作業でコピー(シートをコピーできない場合)
スプレッドシートを準備
- スプレッドシートを新規作成し、シート名を
Settingsにする。 -
Settingsに以下の表を入力。
| キー | 値 | 説明 |
|---|---|---|
| logo_drive_url | https://drive.google.com/file/d/1-OoaA6odpf8LAgCvnmZfopnff-CxfWhO/view?usp=drive_link | 一度だけ設定。Driveの共有は閲覧可(実行ユーザーが読めること) |
| logo_small_width_pt | 80 | ロゴ「小」幅(pt) |
| logo_large_width_pt | 160 | ロゴ「大」幅(pt) |
| margin_pt | 18 | 四隅配置の余白(pt) |
| default_logo_position | TOP_RIGHT | ロゴの既定位置 TOP_LEFT / TOP_RIGHT / BOTTOM_LEFT / BOTTOM_RIGHT |
| default_text_size_pt | 11 | フッター文字サイズの既定 |
| default_text_position | BOTTOM_RIGHT | テキストの既定位置 BOTTOM_LEFT / BOTTOM_CENTER / BOTTOM_RIGHT |
| footer_text_preset_1 | Example Corp Confidential | {{page}} | プリセット(複数行追加可){{page}} は現在のページ数、{{total}} は総ページ数を表示 |
| footer_text_preset_2 | Example Corp Internal Use Only | {{page}} |
Apps Scriptをコピー
- スプレッドシートの上部メニューから
拡張機能>Apps Scriptを開く。 -
コード.gsに以下のコードを貼り付け。
コード(`コード.gs`)
/**
* Google Slides の全スライドにロゴとフッターを挿入(再実行時は自分が入れた要素を削除)
* 依存: SlidesApp / DriveApp / SpreadsheetApp / HtmlService
*/
const CONFIG = {
SHEET: { NAME: 'Settings' },
KEYS: {
LOGO_URL: 'logo_drive_url',
LOGO_SMALL_WIDTH: 'logo_small_width_pt',
LOGO_LARGE_WIDTH: 'logo_large_width_pt',
MARGIN: 'margin_pt',
DEFAULT_LOGO_POS: 'default_logo_position', // TOP_RIGHT / BOTTOM_RIGHT / BOTTOM_LEFT / TOP_LEFT
DEFAULT_TEXT_SIZE: 'default_text_size_pt',
DEFAULT_TEXT_POS: 'default_text_position' // BOTTOM_LEFT / BOTTOM_CENTER / BOTTOM_RIGHT
},
LAYOUT: {
MARGIN_PTS: 18,
LOGO_WIDTH_PTS: 140,
TEXT_BOX_WIDTH_PTS: 216,
TEXT_BOX_HEIGHT_PTS: 18,
},
TEXT_STYLE: {
FONT_SIZE: 8, // フォールバック(設定シートがなければ使用)
COLOR: '#808080',
},
TAGS: {
LOGO: 'BRAND_LOGO_INSERT_V1',
TEXT: 'BRAND_TEXT_INSERT_V1',
}
};
/* ---------------- UI ---------------- */
function doGet() {
return HtmlService.createHtmlOutputFromFile('index.html').setTitle('Slides Branding');
}
/* ---------------- Public entry (UIラッパ) ---------------- */
function addBrandingUi(presIdOrUrl, logoInput, footerInput) {
const presentationId = extractPresentationId_(presIdOrUrl);
if (!presentationId) throw new Error('プレゼンURLまたはIDを正しく入力してください。');
// ロゴ
const logoSource = (logoInput.logoSource || 'SETTINGS').toUpperCase();
const overrideLogoUrlOrId = String(logoInput.logoOverride || '').trim();
const logoSizeMode = (logoInput.logoSizeMode || 'LARGE').toUpperCase();
const logoCustomWidthPt = Number(logoInput.logoCustomPt || 0);
const logoPosition = (logoInput.logoPos || '').toUpperCase();
// フッター
const footerMode = (footerInput.footerMode || 'NONE').toUpperCase();
const footerText = footerMode === 'NONE' ? '' : String(footerInput.footerText || '').trim();
const textColorMode = (footerInput.textColorMode || 'AUTO').toUpperCase();
const customColorHex = String(footerInput.customColor || '');
const textPosition = (footerInput.textPos || 'BOTTOM_RIGHT').toUpperCase();
const textSizeMode = (footerInput.textSizeMode || 'DEFAULT').toUpperCase(); // 'DEFAULT' | 'CUSTOM'
const textSizePt = Number(footerInput.textSizePt || 0);
return addBranding(
presentationId,
footerText,
textColorMode,
customColorHex,
textPosition,
logoPosition,
logoSizeMode,
logoCustomWidthPt,
(logoSource === 'OVERRIDE' && overrideLogoUrlOrId) ? overrideLogoUrlOrId : '',
textSizeMode,
textSizePt
);
}
/* ---------------- Public entry (本体) ---------------- */
function addBranding(presentationId,
footerText,
textColorMode,
customColorHex,
textPosition,
logoPosition,
logoSizeMode,
customLogoWidthPt,
overrideLogoUrlOrId,
textSizeMode,
textSizePt) {
if (!presentationId) throw new Error('プレゼンテーションIDがありません。');
const settings = loadSettings_();
applySettingsToConfig_(settings);
const pres = SlidesApp.openById(presentationId);
const slides = pres.getSlides();
const pageWidth = pres.getPageWidth();
const pageHeight = pres.getPageHeight();
const total = slides.length;
const logoBlob = getLogoBlob_(overrideLogoUrlOrId || settings.logoUrl);
const logoW = resolveLogoWidthPt_(logoSizeMode, customLogoWidthPt, settings);
slides.forEach((slide, index) => {
cleanSlide(slide);
if (logoBlob) insertLogo_(slide, logoBlob, (logoPosition || settings.defaultLogoPos), pageWidth, pageHeight, logoW);
if (footerText) {
const resolvedText = resolveFooterText_(footerText, index + 1, total);
const color = (textColorMode === 'CUSTOM' && /^#?[0-9a-fA-F]{6}$/.test(customColorHex || ''))
? normalizeHex_(customColorHex)
: (autoTextColorForSlide_(slide) || CONFIG.TEXT_STYLE.COLOR);
const sizePt = (String(textSizeMode).toUpperCase() === 'CUSTOM' && Number(textSizePt) > 0)
? Number(textSizePt)
: (settings.defaultTextSizePt || CONFIG.TEXT_STYLE.FONT_SIZE);
insertFooterText_(slide, resolvedText, pageWidth, pageHeight, (textPosition || settings.defaultTextPos), color, sizePt);
}
});
return `✅ ${slides.length} 枚のスライドを処理しました。`;
}
/* ---------------- Settings ---------------- */
function loadSettings_() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName(CONFIG.SHEET.NAME);
const out = {
logoUrl: '',
sizeSmall: 120,
sizeLarge: 180,
margin: 18,
defaultLogoPos: 'BOTTOM_RIGHT',
defaultTextSizePt: 8,
defaultTextPos: 'BOTTOM_RIGHT',
};
if (!sh) return out;
const values = sh.getRange(1, 1, Math.max(1, sh.getLastRow()), 2).getValues();
const map = {};
values.forEach(([k, v]) => { if (k) map[String(k).trim()] = v; });
out.logoUrl = map[CONFIG.KEYS.LOGO_URL] || out.logoUrl;
out.sizeSmall = num_(map[CONFIG.KEYS.LOGO_SMALL_WIDTH], out.sizeSmall);
out.sizeLarge = num_(map[CONFIG.KEYS.LOGO_LARGE_WIDTH], out.sizeLarge);
out.margin = num_(map[CONFIG.KEYS.MARGIN], out.margin);
out.defaultLogoPos = (map[CONFIG.KEYS.DEFAULT_LOGO_POS] || out.defaultLogoPos).toString().toUpperCase();
out.defaultTextSizePt = num_(map[CONFIG.KEYS.DEFAULT_TEXT_SIZE], out.defaultTextSizePt);
out.defaultTextPos = (map[CONFIG.KEYS.DEFAULT_TEXT_POS] || out.defaultTextPos).toString().toUpperCase();
return out;
}
function applySettingsToConfig_(s) {
CONFIG.LAYOUT.MARGIN_PTS = s.margin || CONFIG.LAYOUT.MARGIN_PTS;
CONFIG.LAYOUT.LOGO_WIDTH_PTS = s.sizeLarge || CONFIG.LAYOUT.LOGO_WIDTH_PTS;
CONFIG.TEXT_STYLE.FONT_SIZE = s.defaultTextSizePt || CONFIG.TEXT_STYLE.FONT_SIZE;
}
function getUiSettings() {
const s = loadSettings_();
const fileId = extractDriveFileId_(s.logoUrl || '');
return {
logoUrl: s.logoUrl || '',
logoFileId: fileId || '',
sizeSmallPt: s.sizeSmall || 120,
sizeLargePt: s.sizeLarge || 180,
marginPt: s.margin || 18,
defaultLogoPosition: s.defaultLogoPos || 'BOTTOM_RIGHT',
defaultTextSizePt: s.defaultTextSizePt || 8,
defaultTextPosition: s.defaultTextPos || 'BOTTOM_RIGHT',
footerPresets: readFooterPresets_(),
};
}
function readFooterPresets_() {
const sh = SpreadsheetApp.getActive().getSheetByName(CONFIG.SHEET.NAME);
if (!sh) return [];
const vals = sh.getRange(1,1, Math.max(1, sh.getLastRow()), 2).getValues();
const out = [];
vals.forEach(([k,v]) => {
if (k && String(k).toLowerCase().startsWith('footer_text_preset_') && v) {
out.push(String(v)); // 値は {{page}} / {{total}} のみ対応
}
});
return out;
}
/* ---------------- Helpers ---------------- */
function extractPresentationId_(s) {
if (!s) return '';
const m = String(s).match(/\/d\/([a-zA-Z0-9_-]+)/);
return m ? m[1] : String(s);
}
function extractDriveFileId_(s) {
if (!s) return null;
let m = String(s).match(/\/d\/([a-zA-Z0-9_-]+)/); if (m) return m[1];
m = String(s).match(/[?&]id=([a-zA-Z0-9_-]+)/); if (m) return m[1];
m = String(s).match(/([a-zA-Z0-9_-]{20,})/); return m ? m[1] : null;
}
function num_(v, d) { const n = Number(v); return isFinite(n) ? n : d; }
/* ---------------- Core ops ---------------- */
function cleanSlide(slide) {
slide.getPageElements().forEach(el => {
const desc = safeGetDescription_(el);
if ([CONFIG.TAGS.LOGO, CONFIG.TAGS.TEXT].includes(desc)) el.remove();
});
}
function safeGetDescription_(el) { try { return el.getDescription(); } catch (_) { return null; } }
function getLogoBlob_(urlOrId) {
if (!urlOrId) return null;
try {
const id = extractDriveFileId_(urlOrId) || urlOrId;
return DriveApp.getFileById(id).getBlob();
} catch (e) {
return null; // Drive に置く前提
}
}
function resolveLogoWidthPt_(mode, custom, s) {
switch (String(mode || '').toUpperCase()) {
case 'SMALL': return s.sizeSmall || 120;
case 'LARGE': return s.sizeLarge || 180;
case 'CUSTOM': return Math.max(8, Number(custom) || 0);
default: return s.sizeLarge || 180;
}
}
function resolveFooterText_(text, page, total) {
// シンプル仕様: {{page}} / {{total}} のみ対応(大文字小文字は無視)
let s = String(text);
s = s.replace(/\{\{\s*page\s*\}\}/gi, String(page));
s = s.replace(/\{\{\s*total\s*\}\}/gi, String(total));
return s;
}
function insertLogo_(slide, blob, logoPosition, pageWidth, pageHeight, logoWidthPt) {
const image = slide.insertImage(blob);
const aspect = image.getInherentHeight() / image.getInherentWidth();
const w = Math.max(8, logoWidthPt || CONFIG.LAYOUT.LOGO_WIDTH_PTS);
const h = w * aspect;
const m = CONFIG.LAYOUT.MARGIN_PTS;
image.setWidth(w).setHeight(h);
switch (String(logoPosition || '').toUpperCase()) {
case 'TOP_LEFT':
image.setLeft(m).setTop(m); break;
case 'TOP_RIGHT':
image.setLeft(pageWidth - w - m).setTop(m); break;
case 'BOTTOM_LEFT':
image.setLeft(m).setTop(pageHeight - h - m); break;
case 'BOTTOM_RIGHT':
default:
image.setLeft(pageWidth - w - m).setTop(pageHeight - h - m); break;
}
image.setDescription(CONFIG.TAGS.LOGO);
}
function insertFooterText_(slide, text, pageWidth, pageHeight, textPosition, colorHex, textSizePt) {
const { MARGIN_PTS: m, TEXT_BOX_WIDTH_PTS: w, TEXT_BOX_HEIGHT_PTS: h } = CONFIG.LAYOUT;
let x = m; // BOTTOM_LEFT 既定
const pos = String(textPosition || '').toUpperCase();
if (pos === 'BOTTOM_RIGHT') x = pageWidth - w - m;
if (pos === 'BOTTOM_CENTER') x = (pageWidth - w) / 2;
const y = pageHeight - h - m;
const box = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, x, y, w, h);
box.setDescription(CONFIG.TAGS.TEXT);
const range = box.getText();
range.setText(text);
const style = range.getTextStyle();
style.setFontSize(textSizePt || CONFIG.TEXT_STYLE.FONT_SIZE);
style.setForegroundColor(normalizeHex_(colorHex || CONFIG.TEXT_STYLE.COLOR));
let align = SlidesApp.ParagraphAlignment.START;
if (pos === 'BOTTOM_RIGHT') align = SlidesApp.ParagraphAlignment.END;
if (pos === 'BOTTOM_CENTER') align = SlidesApp.ParagraphAlignment.CENTER;
range.getParagraphs().forEach(p => p.getRange().getParagraphStyle().setParagraphAlignment(align));
}
/* ---------------- Text color AUTO ---------------- */
function autoTextColorForSlide_(slide) {
const color = firstTextColorOnSlide_(slide);
if (color) return color;
const bg = getSlideBackgroundColor_(slide);
if (bg) return luminance_(bg) > 0.6 ? '#000000' : '#FFFFFF';
return null;
}
function firstTextColorOnSlide_(slide) {
const els = slide.getPageElements();
for (const el of els) {
try {
const desc = safeGetDescription_(el);
if ([CONFIG.TAGS.LOGO, CONFIG.TAGS.TEXT].includes(desc)) continue;
if (el.asShape) {
const shape = el.asShape();
if (shape.getText) {
const tr = shape.getText();
if (tr && tr.getText().trim()) {
const st = tr.getTextStyle();
const c = tryGetHexColor_(st);
if (c) return c;
}
}
}
} catch (_) {}
}
return null;
}
function tryGetHexColor_(textStyle) {
try {
const fc = textStyle.getForegroundColor();
if (!fc) return null;
if (fc.getColorType && fc.getColorType() === SlidesApp.ColorType.RGB) {
const rgb = fc.asRgbColor();
if (rgb && rgb.asHexString) return normalizeHex_(rgb.asHexString());
}
} catch (_) {}
return null;
}
function getSlideBackgroundColor_(slide) {
try {
const bg = slide.getBackground();
if (!bg || !bg.getFill) return null;
const fill = bg.getFill();
if (!fill) return null;
if (fill.getSolidFill && fill.getSolidFill()) {
const col = fill.getSolidFill().getColor();
if (!col) return null;
if (col.getColorType && col.getColorType() === SlidesApp.ColorType.RGB) {
const hex = col.asRgbColor().asHexString();
return normalizeHex_(hex);
}
}
} catch (_) {}
return null;
}
/* ---------------- small utils ---------------- */
function normalizeHex_(hex) { let h = String(hex || '').trim(); if (!h) return ''; if (h[0] !== '#') h = '#' + h; return h.toUpperCase(); }
function luminance_(hex) { const h = normalizeHex_(hex).slice(1); const r = parseInt(h.slice(0,2),16)/255; const g = parseInt(h.slice(2,4),16)/255; const b = parseInt(h.slice(4,6),16)/255; const a=[r,g,b].map(v=> (v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055,2.4))); return 0.2126*a[0]+0.7152*a[1]+0.0722*a[2]; }
- 次に
index.htmlを追加(ファイル名はindex)。
コード(`index.html`)
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<style>
:root { --gap: 12px; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", sans-serif; padding: 16px; max-width: 880px; margin: 0 auto; color: #111; }
h1 { font-size: 22px; margin: 0 0 6px; }
fieldset { border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin: 16px 0; }
legend { padding: 0 8px; font-weight: 700; }
label { font-weight:500; }
input[type="text"], input[type="url"], input[type="number"] { padding: 6px; border: 1px solid #ddd; border-radius: 6px; font: inherit; }
.hint { color:#666; font-size:12px; }
.status { margin-top:10px; font-size:14px; }
.ok { color:#0a7; }
.ng { color:#c33; }
.radio-row { display:flex; gap:16px; row-gap: 0; flex-wrap: wrap; align-items:center; }
.logo-preview { display:flex; align-items:center; gap:10px; }
.logo-preview img { max-height:40px; border:1px solid #eee; border-radius:4px; }
button { margin-top: 16px; padding: 10px 16px; border: 0; border-radius: 8px; background:#1a73e8; color:#fff; cursor:pointer; font-weight:600; }
#footerPresetsMount label { display: block; margin: 10px 0; }
</style>
</head>
<body>
<h1>Slides Branding(ブランド化ツール)</h1>
<p class="hint">Googleスライドにロゴとフッターを挿入します。<br>よく使う設定はスプレッドシートで管理します。</p>
<label>Google スライドのURL</label>
<input id="presUrl" type="url" placeholder="https://docs.google.com/presentation/d/{fileId}/edit..." style="width:100%;" />
<fieldset>
<legend>ロゴ</legend>
<div>
<label><input type="radio" name="logoSource" value="SETTINGS" checked> 設定シートのロゴ</label>
<div id="settingsLogoArea" class="logo-preview" style="margin:6px 0;"></div>
</div>
<div style="margin-top:8px;">
<label><input type="radio" name="logoSource" value="OVERRIDE"> カスタム</label>
<input id="logoOverride" type="text" placeholder="https://drive.google.com/file/d/{fileId}/view..." style="width:98%; margin-top:4px;" />
<div class="hint">Googleドライブに画像をアップロードし、そのURLを入力します。</div>
</div>
<label style="margin-top:12px; display:block;">ロゴサイズ</label>
<div class="radio-row">
<label><input type="radio" name="logoSize" value="LARGE" checked> 大(<span id="largePtLabel">-</span>)</label>
<label><input type="radio" name="logoSize" value="SMALL"> 小(<span id="smallPtLabel">-</span>)</label>
<label><input type="radio" name="logoSize" value="CUSTOM"> カスタム 幅 <input id="logoCustomPt" type="number" min="8" step="1" style="width:80px;"> pt</label>
</div>
<label style="margin-top:12px; display:block;">ロゴの位置</label>
<div class="radio-row" id="logoPosRadios">
<label><input type="radio" name="logoPos" value="TOP_LEFT"> 左上</label>
<label><input type="radio" name="logoPos" value="TOP_RIGHT"> 右上</label>
<label><input type="radio" name="logoPos" value="BOTTOM_RIGHT"> 右下</label>
<label><input type="radio" name="logoPos" value="BOTTOM_LEFT"> 左下</label>
</div>
</fieldset>
<fieldset>
<legend>フッター</legend>
<div id="footerRadios" class="radio-row" style="flex-direction:column; align-items:flex-start;">
<label><input type="radio" name="footerMode" value="NONE" checked> なし</label>
<div id="footerPresetsMount"></div>
<label>
<input type="radio" name="footerMode" value="CUSTOM"> カスタム
<input id="footerCustom" type="text" placeholder="例: Confidential | {{page}} / {{total}}" style="width:400px; margin-left:6px;">
<div class="hint">プレースホルダ: <code>{{page}}</code>(現在) / <code>{{total}}</code>(総)。</div>
</label>
</div>
<label style="margin-top:12px; display:block;">テキストの色</label>
<div class="radio-row">
<label><input type="radio" name="textColor" value="AUTO" checked> 自動</label>
<label><input type="radio" name="textColor" value="CUSTOM"> カスタム <input id="customColor" type="color" value="#808080"> <span id="customColorHex">#808080</span></label>
</div>
<div class="hint">自動は、スライドで使っているテキストの色に合わせます。</div>
<label style="margin-top:12px; display:block;">テキストのサイズ</label>
<div class="radio-row">
<label><input type="radio" name="textSizeMode" value="DEFAULT" checked> <span id="defaultTextSizeLabel">-</span> pt</label>
<label><input type="radio" name="textSizeMode" value="CUSTOM"> カスタム <input id="customTextSizePt" type="number" min="6" step="1" style="width:80px;"> pt</label>
</div>
<label style="margin-top:12px; display:block;">テキストの位置</label>
<div class="radio-row">
<label><input type="radio" name="textPos" value="BOTTOM_LEFT"> 左下</label>
<label><input type="radio" name="textPos" value="BOTTOM_CENTER"> 中央下</label>
<label><input type="radio" name="textPos" value="BOTTOM_RIGHT" checked> 右下</label>
</div>
</fieldset>
<button id="runBtn">挿入する</button>
<div id="status" class="status"></div>
<script>
let settingsCache = null;
function setStatus(msg, cls='') { const s = document.getElementById('status'); s.textContent = msg||''; s.className='status '+cls; }
function extractPresIdFromUrl(url){ if(!url)return ''; const m=String(url).match(/\/presentation\/d\/([a-zA-Z0-9_-]+)/); return m?m[1]:''; }
function renderSettingsLogo(s){ const area=document.getElementById('settingsLogoArea'); area.innerHTML=''; if(s.logoFileId){ const img=document.createElement('img'); img.src='https://lh3.google.com/u/0/d/'+s.logoFileId; const a=document.createElement('a'); a.href=s.logoUrl;a.target='_blank';a.textContent=s.logoUrl; area.append(img,a);}else{const span=document.createElement('span'); span.className='hint'; span.textContent='設定シートに logo_drive_url を設定してください。'; area.append(span);} }
function populateUI(s){
renderSettingsLogo(s);
document.getElementById('largePtLabel').textContent=s.sizeLargePt+'pt';
document.getElementById('smallPtLabel').textContent=s.sizeSmallPt+'pt';
document.getElementById('defaultTextSizeLabel').textContent = (s.defaultTextSizePt||'-');
const radios=document.querySelectorAll('input[name="logoPos"]');
radios.forEach(r=>{ if(r.value===s.defaultLogoPosition) r.checked=true; });
const mount=document.getElementById('footerPresetsMount');
mount.innerHTML='';
(s.footerPresets||[]).forEach((t,i)=>{
const lbl=document.createElement('label');
lbl.innerHTML = `<input type=\"radio\" name=\"footerMode\" value=\"PRESET_${i}\"> ${t}`;
mount.appendChild(lbl);
});
const presetCount = (s.footerPresets || []).length;
if (presetCount >= 1) {
const r0 = document.querySelector('input[name="footerMode"][value="PRESET_0"]');
if (r0) r0.checked = true;
}
}
function runInsert(){
const url=document.getElementById('presUrl').value.trim();
const presId=extractPresIdFromUrl(url);
if(!presId){ setStatus('❌ スライドのURLを正しく入力してください。','ng'); return; }
const logoSource=document.querySelector('input[name="logoSource"]:checked').value;
const logoOverride=document.getElementById('logoOverride').value.trim();
const logoSize=document.querySelector('input[name="logoSize"]:checked').value;
const logoCustomPt=Number(document.getElementById('logoCustomPt').value||0);
const logoPos=document.querySelector('input[name="logoPos"]:checked').value;
const footerChoice=document.querySelector('input[name="footerMode"]:checked').value;
let footerMode='NONE', footerText='';
if(footerChoice==='CUSTOM'){ footerMode='CUSTOM'; footerText=document.getElementById('footerCustom').value.trim(); }
else if(footerChoice.startsWith('PRESET_')){ const idx=Number(footerChoice.split('_')[1]); footerMode='PRESET'; footerText=(settingsCache.footerPresets||[])[idx]||''; }
const textColor=document.querySelector('input[name="textColor"]:checked').value;
const customColor=document.getElementById('customColor').value;
const textSizeMode = document.querySelector('input[name="textSizeMode"]:checked').value; // DEFAULT / CUSTOM
const textSizePt = Number(document.getElementById('customTextSizePt').value || 0);
const textPos=document.querySelector('input[name="textPos"]:checked').value;
setStatus('実行中…');
google.script.run
.withSuccessHandler(msg=>setStatus(msg,'ok'))
.withFailureHandler(err=>setStatus('❌ '+(err&&err.message?err.message:err),'ng'))
.addBrandingUi(
presId,
{ logoSource, logoOverride, logoSizeMode: logoSize, logoCustomPt, logoPos },
{ footerMode, footerText, textColorMode: textColor, customColor, textPos, textSizeMode, textSizePt }
);
}
document.addEventListener('DOMContentLoaded',()=>{
setStatus('設定を読み込み中…');
google.script.run
.withSuccessHandler(s=>{ settingsCache=s; populateUI(s); setStatus(''); })
.withFailureHandler(err=>setStatus('設定の読み込みに失敗しました: '+(err&&err.message?err.message:err),'ng'))
.getUiSettings();
document.getElementById('customColor').addEventListener('input',e=>{ document.getElementById('customColorHex').textContent=e.target.value.toUpperCase(); });
document.getElementById('runBtn').addEventListener('click',runInsert);
});
</script>
</body>
</html>
Step3 デプロイと実行
- スクリプトエディター右上 デプロイ > 新しいデプロイ
- 種類:ウェブアプリ
- 実行ユーザー:アクセスした人
- アクセス:全員(または組織内の全員)
- 「デプロイ」ボタンを押す
公開URLへアクセスし、スライドのURLを貼り付けて 「挿入する」 を押すと、ロゴとフッターが全スライドに挿入されます。再実行しても前回分は自動削除 され、設定が上書き反映されます。
このツールは、「既存要素を削除してから再挿入する」 というシンプルな考え方で実装しています。試行錯誤で何度実行しても安心です。
仕組みの概要
スライドを開いてページ単位で処理
const pres = SlidesApp.openById(presentationId);
const slides = pres.getSlides();
GoogleスライドのURLからファイルIDを抽出し、SlidesApp を使ってすべてのスライドページを取得。ページ単位で同じ処理を一括実行できます。
ロゴの挿入
function insertLogo_(slide, blob, logoPosition, pageWidth, pageHeight, logoWidthPt) {
const image = slide.insertImage(blob);
const aspect = image.getInherentHeight() / image.getInherentWidth();
const w = Math.max(8, logoWidthPt || CONFIG.LAYOUT.LOGO_WIDTH_PTS);
const h = w * aspect;
const m = CONFIG.LAYOUT.MARGIN_PTS;
image.setWidth(w).setHeight(h);
// 4隅にスナップ
switch (String(logoPosition || '').toUpperCase()) {
case 'TOP_LEFT': image.setLeft(m).setTop(m); break;
case 'TOP_RIGHT': image.setLeft(pageWidth - w - m).setTop(m); break;
case 'BOTTOM_LEFT': image.setLeft(m).setTop(pageHeight - h - m); break;
case 'BOTTOM_RIGHT':
default: image.setLeft(pageWidth - w - m).setTop(pageHeight - h - m); break;
}
image.setDescription(CONFIG.TAGS.LOGO); // 後で安全に削除するためのタグ
}
以前のロゴ・フッターだけを安全に削除
function cleanSlide(slide) {
slide.getPageElements().forEach(el => {
const desc = safeGetDescription_(el);
if ([CONFIG.TAGS.LOGO, CONFIG.TAGS.TEXT].includes(desc)) el.remove();
});
}
setDescription() によるタグ付けで、自分が入れた要素だけを削除できます。これにより、再実行してもロゴやテキストが重複しません。
テンプレート置換({{page}} / {{total}})
function resolveFooterText_(text, page, total) {
let s = String(text);
s = s.replace(/\{\{\s*page\s*\}\}/gi, String(page));
s = s.replace(/\{\{\s*total\s*\}\}/gi, String(total));
return s;
}
{{page}} / {{total}} をページ番号に展開します。例: Confidential | {{page}} / {{total}}
フォントの色を自動で読みやすくする
スライドの既存テキスト色または背景色に基づき、フッターの文字色を自動決定します。明るい背景では黒、暗い背景では白を返すため、黒背景スライドと白背景スライドが混在しても文字が潰れません。
function autoTextColorForSlide_(slide) {
const color = firstTextColorOnSlide_(slide); // 既存テキストから色を拝借
if (color) return color;
const bg = getSlideBackgroundColor_(slide); // だめなら背景色の明度で判定
if (bg) return luminance_(bg) > 0.6 ? '#000000' : '#FFFFFF';
return null; // 取得できないときはフォールバック色
}
-
firstTextColorOnSlide_():スライド内の最初に見つかったテキストの前景色を探します(自分が挿入する要素は除外)。 -
getSlideBackgroundColor_():スライド背景の単色塗りからRGBを取得。 -
luminance_():相対輝度(0〜1)を算出し、0.6 を閾値に黒/白を切り替え。
例:タイトルスライドが黒背景、以降の本文スライドが白背景でも、毎ページで最適な色が自動選択されます。
さらに使いやすくする拡張アイデア
- 複数ロゴへの対応:例えば製品ラインやイベントごとに異なるロゴを登録しておき、実行時に選択できるようにします。これにより、1つのスクリプトで複数ブランドを共通管理できます。
- 配置ルールの細分化:タイトルスライドは左下、本文スライドは右下など、スライドタイプに応じてレイアウトを自動切り替えます。視覚的な統一感を保ちながら柔軟な運用が可能です。
- 監査用メタデータの付与:最終ページに実行者・実行日時・ツールバージョンを小さく挿入しておくと、社内資料の更新管理や監査ログに役立ちます。
-
フッターへの日付自動挿入:
{{date}}プレースホルダをサポートし、生成日や更新日を自動的に表示します。ドキュメントの鮮度が一目で分かります。
これらの拡張は、実際の業務運用で発生する細かなニーズをカバーしつつ、Apps Script一つで“社内標準ツール”として機能させるための現実的な改良です。
機能を変更した後は、公開URLが変わらないように、次の手順で再デプロイしましょう。
スクリプトエディターで [デプロイ] > [デプロイを管理] を選択し、設定画面の右上にある編集ボタン(鉛筆アイコン)を押す。
[バージョン]を「新しいバージョン」にして、[デプロイ]ボタンを押す。
まとめ
- Geminiで作ったスライドを “社内仕様”に一括整形
- ロゴとフッターを全ページへ自動挿入(再実行しても重複せず上書き)
- 背景/既存テキストに基づき、フォント色を自動決定して視認性を担保
設定シートをコピーして、ぜひ今日から運用してみてください!