Skip to main content
Known Participant
July 6, 2024
Answered

Kerning script

  • July 6, 2024
  • 6 replies
  • 3991 views

I'm not happy with the hanging punctuation in photoshop, but I can't work in indesign, where this feature is much more elaborate. I want to make a script with manual kerning on punctuation marks, but I can't figure out what I'm doing wrong.

function adjustPunctuationKerning() {
    var textLayer = app.activeDocument.activeLayer;
    if (textLayer.kind !== LayerKind.TEXT) {
        return; 
    }

    var textItem = textLayer.textItem;

    for (var i = 0; i < textItem.contents.length; i++) {
        var currentCharacter = textItem.contents[i];
        switch (currentCharacter) {
            case '?':
                textItem.characters[i+1].kerning = -300;
                break;
            case '!':
                textItem.characters[i+1].kerning = -100;
                break;
            case '.':
                textItem.characters[i+1].kerning = -100;
                break;
        }
    }
}

adjustPunctuationKerning();

 I don't know js well and i hope photoshop gives kerning manipulation in text layer 

 

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

// apply properties like fonts, size, … to letters of words in selected type layer;
// kerning in this case and for all type layers in the document;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
    var theRegExp = /\?/gi;
    var theTypeLayers = collectTypeLayers ();
    for (var m = 0; m < theTypeLayers.length; m++) {
	changeWordProperties (theTypeLayers[m][1], theRegExp, -500)
    }
};
//////////////////
////// change word properties //////
function changeWordProperties (theId, theRegExp, newKerning) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
//////////////////
try {
// get font of active layer;
var ref = new ActionReference();
ref.putIdentifier( charIDToTypeID("Lyr "), theId ); 
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 hasText = layerDesc.hasKey(stringIDToTypeID("textKey"));
if (hasText == true) {
var textDesc = layerDesc.getObjectValue(stringIDToTypeID('textKey'));
var theText = textDesc.getString(stringIDToTypeID('textKey'));
//var shapeList = textDesc.getList(stringIDToTypeID('textShape'));
var kernRange = textDesc.getList(stringIDToTypeID('kerningRange'));
var rangeList = textDesc.getList(stringIDToTypeID('textStyleRange'));
var idTxtt = charIDToTypeID( "Txtt" );
var idFrom = charIDToTypeID( "From" );
var idT = charIDToTypeID( "T   " );
var idTxLr = charIDToTypeID( "TxLr" );
var idTxt = charIDToTypeID( "Txt " );
var idsetd = charIDToTypeID( "setd" );
// change text;
var desc6 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
    var ref1 = new ActionReference();
    ref1.putIdentifier( idTxLr, theId );
desc6.putReference( idnull, ref1 );
    var desc7 = new ActionDescriptor();
    desc7.putString( idTxt, theText );
//////////////////////////////////////////////////////
var kerningRanges = new ActionList;
var theCounter = 0;
if (kernRange.count > 0) {var thisKerningRange = kernRange.getObjectValue(theCounter)};
// kernRange
for (var m = 0; m < theText.length; m++) {
// check for relevant existing kerning range;
if (kernRange.count > 0) {
    if (thisKerningRange.getInteger(idFrom) == m) {
        kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  thisKerningRange);
        theCounter++;
        if (theCounter < kernRange.count) {
            var thisKerningRange = kernRange.getObjectValue(theCounter)
        }
    };
};
// check for regexp;
if (theText[m].match(theRegExp) != null) {
var desc15 = new ActionDescriptor();
desc15.putInteger( idFrom, m - 1 );
desc15.putInteger( idT, m );
desc15.putInteger( stringIDToTypeID("kerning"), newKerning );
kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  desc15);
};
};
//////////////////////////////////////////////////////
desc7.putList( idTxtt, rangeList );
desc7.putList( stringIDToTypeID( "kerningRange"), kerningRanges);
//////////////////////////////////////////////////////
desc6.putObject( idT, idTxLr, desc7 );
executeAction( idsetd, desc6, DialogModes.NO );
}
}
}
catch (e) {};
app.preferences.rulerUnits = originalRulerUnits;
};
////// collect type layers from active document //////
function collectTypeLayers () {
// 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 && layerDesc.hasKey(stringIDToTypeID("textKey")) == true) {
var visible = layerDesc.getBoolean(stringIDToTypeID("visible"));
var theName = layerDesc.getString(stringIDToTypeID('name'));
var theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
theLayers.push([theName, theID, visible])
};
}
catch (e) {};
};
return theLayers
};

6 replies

c.pfaffenbichler
Community Expert
Community Expert
July 23, 2024

@enperror , have you tested the Script yet? 

enperrorAuthor
Known Participant
November 29, 2024

Hi, yes it works, thx! but there is one problem. The selected kerning is applied to the character, but the previous character becomes 0 kerning. I attached a screenshot, it shows ? has a kerning of -250 and the previous character became 0 kerning

I tried to figure out what the problem is, but it's probably because the kerning is trying to apply to both sides of the character

c.pfaffenbichler
Community Expert
Community Expert
November 29, 2024

Please provide the original file and the resulting file. 

