Skip to main content
Known Participant
December 9, 2023
Answered

Batch aligning text via the shape/smart object below it?

  • December 9, 2023
  • 2 replies
  • 1835 views

Hello everyone! I'm having thousands of text layers that need to be center aligned. The text is not too out of place, but my work needs a certain amount of precision, so it would take a lot of time if I did it manually.

Because all text layers need to be classified into one group, and all other layers are in another group, their order does not go together. Usually I will select the text layer and the object/shape below it manually, then center it by the move tool aligning.

 


But there is another problem when I select the bubble, that the text will center via the entire height/width of the shape. So the text will sometimes be skewed because of the "tail" of the bubble.

So I switched to using the layer selection. This way I can contract the selection and remove the "tail". That makes my work longer as I have to create a selection of the shape, then find its corresponding text layer again (because when the selection is created, the text inside it cannot be selected with ctrl - click or move tool, as far as I have tried). So I had to use the magicwand and it create a selection faster, by hiding the text and creating a selection below it.

And yet another problem: I have to constantly hide the text, create a selection, show the text, then use the aligning action. Thanks to this topic, my life is saved. But I have to run the action for each text layer one by one. Usually if they have shape layers without the "tail", I will prioritize select the shape to align because it's run faster than the "creating selection" action, but not always the case. Not all text layers are in text boxes so i'll skip if it has no shape below. I have attached a psd sample below.

Does anyone have any opinions? If you can help, it would be my great blessing. Thank you for your interest in this topic!

This topic has been closed for replies.
Correct answer c.pfaffenbichler

Thank you, thank you very much!! Your script work perfectly but, sorry for my omission, it seems to only work with vector layers. Could you make it to work if the bubbles are the smart objects/rasterize layers too? Because they're not always shape layers, but they will be put in a certain "text box" group. I will set the group as the name of the script.


The proper way would be determining the kind of the Layer and progress accordingly but the sloppy way is just using a try-clause on the loading of the vector mask and catch with loading the transparency as a Selection … 

