Here’s a similar write-up for our Google Slides image update function, based on the similar framework provided by Russ for writing to Google Sheets without Zapier:
Cloud Function to Update Google Slides Images
Do you need to automatically update images in Google Slides? If you have GCP and the ability to create a cloud function, this article is for you. We’ll show you how to create a function that can update images in your Google Slides presentations with proper positioning and sizing.
Usage
Let’s start with how to use it. Full initial setup instructions are below under Initial Technical Setup.
Step 1) Collect the following info from your Google Slides presentation:
-
presentation_id: Found in your Google Slides URL (e.g., 105tmY-5-v_MWKkKWy0duEc78hpVgHwhmBRlb4VhvwcU)
-
slide_id: Usually ‘p’ for the first slide
Step 2) Assemble the URL and arguments
Your endpoint for the function will have a URL associated with it. Use that and the information to assemble the full URL with arguments:
https://{{cloud function endpoint url}}/omni_to_slides?
presentation_id=YOUR_PRESENTATION_ID
&slide_id=YOUR_SLIDE_ID # typically p for first slide
&replace_image=first # Options: 'first', 'last', or index like '0', '1'
Optional parameters:
-
width: Width in points (default: 768 - 80% of slide width)
-
height: Height in points (default: 432 - 80% of slide height)
-
x_position: X position in points (default: 96 - centered)
-
y_position: Y position in points (default: 54 - centered)
Step 3) Give the service account editor access to the Google Slides presentation
Step 4) Send your image
Send a POST request to the URL with your image file. The function will:
-
Upload the image to Google Drive
-
Replace the specified image on the slide
-
Center and size it appropriately
-
Clean up the temporary file
Initial Technical Setup (GCP console)
This is the easiest recipe for installing a cloud function on GCP and applying the right logic. This assumes you have owner/admin privileges and are in a GCP project with billing enabled.
Step 1) Enable the required APIs
Search for and enable these APIs in the Google Cloud Console:
-
Google Slides API
-
Google Drive API
-
Cloud Functions API
-
Cloud Build API
Step 2) Create a service account
-
Search in the console for “Service Accounts” (IAM & Admin)
-
Click “Create Service Account”
-
Give it a name and description
-
Grant these roles:
-
Cloud Functions Invoker
-
Slides API User
-
Drive API User
Step 3) Create the cloud function
-
Search for “Cloud Functions” in the console
-
Click “Create Function”
-
Choose:
-
Runtime: Python 3.9
-
Trigger: HTTP
-
Authentication: Allow unauthenticated invocations
-
Entry point: omni_to_slides
Replace main.py
with the below code:
import functions_framework
import os
import json
from datetime import datetime
import logging
import google.auth.credentials
from google.oauth2 import service_account
from googleapiclient.http import MediaFileUpload
from googleapiclient.http import MediaIoBaseUpload
from random import randint
import time
import base64
import io
import google.auth
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from typing import Optional
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration
DEFAULT_PRESENTATION_ID = os.getenv('DEFAULT_PRESENTATION_ID', '105tmY-5-v_MWKkKWy0duEc78hpVgHwhmBRlb4VhvwcU')
DEFAULT_SLIDE_ID = os.getenv('DEFAULT_SLIDE_ID', 'p')
DEFAULT_IMAGE_WIDTH = int(os.getenv('DEFAULT_IMAGE_WIDTH', '768')) # 80% of 960 points (slide width)
DEFAULT_IMAGE_HEIGHT = int(os.getenv('DEFAULT_IMAGE_HEIGHT', '432')) # 80% of 540 points (slide height)
DEFAULT_X_POSITION = int(os.getenv('DEFAULT_X_POSITION', '96')) # (960 - 768) / 2 = 96 points from left
DEFAULT_Y_POSITION = int(os.getenv('DEFAULT_Y_POSITION', '54')) # (540 - 432) / 2 = 54 points from top
def get_slides_client(rw_vs_ro: str):
"""
Generate the service credentials to be used to query a google slide
Args:
rw_vs_ro (str): A string, 'r_o' or 'r_w', representing whether creds should be readonly or readwrite
Returns:
tuple: A tuple containing (slides_service, drive_service)
"""
if rw_vs_ro == 'r_o':
scopes = ['https://www.googleapis.com/auth/spreadsheets.readonly']
if rw_vs_ro == 'r_w':
scopes = ['https://www.googleapis.com/auth/spreadsheets','https://www.googleapis.com/auth/presentations','https://www.googleapis.com/auth/drive']
try:
creds, project = google.auth.default(scopes=scopes)
drive_service = build('drive', 'v3', credentials=creds)
service = build('slides', 'v1', credentials=creds)
return service, drive_service
except Exception as e:
logger.error(f"Failed to initialize Google services: {str(e)}")
raise
def transform_data(webhook_data, header_row=False):
"""
Transform the incoming webhook data into a format suitable for Google Sheets.
Args:
webhook_data (dict): The incoming webhook data.
Returns:
list: A list of transformed data rows.
"""
new_values = []
if header_row:
new_values = [[str(key) for key in webhook_data[0].keys()]]
new_values.extend( [[val for val in row.values()] for row in webhook_data])
else:
new_values = [[val for val in row.values()] for row in webhook_data]
return new_values
def get_slide_images(service, presentation_id: str, slide_id: str) -> list:
"""
Get all images on a specific slide.
Args:
service: Google Slides service
presentation_id (str): ID of the presentation
slide_id (str): ID of the slide
Returns:
list: List of image objects on the slide
"""
try:
logger.info(f"Attempting to get slide with ID: {slide_id}")
# Get the slide
slide = service.presentations().pages().get(
presentationId=presentation_id,
pageObjectId=slide_id
).execute()
logger.info(f"Successfully retrieved slide. Found {len(slide.get('pageElements', []))} elements")
# Extract all image elements
images = []
for element in slide.get('pageElements', []):
if 'image' in element:
images.append({
'objectId': element['objectId'],
'transform': element.get('transform', {}),
'size': element.get('size', {})
})
logger.info(f"Found {len(images)} images on the slide")
if images:
logger.info(f"First image objectId: {images[0]['objectId']}")
return images
except HttpError as e:
logger.error(f"Failed to get slide images: {str(e)}")
raise
def update_google_slides(
png_binary_data: bytes,
presentation_id: str,
slide_id: str,
image_id: Optional[str] = None,
replace_image: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
x_position: Optional[int] = None,
y_position: Optional[int] = None
):
"""
Update a Google Slides presentation with a PNG image.
Args:
png_binary_data (bytes): The binary data of the PNG image
presentation_id (str): The ID of the presentation (from the URL)
slide_id (str): The ID of the slide to update (will be used as pageId)
image_id (str, optional): ID of an existing image to replace
replace_image (str, optional): Which image to replace ('first', 'last', or index like '0', '1', etc.)
width (int, optional): Width of the image in points (default: 768 - 80% of slide width)
height (int, optional): Height of the image in points (default: 432 - 80% of slide height)
x_position (int, optional): X position of the image in points (default: 96 - centered)
y_position (int, optional): Y position of the image in points (default: 54 - centered)
Returns:
dict: The response from the Google Slides API
Raises:
HttpError: If the Google Slides API request fails
ValueError: If the image data is invalid or replace_image is invalid
"""
if not png_binary_data:
raise ValueError("No image data provided")
# Use default values if not provided
width = width or DEFAULT_IMAGE_WIDTH
height = height or DEFAULT_IMAGE_HEIGHT
x_position = x_position or DEFAULT_X_POSITION
y_position = y_position or DEFAULT_Y_POSITION
try:
service, drive_service = get_slides_client('r_w')
# Handle image replacement based on position
if replace_image:
logger.info(f"Attempting to replace image with replace_image={replace_image}")
images = get_slide_images(service, presentation_id, slide_id)
if not images:
raise ValueError("No images found on the slide to replace")
if replace_image == 'first':
image_id = images[0]['objectId']
logger.info(f"Selected first image with objectId: {image_id}")
elif replace_image == 'last':
image_id = images[-1]['objectId']
logger.info(f"Selected last image with objectId: {image_id}")
else:
try:
index = int(replace_image)
if index < 0 or index >= len(images):
raise ValueError(f"Image index {index} is out of range. Slide has {len(images)} images.")
image_id = images[index]['objectId']
logger.info(f"Selected image at index {index} with objectId: {image_id}")
except ValueError:
raise ValueError("replace_image must be 'first', 'last', or a valid index number")
# Upload image to Google Drive
logger.info("Uploading image to Google Drive...")
image_data = base64.b64decode(base64.b64encode(png_binary_data).decode('UTF-8'))
fh = io.BytesIO(image_data)
# Create a unique filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
file_metadata = {
'name': f'slide_image_{timestamp}_{randint(0,1000000)}.png',
'mimeType': 'image/png'
}
media = MediaIoBaseUpload(
fh,
mimetype='image/png',
resumable=True
)
image_file_id = None
try:
file = drive_service.files().create(
body=file_metadata,
media_body=media,
fields='id,webContentLink',
supportsAllDrives=True
).execute()
image_file_id = file.get('id')
logger.info(f"Image uploaded successfully with ID: {image_file_id}")
# Make the file publicly accessible
logger.info("Setting file permissions...")
drive_service.permissions().create(
fileId=image_file_id,
body={
'type': 'anyone',
'role': 'reader',
'allowFileDiscovery': False
},
fields='id'
).execute()
# Wait for permissions to propagate
logger.info("Waiting for permissions to propagate...")
time.sleep(5)
# Get the direct download link
file = drive_service.files().get(
fileId=image_file_id,
fields='webContentLink'
).execute()
image_url = file.get('webContentLink')
if not image_url:
raise ValueError("Failed to get public URL for the image")
logger.info(f"Using image URL: {image_url}")
if image_id:
logger.info(f"Preparing to replace image with ID: {image_id}")
requests = [
{
'replaceImage': {
'imageObjectId': image_id,
'imageReplaceMethod': 'CENTER_INSIDE',
'url': image_url
}
}
]
else:
logger.info("Creating new image")
requests = [
{
'createImage': {
'url': image_url,
'elementProperties': {
'pageId': slide_id,
'size': {
'width': {'magnitude': width, 'unit': 'PT'},
'height': {'magnitude': height, 'unit': 'PT'}
},
'transform': {
'scaleX': 1,
'scaleY': 1,
'translateX': x_position,
'translateY': y_position,
'unit': 'PT'
}
}
}
}
]
logger.info("Updating Google Slides presentation...")
response = service.presentations().batchUpdate(
presentationId=presentation_id,
body={'requests': requests}
).execute()
logger.info("Successfully updated presentation")
# Wait a bit to ensure Slides has accessed the image
time.sleep(5)
return response
except HttpError as e:
logger.error(f"Failed to create/upload image: {str(e)}")
raise
finally:
# Clean up the temporary file
if image_file_id:
try:
logger.info(f"Cleaning up temporary file: {image_file_id}")
drive_service.files().delete(fileId=image_file_id).execute()
except Exception as e:
logger.error(f"Failed to clean up temporary file: {str(e)}")
except HttpError as e:
logger.error(f"Google Slides API error: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise
@functions_framework.http
def omni_to_slides(request):
"""
Cloud Function to handle image uploads and update Google Slides.
Accepts webhook requests with query parameters for customization.
Required Query Parameters:
presentation_id (str): ID of the Google Slides presentation
slide_id (str): ID of the slide to update
Optional Query Parameters:
width (int): Width of the image in points (default: 300)
height (int): Height of the image in points (default: 200)
x_position (int): X position of the image in points (default: 100)
y_position (int): Y position of the image in points (default: 100)
image_id (str): ID of existing image to replace
replace_image (str): Which image to replace ('first', 'last', or index like '0', '1', etc.)
Args:
request (flask.Request): The request object containing the image file
Returns:
tuple: A tuple containing (response_message, status_code)
Expected request format:
- Multipart form data with an image file
- Required query parameters: presentation_id, slide_id
- Optional query parameters for customization
"""
try:
# Get required query parameters
presentation_id = request.args.get('presentation_id')
slide_id = request.args.get('slide_id')
# Validate required parameters
if not presentation_id:
return json.dumps({
"status": "error",
"message": "presentation_id is required"
}), 400
if not slide_id:
return json.dumps({
"status": "error",
"message": "slide_id is required"
}), 400
# Get optional query parameters with defaults
width = request.args.get('width', type=int, default=DEFAULT_IMAGE_WIDTH)
height = request.args.get('height', type=int, default=DEFAULT_IMAGE_HEIGHT)
x_position = request.args.get('x_position', type=int, default=DEFAULT_X_POSITION)
y_position = request.args.get('y_position', type=int, default=DEFAULT_Y_POSITION)
image_id = request.args.get('image_id')
replace_image = request.args.get('replace_image')
if not request.files:
return json.dumps({
"status": "error",
"message": "No file found in the request"
}), 400
# Get the first file from the request
file = next(iter(request.files.values()))
# Read the binary data
png_binary_data = file.stream.read()
# Save a copy locally for backup/debugging
with open('captured_image.png', 'wb') as f:
f.write(png_binary_data)
# Update the Google Slides presentation
response = update_google_slides(
png_binary_data=png_binary_data,
presentation_id=presentation_id,
slide_id=slide_id,
image_id=image_id,
replace_image=replace_image,
width=width,
height=height,
x_position=x_position,
y_position=y_position
)
logger.info(f"Successfully updated Google Slides presentation: {response}")
return json.dumps({
"status": "success",
"message": "Image successfully added to presentation",
"presentation_id": presentation_id,
"slide_id": slide_id,
"parameters": {
"width": width,
"height": height,
"x_position": x_position,
"y_position": y_position,
"replace_image": replace_image
}
}), 200
except HttpError as e:
logger.error(f"Google API error: {str(e)}")
return json.dumps({
"status": "error",
"message": f"Google API error: {str(e)}"
}), 500
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return json.dumps({
"status": "error",
"message": f"Error processing request: {str(e)}"
}), 500
@functions_framework.http
def error_handler(request):
"""
Custom error handler to help with debugging deployment issues.
Args:
request (flask.Request): The request object
Returns:
tuple: A tuple containing (error_message, status_code)
"""
try:
raise Exception("Test error handling")
except Exception as e:
logger.error(f"Caught error: {str(e)}")
return json.dumps({
"status": "error",
"message": str(e)
}), 500
Replace requirements.txt
with below
blinker==1.9.0
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
cloudevents==1.11.0
deprecation==2.1.0
Flask==3.1.0
functions-framework==3.*
google-api-core==2.24.2
google-api-python-client==2.*
google-auth==2.*
google-auth-httplib2==0.*
google-auth-oauthlib==0.*
googleapis-common-protos==1.69.2
gunicorn==23.0.0
httplib2==0.22.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
oauthlib==3.2.2
packaging==24.2
proto-plus==1.26.1
protobuf==6.30.2
pyasn1==0.6.1
pyasn1_modules==0.4.1
pyparsing==3.2.3
requests==2.32.3
requests-oauthlib==2.0.0
rsa==4.9
uritemplate==4.1.1
urllib3==2.3.0
watchdog==6.0.0
Werkzeug==3.1.3
Click the + icon on the upper right of the file selector and create a file called service_account.json
You will then paste the contents of your service_account.json file you downloaded when we created the service account key.
Click “Deploy”
The function will be built and deployed. Once complete, you’ll get the function URL that you can use to update your slides.
Features
-
Automatically centers images on slides
-
Maintains aspect ratio
-
Can replace existing images or add new ones
-
Configurable size and position
-
Handles large images efficiently
-
Cleans up temporary files automatically
Technical Details
The function uses:
-
Python 3.9 runtime
-
Google Cloud Functions
-
Google Slides API
-
Google Drive API
-
Application Default Credentials