Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.28.1 - Unreleased

### Added

- Slides: allow `insert-image` and `replace-slide` to use public HTTPS image URLs without temporary Drive sharing. (#825) — thanks @sebsnyk.

### Fixed

- Docs: update the Docker authentication example to persist file-keyring tokens with `GOG_HOME`. (#828, #830) — thanks @WadydX.
Expand Down
4 changes: 2 additions & 2 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,12 @@ Generated from `gog schema --json`.
- [`gog slides (slide) delete-slide <presentationId> <slideId>`](commands/gog-slides-delete-slide.md) - Delete a slide by object ID
- [`gog slides (slide) export (download,dl) <presentationId> [flags]`](commands/gog-slides-export.md) - Export a Google Slides deck (pdf|pptx)
- [`gog slides (slide) info (get,show) <presentationId>`](commands/gog-slides-info.md) - Get Google Slides presentation metadata
- [`gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> <image> [flags]`](commands/gog-slides-insert-image.md) - Insert an image at a position and size on an existing slide
- [`gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> [<image>] [flags]`](commands/gog-slides-insert-image.md) - Insert a local or public image at a position and size
- [`gog slides (slide) insert-text <presentationId> <objectId> <text> [flags]`](commands/gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [`gog slides (slide) list-slides <presentationId>`](commands/gog-slides-list-slides.md) - List all slides with their object IDs
- [`gog slides (slide) raw <presentationId> [flags]`](commands/gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [`gog slides (slide) read-slide <presentationId> <slideId>`](commands/gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [`gog slides (slide) replace-slide <presentationId> <slideId> <image> [flags]`](commands/gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [`gog slides (slide) replace-slide <presentationId> <slideId> [<image>] [flags]`](commands/gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
- [`gog slides (slide) replace-text <presentationId> <find> <replacement> [flags]`](commands/gog-slides-replace-text.md) - Find-and-replace text across a presentation
- [`gog slides (slide) thumbnail (thumb) <presentationId> <slideId> [flags]`](commands/gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
- [`gog slides (slide) update-notes <presentationId> <slideId> [flags]`](commands/gog-slides-update-notes.md) - Update speaker notes on an existing slide
Expand Down
4 changes: 2 additions & 2 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,12 +644,12 @@ Generated pages: 646.
- [gog slides delete-slide](gog-slides-delete-slide.md) - Delete a slide by object ID
- [gog slides export](gog-slides-export.md) - Export a Google Slides deck (pdf|pptx)
- [gog slides info](gog-slides-info.md) - Get Google Slides presentation metadata
- [gog slides insert-image](gog-slides-insert-image.md) - Insert an image at a position and size on an existing slide
- [gog slides insert-image](gog-slides-insert-image.md) - Insert a local or public image at a position and size
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation
- [gog slides thumbnail](gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
- [gog slides update-notes](gog-slides-update-notes.md) - Update speaker notes on an existing slide
Expand Down
7 changes: 4 additions & 3 deletions docs/commands/gog-slides-insert-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.

Insert an image at a position and size on an existing slide
Insert a local or public image at a position and size

## Usage

```bash
gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> <image> [flags]
gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> [<image>] [flags]
```

## Parent
Expand All @@ -28,7 +28,7 @@ gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> <ima
| `--enable-commands-exact` | `string` | | Comma-separated list of exact enabled commands; dot paths allowed and parent commands do not enable children |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `--height` | `float64` | 0 | Image height, in --unit; omit to keep the image's aspect ratio |
| `--height` | `float64` | 0 | Image height, in --unit; required with --url, local files preserve aspect ratio when omitted |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
Expand All @@ -37,6 +37,7 @@ gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> <ima
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `--unit` | `string` | PT | Measurement unit for x/y/width/height (PT or EMU) |
| `--url` | `string` | | Public HTTPS image URL to insert directly |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
| `--width` | `float64` | | Image width, in --unit |
Expand Down
5 changes: 3 additions & 2 deletions docs/commands/gog-slides-replace-slide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.

Replace the image on an existing slide in-place
Replace an existing slide image from a local file or public URL

## Usage

```bash
gog slides (slide) replace-slide <presentationId> <slideId> <image> [flags]
gog slides (slide) replace-slide <presentationId> <slideId> [<image>] [flags]
```

## Parent
Expand Down Expand Up @@ -37,6 +37,7 @@ gog slides (slide) replace-slide <presentationId> <slideId> <image> [flags]
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `--url` | `string` | | Public HTTPS image URL to use directly |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
Expand Down
4 changes: 2 additions & 2 deletions docs/commands/gog-slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ gog slides (slide) <command> [flags]
- [gog slides delete-slide](gog-slides-delete-slide.md) - Delete a slide by object ID
- [gog slides export](gog-slides-export.md) - Export a Google Slides deck (pdf|pptx)
- [gog slides info](gog-slides-info.md) - Get Google Slides presentation metadata
- [gog slides insert-image](gog-slides-insert-image.md) - Insert an image at a position and size on an existing slide
- [gog slides insert-image](gog-slides-insert-image.md) - Insert a local or public image at a position and size
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation
- [gog slides thumbnail](gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
- [gog slides update-notes](gog-slides-update-notes.md) - Update speaker notes on an existing slide
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/slides.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ type SlidesCmd struct {
ReadSlide SlidesReadSlideCmd `cmd:"" name:"read-slide" help:"Read slide content: speaker notes, text elements, and images"`
Thumbnail SlidesThumbnailCmd `cmd:"" name:"thumbnail" aliases:"thumb" help:"Get or download a rendered thumbnail for a slide"`
UpdateNotes SlidesUpdateNotesCmd `cmd:"" name:"update-notes" help:"Update speaker notes on an existing slide"`
ReplaceSlide SlidesReplaceSlideCmd `cmd:"" name:"replace-slide" help:"Replace the image on an existing slide in-place"`
InsertImage SlidesInsertImageCmd `cmd:"" name:"insert-image" help:"Insert an image at a position and size on an existing slide"`
ReplaceSlide SlidesReplaceSlideCmd `cmd:"" name:"replace-slide" help:"Replace an existing slide image from a local file or public URL"`
InsertImage SlidesInsertImageCmd `cmd:"" name:"insert-image" help:"Insert a local or public image at a position and size"`
InsertText SlidesInsertTextCmd `cmd:"" name:"insert-text" help:"Insert text into an existing page element (shape or table) by objectId"`
ReplaceText SlidesReplaceTextCmd `cmd:"" name:"replace-text" help:"Find-and-replace text across a presentation"`
Raw SlidesRawCmd `cmd:"" name:"raw" help:"Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)"`
Expand Down
116 changes: 55 additions & 61 deletions internal/cmd/slides_insert_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ import (
// existing slide. Unlike add-slide (which lays a full-bleed image on a new
// slide), this places a sized element on a slide you already have, so callers
// can build native decks via the Slides API and still drop in a logo, chart,
// or badge at a precise location. It reuses the same private-image flow as
// add-slide: upload to Drive, grant a temporary read permission so the Slides
// image fetcher can read it, create the image, then delete the temp file.
// or badge at a precise location. Local files use the same temporary Drive
// upload flow as add-slide; public HTTPS URLs are passed directly to Slides.
type SlidesInsertImageCmd struct {
PresentationID string `arg:"" name:"presentationId" help:"Presentation ID"`
SlideID string `arg:"" name:"slideId" help:"Slide object ID to place the image on"`
Image string `arg:"" name:"image" help:"Local image file (PNG/JPG/GIF)" type:"existingfile"`
Image string `arg:"" optional:"" name:"image" help:"Local image file (PNG/JPG/GIF)" type:"existingfile"`
URL string `name:"url" help:"Public HTTPS image URL to insert directly"`
X float64 `name:"x" default:"0" help:"Left position of the image, in --unit"`
Y float64 `name:"y" default:"0" help:"Top position of the image, in --unit"`
Width float64 `name:"width" required:"" help:"Image width, in --unit"`
Height float64 `name:"height" default:"0" help:"Image height, in --unit; omit to keep the image's aspect ratio"`
Height float64 `name:"height" default:"0" help:"Image height, in --unit; required with --url, local files preserve aspect ratio when omitted"`
Unit string `name:"unit" enum:"PT,EMU" default:"PT" help:"Measurement unit for x/y/width/height (PT or EMU)"`
}

Expand All @@ -55,41 +55,40 @@ func (c *SlidesInsertImageCmd) Run(ctx context.Context, flags *RootFlags) error
return usage("--height cannot be negative")
}

// Validate image format.
ext := strings.ToLower(filepath.Ext(c.Image))
var mimeType string
switch ext {
case extPNG:
mimeType = mimePNG
case imageExtJPG, imageExtJPEG:
mimeType = imageMimeJPEG
case imageExtGIF:
mimeType = imageMimeGIF
default:
return usagef("unsupported image format %q (use PNG, JPG, or GIF)", ext)
source, err := resolveSlidesImageSource(c.Image, c.URL)
if err != nil {
return err
}

// Resolve height from the image's aspect ratio when not supplied.
height := c.Height
if height == 0 {
ar, err := imageAspectRatio(c.Image)
if err != nil {
return fmt.Errorf("determine image aspect ratio (pass --height to skip): %w", err)
if source.imageURL != "" {
return usage("--height is required with --url")
}
ar, aspectErr := imageAspectRatio(source.localPath)
if aspectErr != nil {
return fmt.Errorf("determine image aspect ratio (pass --height to skip): %w", aspectErr)
}
height = c.Width * ar
}

if dryRunErr := dryRunExit(ctx, flags, "slides.insert-image", map[string]any{
dryRunPayload := map[string]any{
"presentation_id": presentationID,
"slide_id": slideID,
"image": c.Image,
"mime_type": mimeType,
"x": c.X,
"y": c.Y,
"width": c.Width,
"height": height,
"unit": c.Unit,
}); dryRunErr != nil {
}
if source.imageURL != "" {
dryRunPayload["url"] = source.imageURL
} else {
dryRunPayload["image"] = source.localPath
dryRunPayload["mime_type"] = source.mimeType
}
if dryRunErr := dryRunExit(ctx, flags, "slides.insert-image", dryRunPayload); dryRunErr != nil {
return dryRunErr
}

Expand All @@ -113,46 +112,41 @@ func (c *SlidesInsertImageCmd) Run(ctx context.Context, flags *RootFlags) error
return fmt.Errorf("slide %q not found in presentation", slideID)
}

driveSvc, err := driveService(ctx, account)
if err != nil {
return err
}

// Upload the image to Drive as a temporary file.
imgFile, err := os.Open(c.Image)
if err != nil {
return fmt.Errorf("open image: %w", err)
}
defer imgFile.Close()

driveFile, err := driveSvc.Files.Create(&drive.File{
Name: filepath.Base(c.Image),
MimeType: mimeType,
}).Media(imgFile).Fields("id, webContentLink").Context(ctx).Do()
if err != nil {
return fmt.Errorf("upload image to Drive: %w", err)
}

// Clean up the temporary Drive file when done. Use a cancellation-immune
// context so the public temp file is still removed if the request context
// was canceled, and surface a loud warning if deletion fails (otherwise the
// uploaded image stays world-readable until someone removes it by hand).
defer func() {
if delErr := driveSvc.Files.Delete(driveFile.Id).Context(context.WithoutCancel(ctx)).Do(); delErr != nil {
u.Err().Linef("Warning: failed to delete temporary Drive image %s; it may remain publicly readable until removed: %v", driveFile.Id, delErr)
imageURL := source.imageURL
if imageURL == "" {
driveSvc, driveErr := driveService(ctx, account)
if driveErr != nil {
return driveErr
}
}()

// Make publicly readable so the Slides API can fetch it.
_, err = driveSvc.Permissions.Create(driveFile.Id, &drive.Permission{
Type: "anyone",
Role: "reader",
}).Context(ctx).Do()
if err != nil {
return fmt.Errorf("set image permissions: %w", err)
imgFile, openErr := os.Open(source.localPath)
if openErr != nil {
return fmt.Errorf("open image: %w", openErr)
}
defer imgFile.Close()

driveFile, uploadErr := driveSvc.Files.Create(&drive.File{
Name: filepath.Base(source.localPath),
MimeType: source.mimeType,
}).Media(imgFile).Fields("id, webContentLink").Context(ctx).Do()
if uploadErr != nil {
return fmt.Errorf("upload image to Drive: %w", uploadErr)
}
defer func() {
if delErr := driveSvc.Files.Delete(driveFile.Id).Context(context.WithoutCancel(ctx)).Do(); delErr != nil {
u.Err().Linef("Warning: failed to delete temporary Drive image %s; it may remain publicly readable until removed: %v", driveFile.Id, delErr)
}
}()

_, permissionErr := driveSvc.Permissions.Create(driveFile.Id, &drive.Permission{
Type: "anyone",
Role: "reader",
}).Context(ctx).Do()
if permissionErr != nil {
return fmt.Errorf("set image permissions: %w", permissionErr)
}
imageURL = driveImageDownloadURL(driveFile.Id)
}

imageURL := driveImageDownloadURL(driveFile.Id)
imageID := fmt.Sprintf("img_%d", time.Now().UnixNano())

err = batchUpdateSlidesImageRequests(ctx, slidesSvc, presentationID, &slides.BatchUpdatePresentationRequest{
Expand Down
Loading
Loading