Let’s say you have a bunch of Google Documents with various images in them – say a logo or photographs – anything, that you’d like to replace with different images. Well, one way would be to go in there and do it all manually. If the images were differently sized you might need to do some adjustments, but eventually you’d get it done.
But you can do it systematically with Apps Script. They key to it is to be able to recognize an image in a document so you can replace it with another.
The example
Let’s start with a document with some pictures in it. You might have a number of documents where you are using the pictures below – and you need to replace the images when there’s a change.

Let’s say this

And then finally,

Here’s how it works.
You create a template document which contains all the images you want to swap – the before and after.

This template can contain anything you like, in addition to a table which has exactly 2 images on each row- the first image is the image to look for, and the second image is what to replace it with. The table can have extra columns if you like, as well as any other stuff. It’s only table rows with exactly two images that are of interest.
Using the template above, any instances of donald and hilary found in the documents being processed will be replaced by ted and bernie. Obama pictures will be left intact for now, since he has no replacement picture in the template.
In a later run, I’d add the picture of bernie next to obama so it could replace his. The parameters Here’s how to control how the processing is done.var params = { template:'1rje8nZ-flJTFlBJO3XYm9mzQ-G0mHEkGlCVhEAXydwE', // this is the document that contains the image swappong template documents:[ '1oI9Pefe5yjkrJKsKw1Mx6ITU_ONdTzZ-PAz3I_d48Qk' // this is a list of documents to apply image swapping to ], scaling:SwapperEnums.SCALE_ORIGINAL_HEIGHT, // change this as required. attributes:SwapperEnums.KEEP_ORIGINAL, sections:['Header', 'Footer', 'Body'] // where to look for images .. normally all 3 }
You specify the id of the template, plus a list of documents to be processed as well as sections of the documents that should be searched for images to change.
Attributes of the document such as any links can be inherited from either the original image, or from the attributes of the replacement image. Similarly the scaling can be inherited, or calculated. Here’s the list of values you can specify. The proper selection of these can be used to keep the format of the document being processed whilst still retaining the proper proportions of the replacement image.
var SwapperEnums = { INHERIT_TEMPLATE:'INHERIT_TEMPLATE', // inherits the attributes/scaling from the template KEEP_ORIGINAL:'KEEP_ORIGINAL', // keeps the attributes/scaling of the original SCALE_ORIGINAL_HEIGHT:'SCALE_ORIGINAL_HEIGHT', // keeps the height of the original image and scales the width SCALE_ORIGINAL_WIDTH:'SCALE_ORIGINAL_WIDTH' // keeps the width of the original image and scales the height };
Automatically generating a list of documents
If you don’t want to specify a list of ids, you can use Creating a pile of files list from Google Drive to generate that list for you.
For example, here’s how to find all the documents in the folders and subfolders of a Drive path, and use that to add document ids to the file path.
/ make sure you have this comment in the code to provoke drive authorization // DriveApp.getFiles() var piles = cUseful.DriveUtils .setService(DriveApp) .getPileOfFiles ( "/books/youtube", "application/vnd.google-apps.document", true ); // just interested in the ids. params.documents = piles.map (function (d) { return d.file.getId(); });
The report
You’ll get a report at the end showing which documents were looked at and how many images were there. If you’ve used the method above to automatically create the list of files, you can enrich that result with details about the files too.
Here’s and example of how you might enrich the result if you would like a report of what happened
// do the image swapping var result = doImageSwapping ( params ); // if used the piles method, we can enrich the results Logger.log (JSON.stringify(result.map (function(d,i) { if (piles) { d.path = piles[i].path; d.folderId = piles[i].folder.getId() } return d; })));
[{ "id": "1oI9Pefe5yjkrJKsKw1Mx6ITU_ONdTzZ-PAz3I_d48Qk", "name": "testswap", "imagesDetected": 5, "imagesReplaced": 3, "path": "/books/youtube/testswap", "folderId": "0B92ExLh4POiZempkRXF0VGZ1RDA" }, { "id": "1rje8nZ-flJTFlBJO3XYm9mzQ-G0mHEkGlCVhEAXydwE", "name": "imageswap", "imagesDetected": 0, "imagesReplaced": 0, "skipped": "Document was same as template - skipped", "path": "/books/youtube/imageswap", "folderId": "0B92ExLh4POiZempkRXF0VGZ1RDA" }, { ... etc..
Video example – changing logo
+Richard Anderson posted this video showing how he used this imageswapper to change the logo on his school’s documents.
The code
You’ll need the cUseful library, since it uses some functions there for blob processing. Note that this technique recognizes identical inline_images even though they have been resized in Docs.
Here’s the key for the cUseful library, and it’s also on github, or below.
The swapper code is simply this. All you have to do is set up the parameters in imageSwapper and run it. As usual I suggest you try it on a copy of your document(s) first. It’s also on github
var SwapperEnums = {
INHERIT_TEMPLATE:'INHERIT_TEMPLATE', // inherits the attributes/scaling from the template
KEEP_ORIGINAL:'KEEP_ORIGINAL', // keeps the attributes/scaling of the original
SCALE_ORIGINAL_HEIGHT:'SCALE_ORIGINAL_HEIGHT', // keeps the height of the original image and scales the width
SCALE_ORIGINAL_WIDTH:'SCALE_ORIGINAL_WIDTH' // keeps the width of the original image and scales the height
};
function imageSwapper () {
var params = {
template:'1rje8nZ-flJTFlBJO3XYm9mzQ-G0mHEkGlCVhEAXydwE', // this is the document that contains the image swappong template
scaling:SwapperEnums.SCALE_ORIGINAL_HEIGHT, // change this as required.
attributes:SwapperEnums.KEEP_ORIGINAL,
sections:['Header', 'Footer', 'Body'] // where to look for images .. normally all 3
}
// there are twp ways of specifying the documents to look at .. one is simply to provide a list of document ids.
// as below
params.documents = [
'1oI9Pefe5yjkrJKsKw1Mx6ITU_ONdTzZ-PAz3I_d48Qk' // this is a list of documents to apply image swapping to
];
// OR another way to populate the documents needed, instead of populating the list manualay
// specify the starting path of the folder to look ( you can sepcify the id if you prefer)
// the type of files
// and whether to recurse (look in lower level paths)
var piles = cUseful.DriveUtils
.setService(DriveApp)
.getPileOfFiles (
"/books/youtube", "application/vnd.google-apps.document", true
);
// just interested in the ids.
params.documents = piles.map (function (d) {
return d.file.getId();
});
// do the image swapping
var result = doImageSwapping ( params );
// if used the piles method, we can enrich the results
Logger.log (JSON.stringify(result.map (function(d,i) {
if (piles) {
d.path = piles[i].path;
d.folderId = piles[i].folder.getId()
}
return d;
})));
}
function doImageSwapping (params) {
// first get the template
var template = DocumentApp.openById(params.template);
if (!template) throw 'could not open template document ' + params.template;
var templateImages = getTemplateImages (template.getBody());
if (!templateImages.length) throw 'no image swaps found in template';
// make sure all the documents exist
return params.documents.map (function (d) {
var doc = DocumentApp.openById(d);
if (!doc) throw 'could not open document ' + d;
return doc;
})
// and do the swapping
.map (function (doc) {
var result = {
id:doc.getId(),
name:doc.getName(),
imagesDetected:0,
imagesReplaced:0
};
// make sure we skip the template
if (result.id === params.template) {
result.skipped = "Document was same as template - skipped" ;
return result;
}
// look in each section asked for
params.sections.forEach (function(section) {
// get all the images in the document
var lump = doc['get'+section]();
if (lump) {
lump.getImages().forEach (function(image) {
var sha = cUseful.Utils.blobDigest (image.getBlob());
result.imagesDetected++;
// check for a match against template
var match = templateImages.filter (function (d) {
return sha === d.sha;
});
// substitute & resize
if (match.length) {
result.imagesReplaced++;
// get the parent of current image, add the new one
var parent = image.getParent();
var newImage = parent.insertInlineImage(parent.getChildIndex(image), match[0].replaceWith.getBlob());
// figure out scaling
var newHeight,newWidth;
if (params.scaling === SwapperEnums.INHERIT_TEMPLATE) {
newHeight = newImage.getHeight();
newWidth = newImage.getWidth();
}
else if (params.scaling === SwapperEnums.KEEP_ORIGINAL) {
newHeight = image.getHeight();
newWidth = image.getWidth();
}
else if (params.scaling === SwapperEnums.SCALE_ORIGINAL_HEIGHT) {
newHeight = image.getHeight();
newWidth = newImage.getWidth() * newHeight / newImage.getHeight() ;
}
else if (params.scaling === SwapperEnums.SCALE_ORIGINAL_WIDTH) {
newWidth = image.getWidth();
newHeight = newImage.getHeight() * newWidth / newImage.getWidth() ;
}
else {
throw params.scaling + ' is invalid scaling treatment'
}
// attribute inheritance
if (params.attributes === SwapperEnums.KEEP_ORIGINAL) {
newImage.setAttributes(image.getAttributes())
}
else if (params.attributes !== SwapperEnums.INHERIT_TEMPLATE) {
throw params.attributes + ' is invalid attribute treatment'
}
// deal with size
newImage.setHeight (newHeight).setWidth(newWidth);
// delete the old one
image.removeFromParent();
}
});
}
});
return result;
});
}
function getTemplateImages(body) {
// return an array of {searchfor:inlineimage, repaceWith:inlineimage , sha:searchSha:the sha of the searched image}
// table = body.getTables()[0];
var imagePairs = body.getTables().reduce(function (images,table) {
getChildrenArray (table).forEach (function (row) {
var rowImages = [];
getChildrenArray (row).forEach (function (cell) {
getChildrenArray (cell).forEach (function (para) {
getChildrenArray(para.asParagraph()).forEach (function(elem) {
if (elem.getType() === DocumentApp.ElementType.INLINE_IMAGE) {
rowImages.push(elem);
}
});
});
});
// if exactly two images were found on this row, then they are replacement templates
if (rowImages.length === 2 ) {
images.push ({
searchFor:rowImages[0],
replaceWith:rowImages[1],
sha:cUseful.Utils.blobDigest (rowImages[0].getBlob())
});
}
});
return images;
},[]);
// check for dups.
imagePairs.forEach(function (d) {
if (imagePairs.filter (function (e) {
return d.sha === e.sha;
}).length !== 1 ) throw 'duplicate search image in template';
});
return imagePairs;
function getChildrenArray (parent) {
var children = [];
for (var i = 0 ; i
- A functional approach to fiddling with sheet data
- A functional approach to updating master sheet
- A recursive extend function for Apps Script
- A webapp to share copies of the contents of folders
- Abstracting services with closures
- Add-on spinner
- Addressing namespace and library methods from google.script.run
- Anonymous user registration with the Apps Script PropertiesService
- Apps for Office – binding example comparison
- Apps Script as a proxy
- Apps Script const scoping problems
- Calculating image dimensions in server side apps script
- Calculating the last day of a given weekday in the month
- Canvasser
- Chaining JavaScript
- Changing class properties dynamically
- Checking the argument types in Apps Script
- Cleaning up a document format
- Cleaning up heading levels
- Column numbers to characters
- Composing functions and functional programming
- Configurable canvas meter
- Convert JSON to XML
- Converting SVG to PNG with JavaScript
- Converting timestamps to dates formula
- Copying canvas and svg images from your Add-on
- Copying to new host location
- Copying to new host location
- Counting script and library usage
- Create sha1 signatures with apps script
- Creating a key digest to use for a cache key or to compare content
- Creating a pile of files list from Google Drive
- Creating and working with transposed sheet data arrays
- Cross Origin Resource sharing (CORS)
- CryptoJS libraries for Google Apps Script
- Custom checking for exponential backoff
- Data wrangling with named columns in Google Spreadsheet
- Dealing with objects that are too large for the property or cache store
- Detecting Spreadsheet tables automatically with Google Apps Script
- Direction minimizer – other usages
- Do something useful with GAS in 5 minutes
- Dynamically creating tables with clusterize.js
- EasyCron Library
- ES6 JavaScript features
- Exponential backoff
- Exponential backoff for promises
- Fiddler and rangeLists
- Fiddling with text fields that look like dates
- Filling ranges in Google Sheets
- Finding a Drive App folder by path
- Finding where Drive hosting is being used in Sites
- Flattening an object with dot syntax
- Flattening and unflattening objects to spreadsheets
- Formatting GraphQL queries
- Formatting sheet column data with fiddler
- From notes to frequencies and back again
- From Xml to JSON
- Generating and managing random lists with JavaScript and Apps Script
- Generating coupon codes with expiry dates
- Generating test data for sheets and tables
- Get GAS library info
- Getting an htmlservice template from a library
- Getting insights into Sheets performance
- Google Drive as cache
- Header formatting with fiddler
- Highlight duplicate rows in a sheet – map and reduce
- Highlight duplicate rows in a sheet – map, filter and every
- How to determine what kind of object something is in Apps Script
- How to get stats about youtube videos in your channel with apps script
- How to pass non stringifyable objects to html service
- How to transpose spreadsheet data with apps script
- Identify duplicates on Drive
- Identifying hosted files
- Implementing a client side progress bar reporting on server side work for htmlService
- Importing Predictwise data
- Improved namespace pattern for Apps Script
- Including the stack in custom errors
- JavaScript closures – how, where and why
- JavaScript currying and functional programming
- JavaScript currying and functional programming – even more
- JavaScript recursion primer
- JSONP and JSON and Google Apps Script webapps
- Loading large JSON datasets into BigQuery with Apps Script
- Logging differences in strings in Apps Script
- Measuring library load speed
- Migrating user and script properties
- Minimizing maps directionfinder api calls
- More client server code sharing
- More recursion – parents and children
- More sheet data fiddling
- Multiple inserts in Fusion Tables
- Namespaces in libraries and scripts
- Normalizing the header level of blank paragraphs
- Optimizing sheet formatting with Apps Script
- Optimizing showing and hiding rows and columns
- Organizing asynchronous calls to google.script.run
- Organizing parallel streams of server calls with google.script.run promises
- Parallel process orchestration with HtmlService
- Passing data to html service
- Patching site html
- Populating sheets with API data using a Fiddler
- Proxy jsonp
- Pseudo binding in HTML service
- Queuing asynchronous tasks with rate limit and concurrency constraints
- Recursive async functions
- Removing duplicate paragraphs
- Reporting file, function and line number in Apps Script
- Resumable uploads – writing large files to Drive with Apps Script
- Reusing html stuff between Apps Script projects
- Roughly matching text
- Serving apps script to JavaScript app
- Sharing code between client and server
- Shortcut for adding nested properties to a JavaScript object
- Simple server side polling
- Sorting Google Sheet DisplayValues
- Squeezing more into (and getting more out of) Cache services
- Styling Gmail html tables
- Summarizing emails to a sheet
- SunCalc
- TimeSimmer : An adjustable timer for apps that need to speed up or slow down time
- Transform dates for add-on transfer
- Transposing sheet data
- Traversing a tree
- Unique values with data fiddler
- Unnesting data to sheet values
- Untangling with promises
- Use Drive properties to find app files
- Use promise instead of callback for settimeout
- Using Advanced Drive service to convert files
- Using Apps Script for xml json conversion
- Using array formulas to improve performance
- Using crossfilter with Google Apps Script
- Using D3 in server side Gas
- Using es6 promises server side in Apps Script
- Using Es6 with Apps Script
- Using exponential backoff with github api – dealing with data “in preparation”
- Using Google sheets via Bigquery from Apps Script
- Using named locks with Google Apps Scripts
- Using promises to orchestrate Html service polling
- Using promises with apps script
- Using the Itunes API with Apps Script
- Using the slideshare API from Apps Script
- Using timing functions to get insight into Sheets
- Watching docs for changes
- Watching for server side changes from the client html service
- What JavaScript engine is Apps Script running on?
- Why Base64
- Zipping to make stuff fit in cache or properties service.
Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.