Adobe Photoshop - Scripts Related to Montaging
Contents
About
Here are a couple of useful Adobe Photoshop scripts. The first takes an ordered folder of image tiles and creates one big montage. The second takes any open image and saves it out to PNG tiles. In both scripts all tiles should/will be all the same size.
To use these scripts, simply create a ".jsx" file, paste in the script, and then you should be able to double click to run the script... if not then open Photoshop and go "File > Scripts > Browse".
These examples deal with quite a few useful operations, like open folder dialog, save file dialogs, alert and confirm dialogs, applying commands like image crop, changing ruler units, using layers, drawing guide lines and so on.
Photoshop Script: Create a montage from ordered image tiles
combine_tiles_into_montage.jsx:
// Assembles a folder full of ordered image tiles into a single montage image
// with each tile in a separate layer. Before processing and tile translation
// occurs, you will be prompted to enter the number of columns and whether
// tiles are ordered by x or y first. If possible, I recommend you name tiles
// like so: "my_tile_x=0&y=0.jpg". Tiles *should* work as any image type, but
// I have had trouble before with .png, so you may need to convert to .jpg.
// All tiles are added to the first tile, and then saved as a PSD (with one
// tile per layer) at the end.
//
// TIPS:
// > This has been tested up to 80x80 (256x256 pixel tiles), but over 30,000
// in any dimension is not supported by all photoshop versions.
// > The final PSD won't save if > 2 GB (easily fixed by saving as a TIF)
// > Images can be different types, but certain image "modes", including
// "Indexed" color, fail (fixed by Image > Mode > RGB Color).
#target photoshop
// Open folder selection dialog (for user to chose folder):
alert("You will be prompted for the folder containing your tiles.\n" +
"Tiles should be named like 'x=10&y=4.jpg' so they sort well, " +
"all of the identical dimension and the folder should contain " +
"no other image files.");
var folder = Folder.selectDialog();
if (!folder) {alert("Cancelled"); exit;}
// Set units and guess the number of columns using square root:
var origUnits = app.preferences.rulerUnits; // Can delete.
app.preferences.rulerUnits = Units.POINTS;
var files = folder.getFiles(/\.(jpg|jpeg|tif|tiff|bmp|png|eps)$/i);
files.sort();
var numColGuess = Math.ceil(Math.sqrt(files.length));
// Prompt the user to enter the number of columns and if x appears first:
var numCol = prompt("Found " + files.length + " images.\n" +
"How many columns are there?", numColGuess);
var numRows = Math.ceil(files.length / numCol);
var answer = prompt("Grid will be " + numCol + "x" + numRows + " tiles.\n" +
"Does x or y appear first in the filenames?", "x");
var orderedWithYFirst = (answer == "y" || answer == "Y");
if (!answer || numCol == 0 || numRows == 0) {alert("Bad values"); exit;}
// Open first file to determine dimensions:
var firstTile = app.open(File(files[0]));
var tileWidth = firstTile.width;
var tileHeight = firstTile.height;
// Resize first file to a size that will fit all tiles:
var firstLayer = firstTile.layers.length - 1; // Most likely 0.
firstTile.layers[firstLayer].name = files[0].name.slice(0,-4);
firstTile.resizeCanvas(firstTile.width * numCol,
firstTile.height * numRows,
AnchorPosition.TOPLEFT);
// Zoom to fit whole image on screen:
doMenuItemNoInteraction = function(item) {
var ref = new ActionReference();
ref.putEnumerated(app.charIDToTypeID("Mn "), app.charIDToTypeID("MnIt"), item);
var desc = new ActionDescriptor();
desc.putReference(app.charIDToTypeID("null"), ref);
executeAction(app.stringIDToTypeID("select"), desc, DialogModes.NO);
}
doMenuItemNoInteraction(app.charIDToTypeID('FtOn')); // Fit all on screen.
// For each tile: open, transfer into a new layer, and re-position:
for (var i = 1; i < files.length; i++) {
var tile = app.open(File(files[i]));
if (tile.layers.length > 1) {
tile.flatten();
};
tile.layers[0].duplicate(firstTile, ElementPlacement.PLACEATBEGINNING);
tile.close(SaveOptions.DONOTSAVECHANGES);
var layer = firstTile.layers[0];
layer.name = files[i].name.slice(0,-4); // Omit the file extension.
// Determine current column and row:
var col = Math.floor(i / numRows);
var row = i - (col * numRows);
if (orderedWithYFirst) {
row = Math.floor(i / numCol);
col = i - (row * numCol);
}
// Calculate the x/y offsets and translate image:
var xOffset = (tileWidth * col) - layer.bounds[0];
var yOffset = (tileHeight * row) - layer.bounds[1];
layer.translate(xOffset, yOffset);
};
// Save as new file;
var basename = firstTile.name.match(/(.*)\.[^\.]+$/)[1];
var docPath = firstTile.path;
psdOpts = new PhotoshopSaveOptions();
psdOpts.embedColorProfile = true;
psdOpts.alphaChannels = false;
psdOpts.layers = true;
psdOpts.spotColors = true;
firstTile.saveAs((new File(docPath+'/'+basename.slice(0,-4)+"_comb.psd")),psdOpts,false);
app.preferences.rulerUnits = origUnits;
Photoshop Script: Decompose an image into a grid of equal tiles
split_image_into_png_tiles.jsx:
// Takes the currently opened image, and allows the user to split it into
// a grid of PNG tiles, by specifying an number of columns and rows and a
// base file prefix.
//
// Tiles are created by duplicating the original image, cropping then saving.
// Tiles are saved in the form: 'C:/path/prefix0,0.png'
#target photoshop
if (documents.length != 1) {
alert("Must open exactly one image in photoshop first");
} else {
// Prompt user for number of columns and rows:
var cols = parseInt(prompt("How many columns?", 4));
var rows = parseInt(prompt("How many rows?", 4));
var total = cols*rows;
// Determine target tile size:
var image = app.documents[0];
var tileWidth = image.width / cols;
var tileHeight = image.height / rows;
// Draw guides along cuts:
for(col = 0; col <= cols; col++) {
image.guides.add(Direction.VERTICAL, col * tileWidth);
}
for(row = 0; row <= rows; row++) {
image.guides.add(Direction.HORIZONTAL, row * tileHeight);
}
// Prompt user to confirm, and for file prefix to save out to:
var savePath = File.saveDialog("Save Image File Prefix", "");
if(!savePath) {alert("Cancelled"); exit;}
if(!confirm("Create " + total + " tiles, each of " +
tileWidth + " x " + tileHeight + "?\n\n\n" +
"Tiles will be saved as '" + savePath.fsName +
"1,1.png' '...1,2.png' etc")) { exit; }
// For each tile:
for(row = 0; row < rows; row++) {
for(col = 0; col < cols; col++) {
// Determine crop coordinates (in pixels):
var top = row * tileHeight;
var bottom = top + tileHeight;
var left = col * tileWidth;
var right = left + tileWidth;
// Duplicate image, crop, save as PNG and close:
var tile = image.duplicate(); // Duplicate file.
cropCurrentDocument(top, left, bottom, right);
saveTileAsPng(tile, savePath.fsName, col, row);
tile.close(SaveOptions.DONOTSAVECHANGES);
}
}
}
function saveTileAsPng(img, origFilePath, col, row) {
var newFilePath = origFilePath + "_" + col + "," + row + ".png";
var newFile = new File(newFilePath);
var pngSaveOptions = new PNGSaveOptions();
activeDocument.saveAs(newFile, pngSaveOptions, true, Extension.LOWERCASE);
}
// Crops active document by a rectangle with the given pixel coordinages.
function cropCurrentDocument(top, left, bottom, right){
app.preferences.rulerUnits = Units.PIXELS;
activeDocument.selection.select(
[[left, top], [right, top], [right, bottom], [left, bottom]],
SelectionType.REPLACE, 0, false);
executeAction(charIDToTypeID( "Crop" ), new ActionDescriptor(),
DialogModes.NO );
}
Photoshop Script: Edge padding / extending the edges of an image by a set nuber of pixels
edge_padding.jsx:
// Modifies all currently opened images, by expanding their outer-most
// edges by the number of pixels specified by the user - insetting them
// first the the same number of pixels first if specified. This is known
// as "edge padding".
//
// This operation can be useful in creating textures for 3D programs where
// you want to display right to the edge of a texture, but the texture
// doesn't wrap. If you extend the border by 2 pixels, then just display
// the inner area and you won't see the artifacts of the wrapped edges.
//
// NOTE: If all the images you wish to apply edge padding to are equal size
// it might be faster to go Window > Action, then create a new action, and
// record the process of you selecting the areas and using the "[ctrl]+[t]"
// tranform tool four times to expand the edges in the same layer, save a
// copy, close and stop recording. Then just repeat on all remaining images.
#target photoshop
if (documents.length == 0) {
alert("Must open all documents you want to modify first.");
} else {
if (!confirm("WARNING: Script will be applied to all " + documents.length +
" currently open images. \nIt will extend their border by " +
"the amount you specify. Only hit ok if you have the right " +
"images open and are prepared to continue.")) { exit; }
// Prompt user for border size and inset amount:
var border = parseInt(prompt("How many border pixels?",10));
var insetAns = prompt("Inset before adding border? (y/n)","y");
var inset = (insetAns == "y") || (insetAns == "Y") || (insetAns == "yes")
if (!confirm("Add a border of " + border + " to all open images?\n" +
"May take a while.")) { exit; }
for (var i = 0; i < documents.length; i++) {
activeDocument = app.documents[i];
extendPixelBorderCurrentDoc(border, inset);
}
alert("Border expansion finished. \n" +
"No changes have been saved you do that part yourself. " +
"Chose between 'save a copy' or 'save all' carefully.");
}
// Takes the currently open document, and extends the outer most pixels
// outwards by a certain number of pixels.
// Input:
// b - number of pixels to expand border in X and Y.
// inset - if true, will keep the current document size by first
// resizing the image to be b*2 pixels less in X and Y
// and then expanding borders.
function extendPixelBorderCurrentDoc(b, inset) {
var img = activeDocument;
var inW = inset ? img.width - 2 * b : img.width; // Inner width.
var inH = inset ? img.height - 2 * b : img.height; // Inner height.
var outW = inset ? img.width : img.width + 2 * b; // Outer width.
var outH = inset ? img.height : img.height + 2 * b; // Outer height.
if (inset) {
img.resizeImage(UnitValue(inW,"px"), UnitValue(inH,"px"),
null, ResampleMethod.BICUBIC);
}
img.resizeCanvas(UnitValue(outW,"px"), UnitValue(outH,"px"),
AnchorPosition.MIDDLECENTER);
// Extend top, bottom, left and right borders:
copySelection(b, b, b+1, inW+b, "n", 0, b, 1, b);
copySelection(inH+b-1, b, inH+b, inW+b, "s", inH+b, b, 1, b);
copySelection(b, b, inH+b, b+1, "e", b, 0, b, 1);
copySelection(b, inW+b-1, inH+b, inW+b, "w", b, inW+b, b, 1);
// Extend each corner pixel:
copySelection(b, b, b+1, b+1, "ne", 0, 0, b, b);
copySelection(b, inW+b-1, b+1, inW+b, "nw", 0, inW+b, b, b);
copySelection(inH+b-1, inW+b-1, inH+b, inW+b, "sw", inH+b, inW+b, b, b);
copySelection(inH+b-1, b, inH+b, b+1, "se", inH+b, 0, b, b);
}
// With the currently active document, selects a rectangle
// (top,left,bottom,right), then copies and pastes it to a
// new layer in the same document - scaling by the amount given
// (scaleX, scaleY) and moving to new location (newTop, newBottom).
function copySelection(top, left, bottom, right, newLayerName,
newTop, newLeft, scaleX, scaleY){
app.preferences.rulerUnits = Units.PIXELS;
var img = activeDocument;
// Set the (last) background layer as the current active (selected) layer:
img.activeLayer = img.layers[img.layers.length - 1];
// Select top/left/bottom/right rectangle:
img.selection.select(
[[left,top],[right,top],[right,bottom],[left,bottom]],
SelectionType.REPLACE, 0, false);
img.selection.copy(false);
img.paste();
var newLayer = img.activeLayer; // Selection is pasted to a new layer.
newLayer.name = newLayerName;
newLayer.isBackgroundLayer = false;
newLayer.allLocked = false;
newLayer.positionLocked = false;
safeLayerTranslate(newLayer, -left, -top); // Move to origin (0,0)
newLayer.resize(scaleX * 100, scaleY * 100, AnchorPosition.TOPLEFT);
newLayer.translate(newLeft, newTop);
}
// Translates the given layer by the given amount.
// In theory you could just call:
// layer.translate(x, y);
// HOWEVER, there is a bug in my Photoshop v6 where sometimes translating a
// layer too many pixels in the negative direction will cause it to dissappear
// (even if it should still be in limits). Looping with a series of
// smaller translates gets around this, but is very slow.
function safeLayerTranslate(layer, x, y) {
// A safe distance you can negatively translate without error. For me
// 8 was as far as I could negatively translate: 9 will fail.
var step = 8;
if(y > 0) {
layer.translate(0, y);
} else {
for (var i = 0; i > y; i-=step)
layer.translate(0, (i-step > y) ? -step : y-i);
}
if(x > 0) {
layer.translate(x, 0);
} else {
for (var i = 0; i > x; i-=step)
layer.translate((i-step > x) ? -step : x-i, 0);
}
}