Skip to content

Instantly share code, notes, and snippets.

@coccoinomane
Last active January 13, 2024 10:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save coccoinomane/bef1b29628be234978f6d286f4e719ba to your computer and use it in GitHub Desktop.
Save coccoinomane/bef1b29628be234978f6d286f4e719ba to your computer and use it in GitHub Desktop.
Make images submitted via a Google Form publicly viewable.
/**
* Make images submitted via a Google Form publicly viewable.
*
* Updated version at https://gist.github.com/bef1b29628be234978f6d286f4e719ba
*
* INSTRUCTIONS
*
* 1. Open the response spreadsheet.
* 2. Open the script editor (Tools -> Script Editor), paste the content of this
* file inside the editor, and save (Ctrl+S or Cmd+S).
* 3. Take note of the names of all the form questions/spreadsheet columns containing
* image URLs, and insert them into the IMAGE_COLUMNS variable, below.
* 4. Click on the clock icon in the script editor and set the following trigger:
* Run: onFormSubmit
* Events: From spreadsheet - On form submit.
* 5. After you save, you'll be asked to give permissions; do it.
* 6. Submit a form response containing one or more uploaded images.
*
* If everything goes fine, the uploaded images will be publicly viewable. You can test
* this by opening each image URL in the spreadsheet in an incognito window; if you see
* it, it worked.
*
* TROUBLESHOOTING
* If the code does not work or does not fire, please:
* - 1) set the DEBUG variable below to true.
* - 2) Submit a form response.
* - 3) Open the script editor and open both the error log (View -> Stackdriver Error
* Reporting) and the basic log (View -> Log).
*
* Here's a list of common errors and their fixes:
*
* - "You do not have permission to call getFileById" in the error log: This means that
* the script could not access the image because it was not authorized by Google Drive.
* To authorize the script, delete the trigger (Edit -> Current project's triggers)
* and add it back.
*/
/**
* CHANGE HERE
*/
/* Names of the form fields containing image URLs */
var IMAGE_COLUMNS = [
'Image'
];
/* Whether to print log messages using Logger.log() */
var DEBUG = false;
/**
* DO NOT TOUCH FROM HERE ON
*/
/**
* Process the image URL in a from submission.
*
* Attach to the "On form submit" trigger.
*/
function onFormSubmit( e ) {
/* Debug */
DEBUG && Logger.log( "Named Values");
DEBUG && Logger.log( e.namedValues );
DEBUG && Logger.log( "Range Values");
DEBUG && Logger.log( e.range.getValues() );
/* Function that will set permissions for each image */
var callback = processImage;
var callbackArgs = [
DriveApp.Access.ANYONE_WITH_LINK,
DriveApp.Permission.VIEW
];
/* Process each image in turn */
for (var i = 0; i < IMAGE_COLUMNS.length; i++) {
var imageColumn = IMAGE_COLUMNS[i];
/* Return if image has not been submitted or if the field is not present */
if ( Object.keys( e.namedValues ).indexOf( imageColumn ) === -1 ) {
Logger.log( "WARNING: Form does not have field named ' " + imageColumn + "', will skip" );
continue;
}
else if ( ! e.namedValues[ imageColumn ][0] ) {
Logger.log( "WARNING: Field ' " + imageColumn + "' is empty, will skip" );
continue;
}
/* Process the image URL */
processField( e.range, imageColumn, callback, callbackArgs );
}
}
/**
* Apply a function to a cell within a row.
*
* The cell is found by means of its column header; its content is overwritten
* by the value returned by the callback function.
*
* GOOGLE FORM
* If applied to a Google Form submission via the trigger 'onFormSubmit', then
* set rowRange = e.range. Note however that this function only replaces what is
* shown in the spreadsheet: the response stored in the form won't be changed.
*
* @arg {rowRange} rowRange - Range containing the row containing the cell to be
* modified. If the range contains multiple rows, only the first row will be considered.
* @arg {string} header - Header of the column with the cell to process.
* @arg {function} callback - Function that will be applied to the cell; its return value
* will overwrite the cell's content. Its arguments are the original cell value (passed
* automatically) and the values in callbackArgs.
* @arg {array} callbackArgs - Arguments to the callback function, given as an array.
* The old cell value will
* @return {mixed} - The new value for the cell; return undefined if the header is not
* found or if there's a mismatch between the sheet headers and the number of values in
* the first row of the range, or if the callback is not a valid function.
*/
function processField( rowRange, cellHeader, callback, callbackArgs ) {
/* Get sheet headers */
var sheet = rowRange.getSheet();
var sheetHeaders = sheet.getRange( 1, 1, 1, sheet.getLastColumn() ).getValues()[0];
/* Find position in headers of the cell column */
var cellColumnIndex = sheetHeaders.indexOf( cellHeader );
/* Debug */
DEBUG && Logger.log( "processField: Sheet has the following headers:" );
DEBUG && Logger.log( "processField: " + sheetHeaders );
DEBUG && Logger.log( "processField: Cell column '" + cellHeader + "' found at index " + cellColumnIndex );
if ( cellColumnIndex === -1 ) {
return undefined;
}
/* Get range values */
var values = rowRange.getValues()[0];
if ( values.length <= cellColumnIndex ) {
Logger.log( "processField: Field probably has too few fields, skipping." );
return undefined;
}
/* Get current value of cell */
var cellValue = values[ cellColumnIndex ];
/* Debug */
DEBUG && Logger.log( "processField: Cell's current value: '" + cellValue + "'" );
/* Check that the callback is a valid function */
if ( typeof callback !== 'function' ) {
Logger.log( 'processField: Callback is not a function' );
return undefined;
}
/* Prepend the cell value to the callback arguments */
var args = callbackArgs.slice();
args.unshift( cellValue );
/* Debug */
DEBUG && Logger.log( "processField: Callback will be called with following arguments:" );
DEBUG && Logger.log( "processField: " + args );
/* Process the cell's content by applying the callback to the old
cell value. The return value will be inserted in the cell */
var newCellValue = callback.apply( null, args );
/* Replace the cell's content in the spreadsheet */
values[ cellColumnIndex ] = newCellValue;
rowRange.setValues( [ values ] );
/* Debug */
DEBUG && Logger.log( "processField: Cell's new value: '" + newCellValue + "'" );
}
/**
* Process a Google Drive image so that it can be embedded in a Google
* Document.
*
* The functions does the following:
*
* - Returns a more share-friendly image URL.
* - Assigns the image the given access & permission levels.
* - Proportionally rescale the image to a maximum width (TODO)
*
* @arg {string} imageUrl - The image URL in Google Drive.
* @arg {access} newAccess - The access level to be assigned to the image.
* Default is DriveApp.Access.ANYONE_WITH_LINK.
* @arg {permission} newPermissions - The permission level to be assigned to the image.
* Default is DriveApp.Permission.VIEW.
* @arg {int} maxPixelWidth - The maximum width for the image. If the image has a
* width in pixels bigger than number, its width will be scaled down to this number.
* The height will be scaled down proportionally. Nothing will be done unless a number
* is given for this argument.
* @return {string|bool} - The share-ready URL of the image in Google Drive;
* false for failure.
*/
function processImage( imageUrl, newAccess, newPermissions, maxPixelWidth ) {
newAccess = ( typeof newAccess !== 'undefined' ) ? newAccess : DriveApp.Access.ANYONE_WITH_LINK;
newPermissions = ( typeof newPermissions !== 'undefined' ) ? newPermissions : DriveApp.Permission.VIEW;
// ====================
// = Change image URL =
// ====================
var newImageUrl = imageUrl.replace( 'open?', 'uc?export=view&' );
/* Debug */
DEBUG && Logger.log( "Image URL = " + imageUrl );
DEBUG && Logger.log( "New Image URL = " + newImageUrl );
// =====================
// = Assig permissions =
// =====================
var fileId = getIdFromUrl( newImageUrl );
if ( ! fileId ) {
Logger.log( "Image is not a Google Drive file, or failed to get its Drive ID" );
Logger.log( "Image URL: " + newImageUrl );
return newImageUrl;
}
/* Debug */
DEBUG && Logger.log( "Image ID in Google Drive = " + fileId );
/* Get the Google Drive file */
var imageDriveFile = DriveApp.getFileById( fileId );
if ( ! imageDriveFile ) {
Logger.log( "Could not get Drive file corresponding to ID " + fileId );
return newImageUrl;
}
/* Debug */
DEBUG && Logger.log( "Retrieved file from Google Drive" );
DEBUG && Logger.log( "File currently has the following access:" );
DEBUG && Logger.log( imageDriveFile.getSharingAccess() );
DEBUG && Logger.log( "File currently has the following permissions:" );
DEBUG && Logger.log( imageDriveFile.getSharingPermission() );
/* Change image access & permissions to public view */
imageDriveFile.setSharing( newAccess, newPermissions );
/* Debug */
DEBUG && Logger.log( "Updated file's access & permission in Google Drive" );
DEBUG && Logger.log( "File now has the following access:" );
DEBUG && Logger.log( imageDriveFile.getSharingAccess() );
DEBUG && Logger.log( "File now has the following permissions:" );
DEBUG && Logger.log( imageDriveFile.getSharingPermission() );
return newImageUrl;
}
/**
* Extract ID of Google Drive file from its URL.
*
* Works with these types of URLs:
* - https://drive.google.com/file/d/1JFEQIVH1CsVjMsP4WBAoNY-gaUmDkjCo/view
* - https://drive.google.com/uc?export=view&id=1IMh0vGytbKQb-mK2TTjgqw7jHKVl00Eq
*
* Adjust the regex to add different kinds of URLs.
*
* Returns null if the ID could not be parsed from the URL.
*/
function getIdFromUrl(url) {
var output = null; // initialize output
var re = /[\/&?](?:d\/|id\=)(.+)\/?/;
var results = re.exec( url );
if ( results.hasOwnProperty( '1' ) ) {
output = results[1];
}
return output;
}
@coccoinomane
Copy link
Author

Now supporting Google Forms with multiple image fields

@msalero
Copy link

msalero commented Aug 27, 2020

It did not work for me. In order to fix, What other commun issues have you detected?

The type of url in my google sheet is: https://drive.google.com/open?id=1cSbR1Y75HKo8PrLvWEQ57E7M5Qh9aPB3

@coccoinomane
Copy link
Author

Hi, your image URL format is correct.
Can you please set the DEBUG variable to true and send me the Logger log and, if you have enabled it, the error log?
Thanks,
Guido

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment