Skip to main content
Known Participant
March 11, 2024
Question

Automating Student Portrait Layouts Using Templates in Photoshop Without Coding

  • March 11, 2024
  • 7 replies
  • 3813 views

Hi!

I'm a professional photographer tasked with taking individual student portraits at various schools each year. After the photo sessions, I usually edit these images in Photoshop, arranging them in an organized manner on a single page. Each student's photo is accompanied by a frame and their name, which I manually add beneath each picture. The names are derived from the photo filenames.I am exploring ways to streamline my workflow by automatically generating this layout using a template. The goal is to place all the individual portraits on one page and automatically add each student's name beneath their photo, utilizing the filenames as the source for the names, all without engaging in complex scripting or coding.

 

Does Photoshop offer any built-in features or perhaps an external plugin that could automate the creation of these layouts from a template? I'm specifically looking for a solution that can auto-populate a template with the photos and their corresponding names, thereby reducing the manual effort involved in the layout and labeling process.I'm open to any advice, guides, or tool recommendations that can help achieve this with minimal manual intervention. If anyone knows of a method to efficiently utilize templates in Photoshop for this purpose, or if there's a more straightforward approach to accomplishing this task, I would greatly appreciate your insights.

 

Thank you so much for your support and suggestions!

7 replies

Participant
October 17, 2025

I am needing something really similar.

I have a base template for sports teams.

Inside the photos, I have a "subject" group/layer, which is where extracted photos will go inside.

I want to fill the layer with the subject in the template, and then export as a certain size jpeg.

 

 

Stephen Marsh
Community Expert
Community Expert
October 17, 2025

@MIke27635216khlb 

 

If you have tried the various suggestions from earlier in the topic, or have no problems with using a custom script, then you will need to provide sample files, examples of the finished results etc. You can make the samples low resolution and redact any personal details/facial features etc.

Stephen Marsh
Community Expert
Community Expert
March 18, 2024

@Adi1231234 

 

Have you tried the latest script from @c.pfaffenbichler ?

c.pfaffenbichler
Community Expert
Community Expert
March 16, 2024

If you create a template with frames, populate it with the images you can use a relatively simple script to add the names as Type Layers. 

// add type layer with smart objects’ names;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
// set to pixels;
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
// process;
var theSO = collectSmartObjectsBounds ();
for (var m = 0; m < theSO.length; m++) {
var thisOne = theSO[m];
addTypeLayer (thisOne[0], [thisOne[2][0]+(thisOne[2][2]-thisOne[2][0])/2, thisOne[2][3]+35])
};
// reset;
app.preferences.rulerUnits = originalRulerUnits;
};
////// add type layer //////
function addTypeLayer (theString, theArray) {
var thisLayer = activeDocument.artLayers.add();
thisLayer.kind = LayerKind.TEXT;
thisLayer.name = theString;
var thisLayerRef = thisLayer.textItem;
thisLayerRef.kind = TextType.POINTTEXT;
thisLayerRef.size = 8;
thisLayerRef.font = "Arial-Bold";
var theColor = new SolidColor();
theColor.rgb.red = 0;
theColor.rgb.green = 0;
theColor.rgb.blue = 0;
thisLayerRef.color = theColor;
thisLayerRef.justification = Justification.CENTER;		
thisLayerRef.position = theArray;
thisLayer.blendMode = BlendMode.NORMAL;
thisLayer.opacity = 100;
thisLayer.fillOpacity = 100;
thisLayerRef.useAutoLeading = true;
//thisLayerRef.leading = 0;
thisLayerRef.horizontalScale = 100;
thisLayerRef.verticalScale = 100;
thisLayerRef.contents = theString;
return app.activeDocument.activeLayer
};
////// collect smart objects, probably based on code by paul, mike or x //////
function collectSmartObjectsBounds () {
// the file;
var myDocument = app.activeDocument;
// get number of layers;
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
var applicationDesc = executeActionGet(ref);
var theNumber = applicationDesc.getInteger(stringIDToTypeID("numberOfLayers"));
// process the layers;
var theLayers = new Array;
for (var m = 0; m <= theNumber; m++) {
try {
var ref = new ActionReference();
ref.putIndex( charIDToTypeID( "Lyr " ), m);
var layerDesc = executeActionGet(ref);
var layerSet = typeIDToStringID(layerDesc.getEnumerationValue(stringIDToTypeID("layerSection")));
var isBackground = layerDesc.getBoolean(stringIDToTypeID("background"));
// if not layer group collect values;
if (layerSet != "layerSectionEnd" && layerSet != "layerSectionStart" && isBackground != true) {
var theName = layerDesc.getString(stringIDToTypeID('name'));
var theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
if(layerDesc.hasKey(stringIDToTypeID('smartObject'))) {
var soDesc = layerDesc.getObjectValue(stringIDToTypeID('smartObject'));
var theFileRef = soDesc.getString(stringIDToTypeID('fileReference'));
var theDocID = soDesc.getString(stringIDToTypeID('documentID'));
var isLinked = soDesc.getBoolean(stringIDToTypeID('linked'));
if (isLinked == true) {
var isMissing = soDesc.getBoolean(stringIDToTypeID('linkMissing'));
var isChanged = soDesc.getBoolean(stringIDToTypeID('linkChanged'));
var thePath = soDesc.getPath(stringIDToTypeID('link'));
} else {
var isMissing = undefined;
var isChanged = undefined;
var thePath = undefined
};
/*var x = soDesc.getList(stringIDToTypeID("compsList"));
var theCompsList = soDesc.getObjectValue(stringIDToTypeID("compsList"));
if (theCompsList.count > 2) {
var theCompsList = theCompsList.getList(stringIDToTypeID("compList"));
var theSOComps = new Array;
for (var n = 0; n < theCompsList.count; n++) {
var thisOne = theCompsList.getObjectValue(n);
var compName = thisOne.getString(stringIDToTypeID("name"));
var compID = thisOne.getInteger(stringIDToTypeID("ID"));
var theComment = thisOne.getString(stringIDToTypeID("comment"));
theSOComps.push([compName, compID, theComment]);
};
theLayers.push([theName, theID, theFileRef, theDocID])
theLayers.push([theName, theID, theFileRef, theDocID, theSOComps])
};*/
var theBounds = layerDesc.getObjectValue(stringIDToTypeID("bounds"));
var theseBounds = [theBounds.getUnitDoubleValue(stringIDToTypeID("left")), theBounds.getUnitDoubleValue(stringIDToTypeID("top")), theBounds.getUnitDoubleValue(stringIDToTypeID("right")), theBounds.getUnitDoubleValue(stringIDToTypeID("bottom"))];
var theWidth = theseBounds[2] - theseBounds[0];
var theHeight = theseBounds[3] - theseBounds[1];
var theCenter = [theseBounds[0]+theWidth/2, theseBounds[1]+theHeight/2];
theLayers.push([theName, theID, theseBounds, theWidth, theHeight, theCenter, isLinked, isMissing, isChanged, thePath, theFileRef, theDocID])
}
}
}
catch (e) {};
};
return theLayers
};
////// based on code by mike hale, via paul riggott //////
function selectLayerByID(id,add){ 
add = undefined ? add = false:add 
var ref = new ActionReference();
ref.putIdentifier(charIDToTypeID("Lyr "), id);
var desc = new ActionDescriptor();
desc.putReference(charIDToTypeID("null"), ref );
if(add) desc.putEnumerated( stringIDToTypeID( "selectionModifier" ), stringIDToTypeID( "selectionModifierType" ), stringIDToTypeID( "addToSelection" ) ); 
desc.putBoolean( charIDToTypeID( "MkVs" ), false ); 
try{
executeAction(charIDToTypeID("slct"), desc, DialogModes.NO );
}catch(e){
alert(e.message); 
}
};

 

Known Participant
March 16, 2024

Thank you for sharing your script with me. I appreciate your effort to help streamline my workflow. The script you've provided is interesting and focuses on creating text layers for naming Smart Objects in Photoshop, which is indeed a useful function.

However, my primary goal is slightly different. I am looking for a way to automatically insert individual student portraits into predefined frames within a Photoshop document and then automatically add each student's name, taken from the photo filenames, beneath their respective photo. This process involves both the placement and scaling of images within frames and the generation of text layers for the names.

The challenge I'm facing is how to automate the arrangement of these photos and their corresponding names in a template, to minimize manual work as much as possible. Your script gives me some ideas on handling text layers, but I'm still in need of a solution that can also automatically place and fit the images into the frames.

Do you have any insights, or could you suggest modifications to your script that might address these specific needs? I'm looking for a streamlined approach that combines both image placement and text labeling in an automated fashion.

Thank you again for your contribution and looking forward to any further advice you might have

c.pfaffenbichler
Community Expert
Community Expert
March 17, 2024
quote

Thank you for sharing your script with me. I appreciate your effort to help streamline my workflow. The script you've provided is interesting and focuses on creating text layers for naming Smart Objects in Photoshop, which is indeed a useful function.

However, my primary goal is slightly different. I am looking for a way to automatically insert individual student portraits into predefined frames within a Photoshop document and then automatically add each student's name, taken from the photo filenames, beneath their respective photo. This process involves both the placement and scaling of images within frames and the generation of text layers for the names.

The challenge I'm facing is how to automate the arrangement of these photos and their corresponding names in a template, to minimize manual work as much as possible. Your script gives me some ideas on handling text layers, but I'm still in need of a solution that can also automatically place and fit the images into the frames.

Do you have any insights, or could you suggest modifications to your script that might address these specific needs? I'm looking for a streamlined approach that combines both image placement and text labeling in an automated fashion.

Thank you again for your contribution and looking forward to any further advice you might have


By @Adi1231234

Once you have set up a template with Frames (for example) a Script to populate those with images need not be too complex, depending on what you want to achieve.

 

