星期五, 2月 20, 2026

[GAS] 這個應用程式是由 Google Apps Script 的使用者建立

把 [網頁應用程式] 部屬出去執行後,在網頁上方匯出出現 [這個應用程式是由 Google Apps Script 的使用者建立] 安全性警告,這個沒有辦法直接移除,常見繞道隱藏方式為把網頁內嵌在 Google Site 內

直接拿 [JS] 基礎練習 UI 畫面來呈現
XFrameOptionsMode
  • ALLOWALL:No X-Frame-Options header will be set. This will let any site iframe the page, so the developer should implement their own protection against clickjacking.
  • DEFAULT:Sets the default value for the X-Frame-Options header, which preserves normal security assumptions. If a script does not set an X-Frame-Options mode, Apps Script uses this mode as the default.
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('圖書館館藏管理系統')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
一開始嚐試把 GAS 網頁應用程式內嵌在 Google Site 時,並沒有指定 ALLOWALL,但一樣可以正常顯示,大概是 DEFAULT 會把 Google 相關服務設定為白名單或信任服務之類,所以不需要特別指定,萬一是要把 GAS 網頁應用程式內嵌在其他平台,就一定要明確設定 ALLOWALL

GAS 警語隱藏是 Goolge Site 標誌,下面則是內崁的 GAS 網頁應用程式,已經不會出現官方制式警語   

星期三, 2月 18, 2026

[JS] 基礎練習

該筆記會在 GAS 上練習 js
  • 資料來源:陣列、物件概念
  • 基礎函示:push、filter 使用
  • DOM API:使用 API 來顯示資料,而非使用 innerHTML
  • addEventListener 註冊事件:DOMContentLoaded 和 Click 事件

gs code
/**
 * 處理 HTTP GET 請求
 * @return {HtmlOutput}
 */
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('圖書館館藏管理系統');
}

/**
 * 嵌入 HTML 檔案內容
 * @param {string} filename 檔案名稱
 * @return {string} 檔案內容
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>新增書籍館藏</h2>
    <div class="input-group">
      <select class="bookCategory">
        <option value="程式語言">程式語言</option>
        <option value="文學小說">文學小說</option>
        <option value="商業理財">商業理財</option>
      </select>
      <input type="text" placeholder="書籍名稱" class="bookTitle">
      <input type="text" placeholder="作者" class="bookAuthor">
      <input type="button" class="addBtn" value="加入館藏">
    </div>

    <h2>館藏查詢與篩選</h2>
    <div class="filterGroup">
      <input type="button" value="全部">
      <input type="button" value="程式語言">
      <input type="button" value="文學小說">
      <input type="button" value="商業理財">
    </div>

    <ul class="bookList"></ul>

    <?!= include('javascript'); ?>
  </body>
</html>

js.html
<script>
/**
 * 書籍測試資料
 */
let libraryData = [
  { title: "深入淺出 JavaScript", author: "Eric Freeman", category: "程式語言" },
  { title: "哈利波特", author: "J.K. Rowling", category: "文學小說" },
  { title: "原子習慣", author: "James Clear", category: "商業理財" },
  { title: "Clean Code", author: "Robert C. Martin", category: "程式語言" }
];

// 選取 DOM 元素
const bookList = document.querySelector('.bookList');
const bookTitle = document.querySelector('.bookTitle');
const bookAuthor = document.querySelector('.bookAuthor');
const bookCategory = document.querySelector('.bookCategory');
const addBtn = document.querySelector('.addBtn');
const filterGroup = document.querySelector('.filterGroup');

/**
 * 使用純 DOM API 產生書籍列表
 * @param {Array} data 欲顯示的書籍陣列
 */
function renderLibrary(data) {
  // 清空目前列表
  while (bookList.firstChild) {
    bookList.removeChild(bookList.firstChild);
  }

  // 建立新節點
  data.forEach(function(book) {
    const li = document.createElement('li');
    
    // 建立書籍資訊容器
    const infoSpan = document.createElement('span');
    infoSpan.textContent = `[${book.category}] 《${book.title}》 - 作者:${book.author}`;
    
    li.appendChild(infoSpan);
    bookList.appendChild(li);
  });
}

