Google Slides¶
Install: uv add googlekit
Enable the Google Slides API in Google Cloud Console. Export and sharing go through the Drive API — request Drive scopes yourself when needed; GoogleKit never adds them silently.
Official Google docs: Slides API guides · REST reference · presentations · Page elements · Enable API
Client¶
from googlekit import GoogleKit
# or: from googlekit.gslides import SlidesClient
client = GoogleKit.from_oauth("client_secret.json", services=["gslides"])
slides = client.slides
Standalone:
from googlekit.gslides import SlidesClient
slides = SlidesClient.from_oauth("client_secrets.json", token_path="token_slides.json")
# slides = SlidesClient.from_service_account("sa.json", subject="user@example.com")
# slides = SlidesClient.from_adc(quota_project_id="my-project")
Default scopes use ScopeProfile.READWRITE → https://www.googleapis.com/auth/presentations. Use ScopeProfile.READONLY for presentations.readonly, or pass an explicit ScopeSet.
Managers¶
| Manager | Role |
|---|---|
slides.presentations |
Create, get, raw batchUpdate, export, share |
slides.pages |
Add / delete / duplicate / reorder slides; list IDs; get page |
slides.elements |
Create shapes, move, resize, transform, group / ungroup |
slides.text |
Insert / delete / replace / style text and paragraphs |
slides.images |
Insert, replace, position/size images |
slides.tables |
Create tables, write cells, insert/delete rows/cols, format cells |
Optional shortcuts vs managers¶
Both appear after slides. (typed as SlidesAPI).
| Shortcut | Delegates to |
|---|---|
create_presentation(title) |
presentations.create(title) |
get_presentation(id) |
presentations.get(id) |
add_slide(id, layout=..., ...) |
pages.add(id, ...) |
# Manager
deck = slides.presentations.create("Pitch Deck")
slides.pages.add(deck.id)
# Shortcut (equivalent)
deck = slides.create_presentation("Pitch Deck")
slides.add_slide(deck.id)
Units: EMU and points¶
Slides sizes and transforms use English Metric Units (EMU).
| Constant | Value |
|---|---|
EMU_PER_PT |
12700 (1 point = 12700 EMUs) |
EMU_PER_INCH |
914400 |
from googlekit.gslides import Size, AffineTransform, pt_to_emu, inches_to_emu
from googlekit.gslides.models import EMU_PER_PT
assert EMU_PER_PT == 12700
pt_to_emu(72) # 914400 (one inch)
Size.from_pt(300, 100) # width/height in EMUs
Size.from_inches(5, 3)
AffineTransform.translate_pt(50, 80)
Helpers accept convenient *_pt kwargs and convert internally. Prefer those unless you need an explicit Size / AffineTransform.
Layouts (PredefinedLayout)¶
When adding slides, pass a PredefinedLayout (or matching string):
| Layout | Typical use |
|---|---|
BLANK |
Empty canvas (default for pages.add) |
TITLE |
Title slide |
TITLE_AND_BODY |
Title + body |
TITLE_AND_TWO_COLUMNS |
Title + two columns |
TITLE_ONLY |
Title only |
SECTION_HEADER |
Section divider |
SECTION_TITLE_AND_DESCRIPTION |
Section title + description |
ONE_COLUMN_TEXT |
Single text column |
MAIN_POINT |
Main point |
BIG_NUMBER |
Large number callout |
CAPTION_ONLY |
Caption only |
from googlekit.gslides import PredefinedLayout
slides.pages.add(presentation_id, layout=PredefinedLayout.TITLE_AND_BODY)
Presentations (slides.presentations)¶
create(title="Untitled presentation") → Presentation¶
presentation = slides.presentations.create("Pitch Deck")
print(presentation.id, presentation.title, presentation.revision_id)
print(presentation.slide_ids()) # ordered slide object IDs
print(presentation.page_size) # Size in EMUs when present
New presentations include an initial slide.
get(presentation_id) → Presentation¶
presentation = slides.presentations.get(presentation_id)
for slide in presentation.slides:
print(slide.object_id, len(slide.elements))
for el in slide.elements:
print(" ", el.object_id, el.kind, el.size, el.transform)
# element kinds: shape | image | table | line | video | sheetsChart | wordArt | group | …
batch_update(presentation_id, requests, *, write_control=None) → BatchUpdateResult¶
result = slides.presentations.batch_update(
presentation_id,
[{"createSlide": {"slideLayoutReference": {"predefinedLayout": "BLANK"}}}],
write_control={"requiredRevisionId": presentation.revision_id},
)
print(result.presentation_id, result.replies)
export(presentation_id, export_format, destination=None)¶
Exports via Drive. Requires the gdrive extra and a Drive scope (drive, drive.readonly, or drive.file). Scopes are not added automatically.
slides.presentations.export(presentation_id, "pdf", destination="deck.pdf")
slides.presentations.export(presentation_id, "pptx") # bytes when destination omitted
share(presentation_id, *, email=None, role="reader", type="user", domain=None, send_notification=False)¶
Requires gdrive and a Drive write scope (drive or drive.file).
slides.presentations.share(
presentation_id,
email="reviewer@example.com",
role="commenter",
send_notification=True,
)
Authorize with Drive when needed:
from googlekit.auth.scopes import Scope, ScopeSet
from googlekit.gslides import SlidesClient
slides = SlidesClient.from_oauth(
"client_secrets.json",
scopes=ScopeSet.of(Scope.PRESENTATIONS, Scope.DRIVE_FILE),
)
Pages (slides.pages)¶
add(presentation_id, *, layout=BLANK, insertion_index=None, object_id=None)¶
from googlekit.gslides import PredefinedLayout
result = slides.pages.add(
presentation_id,
layout=PredefinedLayout.TITLE_AND_BODY,
insertion_index=1,
object_id="slide_title",
)
# createSlide reply includes the new objectId when not supplied
delete(presentation_id, slide_object_id)¶
duplicate(presentation_id, slide_object_id, *, object_ids=None)¶
slides.pages.duplicate(presentation_id, "slide_title")
slides.pages.duplicate(
presentation_id,
"slide_title",
object_ids={"slide_title": "slide_title_copy"},
)
reorder(presentation_id, slide_object_ids, insertion_index)¶
Moves the listed slides (order preserved) to a zero-based insertion index.
ids = slides.pages.get_ids(presentation_id)
slides.pages.reorder(presentation_id, [ids[-1]], insertion_index=0)
get_ids(presentation_id) → list[str]¶
get_page(presentation_id, page_object_id) → dict¶
Fetches a raw page resource (slide, layout, master, or notes).
list_slides(presentation_id) → Presentation¶
Convenience wrapper around presentations.get.
Elements (slides.elements)¶
create_shape(presentation_id, page_object_id, shape_type=TEXT_BOX, …)¶
from googlekit.gslides import ShapeType, Size, AffineTransform
slides.elements.create_shape(
presentation_id,
page_id,
ShapeType.TEXT_BOX,
object_id="intro_box",
width_pt=400,
height_pt=80,
x_pt=60,
y_pt=120,
)
# Or explicit EMU size / transform:
slides.elements.create_shape(
presentation_id,
page_id,
ShapeType.RECTANGLE,
size=Size.from_pt(200, 100),
transform=AffineTransform.translate_pt(40, 40),
)
# ShapeType: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, …
delete(presentation_id, object_id)¶
move — preserves scale¶
move sets translation in points but keeps the current scale/shear from the element. Absolute updates that reset scale to 1 would shrink/inflate already-sized objects.
slides.elements.move(presentation_id, "intro_box", x_pt=100, y_pt=150)
# apply_mode defaults to "ABSOLUTE"
resize — scale from current size¶
Create paths set full EMU size with scaleX/Y = 1. Resize therefore sets:
scale = target_emu / current_size_emu
(not raw EMU magnitudes, which would inflate by ~12700×).
slides.elements.resize(
presentation_id,
"intro_box",
width_pt=500,
height_pt=100,
x_pt=60, # optional; omit to keep current translation
y_pt=120,
)
set_transform(presentation_id, object_id, transform, *, apply_mode="ABSOLUTE")¶
slides.elements.set_transform(
presentation_id,
"intro_box",
AffineTransform(scale_x=1.5, scale_y=1.5, translate_x_emu=pt_to_emu(50), translate_y_emu=pt_to_emu(50)),
)
group / ungroup¶
slides.elements.group(presentation_id, ["shape_a", "shape_b"], group_object_id="grp1")
slides.elements.ungroup(presentation_id, ["grp1"])
Text (slides.text)¶
Text lives on shapes (and table cells via the tables manager). Use the shape/table object ID.
insert(presentation_id, object_id, text, *, insertion_index=0)¶
slides.text.insert(presentation_id, "intro_box", "Built with GoogleKit")
slides.text.insert(presentation_id, "intro_box", " — v1", insertion_index=18)
delete(presentation_id, object_id, *, start_index=0, end_index=None)¶
Omit end_index to delete all text (textRange.type = ALL).
slides.text.delete(presentation_id, "intro_box")
slides.text.delete(presentation_id, "intro_box", start_index=0, end_index=5)
replace_all / replace_placeholders¶
slides.text.replace_all(presentation_id, "{{TITLE}}", "Q3 Plan")
slides.text.replace_all(
presentation_id,
"draft",
"final",
match_case=False,
page_object_ids=[slide_ids[0]],
)
slides.text.replace_placeholders(
presentation_id,
{
"{{title}}": "Q3 Business Review",
"{{speaker}}": "Ada Lovelace",
"{{date}}": "2026-07-12",
},
)
style / style_paragraph¶
from googlekit.gslides import TextStyle, ParagraphStyle
slides.text.style(
presentation_id,
"intro_box",
TextStyle(bold=True, font_size_pt=24, font_family="Arial"),
)
# omit end_index → ALL; or pass start_index/end_index for FIXED_RANGE
slides.text.style_paragraph(
presentation_id,
"intro_box",
ParagraphStyle(alignment="CENTER", line_spacing=100, space_below_pt=8),
)
Foreground colors use Slides color objects, e.g.:
TextStyle(
foreground_color={
"opaqueColor": {"rgbColor": {"red": 0.1, "green": 0.1, "blue": 0.1}}
}
)
Images (slides.images)¶
insert_url(presentation_id, page_object_id, url, …)¶
Public HTTPS URL required.
slides.images.insert_url(
presentation_id,
page_id,
"https://example.com/logo.png",
object_id="logo",
width_pt=160,
height_pt=60,
x_pt=40,
y_pt=20,
)
replace(presentation_id, image_object_id, url, *, image_replace_method="CENTER_INSIDE")¶
position_and_size — same scale math as resize¶
Computes scale = target_emu / current_size_emu so it matches create paths that store full size with scale 1.
slides.images.position_and_size(
presentation_id,
"logo",
x_pt=80,
y_pt=40,
width_pt=200,
height_pt=75,
)
Tables (slides.tables)¶
create(presentation_id, page_object_id, rows, columns, …)¶
result = slides.tables.create(
presentation_id,
page_id,
rows=3,
columns=2,
object_id="metrics",
width_pt=400,
height_pt=180,
x_pt=50,
y_pt=100,
)
write_cell — uses cellLocation, not fake cell IDs¶
Slides table text APIs require the table objectId plus cellLocation (rowIndex / columnIndex). Do not invent per-cell object IDs.
from googlekit.gslides import TextStyle
slides.tables.write_cell(
presentation_id,
"metrics", # table object ID
row_index=0,
column_index=0,
text="Revenue",
style=TextStyle(bold=True, font_size_pt=14),
)
# Implementation: deleteText ALL in that cell, then insertText at insertionIndex 0
Rows and columns¶
slides.tables.insert_rows(
presentation_id, "metrics", number=1, insert_below=True, row_index=0
)
slides.tables.insert_columns(
presentation_id, "metrics", number=1, insert_right=True, column_index=0
)
slides.tables.delete_row(presentation_id, "metrics", row_index=2)
slides.tables.delete_column(presentation_id, "metrics", column_index=1)
format_cells (background)¶
slides.tables.format_cells(
presentation_id,
"metrics",
row_index=0,
column_index=0,
row_span=1,
column_span=2,
background_color={"rgbColor": {"red": 0.2, "green": 0.3, "blue": 0.5}},
)
Models¶
Import from googlekit.gslides:
| Type | Purpose |
|---|---|
Presentation |
id, title, slides, page_size, revision_id, slide_ids() |
SlidePage |
object_id, layout_object_id, elements |
PageElement |
object_id, size, transform, kind |
Size / AffineTransform |
EMU geometry |
TextStyle / ParagraphStyle |
Style field subsets |
PredefinedLayout / ShapeType |
Enums for layouts and shapes |
BatchUpdateResult |
presentation_id, replies, write_control |
pt_to_emu / inches_to_emu |
Unit helpers (EMU_PER_PT = 12700) |
Recipes¶
Create a deck with a text box¶
from googlekit.gslides import PredefinedLayout, ShapeType, SlidesClient, TextStyle
slides = SlidesClient.from_oauth("client_secrets.json", token_path="token_slides.json")
presentation = slides.presentations.create("GoogleKit Demo Deck")
slides.pages.add(
presentation.id,
layout=PredefinedLayout.TITLE_AND_BODY,
insertion_index=1,
object_id="slide_title",
)
page_id = slides.pages.get_ids(presentation.id)[-1]
slides.elements.create_shape(
presentation.id,
page_id,
ShapeType.TEXT_BOX,
object_id="intro_box",
width_pt=400,
height_pt=80,
x_pt=60,
y_pt=120,
)
slides.text.insert(presentation.id, "intro_box", "Built with GoogleKit")
slides.text.style(presentation.id, "intro_box", TextStyle(bold=True, font_size_pt=24))
Fill a template deck¶
slides.text.replace_placeholders(
presentation_id,
{
"{{title}}": "Q3 Business Review",
"{{speaker}}": "Ada Lovelace",
"{{date}}": "2026-07-12",
},
)
Table with header cells¶
slides.tables.create(
presentation_id, page_id, rows=2, columns=2, object_id="grid", width_pt=480, height_pt=160
)
slides.tables.write_cell(presentation_id, "grid", 0, 0, "Metric")
slides.tables.write_cell(presentation_id, "grid", 0, 1, "Value")
slides.tables.write_cell(presentation_id, "grid", 1, 0, "ARR")
slides.tables.write_cell(presentation_id, "grid", 1, 1, "$1.2M")
slides.tables.format_cells(
presentation_id,
"grid",
row_index=0,
column_index=0,
column_span=2,
background_color={"rgbColor": {"red": 0.15, "green": 0.25, "blue": 0.45}},
)
Move and resize without destroying scale¶
# After create_shape / insert_url (size stored in EMU, scale ≈ 1):
slides.elements.move(presentation_id, "intro_box", x_pt=80, y_pt=140)
slides.elements.resize(presentation_id, "intro_box", width_pt=450, height_pt=90)
slides.images.position_and_size(
presentation_id, "logo", x_pt=40, y_pt=20, width_pt=180, height_pt=70
)
Export PDF and share (Drive scopes required)¶
from googlekit.auth.scopes import Scope, ScopeSet
from googlekit.gslides import SlidesClient
slides = SlidesClient.from_oauth(
"client_secrets.json",
scopes=ScopeSet.of(Scope.PRESENTATIONS, Scope.DRIVE_FILE),
)
slides.presentations.export(presentation_id, "pdf", destination="deck.pdf")
slides.presentations.share(presentation_id, email="peer@example.com", role="writer")
Reorder and duplicate slides¶
ids = slides.pages.get_ids(presentation_id)
slides.pages.duplicate(presentation_id, ids[0])
slides.pages.reorder(presentation_id, [ids[-1]], insertion_index=0)