// center layers in group »text« to layers in group »text box선«;
// 2023, use it at your own risk;
if (app.documents.length > 0) {
    activeDocument.suspendHistory("do stuff", "main()");
};
function main () {
var theTextLayers = collectLayerInLayerSetNamed("text");
var theShapeLayers = collectLayerInLayerSetNamed("text box선");
if (theTextLayers.length > 0 && theShapeLayers.length > 0) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
for (var m = 0; m < theTextLayers.length; m++) {
    var bounds1 = theTextLayers[m][5];
    for (var n = 0; n < theShapeLayers.length; n++) {
        var bounds2 = theShapeLayers[n][5];
        if (bounds1[0] > bounds2[0] && bounds1[1] > bounds2[1] && bounds1[2] < bounds2[2] && bounds1[3] < bounds2[3]) {
            loadVectorMask (theShapeLayers[n][2]);
            contractSelection (7);
            var selectionBounds = activeDocument.selection.bounds;
            var theX = Number(selectionBounds[0]+(selectionBounds[2]-selectionBounds[0])/2) - (bounds1[0]+(bounds1[2]-bounds1[0])/2);
            var theY = Number (selectionBounds[1]+(selectionBounds[3]-selectionBounds[1])/2) - (bounds1[1]+(bounds1[3]-bounds1[1])/2);
            activeDocument.selection.deselect();
            duplicateMoveRotateScale (false, theTextLayers[m][2], theX, theY, 100, 100, 0)
        }
    }
};
app.preferences.rulerUnits = originalRulerUnits;
};
};
////// collect layers //////
function collectLayerInLayerSetNamed(groupCheckName) {
// 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);
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 theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
var theIndex = layerDesc.getInteger(stringIDToTypeID('itemIndex'));
var theParentId = layerDesc.getInteger(stringIDToTypeID('parentLayerID'));
var groupName = getLayerNameById (theParentId);
// check name of group;
if (groupName == groupCheckName) {
var theBounds = layerDesc.getObjectValue(stringIDToTypeID("bounds"));
var theseBounds = [theBounds.getUnitDoubleValue(stringIDToTypeID("left")), theBounds.getUnitDoubleValue(stringIDToTypeID("top")), theBounds.getUnitDoubleValue(stringIDToTypeID("right")), theBounds.getUnitDoubleValue(stringIDToTypeID("bottom"))];
theLayers.push([theName, theIndex, theID, groupName, theParentId, theseBounds])
}
};
}
catch (e) {};
};
return theLayers
};
////// get layer’s name by id //////
function getLayerNameById (theId) {
    var ref = new ActionReference();
    ref.putIdentifier( charIDToTypeID("Lyr "), theId); 
    var layerDesc = executeActionGet(ref);
    var theName = layerDesc.getString(stringIDToTypeID('name'));
    return theName
};
////// load vector mask as selection //////
function loadVectorMask (theID) {
try {
    var desc5 = new ActionDescriptor();
        var ref1 = new ActionReference();
        ref1.putProperty( stringIDToTypeID( "channel" ), stringIDToTypeID( "selection" ) );
    desc5.putReference( stringIDToTypeID( "null" ), ref1 );
        var ref2 = new ActionReference();
        var idpath = stringIDToTypeID( "path" );
        ref2.putEnumerated( idpath, idpath, stringIDToTypeID( "vectorMask" ) );
        ref2.putIdentifier( stringIDToTypeID( "layer" ), theID );
    desc5.putReference( stringIDToTypeID( "to" ), ref2 );
    desc5.putInteger( stringIDToTypeID( "version" ), 1 );
    desc5.putBoolean( stringIDToTypeID( "vectorMaskParams" ), true );
executeAction( stringIDToTypeID( "set" ), desc5, DialogModes.NO );
} catch (e) {
    loadTransparencyAsSelection (theID, false)
}
};
////// based on code by mike hale, via paul riggott //////
function selectLayerByID(id,add){
try {
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); 
}
} catch (e) {}
};
////// duplicate layer and move, rotate and scale it //////
function duplicateMoveRotateScale (copyOrNot, theID, theX, theY, theScaleX, theScaleY, theRotation) {
selectLayerByID(theID,false);
/*smartify (activeDocument.activeLayer);
var ref = new ActionReference();
ref.putProperty (stringIDToTypeID ("property"), charIDToTypeID ("LyrI"));
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); 
d = executeActionGet(ref); 
var theID = d.getInteger(charIDToTypeID('LyrI'));*/
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 idIntr = charIDToTypeID( "Intr" );
    var idIntp = charIDToTypeID( "Intp" );
    var idbicubicAutomatic = stringIDToTypeID( "bicubicAutomatic" );
    desc10.putEnumerated( idIntr, idIntp, idbicubicAutomatic );
    var idCpy = charIDToTypeID( "Cpy " );
    desc10.putBoolean( idCpy, copyOrNot );
executeAction( idTrnf, desc10, DialogModes.NO );
return theID
} catch (e) {}
};
////// contract selection //////
function contractSelection (theValue) {
try {
// =======================================================
var idCntc = charIDToTypeID( "Cntc" );
    var desc14 = new ActionDescriptor();
    var idBy = charIDToTypeID( "By  " );
    var idPxl = charIDToTypeID( "#Pxl" );
    desc14.putUnitDouble( idBy, idPxl, theValue );
executeAction( idCntc, desc14, DialogModes.NO );
} catch (e) {}
};
////// load transparency //////
function loadTransparencyAsSelection (theID, theInvert) {
    var idchannel = stringIDToTypeID( "channel" );
    var desc7 = new ActionDescriptor();
        var ref3 = new ActionReference();
        ref3.putProperty( idchannel, stringIDToTypeID( "selection" ) );
    desc7.putReference( stringIDToTypeID( "null" ), ref3 );
        var ref4 = new ActionReference();
        ref4.putEnumerated( idchannel, idchannel, stringIDToTypeID( "transparencyEnum" ) );
        ref4.putIdentifier( stringIDToTypeID( "layer" ), theID );
    desc7.putReference( stringIDToTypeID( "to" ), ref4 );
    desc7.putBoolean(charIDToTypeID("Invr"), theInvert);
executeAction( stringIDToTypeID( "set" ), desc7, DialogModes.NO );
};

 