/**
 * 新增書籍
 */
addBtn.addEventListener('click', function() {
  const title = bookTitle.value.trim();
  const author = bookAuthor.value.trim();
  const category = bookCategory.value;

  if (title === "" || author === "") {
    alert("請完整填寫書名與作者!");
    return;
  }

  const newBook = {
    title: title,
    author: author,
    category: category
  };

  libraryData.push(newBook);
  
  // 重新渲染並清空輸入框
  renderLibrary(libraryData);
  bookTitle.value = "";
  bookAuthor.value = "";
});

/**
 * 篩選資料功能
 */
filterGroup.addEventListener('click', function(e) {
  if (e.target.type !== "button") return;

  const filterType = e.target.value;
  let result;

  if (filterType === "全部") {
    result = libraryData;
  } else {
    result = libraryData.filter(function(book) {
      return book.category === filterType;
    });
  }

  renderLibrary(result);
});

/**
 * 初始化頁面
 */
document.addEventListener('DOMContentLoaded', function() {
  renderLibrary(libraryData);
});

</script>

星期一, 2月 09, 2026

[GAS] Library - 最新程式碼快照 (開發人員模式)

[GAS] 資料庫 (Library) 筆記時有提到版本內有 [最新程式碼快照 (開發人員模式)] 可以使用,但實際應用時發現,原來要存取 [最新程式碼快照 (開發人員模式)] 必須具備 [編輯者] 權限,當初分享 library 給別人使用,因為只授予 [檢視者],就無法存取

星期四, 2月 05, 2026

[GAS] Slide 合併資料

Slide 常見應用場景,在一個範本 Slide 上設定多個 {{文字說明}} 符號,並從 Google Sheet 內抓取資料後,根據實務需求取代 {{文字說明}},該筆記以課程結業證書為例筆記

Slide 範本

gs Code

把 [課程結業證書範本 Slide] 複製至新位置,該筆記是以根目錄內的日期資料夾 (EX:2026-02-05) 去,並把學員資料透過 replaceAllText() 更新至 Slide {{文字說明}}

{{文字說明}} 基本上在 Slide 內是唯一的識別文字就行,前後有沒有符號包起來不是重點,但既然官方教學都使用 {{}} 包起來,就延續該風格囉,Slide 內有三個文字說明,分別為 {{學員姓名}}、{{課程名稱}}、{{日期}}

replaceAllText(findText, replaceText) 函式
  • findText:以該筆記為例,是指 {{文字說明}}
  • replaceText:取代 {{文字說明}} 的內容
function SlideMergeData() {

  const presentation = SlidesApp.getActivePresentation();
  const timeZone = Session.getScriptTimeZone();
  const dateString = Utilities.formatDate(new Date(), timeZone, "yyyy-MM-dd");

  // 設定根 FolderID
  const rootFolderId = GetFolderID();
  const rootFolder = DriveApp.getFolderById(rootFolderId);

  // 檢查並刪除同名的日期資料夾
  const existingFolders = rootFolder.getFoldersByName(dateString);
  while (existingFolders.hasNext()) {
    const oldFolder = existingFolders.next();
    oldFolder.setTrashed(true); // 將舊資料夾移至垃圾桶
  }

  // 建立日期資料夾
  const dateFolder = rootFolder.createFolder(dateString);

  // 複製範本 Slide 來使用
  const templateFile = DriveApp.getFileById(presentation.getId());
  const studentName = "王小強";
  const copyFile = templateFile.makeCopy(studentName, dateFolder);

  // 開啟新 Slide 並進行文字替換
  const newPresentation = SlidesApp.openById(copyFile.getId());
  newPresentation.replaceAllText('{{學員姓名}}', studentName);
  newPresentation.replaceAllText('{{課程名稱}}', "Google Apps Script");
  newPresentation.replaceAllText('{{日期}}', dateString);
  newPresentation.saveAndClose();
}

