Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save techirsh/a3ebc0e9603650678699eb0daad3bae2 to your computer and use it in GitHub Desktop.
Save techirsh/a3ebc0e9603650678699eb0daad3bae2 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment