Import Image as Layer

The end goal is to import an image from a fixed path as a background layer and transform it to fit the canvas. Right now I am struggling with even importing an image at all.
When searching I find entries describing how to do it in XD or entries on how to load objs and txt files or how to open an image and duplicate its background layer into the current document.

I have tried (for the first time in my life) to create my own batchplay code with alchemist, too, but it doesnt do anything and doesnt throw any errors either (though I wonder how it should even work, as i dont see how the code would know in which document to load the image, also, do I need a sessionToken for this?):

const { app, core } = require("photoshop");

async function importGradient() {
  return require("photoshop").action.batchPlay(
    [
        {
           "_obj": "placeEvent",
           "ID": 7,
           "null": {
              "_path": "C:\\STAPELVERARBEITUNG\\Hintergrund_Artikel_Verlauf2000x2000MitteHeller.tif",
              "_kind": "local"
           },
           "freeTransformCenterState": {
              "_enum": "quadCenterState",
              "_value": "QCSAverage"
           },
           "offset": {
              "_obj": "offset",
              "horizontal": {
                 "_unit": "pixelsUnit",
                 "_value": 0
              },
              "vertical": {
                 "_unit": "pixelsUnit",
                 "_value": 0
              }
           },
           "replaceLayer": {
              "_obj": "placeEvent",
              "to": {
                 "_ref": "layer",
                 "_id": 7
              }
           },
           "_isCommand": true
        },
    ],
    {}
  );
};

async function main(){
    await core.executeAsModal(() => {
        importGradient()
    });
}

document.getElementById('myBTN').addEventListener('click', () => { main(); });

Again, if who ever wants to help would like to go the extra step, I would need to check wether the lowest layer is a background layer and unset it if it is, would need to move the new layer to the bottom of the stack and fit it into the current canvas, so I would appreciate help with that, too, on the other hand I dont want to ask for too much at once.
At least conceptual guidance on how to approach this would be awesome! Thanks in advance!

Edit:
This and this suggests I need to create that whole sessionToken shebang (I seriously hate it, becaus it makes life too complicated and would be happy if Adobe could alter their approach). I did that for saving already, so should get that working somehow. Could somebody at least explain how to put the image file, after I have the correct object, as a layer?

I am still trying to figure this out. I am confused, should this work with manifest version 5, api version 2 at all or is my code itself flawed? This UXP stuff is overly complicated to me and I am lost, please, I beg you to help me!

Also, this doesnt even take into account wether the lowest layer in the list is a background layer itself and does not place the layer as the lowest yet. If someone is bored and would like to explain that in one go with the rest, I would be very relieved! Thanks

const photoshop = require("photoshop");


async function importGradient(doc) {
    const Layer = photoshop.Layer;
    let imageLayer = await Layer.createFromFile("C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.tif");
    
    const Layers = photoshop.Layers;
    await doc.layers.add(imageLayer);
    
    await imageLayer.resize(doc.width, doc.height);
};


async function main(){
    const doc = app.activeDocument;
    await core.executeAsModal(() => {
        importGradient(doc);
    });

}

document.getElementById('btn_create').addEventListener('click', () => { main(); });

Basically this code does nothing, not even throw an error, so its hard for me to debug it at all…

Hi @morph3us,
I have used batchplay to import images as Layers.

Basically, in batchplay you cannot just pass the path, you have to create a Session token, maybe try to use a Token in your code.

Please checkout the code below:

async function DebugFileImportModule() {
    let path = 'C:\\Users\\UserName\\Pictures\\bitmap.png';
    let entry = await fs.getEntryWithUrl(`file:/${path}`);  // TESTING -> DIRECT_PATH
    console.log("entry", entry)

    let token = fs.createSessionToken(entry);
    console.log("token", token)
    let result = await require('photoshop').action.batchPlay([{
        _obj: "placeEvent",
        "target": {
            _path: token, // Rather than a system path, this expects a session token
            _kind: "local",
        },
     }
     ], {});
}

Don’t forget to add localFileSystemPermission in manifest.json
Minfest Example:

"requiredPermissions": {
    "localFileSystem": "fullAccess",

Your code is throwing following error → File_UXP_Module.jsx?3cda:38 error TypeError: Cannot read properties of undefined (reading ‘createFromFile’)

Also always wrap executeAsModule callback inside the try-catch block, or it will not throw errors.

Example Try-Catch:

async function runModalFunction() {
    try {
        await executeAsModal(DebugFileImportModule, { commandName: "Action Commands" });
    }
    catch (e) {
        console.log("error", e)

        if (e.number == 9) {
            showAlert("executeAsModal was rejected (some other plugin is currently inside a modal scope)");
        }
    }
}

Hope This helps.

I hope I implemented everything correctly. The token seems to be recognized, as far as I can see.
No image is being imported, though…

MANIFEST

INDEX.JS

const { app, core } = require("photoshop");
const fs = require('uxp').storage.localFileSystem;
const photoshop = require("photoshop");

async function importGradient() {
    console.log("asdf");
    let entry = await fs.getEntryWithUrl("file://C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.tif");  // TESTING -> DIRECT_PATH
    console.log("entry", entry)

    let token = fs.createSessionToken(entry);
    console.log("token", token)
    let result = await require('photoshop').action.batchPlay([{
        _obj: "placeEvent",
        "target": {
            _path: token, // Rather than a system path, this expects a session token
            _kind: "local",
        },
     }
     ], {});
}

async function main(){
      await core.executeAsModal(() => {
        try {
            importGradient();
        }
        catch (e) {
            console.log("error", e)

            if (e.number == 9) {
                showAlert("executeAsModal was rejected (some other plugin is currently inside a modal scope)");
            }
        }
    });

    console.log("ran through");
}

document.getElementById('btnCreateFolders').addEventListener('click', () => { main(); });

DEBUG

Try replacing file:// with file:/, cause this is showing an error to me.
any reason for using // ?

use path format as given in my previous example.

Also, if there is backward-slash (\) use backward-slash only do not use forward slashes , if there is double backward-slash (\\) use those only.
Do not use path given by windows os as it is, replace/transform it into example’s path format.

Like this → let path = 'C:\\Users\\UserName\\Pictures\\bitmap.png';
and pass this path variable to entryWithURL.

I really appreciate you trying to help me! I am very sorry for looking so dumb. I have created some stuff with extend script a while back and that went a lot smoother. I dont know, I dont seem to get warm with UXP…

To be honest I tried a bunch of different versions with forward and backward slashes and none worked. The one you saw in the last post was just my latest attempt at the time.

I changed my “importGradient” function, still I do not get any error messages:

async function importGradient() {
    let path = 'C:\\STAPELVERARBEITUNG\\Hintergrund_Artikel_Verlauf2000x2000MitteHeller.tif';
    let entry = await fs.getEntryWithUrl(`file:/${path}`);  // TESTING -> DIRECT_PATH
    console.log("entry", entry)

    let token = fs.createSessionToken(entry);
    console.log("token", token)
    let result = await require('photoshop').action.batchPlay([{
        _obj: "placeEvent",
        "target": {
            _path: token, // Rather than a system path, this expects a session token
            _kind: "local",
        },
     }
     ], {});
}

I am really sorry, I feel like a baby needing to be held by its hand and it annoys me too…

That’s okay, it also took me 3-4 days to get it working in my case. Also there is lack of documentation/examples.

Anyway,
Have you tried using different image formats like .png or .jpeg instead of .tif

Maybe there is permission for file type/file extension.

Hi, I added a new feature into Alchemist to automatically convert _path into tokens in the generated source code. Right now it is in version 2.7.0

Here: GitHub - jardicc/alchemist: DevTool for plugin developers

2 Likes

Thank you so much for adding the automatic conversion just for me!
But it doesnt seem to work:

Also, I tried to change the line
_path: token,
in the batch play, directly with:
_path: await tokenify(“C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.tif”),
which didnt help either.

Using a JPG didnt help, too… I am not so advanced with all of this async await stuff, maybe having the batchPlay inside of an async function (which is called inside of a “await core.executeAsModal(() => {}” by an async function itself) doesnt work?

I think you should use “code” tab instead of “content” tab.

Content shows what was recorded.
Code shows how it should be done to repeat it.

Hi, I’m trying to get the same thing, but I’ve got a problem.
After retrieving the code from Alchemist to import an image as a layer, I test it in the console and I get this error, but I don’t understand why, because I didn’t change anything in the retrieved code…

Thanks in advance for your help !

Sounds like you’re using manifest v4, upgrade to 5 and see it that resolves your issue

That’s right, my bad… Thanks you !

2 Likes

People, I am so sorry… For the life of me, I can not get this to work! Do you know that feeling when your gut just quenches together because you are biting into a subject and it just wont work out?
All of this modal stuff is tripping me up and I look really stupid here.

I have downloaded the vanilla Javascript UXP sample from github.
I changed the manifest to include:

“requiredPermissions”: {
“localFileSystem”: “fullAccess”
},
“host”: [
{
“app”: “PS”,
“minVersion”: “24.6.0”,
“data”: {
“apiVersion”: 2
}
}
],
“manifestVersion”: 5,

Then I copied the following from Alchemist’s “Code” tab into the index.js:

// Please make sure that file system access permission in manifest.json has correct value.

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const {localFileSystem: fs} = require("uxp").storage;

async function tokenify(url){
   return fs.createSessionToken(await fs.getEntryWithUrl("file:" + url));
}

async function actionCommands() {
   const result = await batchPlay(
      [
         {
            _obj: "placeEvent",
            ID: 5,
            null: {
               _path: await tokenify("C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.jpg"),
               _kind: "local"
            },
            freeTransformCenterState: {
               _enum: "quadCenterState",
               _value: "QCSAverage"
            },
            offset: {
               _obj: "offset",
               horizontal: {
                  _unit: "pixelsUnit",
                  _value: 0
               },
               vertical: {
                  _unit: "pixelsUnit",
                  _value: 0
               }
            },
            replaceLayer: {
               _obj: "placeEvent",
               to: {
                  _ref: "layer",
                  _id: 5
               }
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}

async function runModalFunction() {
   await executeAsModal(actionCommands, {"commandName": "Action Commands"});
}

await runModalFunction();

And when I run it I get this:

I dont know what to say… Am I really that dumb? This is driving me crazy, I wanted to create a quick script for work in July, now its end of September… My boss is getting a bit impatient (also, this is just a small bit of what I need to do, I am very afraid of the rest and then putting together all my little test scripts into one Oo)…
Do I have a fundamental conceptual problem here? Is there something specific I should internalize before proceeding? I dont even know what I dont know… Thats whats troubling me. Most of the time when I got a problem I know what to google and I do not understand the solution, but can work on understanding it. With UXP I feel completely lost…

One side idea: I am using a workplay computer. I am not an Admin on this machine. Maybe this hinders the import?

I think your problem is that you are using await on calling await runModalFunction();
This is not possible as it is on top level (not wrapped inside an async function).

Change it simply to (without await): runModalFunction();

1 Like

I understand you. People are doing their job when they get an idea or are asked to create a new plugin which seems easy at the beginning but since it is a completely new experience there might be a steep learning curve and a lot of traps to learn which is kind of frustrating. Also knowing that new skills might not have a use in the future might be daunting as well.

That is the reason why I am showing an advertisement: “Break the curse! Hire a Professional to turn your code into gold.” right in the Alchemist. If people trying to make a plugin are not feeling comfortable in their situation… they can hire me and get back to work they enjoy. :slight_smile:

1 Like

Thanks tomzag, after Aurore had no issue with the extra await, I thought I made a mistake. Your suggestion was correct, logical, and made it work!

I now have a working script that imports a layer and then resizes it to the canvas size:

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const {localFileSystem: fs} = require("uxp").storage;

const app = require('photoshop').app;
const currentDocument = app.activeDocument;

let docWidth = currentDocument.width;
let docHeight = currentDocument.height;

const gradientSize = 2000;

const scaleX = (docWidth / gradientSize) * 100;
const scaleY = (docHeight / gradientSize) * 100;

async function tokenify(url){
   return fs.createSessionToken(await fs.getEntryWithUrl("file:" + url));
}

async function importLayer() {
   const result = await batchPlay(
      [
         {
            _obj: "placeEvent",
            ID: 5,
            null: {
               _path: await tokenify("C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.jpg"),
               _kind: "local"
            },
            freeTransformCenterState: {
               _enum: "quadCenterState",
               _value: "QCSAverage"
            },
            offset: {
               _obj: "offset",
               horizontal: {
                  _unit: "pixelsUnit",
                  _value: 0
               },
               vertical: {
                  _unit: "pixelsUnit",
                  _value: 0
               }
            },
            replaceLayer: {
               _obj: "placeEvent",
               to: {
                  _ref: "layer",
                  _id: 5
               }
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}

async function resizeLayer() {
   const result = await batchPlay(
      [
         {
            _obj: "transform",
            freeTransformCenterState: {
               _enum: "quadCenterState",
               _value: "QCSAverage"
            },
            offset: {
               _obj: "offset",
               horizontal: {
                  _unit: "pixelsUnit",
                  _value: 0
               },
               vertical: {
                  _unit: "pixelsUnit",
                  _value: 0
               }
            },
            width: {
               _unit: "percentUnit",
               _value: scaleX
            },
            height: {
               _unit: "percentUnit",
               _value: scaleY
            },
            replaceLayer: {
               _obj: "transform",
               from: {
                  _ref: "layer",
                  _id: 5
               },
               to: {
                  _ref: "layer",
                  _id: 5
               }
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}


async function runModalFunction() {
   await executeAsModal(importLayer, {"commandName": "importLayer"});
   await executeAsModal(resizeLayer, {"commandName": "resizeLayer"});
}

runModalFunction();

I do know there are a few things to reorganize once I implement this snippet into my bigger script, but I am happy it runs for now.
May I ask two last questions to properly understand what I am doing?
1.) In both batchplay functions, there is a “replaceLayer” section with a “ref” and “id”.
While importing the layer, what happens if another layer already has the id 5? It would be replaced, no? Shouldnt it find the first “unused” ID instead?

2.) Couldnt using the hardcoded id of 5 in the resizeLayer function potentially conflict if the above was fixed? Like, if another layer had the ID of 5, it would rescale the wrong layer or does it work on the selection?

Reading the documentation about batchplay, it says

  • The target for the command. This describes the (DOM) element that the command should operate on. The target is specified via the _target keyword. This property is sometimes omitted. If omitted, the command operates on a default element. The default element is typically the object that is active in the UI.

So what does the “replaceLayer” section in both batchPlay functions refer to?

edit: ChatGPT says I can delete it and then its using the active layer automatically. Is that true?

Here is an updated version of my script. Any advice on how to make it cleaner?

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const {localFileSystem: fs} = require("uxp").storage;

const app = require('photoshop').app;


async function tokenify(url){
   return fs.createSessionToken(await fs.getEntryWithUrl("file:" + url));
}

async function importLayer() {
   const result = await batchPlay(
      [
         {
            _obj: "placeEvent",
            null: {
               _path: await tokenify("C:/STAPELVERARBEITUNG/Hintergrund_Artikel_Verlauf2000x2000MitteHeller.jpg"),
               _kind: "local"
            },
            freeTransformCenterState: {
               _enum: "quadCenterState",
               _value: "QCSAverage"
            },
            offset: {
               _obj: "offset",
               horizontal: {
                  _unit: "pixelsUnit",
                  _value: 0
               },
               vertical: {
                  _unit: "pixelsUnit",
                  _value: 0
               }
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}

async function resizeLayer(scaleX, scaleY) {
   const result = await batchPlay(
      [
         {
            _obj: "transform",
            freeTransformCenterState: {
               _enum: "quadCenterState",
               _value: "QCSAverage"
            },
            offset: {
               _obj: "offset",
               horizontal: {
                  _unit: "pixelsUnit",
                  _value: 0
               },
               vertical: {
                  _unit: "pixelsUnit",
                  _value: 0
               }
            },
            width: {
               _unit: "percentUnit",
               _value: scaleX
            },
            height: {
               _unit: "percentUnit",
               _value: scaleY
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}

async function sendToBack() {
   const result = await batchPlay(
      [
         {
            _obj: "move",
            _target: [
               {
                  _ref: "layer",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            to: {
               _ref: "layer",
               _enum: "ordinal",
               _value: "back"
            },
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {}
   );
}


async function main() {
    const currentDoc = app.activeDocument;
    const gradientSize = 2000;
    
    await executeAsModal(importLayer, {commandName: "importLayer"});
    
    let scaleX = (currentDoc.width / gradientSize) * 100;
    let scaleY = (currentDoc.height / gradientSize) * 100;

    await executeAsModal(() => resizeLayer(scaleX, scaleY), {commandName: "resizeLayer", args: [scaleX, scaleY]});
    
    await executeAsModal(sendToBack, {commandName: "sendToBack"});
}

main();
1 Like

Jarda, this token that you inserted in the new version of alchemist is excellent,
a question, if I want to recall a file in the uxp plugin folder how should I proceed.

thanks for all your work

You should change file in tokenify function into different protocol.

1 Like