2 replies

c.pfaffenbichler
Community Expert
Community Expert
December 9, 2023

 

// center layers in group »text« to layers in group »text box선«;
// 2023, use it at your own risk;
if (app.documents.length > 0) {
    activeDocument.suspendHistory("do stuff", "main()");
};
function main () {
var theTextLayers = collectLayerInLayerSetNamed("text");
var theShapeLayers = collectLayerInLayerSetNamed("text box선");
if (theTextLayers.length > 0 && theShapeLayers.length > 0) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
for (var m = 0; m < theTextLayers.length; m++) {
    var bounds1 = theTextLayers[m][5];
    for (var n = 0; n < theShapeLayers.length; n++) {
        var bounds2 = theShapeLayers[n][5];
        if (bounds1[0] > bounds2[0] && bounds1[1] > bounds2[1] && bounds1[2] < bounds2[2] && bounds1[3] < bounds2[3]) {
            loadVectorMask (theShapeLayers[n][2]);
            contractSelection (7);
            var selectionBounds = activeDocument.selection.bounds;
            var theX = Number(selectionBounds[0]+(selectionBounds[2]-selectionBounds[0])/2) - (bounds1[0]+(bounds1[2]-bounds1[0])/2);
            var theY = Number (selectionBounds[1]+(selectionBounds[3]-selectionBounds[1])/2) - (bounds1[1]+(bounds1[3]-bounds1[1])/2);
            activeDocument.selection.deselect();
            duplicateMoveRotateScale (false, theTextLayers[m][2], theX, theY, 100, 100, 0)
        }
    }
};
app.preferences.rulerUnits = originalRulerUnits;
};
};
////// collect layers //////
function collectLayerInLayerSetNamed(groupCheckName) {
// 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);
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 theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
var theIndex = layerDesc.getInteger(stringIDToTypeID('itemIndex'));
var theParentId = layerDesc.getInteger(stringIDToTypeID('parentLayerID'));
var groupName = getLayerNameById (theParentId);
// check name of group;
if (groupName == groupCheckName) {
var theBounds = layerDesc.getObjectValue(stringIDToTypeID("bounds"));
var theseBounds = [theBounds.getUnitDoubleValue(stringIDToTypeID("left")), theBounds.getUnitDoubleValue(stringIDToTypeID("top")), theBounds.getUnitDoubleValue(stringIDToTypeID("right")), theBounds.getUnitDoubleValue(stringIDToTypeID("bottom"))];
theLayers.push([theName, theIndex, theID, groupName, theParentId, theseBounds])
}
};
}
catch (e) {};
};
return theLayers
};
////// get layer’s name by id //////
function getLayerNameById (theId) {
    var ref = new ActionReference();
    ref.putIdentifier( charIDToTypeID("Lyr "), theId); 
    var layerDesc = executeActionGet(ref);
    var theName = layerDesc.getString(stringIDToTypeID('name'));
    return theName
};
////// load vector mask as selection //////
function loadVectorMask (theID) {
    var desc5 = new ActionDescriptor();
        var ref1 = new ActionReference();
        ref1.putProperty( stringIDToTypeID( "channel" ), stringIDToTypeID( "selection" ) );
    desc5.putReference( stringIDToTypeID( "null" ), ref1 );
        var ref2 = new ActionReference();
        var idpath = stringIDToTypeID( "path" );
        ref2.putEnumerated( idpath, idpath, stringIDToTypeID( "vectorMask" ) );
        ref2.putIdentifier( stringIDToTypeID( "layer" ), theID );
    desc5.putReference( stringIDToTypeID( "to" ), ref2 );
    desc5.putInteger( stringIDToTypeID( "version" ), 1 );
    desc5.putBoolean( stringIDToTypeID( "vectorMaskParams" ), true );
executeAction( stringIDToTypeID( "set" ), desc5, DialogModes.NO );
};
////// based on code by mike hale, via paul riggott //////
function selectLayerByID(id,add){
try {
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); 
}
} catch (e) {}
};
////// duplicate layer and move, rotate and scale it //////
function duplicateMoveRotateScale (copyOrNot, theID, theX, theY, theScaleX, theScaleY, theRotation) {
selectLayerByID(theID,false);
/*smartify (activeDocument.activeLayer);
var ref = new ActionReference();
ref.putProperty (stringIDToTypeID ("property"), charIDToTypeID ("LyrI"));
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt")); 
d = executeActionGet(ref); 
var theID = d.getInteger(charIDToTypeID('LyrI'));*/
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 idIntr = charIDToTypeID( "Intr" );
    var idIntp = charIDToTypeID( "Intp" );
    var idbicubicAutomatic = stringIDToTypeID( "bicubicAutomatic" );
    desc10.putEnumerated( idIntr, idIntp, idbicubicAutomatic );
    var idCpy = charIDToTypeID( "Cpy " );
    desc10.putBoolean( idCpy, copyOrNot );
executeAction( idTrnf, desc10, DialogModes.NO );
return theID
} catch (e) {}
};
////// contract selection //////
function contractSelection (theValue) {
try {
// =======================================================
var idCntc = charIDToTypeID( "Cntc" );
    var desc14 = new ActionDescriptor();
    var idBy = charIDToTypeID( "By  " );
    var idPxl = charIDToTypeID( "#Pxl" );
    desc14.putUnitDouble( idBy, idPxl, theValue );
executeAction( idCntc, desc14, DialogModes.NO );
} catch (e) {}
};

 