c.pfaffenbichler
Community Expert
c.pfaffenbichlerCommunity ExpertCorrect answer
Community Expert
July 22, 2024

// apply properties like fonts, size, … to letters of words in selected type layer;
// kerning in this case and for all type layers in the document;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
    var theRegExp = /\?/gi;
    var theTypeLayers = collectTypeLayers ();
    for (var m = 0; m < theTypeLayers.length; m++) {
	changeWordProperties (theTypeLayers[m][1], theRegExp, -500)
    }
};
//////////////////
////// change word properties //////
function changeWordProperties (theId, theRegExp, newKerning) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
//////////////////
try {
// get font of active layer;
var ref = new ActionReference();
ref.putIdentifier( charIDToTypeID("Lyr "), theId ); 
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 hasText = layerDesc.hasKey(stringIDToTypeID("textKey"));
if (hasText == true) {
var textDesc = layerDesc.getObjectValue(stringIDToTypeID('textKey'));
var theText = textDesc.getString(stringIDToTypeID('textKey'));
//var shapeList = textDesc.getList(stringIDToTypeID('textShape'));
var kernRange = textDesc.getList(stringIDToTypeID('kerningRange'));
var rangeList = textDesc.getList(stringIDToTypeID('textStyleRange'));
var idTxtt = charIDToTypeID( "Txtt" );
var idFrom = charIDToTypeID( "From" );
var idT = charIDToTypeID( "T   " );
var idTxLr = charIDToTypeID( "TxLr" );
var idTxt = charIDToTypeID( "Txt " );
var idsetd = charIDToTypeID( "setd" );
// change text;
var desc6 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
    var ref1 = new ActionReference();
    ref1.putIdentifier( idTxLr, theId );
desc6.putReference( idnull, ref1 );
    var desc7 = new ActionDescriptor();
    desc7.putString( idTxt, theText );
//////////////////////////////////////////////////////
var kerningRanges = new ActionList;
var theCounter = 0;
if (kernRange.count > 0) {var thisKerningRange = kernRange.getObjectValue(theCounter)};
// kernRange
for (var m = 0; m < theText.length; m++) {
// check for relevant existing kerning range;
if (kernRange.count > 0) {
    if (thisKerningRange.getInteger(idFrom) == m) {
        kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  thisKerningRange);
        theCounter++;
        if (theCounter < kernRange.count) {
            var thisKerningRange = kernRange.getObjectValue(theCounter)
        }
    };
};
// check for regexp;
if (theText[m].match(theRegExp) != null) {
var desc15 = new ActionDescriptor();
desc15.putInteger( idFrom, m - 1 );
desc15.putInteger( idT, m );
desc15.putInteger( stringIDToTypeID("kerning"), newKerning );
kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  desc15);
};
};
//////////////////////////////////////////////////////
desc7.putList( idTxtt, rangeList );
desc7.putList( stringIDToTypeID( "kerningRange"), kerningRanges);
//////////////////////////////////////////////////////
desc6.putObject( idT, idTxLr, desc7 );
executeAction( idsetd, desc6, DialogModes.NO );
}
}
}
catch (e) {};
app.preferences.rulerUnits = originalRulerUnits;
};
////// collect type layers from active document //////
function collectTypeLayers () {
// 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 && layerDesc.hasKey(stringIDToTypeID("textKey")) == true) {
var visible = layerDesc.getBoolean(stringIDToTypeID("visible"));
var theName = layerDesc.getString(stringIDToTypeID('name'));
var theID = layerDesc.getInteger(stringIDToTypeID('layerID'));
theLayers.push([theName, theID, visible])
};
}
catch (e) {};
};
return theLayers
};
c.pfaffenbichler
Community Expert
Community Expert
July 21, 2024

Edit: The Script is kind of a proof-of-concept (based on some old scripts, so it contains superfluous lines) and applies to the selected Type Layer. 

 

 

