UXP layer text replacement bug - Sometimes text will unexpectedly be resized

In certain situations, if text layers were previously resized using the transform tool, they will revert back to the pre-transform size after the text is replaced in UXP.

Does anyone here do layer text replacement in UXP? If so have you seen this and do you know of a workaround? This affects CC 2021 and CC 2022 beta, for both Windows and Mac (including M1).

I created a video showing how to recreate the issue and a test plugin to recreated it. I’m hoping someone knows of a fix. I may have to pull from the text key to get the current size and then force a resize back to the prior size if it changes.

CCX test plugin download - https://damonbell.com/downloads/text-resize-bug_PS.ccx

Interesting bug :thinking:
I’ve replaced texts using the same descriptor in the past, but I probably just didn’t see the bug as I wasn’t resizing the text layer.

Just some ideas:
Have you tried to get and set the whole textKey descriptor instead of just the textKey property? The naming here is a bit confusing, as there’s a textKey property (the actual string content) inside of the textKey descriptor.

export function __setTextKey(textKey) {
  return {
    _obj: 'set',
    _target: {
      _ref: 'textLayer',
      _enum: 'ordinal',
      _value: 'targetEnum',
    },
    to: textKey
  }
}

Also, the bug looks a bit as if the transform matrix is getting reapplied. If setting the whole textKey didn’t work, you could try to set the transform property to undefined before setting the textKey.
Interestingly enough, there are two of them, one directly in the textKey descriptor, the other one in the textShape descriptor.

Just speculations though, maybe it works, maybe it doesn’t :smiley:

That is exactly how I am doing the text replacement. You can download the test plugin I made for the bug here.
CCX test plugin download - https://damonbell.com/downloads/text-resize-bug_PS.ccx

I was going to start trying to mess with the transform. If that doesn’t work then maybe I will try comparing the size pre and post size and resizing if needed and hope it doesn’t shift the position.

Another workaround would maybe be to first create a dummy text layer, replace the text in the dummy layer, and then delete the dummy text layer. It only resizes on the first replace once after the doc is opened. After that, it only happens on the second replace for each text item. So by doing a dummy test replace first, the real layers that need text replaced won’t have the issue if only replacing the text once. Kind of a clunky workaround but that should work…assuming the bug stay as is and doesn’t change in future Photoshop releases.

I downloaded the example before, it’s not exactly the same though:

 "to": {
      "_obj": "textLayer",
      "textKey": newText
   },

vs

 to: textKey

while textKey is the whole textKey descriptor in my example (which you get via batchPlay before updating and setting it)

OK, thanks for noticing the difference. I am going to play around with it later today. I will post if I figure anything out.

I sort of follow what you are getting at with the export function but I have never used an export function before so I’m having a difficult time trying to implement it. I think that may work though. I need to read up some more on how to implement that.

What I figured out was that when the issue occurs, the text size is changing for the size value here.

textKey.textStyleRange[0].textStyle.size._value;

So I figured that I would first get the text size before replacing the text. Then when setting the text, I would set the text and size only but nothing else.

Getting the text size from this function works

async function getTextSize(){

const result = await batchPlay(
    
[
   {
      "_obj": "get",
      "_target": [
         {
            "_property": "textKey"
         },
         {
            "_ref": "textLayer",
            "_enum": "ordinal",
            "_value": "targetEnum"
         }
      ],
      "_options": {
         "dialogOptions": "dontDisplay"
      }
   }
],{
   "synchronousExecution": false,
   "modalBehavior": "execute"
});    
          
return result[0].textKey.textStyleRange[0].textStyle.size._value;
    
}

Setting the values from this descriptor doesn’t work. It seems like it should work though.

function changeLayerTextAndSize(newText,textSize){
       
return{

   "_obj": "set",
   "_target": [
      {
         "_ref": "textLayer",
         "_enum": "ordinal",
         "_value": "targetEnum"
      }
   ],
   "to": {
      "_obj": "textLayer",
      "textKey": newText,
      "textStyleRange": [
         {
            "_obj": "textStyleRange",
            "textStyle": {
               "_obj": "textStyle",                
               "size": {
                  "_unit": "pointsUnit",  
                  "_value": textSize
               }
            }
         }
      ]
   },
   "_isCommand": true
  
}    
         
}