c.pfaffenbichler
Community Expert
Community Expert
December 9, 2023

Actually the Script I posted is crude (to put it mildly); for example there is no check to make sure a »text« Layer does not have more than one corresponding »text box선« Layers. 

As it is the »text« Layer would just be aligned twice. 

 

If the bubles are always elliptical it should also be possible to determine the bigger of the subPathItems from the Shape Layers instead of using Selections and that might speed up the processquite a bit – which could make a difference for images with a lot of speech-bubbles. 

Edit: This should also help prevent issues if the tail is particularly broad/big and might not be »removed« by the contraction of the Selection. 

Known Participant
December 9, 2023

Thank you, thank you very much!! Your script work perfectly but, sorry for my omission, it seems to only work with vector layers. Could you make it to work if the bubbles are the smart objects/rasterize layers too? Because they're not always shape layers, but they will be put in a certain "text box" group. I will set the group as the name of the script.

c.pfaffenbichler
Community Expert
Community Expert
December 9, 2023

Why do you use a Layer Style Stroke instead of a proper Shape Layer Stroke? 

 

»I have to constantly hide the text, create a selection, show the text, then use the aligning action.«

Why do you need to hide the Type Layer? 

cmd-alt-clicking the speechbubble atop the Type Layer should work to identify the corresponding Shape Layer. 

 

Known Participant
December 9, 2023

Hello, thank you for your respond!

About the Layer Style Stroke, it's not mine, I just took some features from my job for the sample. I can't decide for myself what the original layer should be.

And the ctrl-alt-clicking layer, I already known this shortcut, but it won't work if the selection is created (or did I misunderstand you?)

By the way, to click both text and shape is also a time-consuming work if you have to do it manually with each text layer. In the past, I used the method of selecting the text and bubble layers, and run the action of duplicating them, merging them into one rasterized layer, and deleting it after getting the selection. But when I found the mentioned topic, I prefer to use the "magicwand script" combined with the action if the bubble has a "tail".

c.pfaffenbichler
Community Expert
Community Expert
December 9, 2023

You mention Smart Objects, but in the sample file all the speech bubbles were Shape Layers, so I disregarded the Smart Object-option. 

That would need further amendments to load the transparency instead of the Vector Mask …