執行結果

該筆記只有使用 replaceAllText() 取代文字,還有兩個主題分別為
  1. replaceAllShapesWithImage 取代圖片
  2. BatchUpdate:有效能考量使用,需要開啟 Google Drive API 服務
目前沒有使用到,單純紀錄

星期五, 1月 30, 2026

[GAS] Google Slide 轉圖檔

透過 Presentations.Pages.getThumbnail 把 Google Slide 轉成圖檔,精確說是把 Slide 的每一頁轉成縮圖

Request

參數 說明
presentationId Slide ID
pageObjectId Slide 內每頁投影片 ID
thumbnailProperties 縮圖屬性
mimeType 未指定的話,預設為 PNG
thumbnailSize 未指定的話,伺服器會選擇圖片的預設大小。有 LARGE (1600 像素)、MEDIUM (800 像素)、SMALL (200 像素) 可以選擇


Response

參數 說明
width 縮圖圖片的正向寬度 (以像素為單位)
height 縮圖圖片的正向高度 (以像素為單位)
contentUrl
縮圖伺服器網址,該縮圖只會留存 30 分鐘且只有請求者可以存取該圖片


限制

IntelliSense 或是文件上都會提到 Presentations.Pages.getThumbnail 是高成本讀取操作,在 Usage limits 有特別列出存取限制

限制 次數 說明
Per minute per project 300 次/分鐘 所有執行該 Script 使用者加總次數,每分鐘不能超過 300 次
Per minute per user per project 60 次/分鐘 一位使用者每分鐘最多只能請求 60 次


gs Code

使用 Presentations.Pages.getThumbnail 必須在服務內加入 Goolge Slides API 且產生的 Slide 每頁縮圖會存放在伺服器上,必須透過 response.contentUrl 取回圖片

function runSlideExportAndShare() {

  // 從 PropertiesService 取出 presentationId 和 folderId
  const scriptProperties = PropertiesService.getScriptProperties();
  const properties = scriptProperties.getProperties();
  const presentationId = properties["PresentationID"];
  const folderId = GetProperties("FolderID");

  convertSlidesToImagesAndShare(presentationId, folderId);
}

/**
 * 將 Google Slide 內的每一頁轉換為圖片,存儲於 Google Drive 上,並將圖片設定為「知道連結者皆可檢視」
 * * @param {string} presentationId 簡報 ID
 * @param {string} folderId 圖片存放資料夾 ID
 */
function convertSlidesToImagesAndShare(presentationId, folderId) {

  const presentation = SlidesApp.openById(presentationId);
  const slides = presentation.getSlides();
  const folder = DriveApp.getFolderById(folderId);

  slides.forEach((slide, index) => {
    const pageObjectId = slide.getObjectId();

    // 呼叫 Slides API 取得縮圖 URL
    const thumbnailResponse = Slides.Presentations.Pages.getThumbnail(
      presentationId,
      pageObjectId,
      {
        'thumbnailProperties.mimeType': 'PNG',
        'thumbnailProperties.thumbnailSize': 'LARGE'
      }

    );

    const imageUrl = thumbnailResponse.contentUrl;
    const imageName = `Slide_${index + 1}.png`;

    try {
      // 透過 UrlFetchApp 取回圖片
      const response = UrlFetchApp.fetch(imageUrl);
      const blob = response.getBlob().setName(imageName);

      // 在指定資料夾建立檔案
      const file = folder.createFile(blob);

      // 設定權限:知道連結的任何人 (ANYONE_WITH_LINK) 都可以檢視 (VIEW)
      file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

    } catch (e) {
      console.error(`處理第 ${index + 1} 頁時發生錯誤: ${e.message}`);
    }
  });
}

執行結果

Slide2Imge 內有兩頁投影片,轉出兩張圖片來