1

I'd like to create an image, a matplotlib graph, in Python, and then automatically insert it into a google slides presentation.

I already have an automated google slides presentation. It updates lots of text and also swaps some text for a few set photos, i.e. those 5 photos are always the same. Using the Google API documentation, figuring all this out took quite a bit of time. I relied heavily on the Google API Github Documentation, the Core Python Programming Blog, and Replacing text & images with the Google Slides API.

While there were examples where an image was shared with the Google Service API account, I never got that to work. I've only been able to get it to update the text to an image by making the images in my google drive viewable to the public. Otherwise (i.e. when the service account had access but the public did not) I'd get this error:

<HttpError 400 when requesting [image URL] returned "Invalid requests[2].replaceAllShapesWithImage: There was a problem retrieving the image. The provided image should be publicly accessible, within size limit, and in supported formats.". Details: "Invalid requests[2].replaceAllShapesWithImage: There was a problem retrieving the image. The provided image should be publicly accessible, within size limit, and in supported formats.">

If I changed the image sharing to public it would then work.

I know that sometimes google's API just doesn't work, as seen here and I'd get that error as well. Just have to wait for a few hours or until tomorrow and then it works again, so long as the images are public. Usually, when this is the case I get this error instead:

HttpError 400 when requesting [image URL] "Invalid requests[0].replaceAllShapesWithImage[or createImage]: Access to the provided image was forbidden."

How to save an image created in Python to a google drive folder with the google API and make it publicly accessible? OR! even better, how to save a photo to a google drive folder and get the image batch update to work when it's shared with the google service account that is creating the slides, but not making it fully public?

This is my code so far:

from matplotlib import pyplot as plt
import json
import unidecode
from oauth2client.service_account import ServiceAccountCredentials
from httplib2 import Http
from apiclient import discovery
import pandas as pd


plt.bar(['hi', 'how', 'are', 'you'], [3,6,3,8], color="skyblue")
plt.savefig('test_bar_plot.png')


# google slides authorization
json_key  = json.load(open('../../creds/creds_file.json'))
scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] #would love to reduce scope if possible
creds = ServiceAccountCredentials.from_json_keyfile_dict(json_key, scope)

# file to copy for slide deck
TMPLFILE='test_run_slide_deck_template'

HTTP = creds.authorize(Http())
DRIVE =  discovery.build('drive',  'v3', http=HTTP)
SLIDES = discovery.build('slides', 'v1', http=HTTP)

# find slide template, delete copy if it already exists, then make a new copy
file_list = pd.DataFrame(DRIVE.files().list().execute().get('files'))
tempfileSTR = 'test_run_slide_deck'
new_file = '_copy'

try:
    DECK_ID= file_list[file_list.name == tempfileSTR+new_file]['id'].values[0]
    print("** Found File ID for %s" % (tempfileSTR+new_file))
    file = DRIVE.files().delete(fileId=DECK_ID).execute()
    print("** Deleting file for %s" % (tempfileSTR+new_file))
except IndexError:
    print("** Did NOT Find File ID for %s" % (tempfileSTR+new_file))
rsp = DRIVE.files().list(q="name='%s'" % TMPLFILE).execute().get('files')[0]
DATA = {'name': tempfileSTR+new_file, 'parents':['14CvIXemPv5fGcLaiTj_HfMXZS_OKI6Lv']}
print('** Copying template %r as %r' % (rsp['name'], DATA['name']))
DECK_ID = DRIVE.files().copy(body=DATA, fileId=rsp['id']).execute().get('id')

# get the presentation and slides 
presentation = SLIDES.presentations().get(presentationId=DECK_ID).execute()
slides = presentation.get('slides')

# create dictionary of what to update for text
report_dict = {"{{change_to_hey_SO}}":"Hi StackOverflow"
           ,"{{change_to_farewell}}":"Bye StackOverflow"}
reqs =[]
for key in report_dict:
    reqs.append({'replaceAllText': {'containsText': {'text': key},
        'replaceText': report_dict[key]}},)

# create dictionary for what to update for an image already in the drive 
def find_img(IMG_FILE):
    rsp = DRIVE.files().list(q="name='%s'" % IMG_FILE).execute().get('files')[0]
    baseURL = "http://drive.google.com/uc?export=view&id="
    return baseURL+rsp['id']

def image_section(image_replace_text, image_file_name):
    image_json = {
        'replaceAllShapesWithImage': {
        'imageUrl':  find_img(image_file_name),
        'imageReplaceMethod': 'CENTER_INSIDE',
        'containsText': {
            'text': image_replace_text,
            'matchCase': True
            }
        }
    }
    return image_json



