Search code examples
pythongoogle-drive-apigoogle-api-python-clientgoogle-slides-api

Why do I get this error most of the time trying to use the google API to upload and insert into slides a picture?


I am trying to generate slideshows automatically on Google Slides via the api in python3 and this involves uploading a graph to a particular slide. I created a service account with the slides and drive API enabled and I have the security key downloaded to a file called 'secret.json'.

My setup currently is that I have a template slideshow with {{tag_name}} tags in the text boxes for text replacements and rectangle shapes with {{graph_name}} tags for the images. So I have some code that copies this template slideshow to a folder, uploads an image to that folder, and replaces the shape with the image. The trick with the picture is that it must be viewable and downloadable by anybody with the link. If I upload a photo manually and adjust that permission manually in the web app, this works fine. However, if I do this with the API in a Jupyter notebook, the cell that replaces the shape in the slide with the image will usually throw an error. There are a few times though where if I rerun the cell with the same image, or a different image, it will actually work. This is the error it usually throws: <HttpError 400 when requesting https://slides.googleapis.com/v1/presentations/1yv3qJImZYBONjhOvBWgmYUf7PYaaFHpzWdoMWG7ZE_o:batchUpdate?alt=json returned "Invalid requests[0].replaceAllShapesWithImage: Access to the provided image was forbidden.". Details: "Invalid requests[0].replaceAllShapesWithImage: Access to the provided image was forbidden."> I can't seem to figure out what makes it work sometimes, because I can't get it to work always, but I'm suspicious that it could have to do with what I do in my Safari browser while I'm checking to see what was made, and perhaps some action with that is sometimes fixing the authentication? If I run the part of the code that updates the image permissions in my Jupiter notebook, I can go in on Safari and see that the permission is indeed set to AnybodyWithLink.

Does anyone know what I'm doing wrong or missing?

from google_auth_httplib2 import httplib2
from apiclient import discovery
from oauth2client.service_account import ServiceAccountCredentials
from googleapiclient.http import MediaFileUpload

TEMPLATE_ID = '1JVuNOJxqajBxTkdASgnbJkiQLZtt9F1E-yCAaOU1fPQ'
DEST_FOLDER_ID = '1pVakIO5ZlF1HRi7XJFM1D4llajU9pGqs'


def get_service(secrets_path, service_scope, service_build, service_version):
    credentials = ServiceAccountCredentials.from_json_keyfile_name(
        secrets_path, 
        scopes= service_scope
    )
    service = discovery.build(
        service_build,
        service_version,
        http=credentials.authorize(httplib2.Http()),
        cache_discovery=False
    )
    return service

def replace_shape_with_image(url: str, presentation_id: str, slides_service, contains_text: str=None, pages = []):
    slides_service.presentations().batchUpdate(
        body={
            "requests": [
                {
                    "replaceAllShapesWithImage": {
                        "imageUrl": url,
                        "replaceMethod": "CENTER_INSIDE",
                        "containsText": {
                            "text": "{{" + contains_text + "}}",
                        },
                        "pageObjectIds": pages
                    }
                }
            ]
        },
        presentationId=presentation_id
    ).execute()

# Get api services
drive_service = get_service(
            './secret.json',
            'https://www.googleapis.com/auth/drive',
            'drive',
            'v3'
        )
slides_service =  get_service(
            "./secret.json",
            ['https://www.googleapis.com/auth/presentations','https://www.googleapis.com/auth/drive'],
            'slides',
            'v1'
        )

# Create slideshow from copy
r = drive_service.files().copy(
        fileId=TEMPLATE_ID,
        body={'name': 'Slideshow', 'parents': DEST_FOLDER_ID},
        supportsTeamDrives=True).execute()
file_id = r.get('id')

# Get the slide ID's
r = slides_service.presentations().get(
    presentationId=file_id,
    fields='slides(objectId)'
).execute()

slides = [s['objectId'] for s in r['slides']]
slide_with_graph_placeholder = slides[-1]

# Upload a graph .png file to the folder
file_metadata = {
            'name': 'photo.png',
            'parents': [DEST_FOLDER_ID]
        }
media = MediaFileUpload('./path/to/image.png',
                        mimetype='image/png', resumable=True)
file = drive_service.files().create(body=file_metadata, media_body=media, fields='*').execute()

#.Update file permissions for the image file to be anyone with link
drive_service.permissions().create(
    fileId=file.get("id"),
    body={
    'type': 'anyone',
    'value': 'anyone',
    'kind': 'drive#permission',
    'role': 'reader'
    }
    ).execute()

# This is the function call that throws the exception 
try:
    replace_shape_with_image(
        url=file.get('webContentLink'),
        presentation_id=file_id,
        slides_service=slides_service,
        contains_text='graph',
        pages=[slide_with_graph_placeholder]
    )
except Exception as e:
    print(e)

I have tried playing with the contents of the permissions upload and trying different combinations of things I've seen in the metadata from other folders.


Solution

  • From your error message and your following reply,

    What.I should have written is that if I rerun the cell in Jupyter notebook for the image replacement function call later on after messing around looking at stuff in the web browser in the drive/slides web apps, an image that previously would get that error thrown, would work seemingly randomly. Not every time that I fiddled around in the browser did the replacement work after. There were also a couple times where the error didn't get thrown even the first time.

    In this case, I'm worried that the origin of the error might be related to the permission of the file. But, this is just my guess. So, in order to remove the issue of permission, how about changing the endpoint of the image URL? When this is reflected in your script, how about the following modification?

    From:

    file = drive_service.files().create(body=file_metadata, media_body=media, fields='*').execute()
    
    #.Update file permissions for the image file to be anyone with link
    drive_service.permissions().create(
        fileId=file.get("id"),
        body={
        'type': 'anyone',
        'value': 'anyone',
        'kind': 'drive#permission',
        'role': 'reader'
        }
        ).execute()
    
    # This is the function call that throws the exception 
    try:
        replace_shape_with_image(
            url=file.get('webContentLink'),
            presentation_id=file_id,
            slides_service=slides_service,
            contains_text='graph',
            pages=[slide_with_graph_placeholder]
        )
    except Exception as e:
        print(e)
    

    To:

    file = drive_service.files().create(body=file_metadata, media_body=media, fields='*').execute()
    
    # This is the function call that throws the exception
    try:
        replace_shape_with_image(
            url=file.get('thumbnailLink').replace("=s220", "=s1000"),
            presentation_id=file_id,
            slides_service=slides_service,
            contains_text='graph',
            pages=[slide_with_graph_placeholder]
        )
    except Exception as e:
        print(e)
    
    • In this modification, I removed drive_service.permissions().create() and modified the image URL.
    • When I tested your script by reflecting on my proposed modification, I confirmed that the image could be inserted.