// Google Slides API Service
import { google } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import type { slides_v1 } from 'googleapis';
import { v4 as uuidv4 } from 'uuid';
export class SlidesService {
private slides: slides_v1.Slides;
private authClient: OAuth2Client;
constructor(authClient: OAuth2Client) {
this.slides = google.slides({ version: 'v1', auth: authClient });
this.authClient = authClient;
}
/**
* Create a new presentation with slides
*/
async createPresentation(
name: string,
slides: Array<{ title: string; body: string }>,
parentFolder?: string
): Promise<{ id: string; name: string; webViewLink: string }> {
// Create presentation
const presentation = await this.slides.presentations.create({
requestBody: { title: name }
});
const presentationId = presentation.data.presentationId!;
// Move to parent folder if specified
if (parentFolder) {
const drive = google.drive({ version: 'v3', auth: this.authClient });
await drive.files.update({
fileId: presentationId,
addParents: parentFolder,
removeParents: 'root',
supportsAllDrives: true,
});
}
// Create slides with content
for (const slide of slides) {
const slideObjectId = `slide_${uuidv4().substring(0, 8)}`;
// Create slide
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: {
requests: [{
createSlide: {
objectId: slideObjectId,
slideLayoutReference: { predefinedLayout: 'TITLE_AND_BODY' },
}
}]
}
});
// Get placeholders
const slidePage = await this.slides.presentations.pages.get({
presentationId,
pageObjectId: slideObjectId,
});
let titlePlaceholderId = '';
let bodyPlaceholderId = '';
slidePage.data.pageElements?.forEach((el) => {
if (el.shape?.placeholder?.type === 'TITLE') {
titlePlaceholderId = el.objectId!;
} else if (el.shape?.placeholder?.type === 'BODY') {
bodyPlaceholderId = el.objectId!;
}
});
// Insert text
const requests: slides_v1.Schema$Request[] = [];
if (titlePlaceholderId && slide.title) {
requests.push({
insertText: {
objectId: titlePlaceholderId,
text: slide.title,
insertionIndex: 0
}
});
}
if (bodyPlaceholderId && slide.body) {
requests.push({
insertText: {
objectId: bodyPlaceholderId,
text: slide.body,
insertionIndex: 0
}
});
}
if (requests.length > 0) {
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: { requests }
});
}
}
return {
id: presentationId,
name: presentation.data.title || name,
webViewLink: `https://docs.google.com/presentation/d/${presentationId}`
};
}
/**
* Get content of a presentation
*/
async getPresentationContent(presentationId: string): Promise<{
title: string;
slides: Array<{ id: string; title?: string; body?: string }>;
}> {
const presentation = await this.slides.presentations.get({ presentationId });
const slidesContent: Array<{ id: string; title?: string; body?: string }> = [];
for (const slide of presentation.data.slides || []) {
const slideData: { id: string; title?: string; body?: string } = {
id: slide.objectId!
};
for (const element of slide.pageElements || []) {
if (element.shape?.placeholder?.type === 'TITLE' || element.shape?.placeholder?.type === 'CENTERED_TITLE') {
slideData.title = this.extractText(element);
} else if (element.shape?.placeholder?.type === 'BODY' || element.shape?.placeholder?.type === 'SUBTITLE') {
slideData.body = this.extractText(element);
}
}
slidesContent.push(slideData);
}
return {
title: presentation.data.title || '',
slides: slidesContent
};
}
/**
* Update a slide's content
*/
async updateSlide(
presentationId: string,
slideId: string,
updates: { title?: string; body?: string }
): Promise<void> {
const slidePage = await this.slides.presentations.pages.get({
presentationId,
pageObjectId: slideId,
});
const requests: slides_v1.Schema$Request[] = [];
for (const element of slidePage.data.pageElements || []) {
if (element.shape?.placeholder?.type === 'TITLE' && updates.title !== undefined) {
// Delete existing text and insert new
const textLength = this.getTextLength(element);
if (textLength > 0) {
requests.push({
deleteText: {
objectId: element.objectId!,
textRange: { type: 'ALL' }
}
});
}
requests.push({
insertText: {
objectId: element.objectId!,
text: updates.title,
insertionIndex: 0
}
});
} else if (element.shape?.placeholder?.type === 'BODY' && updates.body !== undefined) {
const textLength = this.getTextLength(element);
if (textLength > 0) {
requests.push({
deleteText: {
objectId: element.objectId!,
textRange: { type: 'ALL' }
}
});
}
requests.push({
insertText: {
objectId: element.objectId!,
text: updates.body,
insertionIndex: 0
}
});
}
}
if (requests.length > 0) {
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: { requests }
});
}
}
/**
* Create a text box on a slide
*/
async createTextBox(
presentationId: string,
slideId: string,
text: string,
position: { x: number; y: number; width: number; height: number }
): Promise<{ objectId: string }> {
const objectId = `textbox_${uuidv4().substring(0, 8)}`;
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: {
requests: [
{
createShape: {
objectId,
shapeType: 'TEXT_BOX',
elementProperties: {
pageObjectId: slideId,
size: {
width: { magnitude: position.width, unit: 'PT' },
height: { magnitude: position.height, unit: 'PT' }
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: position.x,
translateY: position.y,
unit: 'PT'
}
}
}
},
{
insertText: {
objectId,
text,
insertionIndex: 0
}
}
]
}
});
return { objectId };
}
/**
* Create a shape on a slide
*/
async createShape(
presentationId: string,
slideId: string,
shapeType: string,
position: { x: number; y: number; width: number; height: number },
style?: {
fillColor?: { red?: number; green?: number; blue?: number };
outlineColor?: { red?: number; green?: number; blue?: number };
}
): Promise<{ objectId: string }> {
const objectId = `shape_${uuidv4().substring(0, 8)}`;
const requests: slides_v1.Schema$Request[] = [
{
createShape: {
objectId,
shapeType: shapeType as slides_v1.Schema$CreateShapeRequest['shapeType'],
elementProperties: {
pageObjectId: slideId,
size: {
width: { magnitude: position.width, unit: 'PT' },
height: { magnitude: position.height, unit: 'PT' }
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: position.x,
translateY: position.y,
unit: 'PT'
}
}
}
}
];
if (style) {
const shapeProperties: slides_v1.Schema$ShapeProperties = {};
if (style.fillColor) {
shapeProperties.shapeBackgroundFill = {
solidFill: { color: { rgbColor: style.fillColor } }
};
}
if (style.outlineColor) {
shapeProperties.outline = {
outlineFill: { solidFill: { color: { rgbColor: style.outlineColor } } }
};
}
// Only add updateShapeProperties if there are properties to update
const fields = Object.keys(shapeProperties).join(',');
if (fields) {
requests.push({
updateShapeProperties: {
objectId,
shapeProperties,
fields
}
});
}
}
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: { requests }
});
return { objectId };
}
/**
* Format text in a shape
*/
async formatText(
presentationId: string,
objectId: string,
format: {
bold?: boolean;
italic?: boolean;
underline?: boolean;
fontSize?: number;
foregroundColor?: { red?: number; green?: number; blue?: number };
}
): Promise<void> {
const textStyle: slides_v1.Schema$TextStyle = {};
const fields: string[] = [];
if (format.bold !== undefined) {
textStyle.bold = format.bold;
fields.push('bold');
}
if (format.italic !== undefined) {
textStyle.italic = format.italic;
fields.push('italic');
}
if (format.underline !== undefined) {
textStyle.underline = format.underline;
fields.push('underline');
}
if (format.fontSize !== undefined) {
textStyle.fontSize = { magnitude: format.fontSize, unit: 'PT' };
fields.push('fontSize');
}
if (format.foregroundColor) {
textStyle.foregroundColor = { opaqueColor: { rgbColor: format.foregroundColor } };
fields.push('foregroundColor');
}
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: {
requests: [{
updateTextStyle: {
objectId,
textRange: { type: 'ALL' },
style: textStyle,
fields: fields.join(',')
}
}]
}
});
}
/**
* Set slide background
*/
async setSlideBackground(
presentationId: string,
slideId: string,
color: { red?: number; green?: number; blue?: number }
): Promise<void> {
await this.slides.presentations.batchUpdate({
presentationId,
requestBody: {
requests: [{
updatePageProperties: {
objectId: slideId,
pageProperties: {
pageBackgroundFill: {
solidFill: { color: { rgbColor: color } }
}
},
fields: 'pageBackgroundFill.solidFill.color'
}
}]
}
});
}
private extractText(element: slides_v1.Schema$PageElement): string {
let text = '';
if (element.shape?.text?.textElements) {
for (const textElement of element.shape.text.textElements) {
if (textElement.textRun?.content) {
text += textElement.textRun.content;
}
}
}
return text.trim();
}
private getTextLength(element: slides_v1.Schema$PageElement): number {
return this.extractText(element).length;
}
}