# add image update to the list for batch updating 
reqs.append(image_section('{{make_me_a_rooster}}', 'rooster_test.png')) 
    
 

# take saved bar plot, 'test_bar_plot.png', upload it to google drive, make it public or make sharing it with the service account work and use the image_section and find_image functions to add it to the reqs updating list for the batch update below.

# [added edit] this uploads the image file, to the same shared folder as the slide deck (shared with user and service account). 
# but does not make the image public and I get the same error as above when it's not public. 
file_metadata = {'name': 'test_bar_plot.png', 'parents':['14CvIXemPv5fGcLaiTj_HfMXZS_OKI6Lv']}
media = discovery.MediaFileUpload('test_bar_plot.png', mimetype='image/png')
file = DRIVE.files().create(body=file_metadata,
                                        media_body=media,
                                        fields='id').execute()
    
# add to reqs list 
reqs.append(image_section('{{update_chart}}', 'test_bar_plot.png')) #, img_lookup

Bar chart

Slide before update

SLIDES.presentations().batchUpdate(body={'requests': reqs}, presentationId=DECK_ID).execute()
print('DONE')

Slide after update

This documentation is the main link for looking up MediaFileUpload and shows no additional parameters in the example and I'm having trouble finding if there are additional ones listed elsewhere.

bauer
  • 11
  • 3
  • In your question, are there 2 questions like 1. `How to save an image created in Python to a google drive folder with the google API and make it publicly accessible?` 2. `how to save a photo to a google drive folder and get the image batch update to work when it's shared with the google service account that is creating the slides, but not making it fully public?`. I couldn't understand about `OR! even better,` you expect. I apologize for this. – Tanaike Feb 25 '21 at 23:52
  • I've now added in code to upload a photo using the MediaFileUpload function. Question 2 would be my first choice, which is now reduced to: how to get the image batch update to work when the photo is uploaded by the google service account, that is also creating the slides, where the photo is not fully, publicly shared? If that's not possible, question 1 is closer to what I've gotten to work before: how to make an API uploaded photo publicly accessible? Does that make sense? – bauer Feb 26 '21 at 01:23
  • When you say "shared with the Google Service API account" you mean trying to share the image to the service account, right? – Martí Feb 26 '21 at 13:01
  • @Martí yes that's what I mean. However, when it is shared it does not seem to have permission to use it (or that's what the error says). And now that I have code to upload the image using the service account, why would the service account that uploaded the image not have permission to use it? – bauer Feb 26 '21 at 13:29
  • Thank you for replying. I noticed that an answer has already ben posted. In this case, I would like to respect the existing answer. – Tanaike Feb 27 '21 at 02:08

2 Answers2

0

You are having problems because the service account is NOT part of your domain, so the permissions will be the same as any unrelated user. Like the documentation states:

Service accounts are not members of your Google Workspace domain, unlike user accounts. If you share Google Workspace assets, like docs or events, with all members in your Google Workspace domain, they are not shared with service accounts. Similarly, Google Workspace assets created by a service account are not created in your Google Workspace domain. As a result, your Google Workspace and Cloud Identity admins can't own or manage these assets.

What is happening to you is that the owner of the slide does not have permissions to add the image (you are trying to add it as a link). The same happens when the service accounts tries to access the slide when it's not public.

The easiest way of making it work is to impersonate a user so the service account can make API calls as if they were the user. Another option is to have a shared folder/drive where both service account and user adds the slices and images (the files there will be automatically shared to all accounts). You can also share the file with the other accounts manually each time you upload or create one.

Martí
  • 2,651
  • 1
  • 4
  • 11
  • Thanks for responding Martí. This gives me hope, but I haven't gotten it to work yet. It seems like the documentation linked is for how a user can impersonate a service account. Is that the thing to do? Looked for documentation on service accounts impersonating a user, but don't see it in there. Am looking into this now: https://github.com/googleapis/google-auth-library-nodejs/issues/916. I did add a parent to the upload metadata, so now the slide deck and the image are in the same folder, shared with the service account, but I still have to manually make the image public to get it to work. – bauer Mar 01 '21 at 19:09
  • Also double checked a user impersonating a service account and it seems like my user account is already set up as an owner for the project. – bauer Mar 01 '21 at 22:54
  • You are trying to impersonate a user as a service account. I'm assuming you are using the python library, so try creating a `ServiceAccountCredentials` and calling `create_delegated`. – Martí Mar 02 '21 at 08:46
0

I also wanted to find a way to upload images to Google Slides using Python. I created a program that uploads images to Google Cloud Storage; creates a signed URL for them; then uses that URL to import images into Google Slides. It's available here under the MIT License and may be a helpful tool/resource.

KBurchfiel
  • 635
  • 6
  • 15