0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Geminiで作ったスライドを"社内仕様"に整える ─ Apps Scriptで自動処理

Posted at

Gemini Canvasで豪華なスライドを簡単に作れるようになりました。ですが、Googleスライドにエクスポートすると「テーマの編集」やマスター(テンプレート)が反映されず、社内ルールのロゴやフッターを毎回手で整える必要があります(2025年11月現在)。

本記事では、Apps Scriptを使いワンクリックで、Gemini製スライドを“社内仕様”に変換する方法を紹介します。ロゴとフッターを全ページに一括挿入し、再実行しても重複しない(上書き更新)のがポイントです。

まず使ってみる(コピーしてすぐ試す)

お使いのGoogle Workspaceの設定によって、試し方が変わります。

Step1 ロゴ画像をGoogleドライブにアップロード

ロゴ画像をGoogleドライブにアップロードします。他の方がこのツールを使う場合、ロゴ画像を共有しておきましょう。

お試し用に次のロゴを準備したので、ご自由にお使いください。

Example Corp Logo

https://drive.google.com/file/d/1-OoaA6odpf8LAgCvnmZfopnff-CxfWhO/view?usp=sharing

Step2-a スプレッドシートをコピー

個人のGoogle Driveで試したり、組織のGoogle Workspaceが組織外のリソースへのアクセスを許可している場合、設定を管理するスプレッドシートをこちらからコピーします:

👉 Slides Branding をコピー

このシートにアクセスできない場合は、後述の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`)
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 デプロイと実行

  1. スクリプトエディター右上 デプロイ > 新しいデプロイ
  2. 種類:ウェブアプリ
  3. 実行ユーザー:アクセスした人
  4. アクセス:全員(または組織内の全員)
  5. 「デプロイ」ボタンを押す

公開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で作ったスライドを “社内仕様”に一括整形
  • ロゴとフッターを全ページへ自動挿入(再実行しても重複せず上書き)
  • 背景/既存テキストに基づき、フォント色を自動決定して視認性を担保

設定シートをコピーして、ぜひ今日から運用してみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?