I probably just need to get the entire descriptor as you said, update the descriptor with the new text string value, and then set the text layer to the updated full descriptor. I need to research more how to actual do that though. The textKey descriptor is complicated.

The export isn’t relevant for the code, it just let’s me import the function in other files. Also, I usually just return the descriptor instead of directly calling batchPlay, so that I can reuse them easier like

photoshop.action.batchPlay([
   doThis(),
   doThat(),
   ...
],{})

I’m not sure why it’s not working but even if you’d get it to work you’d be neglecting the fact that a text can have multiple styles in itself (textStyleRanges). That’s why I suggested to set the textKey descriptor as a whole.
If you’re logging the descriptor for the set event with Alchemist (e.g. for changing the font size), you’ll notice that it’s doing exactly that: It’s passing the whole thing with all the text properties, not just the size.

This is exactly what the function I posted before does:

You also don’t have to worry much about all the other properties of the text, you only need to know where to find the properties you want to change. I always look at the types for that.

For example, you could do something like

const textKey = getTextKey()
textKey.textKey = *new text* // yep, the identical naming sucks
photoshop.action.batchPlay([__setTextKey(textKey)], {})

You don’t have to do this approach usually, but you could check if this also causes the bug.

Btw, here’s a descriptor for setting the fontSize:

export function __setFontSize(fontSize: number) {
  return {
    _obj: 'set',
    _target: [
      {
        _ref: 'property',
        _property: 'textStyle',
      },
      {
        _ref: 'textLayer',
        _enum: 'ordinal',
        _value: 'targetEnum',
      },
    ],
    to: {
      _obj: 'textStyle',
      size: {
        _unit: 'pointsUnit',
        _value: fontSize,
      }
    }
  }
}

As you can see, instead of doing the property nesting and array structure inside of the to object, it’s targeting the textStyle property directly, although it isn’t even a direct property of the textKey descriptor. The textKey descriptor has a textStyleRange property which is an array of

{
  _obj: 'textStyleRange'
  textStyle: TextStyleDescriptor
}

So the whole array structure is neglected in that set descriptor, I assume it’s changing the font-size of all the textStyleRanges.

Just wanted to bring that up as an example showing that the setters don’t always follow the syntax one might expect.

My thought with only passing the size is that Photoshop would leave everything else as is. I tested the new size only function you gave me but updated it to also get and set the units too in addition to the size. Thanks for explaining about it not needing to be nested. This works and Photoshop will update the size but leave everything else as is. However, when the bug occurs, it still ignors the size it is being given. So while the function works, the ug still occurs.

So it seems maybe giving it the entire descriptor may be the answer. However, I can’t get that to do anything. I can get the descriptor into a variable. The descriptor has all of the values and I can see those values are correct. However, the set function does nothing at all. This is the set function I am using.

I tried using this as is without even replacing the text too. I tried just getting the descriptor from one layer, selecting another layer, and then setting it to the descriptor with this function. that doesn’t do anything. The descriptor get works fine and everything is there.

function setTextKey(textKey){   
    
    
return {
   "_obj": "set",
   "_target": [
      {
         "_ref": "textLayer",
         "_enum": "ordinal",
         "_value": "targetEnum"
      }
   ],
   "to": textKey
}

}

Anyway, thanks for trying to help.

However, I have another work-around I am just going to implement until Adobe fixes it. The only time the bug happens is for the first text layer where the text is replaced after opening the document. After that it will only happen is the text is replaced consecutive times (which my plugin doesn’t do).

So the work around is to have the plugin add a temp text layer to the top of the document after it is opened. Then plugin will replace the text in the temp layer and delete the layer. This way, the bug will only happen on the temp later but not all of the other layers that get the text replaced.

This is a clunky workaround but hopefully it is only temporary. It only adds 0.2 seconds to the processing so I can live with that. I just don’t have time to waste a bunch of hours on this so my clunky workaround will just have to be good enough for now.

I really can’t wait for the UPX DOM to be released. It shouldn’t be this difficult just to replace text in a layer. With extendscript it was one line of code. No surgery of complicated descriptors was needed to just replace the text string.

Extendscript is this to do the same thing.
app.activeDocument.activeLayer.textItem.contents="New Text";