// apply properties like fonts, size, … to letters of words in selected type layer;
// kerning in this case;
// 2024, use it at your own risk;
if (app.documents.length > 0) {
    var theRegExp = /\?/gi;
	changeWordProperties (theRegExp, -500)
};
//////////////////
////// change word properties //////
function changeWordProperties (theRegExp, newKerning) {
var originalRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
//////////////////
try {
var theFonts = new Array;
var theStyleRanges = new Array;
var theStyleRanges2 = new Array;
// get font of active layer;
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt") ); 
var layerDesc = executeActionGet(ref);
var layerSet = typeIDToStringID(layerDesc.getEnumerationValue(stringIDToTypeID("layerSection")));
var isBackground = layerDesc.getBoolean(stringIDToTypeID("background"));
var theName = layerDesc.getString(stringIDToTypeID('name'));
// if not layer group collect values;
if (layerSet != "layerSectionEnd" && layerSet != "layerSectionStart" && isBackground != true) {
var hasText = layerDesc.hasKey(stringIDToTypeID("textKey"));
if (hasText == true) {
var textDesc = layerDesc.getObjectValue(stringIDToTypeID('textKey'));
var theText = textDesc.getString(stringIDToTypeID('textKey'));
// get indices for string;
var indicesCount = 0;
var theIndices = new Array;
while ((result = theRegExp.exec(theText))!=null) {
    theIndices.push([result.index, result.index+result[0].length])
};
//var shapeList = textDesc.getList(stringIDToTypeID('textShape'));
var paragraphRangeList = textDesc.getList(stringIDToTypeID('paragraphStyleRange'));
var kernRange = textDesc.getList(stringIDToTypeID('kerningRange'));
var rangeList = textDesc.getList(stringIDToTypeID('textStyleRange'));
var idPxl = charIDToTypeID( "#Pxl" );
var idPnt = charIDToTypeID( "#Pnt" );
var idTxtt = charIDToTypeID( "Txtt" );
var idFrom = charIDToTypeID( "From" );
var idT = charIDToTypeID( "T   " );
var idTxtS = charIDToTypeID( "TxtS" );
var idTxLr = charIDToTypeID( "TxLr" );
var idTxt = charIDToTypeID( "Txt " );
var idsetd = charIDToTypeID( "setd" );
// change text;
var desc6 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
    var ref1 = new ActionReference();
    ref1.putEnumerated( idTxLr, charIDToTypeID( "Ordn" ), charIDToTypeID( "Trgt"  ));
desc6.putReference( idnull, ref1 );
    var desc7 = new ActionDescriptor();
    desc7.putString( idTxt, theText );
//////////////////////////////////////////////////////
var kerningRanges = new ActionList;
var theCounter = 0;
if (kernRange.count > 0) {var thisKerningRange = kernRange.getObjectValue(theCounter)};
// kernRange
for (var m = 0; m < theText.length; m++) {
// check for relevant existing kerning range;
if (kernRange.count > 0) {
    if (thisKerningRange.getInteger(idFrom) == m) {
        kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  thisKerningRange);
        theCounter++;
        if (theCounter < kernRange.count) {
            var thisKerningRange = kernRange.getObjectValue(theCounter)
        }
    };
};
// check for regexp;
if (theText[m].match(theRegExp) != null) {
var desc15 = new ActionDescriptor();
desc15.putInteger( idFrom, m - 1 );
desc15.putInteger( idT, m );
desc15.putInteger( stringIDToTypeID("kerning"), newKerning );
kerningRanges.putObject( stringIDToTypeID( "kerningRange"),  desc15);
};
};
//////////////////////////////////////////////////////
//desc7.putList( idTxtt, list2 );
desc7.putList( idTxtt, rangeList );
desc7.putList( stringIDToTypeID( "kerningRange"), kerningRanges);
//////////////////////////////////////////////////////
desc6.putObject( idT, idTxLr, desc7 );
executeAction( idsetd, desc6, DialogModes.NO );
}
}
}
catch (e) {};
app.preferences.rulerUnits = originalRulerUnits;
};

 

 

c.pfaffenbichler
Community Expert
Community Expert
July 7, 2024

This seems tricky me indeed. 

I have provided Scripts for changing the color of words in the past, but that happens in the textStyleRange, changing the kerning would necessitate splitting the kerningRange at the indices of the character (the question mark in this case). 

enperrorAuthor
Known Participant
July 7, 2024

I tried writing new code, it triggers, but instead of executing the function, I have a kerning 0 applied to all the text

if (app.documents.length > 0) {
    var doc = app.activeDocument; 
    
    for (var i = 0; i < doc.artLayers.length; i++) {
        var layer = doc.artLayers[i];
        
        if (layer.kind === LayerKind.TEXT) {
            var textItem = layer.textItem;
            
            textItem.autoKerning = AutoKernType.MANUAL;
            
            for (var j = 0; j < textItem.contents.length; j++) {
                if (textItem.contents[j] === "?") {
                    textItem.kerning = 220;
                }
            }
        }
    }
    alert("yep.");
} else {
    alert("nope.");
}

Most likely, as @Stephen_A_Marsh wrote to me earlier, UXP is required

c.pfaffenbichler
Community Expert
Community Expert
July 8, 2024
quote

Most likely, as @Stephen_A_Marsh wrote to me earlier, UXP is required

While UXP code may be better at handling it AM code can handle the task, too, DOM code is the useless one in this case. 

c.pfaffenbichler
Community Expert
Community Expert
July 7, 2024

How important is this? 

Do you have to perform this task multiple times every day? 

 

As @Stephen Marsh mentioned, the AM-code that could be used to achieve this is not exactly »easy«. 

enperrorAuthor
Known Participant
July 7, 2024

Since I have to do it in photoshop, it takes me quite a bit of time.

c.pfaffenbichler
Community Expert
Community Expert
July 7, 2024

Please clarify what »quite a bit of time« actually means. 

Stephen Marsh
Community Expert
Community Expert
July 6, 2024

Where did you get the code from?

enperrorAuthor
Known Participant
July 6, 2024

gpt and plus I tried

Stephen Marsh
Community Expert
Community Expert
July 6, 2024

Chat GPT is getting better, but it can often make stuff up to be "helpful".

 

https://theiviaxx.github.io/photoshop-docs/Photoshop/TextItem.html