Skip to content

Instantly share code, notes, and snippets.

@andypiper
Created April 11, 2025 13:16
Show Gist options
  • Save andypiper/f165a38ee270ddbf4dfa900e0cd3fd1f to your computer and use it in GitHub Desktop.
Save andypiper/f165a38ee270ddbf4dfa900e0cd3fd1f to your computer and use it in GitHub Desktop.
Google Slides export speaker notes
/**
* Creates a custom menu in the Google Slides UI when the presentation is opened.
*/
function onOpen() {
SlidesApp.getUi()
.createMenu('Speaker Tools')
.addItem('Export with previews', 'initiateNotesExportWithPreviews')
.addItem('Export without previews', 'initiateNotesExportWithoutPreviews')
.addItem('Export text only', 'initiateNotesExportTextOnly')
.addToUi();
}
// --- Initiator Functions ---
/**
* Initiates the export process for notes WITH slide previews.
*/
function initiateNotesExportWithPreviews() {
initiateNotesExport('with_previews', createNotesDocWithPreviews);
}
/**
* Initiates the export process for notes WITHOUT slide previews (table format).
*/
function initiateNotesExportWithoutPreviews() {
initiateNotesExport('without_previews', createNotesDocWithoutPreviews);
}
/**
* Initiates the export process for notes as TEXT ONLY (numbered).
*/
function initiateNotesExportTextOnly() {
initiateNotesExport('text_only', createNotesDocTextOnly);
}
/**
* Helper function to handle the common parts of initiating an export:
* getting presentation info, prompting for doc name, creating doc, and calling
* the specific export function.
*
* @param {string} formatType A string identifier for the format (e.g., 'with_previews').
* @param {function} exportFunction The specific core export function to call.
*/
function initiateNotesExport(formatType, exportFunction) {
var ui = SlidesApp.getUi();
var presentation = SlidesApp.getActivePresentation();
var presentationId = presentation.getId();
var presentationName = presentation.getName();
// Determine default document name based on format
var formatDesc = formatType.replace('_', ' '); // e.g., "with previews"
var defaultDocName = `${presentationName} - Speaker Notes (${formatDesc})`;
var promptResponse = ui.prompt(
`Export Notes (${formatDesc})`,
'Enter a name for the new notes Doc:',
ui.ButtonSet.OK_CANCEL);
if (promptResponse.getSelectedButton() == ui.Button.OK) {
var docName = promptResponse.getResponseText() || defaultDocName; // Use default if empty
try {
Logger.log(`Creating new document for format [${formatType}]: ${docName}`);
var newDoc = DocumentApp.create(docName);
var outputDocId = newDoc.getId();
var newDocUrl = newDoc.getUrl();
Logger.log(`New document created with ID: ${outputDocId}`);
// Show progress sidebar
var sidebarHtml = `<p>Exporting notes (${formatDesc})... Please wait.</p>`;
ui.showSidebar(HtmlService.createHtmlOutput(sidebarHtml).setTitle('Processing'));
// Call the specific export function passed as an argument
exportFunction(presentationId, outputDocId); // Pass IDs
// Show completion message
var completionHtml = `<p>Export complete! (${formatDesc})<br/><a href="${newDocUrl}" target="_blank">Click here to open the document.</a></p><br/><button onclick="google.script.host.close()">Close</button>`;
ui.showSidebar(HtmlService.createHtmlOutput(completionHtml).setTitle('Success'));
Logger.log(`Notes export (${formatType}) completed successfully.`);
} catch (e) {
Logger.log(`Error during notes export process (${formatType}): ${e}\nStack: ${e.stack}`);
ui.alert(`An error occurred during the export (${formatDesc}): ${e.message}`);
// Close sidebar on error
try { ui.showSidebar(HtmlService.createHtmlOutput('<p>An error occurred. Please check logs.</p><br/><button onclick="google.script.host.close()">Close</button>').setTitle('Error')); } catch (sidebarError) {}
}
} else {
Logger.log(`User cancelled the export (${formatType}).`);
ui.alert(`Notes export (${formatDesc}) cancelled.`);
}
}
// --- Core Export Functions ---
/**
* Exports notes WITH PREVIEWS to a table. (Original logic)
* @param {string} presentationId The ID of the Google Slides presentation.
* @param {string} outputDocId The ID of the Google Document to export notes to.
*/
function createNotesDocWithPreviews(presentationId, outputDocId) {
var presentation = SlidesApp.openById(presentationId);
var out = DocumentApp.openById(outputDocId);
var body = out.getBody();
body.clear();
var table = body.appendTable();
var baseUrl = "https://slides.googleapis.com/v1/presentations/{presentationId}/pages/{pageObjectId}/thumbnail";
var parameters = {
method: "GET",
headers: { "Authorization": "Bearer " + ScriptApp.getOAuthToken() },
muteHttpExceptions: true,
contentType: "application/json"
};
var slides = presentation.getSlides();
Logger.log(`[With Previews] Starting export. Total slides: ${slides.length}`);
for (var i = 0; i < slides.length; i++) {
var slide = slides[i];
if (slide.isSkipped()) {
Logger.log(`[With Previews] Skipping slide ${i + 1} (Skipped)`);
continue;
}
Logger.log(`[With Previews] Processing slide ${i + 1}`);
var text = slide.getNotesPage().getSpeakerNotesShape().getText().asRenderedString();
var row = table.appendTableRow();
var slideObjectId = slide.getObjectId();
var url = baseUrl.replace("{presentationId}", presentationId).replace("{pageObjectId}", slideObjectId);
// Image Cell
var imageCell = row.appendTableCell();
if (imageCell.getNumChildren() > 0 && imageCell.getChild(0).getType() === DocumentApp.ElementType.PARAGRAPH && imageCell.getChild(0).asParagraph().getText() === "") {
imageCell.removeChild(imageCell.getChild(0));
}
try {
var response = UrlFetchApp.fetch(url, parameters);
var responseCode = response.getResponseCode();
var responseBody = response.getContentText();
if (responseCode === 200) {
var responseJson = JSON.parse(responseBody);
if (responseJson && responseJson.contentUrl) {
var imageBlob = UrlFetchApp.fetch(responseJson.contentUrl).getBlob();
imageCell.appendImage(imageBlob).setWidth(160).setHeight(90);
} else { Logger.log(`[With Previews] No contentUrl for slide ${i + 1}. Response: ${responseBody}`); imageCell.appendParagraph("[No URL]"); }
} else { Logger.log(`[With Previews] Error fetching thumb ${i + 1}. Code: ${responseCode}. Body: ${responseBody}`); imageCell.appendParagraph(`[Err: ${responseCode}]`); }
} catch (e) { Logger.log(`[With Previews] Exception fetching thumb ${i + 1}: ${e}`); imageCell.appendParagraph("[Fetch Err]"); }
// Note Cell
var noteCell = row.appendTableCell();
if (noteCell.getNumChildren() > 0 && noteCell.getChild(0).getType() === DocumentApp.ElementType.PARAGRAPH && noteCell.getChild(0).asParagraph().getText() === "") {
noteCell.removeChild(noteCell.getChild(0));
}
noteCell.appendParagraph(text || ""); // Append empty string if text is null/undefined
}
if (table.getNumRows() > 0) {
table.setColumnWidth(0, 160); // Image column width
}
out.saveAndClose();
Logger.log(`[With Previews] Finished export to doc ID: ${outputDocId}`);
}
/**
* Exports notes WITHOUT PREVIEWS to a table.
* @param {string} presentationId The ID of the Google Slides presentation.
* @param {string} outputDocId The ID of the Google Document to export notes to.
*/
function createNotesDocWithoutPreviews(presentationId, outputDocId) {
var presentation = SlidesApp.openById(presentationId);
var out = DocumentApp.openById(outputDocId);
var body = out.getBody();
body.clear();
var table = body.appendTable([['Slide Notes']]); // Add header row
// Optional: format header
table.getRow(0).getCell(0).editAsText().setBold(true);
var slides = presentation.getSlides();
Logger.log(`[Without Previews] Starting export. Total slides: ${slides.length}`);
for (var i = 0; i < slides.length; i++) {
var slide = slides[i];
if (slide.isSkipped()) {
Logger.log(`[Without Previews] Skipping slide ${i + 1} (Skipped)`);
continue;
}
Logger.log(`[Without Previews] Processing slide ${i + 1}`);
var text = slide.getNotesPage().getSpeakerNotesShape().getText().asRenderedString();
var row = table.appendTableRow();
// Note Cell Only
var noteCell = row.appendTableCell();
noteCell.appendParagraph(text || ""); // Append empty string if text is null/undefined
}
// No column width setting needed unless you want to constrain the notes column
out.saveAndClose();
Logger.log(`[Without Previews] Finished export to doc ID: ${outputDocId}`);
}
/**
* Exports notes as TEXT ONLY with numbered headings.
* @param {string} presentationId The ID of the Google Slides presentation.
* @param {string} outputDocId The ID of the Google Document to export notes to.
*/
function createNotesDocTextOnly(presentationId, outputDocId) {
var presentation = SlidesApp.openById(presentationId);
var out = DocumentApp.openById(outputDocId);
var body = out.getBody();
body.clear();
var slides = presentation.getSlides();
Logger.log(`[Text Only] Starting export. Total slides: ${slides.length}`);
var actualSlideNumber = 0; // To handle skipped slides correctly in numbering
for (var i = 0; i < slides.length; i++) {
var slide = slides[i];
if (slide.isSkipped()) {
Logger.log(`[Text Only] Skipping slide ${i + 1} (Skipped)`);
continue;
}
actualSlideNumber++; // Increment only for non-skipped slides
Logger.log(`[Text Only] Processing slide ${i + 1} as Export Slide #${actualSlideNumber}`);
var text = slide.getNotesPage().getSpeakerNotesShape().getText().asRenderedString();
// Add Heading for the slide number
body.appendParagraph(`Slide ${actualSlideNumber}`)
.setHeading(DocumentApp.ParagraphHeading.HEADING2); // Or HEADING1, HEADING3 etc.
// Add notes text
body.appendParagraph(text || "[No speaker notes for this slide]"); // Add placeholder if empty
// Add separator (optional)
body.appendParagraph(""); // Add a blank line for spacing
// or body.appendHorizontalRule();
}
out.saveAndClose();
Logger.log(`[Text Only] Finished export to doc ID: ${outputDocId}`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment