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; })));
and the report would look something like this. You’ll see from below that it’s perfectly okay to have your template in the same pace as your files. It will never replace the template contents if it is provided as a file to process.
[{ "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.
http://youtube.com/watch?v=9EeTHNJ_-lg
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.
Mcbr-v4SsYKJP7JMohttAZyz3TLx7pV4j
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
Why not join our forum, follow the blog or follow me on twitter to ensure you get updates when they are available.