// fill frames in duplicate/s of active document with selected images;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
var theFiles = selectFile (true);
var myDocument = activeDocument;
var theCount = 0;
var thePages = 1;
var theCopy = myDocument.duplicate("file"+thePages, false);
var theFrames =  collectFrames ();
for (var m = theFiles.length-1; m >= 0; m--) {
    fillFrame (theFrames[theCount][2], theFiles[m]);
    theCount++;
    if (theCount == theFrames.length) {
        theCount = 0;
        thePages++;
        activeDocument = myDocument;
        var theCopy = myDocument.duplicate("file"+thePages, false);
        var theFrames =  collectFrames ();
    }
};
};
////// frame //////
function fillFrame (theFrame, theFile) {
selectLayerByID(theFrame,false);
var idpixelsUnit = stringIDToTypeID( "pixelsUnit" );
var idplaceEvent = stringIDToTypeID( "placeEvent" );
var idlayer = stringIDToTypeID( "layer" );
var idoffset = stringIDToTypeID( "offset" );
var desc7 = new ActionDescriptor();
    desc7.putInteger( stringIDToTypeID( "ID" ), theFrame );
    desc7.putPath( stringIDToTypeID( "null" ), new File( theFile ) );
    desc7.putEnumerated( stringIDToTypeID( "freeTransformCenterState" ), stringIDToTypeID( "quadCenterState" ), stringIDToTypeID( "QCSAverage" ) );
        var desc8 = new ActionDescriptor();
        desc8.putUnitDouble( stringIDToTypeID( "horizontal" ), idpixelsUnit, 0.000000 );
        desc8.putUnitDouble( stringIDToTypeID( "vertical" ), idpixelsUnit, 0.000000 );
    desc7.putObject( idoffset, idoffset, desc8 );
        /*var desc9 = new ActionDescriptor();
            var ref1 = new ActionReference();
            ref1.putIdentifier( idlayer, 4 );
        desc9.putReference( stringIDToTypeID( "from" ), ref1 );
            var ref2 = new ActionReference();
            ref2.putIdentifier( idlayer, 34 );
        desc9.putReference( stringIDToTypeID( "to" ), ref2 );
    desc7.putObject( stringIDToTypeID( "replaceLayer" ), idplaceEvent, desc9 );*/
executeAction( idplaceEvent, desc7, DialogModes.NO );
};
////// select file //////
function selectFile (multi) {
if (multi == true) {var theString = "please select files"}
else {var theString = "please select one file"};
if ($.os.search(/windows/i) != -1) {var theFiles = File.openDialog (theString, '*.jpg;*.tif;*.psd;*.png', multi)}
else {var theFiles = File.openDialog (theString, getFiles, multi)};
////// filter files  for mac //////
function getFiles (theFile) {
if (theFile.name.match(/\.(jpg|tif|psd|png)$/i) || theFile.constructor.name == "Folder") {
return true
};
};
return theFiles
};
////// collect layers //////
function collectFrames () {
// get number of layers;
var ref = new ActionReference();
ref.putProperty(stringIDToTypeID('property'), stringIDToTypeID('numberOfLayers'));
ref.putEnumerated( charIDToTypeID("Dcmn"), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
var applicationDesc = executeActionGet(ref);
var theNumber = applicationDesc.getInteger(stringIDToTypeID("numberOfLayers"));
// process the layers;
var theLayers = new Array;
for (var m = 0; m <= theNumber; m++) {
try {
var ref = new ActionReference();
ref.putIndex( charIDToTypeID( "Lyr " ), m);
var layerDesc = executeActionGet(ref);
//checkDesc2 (layerDesc, true);
var layerSet = typeIDToStringID(layerDesc.getEnumerationValue(stringIDToTypeID("layerSection")));
var isBackground = layerDesc.getBoolean(stringIDToTypeID("background"));
// if group collect values;
if (layerSet != "layerSectionEnd" /*&& layerSet != "layerSectionStart" && isBackground != true*/) {
var theName = layerDesc.getString(stringIDToTypeID('name'));
var hasFrame = layerDesc.hasKey(stringIDToTypeID("framedGroup"));
var theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
var theIndex = layerDesc.getInteger(stringIDToTypeID('itemIndex'));
var theColor = typeIDToStringID(layerDesc.getEnumerationValue(stringIDToTypeID("color")));
if (layerSet == "layerSectionStart" && hasFrame == true) {
theLayers.push([theName, theIndex, theID, theColor])
};
};
}
catch (e) {};
};
return theLayers
};
////// based on code by michael l hale //////
function checkDesc2 (theDesc, theAlert) {
var c = theDesc.count;
var str = '';
for(var i=0;i<c;i++){ //enumerate descriptor's keys
str = str + 'Key '+i+' = '+typeIDToStringID(theDesc.getKey(i))+': '+theDesc.getType(theDesc.getKey(i))+'\n'+getValues (theDesc, i)+'\n';
};
if (theAlert == true) {alert("desc\n\n"+str);
return};
return str
};
////// check //////
function getValues (theDesc, theNumber) {
switch (theDesc.getType(theDesc.getKey(theNumber))) {
case DescValueType.ALIASTYPE:
return theDesc.getPath(theDesc.getKey(theNumber));
break;
case DescValueType.BOOLEANTYPE:
return theDesc.getBoolean(theDesc.getKey(theNumber));
break;
case DescValueType.CLASSTYPE:
return theDesc.getClass(theDesc.getKey(theNumber));
break;
case DescValueType.DOUBLETYPE:
return theDesc.getDouble(theDesc.getKey(theNumber));
break;
case DescValueType.ENUMERATEDTYPE:
return (typeIDToStringID(theDesc.getEnumerationValue(theDesc.getKey(theNumber)))+"_"+typeIDToStringID(theDesc.getEnumerationType(theDesc.getKey(theNumber))));
break;
case DescValueType.INTEGERTYPE:
return theDesc.getInteger(theDesc.getKey(theNumber));
break;
case DescValueType.LISTTYPE:
return theDesc.getList(theDesc.getKey(theNumber));
break;
case DescValueType.OBJECTTYPE:
return (theDesc.getObjectValue(theDesc.getKey(theNumber))+"_"+typeIDToStringID(theDesc.getObjectType(theDesc.getKey(theNumber))));
break;
case DescValueType.RAWTYPE:
return theDesc.getReference(theDesc.getData(theNumber));
break;
case DescValueType.REFERENCETYPE:
return theDesc.getReference(theDesc.getKey(theNumber));
break;
case DescValueType.STRINGTYPE:
return theDesc.getString(theDesc.getKey(theNumber));
break;
case DescValueType.UNITDOUBLE:
return (theDesc.getUnitDoubleValue(theDesc.getKey(theNumber))+"_"+typeIDToStringID(theDesc.getUnitDoubleType(theDesc.getKey(theNumber))));
break;
default: 
break;
};
};
////// based on code by mike hale, via paul riggott //////
function selectLayerByID(id,add){ 
	add = undefined ? add = false:add 
	var ref = new ActionReference();
		ref.putIdentifier(charIDToTypeID("Lyr "), id);
		var desc = new ActionDescriptor();
		desc.putReference(charIDToTypeID("null"), ref );
		   if(add) desc.putEnumerated( stringIDToTypeID( "selectionModifier" ), stringIDToTypeID( "selectionModifierType" ), stringIDToTypeID( "addToSelection" ) ); 
		  desc.putBoolean( charIDToTypeID( "MkVs" ), false ); 
	   try{
		executeAction(charIDToTypeID("slct"), desc, DialogModes.NO );
	}catch(e){
	alert(e.message); 
	}
};  




////// duplicate layer and move, rotate and scale it //////
function duplicateMoveRotateScale (theID, theX, theY, theScaleX, theScaleY, theRotation, theSkew, theDuplicate) {
selectLayerByID(theID,false);
try{
var idTrnf = charIDToTypeID( "Trnf" );
    var desc10 = new ActionDescriptor();
    var idnull = charIDToTypeID( "null" );
        var ref6 = new ActionReference();
        ref6.putIdentifier( charIDToTypeID( "Lyr " ), theID );
        ref6.putEnumerated( charIDToTypeID( "Lyr " ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Trgt" ) );
    desc10.putReference( idnull, ref6 );
    var idFTcs = charIDToTypeID( "FTcs" );
    var idQCSt = charIDToTypeID( "QCSt" );
    var idQcsa = charIDToTypeID( "Qcsa" );
    desc10.putEnumerated( idFTcs, idQCSt, idQcsa );
    var idOfst = charIDToTypeID( "Ofst" );
        var desc11 = new ActionDescriptor();
        var idHrzn = charIDToTypeID( "Hrzn" );
        var idPxl = charIDToTypeID( "#Pxl" );
        desc11.putUnitDouble( idHrzn, idPxl, theX );
        var idVrtc = charIDToTypeID( "Vrtc" );
        var idPxl = charIDToTypeID( "#Pxl" );
        desc11.putUnitDouble( idVrtc, idPxl, theY );
    var idOfst = charIDToTypeID( "Ofst" );
    desc10.putObject( idOfst, idOfst, desc11 );
    var idWdth = charIDToTypeID( "Wdth" );
    var idPrc = charIDToTypeID( "#Prc" );
    desc10.putUnitDouble( idWdth, idPrc, theScaleX );
    var idHght = charIDToTypeID( "Hght" );
    var idPrc = charIDToTypeID( "#Prc" );
    desc10.putUnitDouble( idHght, idPrc, theScaleY );
    var idAngl = charIDToTypeID( "Angl" );
    var idAng = charIDToTypeID( "#Ang" );
    desc10.putUnitDouble( idAngl, idAng, theRotation );

    var idskew = stringIDToTypeID( "skew" );
        var desc358 = new ActionDescriptor();
        var idhorizontal = stringIDToTypeID( "horizontal" );
        var idangleUnit = stringIDToTypeID( "angleUnit" );
        desc358.putUnitDouble( idhorizontal, idangleUnit, theSkew );
        var idvertical = stringIDToTypeID( "vertical" );
        var idangleUnit = stringIDToTypeID( "angleUnit" );
        desc358.putUnitDouble( idvertical, idangleUnit, 0.000000 );
    var idpaint = stringIDToTypeID( "paint" );
    desc10.putObject( idskew, idpaint, desc358 );

    var idIntr = charIDToTypeID( "Intr" );
    var idIntp = charIDToTypeID( "Intp" );
    var idbicubicAutomatic = stringIDToTypeID( "bicubicAutomatic" );
    desc10.putEnumerated( idIntr, idIntp, idbicubicAutomatic );
    var idCpy = charIDToTypeID( "Cpy " );
    desc10.putBoolean( idCpy, theDuplicate );
executeAction( idTrnf, desc10, DialogModes.NO );
} catch (e) {}
};
////// get active layer’s id //////
function getLayerId () {
    var ref = new ActionReference();
    ref.putProperty (stringIDToTypeID ("property"), stringIDToTypeID ("layerID"));
    ref.putEnumerated( charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
    return executeActionGet(ref).getInteger(stringIDToTypeID("layerID"));
};

 

Legend
March 14, 2024

This would probably be easier using InDesign rather than Photoshop.

c.pfaffenbichler
Community Expert
Community Expert
March 14, 2024

Well, it would at least be easier to export multi-page-pdfs from Indesign. 

Known Participant
March 15, 2024

Thank you very much for your suggestion. I did explore the option of using the software you recommended. However, after some consideration, I've decided to continue with Photoshop for this task. I'm more familiar with Photoshop's environment and have access to a broader range of tools that are essential for my workflow beyond this specific task. I appreciate the alternative solution, but I'm keen on finding a way to streamline this process within Photoshop, if possible

c.pfaffenbichler
Community Expert
Community Expert
March 14, 2024

How are the images’/Layers’ names supposed to be displayed in the montage exactly

I expect file format suffixes should not be displayed, but what about capitalization, spaces/underscores, numbering, …? 

 

Known Participant
March 15, 2024

Hi, thanks for your answer!

The file format suffixes (e.g., .jpg, .png) should not be displayed in the names beneath each portrait. Other than removing the file extension, the names should appear exactly as they are in the filenames, including any capitalization, spaces/underscores, and numbering.

 

Also, I am attaching an example of how I would like it to look (of course, instead of the children's drawings, there will be pictures)

 

Thanks you!

c.pfaffenbichler
Community Expert
Community Expert
March 14, 2024

Could you please post screenshots with the pertinent Panels (Toolbar, Layers, Options Bar, …) visible for both the file as it is and the intended result? 

Stephen Marsh
Community Expert
Community Expert
March 11, 2024
quote

The goal is to place all the individual portraits on one page and automatically add each student's name beneath their photo, utilizing the filenames as the source for the names, all without engaging in complex scripting or coding.


By @Adi1231234

 

This needs scripting, but it doesn't have to be complex. You can use the built-in Contact Sheet II script:

 

The "Filename as Caption" will include the filename extension (.jpg or .psd etc). The default script could be altered or you can create an action using find/replace text to remove the extension and then use Batch to run the action over all open contact sheet documents.

 

P.S. You may also find the following topic of interest:

 

https://community.adobe.com/t5/photoshop-ecosystem-discussions/automate-contact-sheet-ii-is-there-a-way-to-not-have-the-file-extension-in-the-captions/m-p/14252498

 

Known Participant
March 13, 2024

Thank you for the suggestion regarding using the Contact Sheet II in Photoshop.

However, what I'm actually looking for is a bit different.

I have a specific design ready that includes gray squares. I want to place a student's photo in each gray square and have the student's name, taken from the file name, appear below the photo.

This layout requires precise placement within predefined templates.

Could you advise on how to achieve this, perhaps with a different tool or method?

Thanks for your help!

Stephen Marsh
Community Expert
Community Expert
March 13, 2024

Sorry, I misread your OP and thought that you wanted one student per sheet.

 

Anyway, what you want to do without scripting would leave one last option: Photoshop Variables - AKA, Data Driven Graphics.

 

https://helpx.adobe.com/au/photoshop/using/creating-data-driven-graphics.html

 

Why don't you want to use a script?

 

This sounds similar to the following recent topic using a custom template, but that only used a single image:

 

 
I think that the variables / data driven graphics approach will get you 90% there, then the final PSD output may need adjusting depending on the results of the automated image replacement.