Render layer example

Good day everyone!

I have some wonderful news for anyone interested!

I present to you: Layer display
image

It is nothing special really. Select the layer, export the layer, load the file as ArrayBuffer, convert to uInt8Array and make a url for it.
Though I spent some time searching, in order to find this solution.
Aside from a humble brag ( :pinching_hand: :pinching_hand: :pinching_hand: brag), I would like to share for posterity.
Any improvements will be greatly appreciated.

The Export as PNG function

export const exportLayerAsPngAndLoad = async (layer: Layer): Promise<string> => {
  const { id, name } = layer;
  await core.executeAsModal(
    async () => {
      const destFolder = (await storage.localFileSystem.getDataFolder()).nativePath;
      await selectLayerByID(id);
      const exportCommand = {
        _obj: 'exportSelectionAsFileTypePressed',
        _target: { _ref: 'layer', _enum: 'ordinal', _value: 'targetEnum' },
        fileType: 'png',
        quality: 32,
        metadata: 0,
        destFolder,
        sRGB: true,
        openWindow: false,
        _options: { dialogOptions: 'dontDisplay' },
      };
      await action.batchPlay([exportCommand], { synchronousExecution: true, modalBehavior: 'execute' });
    },
    { commandName: 'Export Layer As PNG' }
  );
 try {
   return loadImageFromDataFolder(name);
 } catch (e) {
   console.error(`Error loading image: ${e}`);
}
  return null;
};

Load Image function

export const loadImageFromDataFolder = async (imageName: string): Promise<string> => {
  const dataFolder = await storage.localFileSystem.getDataFolder();

  const name = imageName.concat('.png');
  const entriesArray = Array.from(await dataFolder.getEntries());
  const image = entriesArray.find((entry) => entry.name === name);

  if (!image) throw new Error(`Image ${imageName}.png Not Found`);
  if (!image.isFile) throw new Error(`${imageName}.png is not a file`);

  const file = image as storage.File;
  const data = (await file.read({ format: storage.formats.binary })) as ArrayBuffer;
  if (data.byteLength === 0) throw new Error(`${imageName}.png is empty`);
  const uInt8Array = new Uint8Array(data);
  const blob = new Blob([uInt8Array]);
  const url = URL.createObjectURL(blob);

  await image.delete();

  return url;
};

Select Layer By ID

export const selectLayerByID = async (layerID: number) => {
  const actionObject = [
    {
      _obj: 'select',
      _target: [
        {
          _ref: 'layer',
          _id: layerID,
        },
      ],
      makeVisible: false,
      layerID: [layerID],
      _isCommand: false,
    },
  ];
  await app.batchPlay(actionObject, { modalBehavior: 'execute' });
};

React Image Display

import React, { FC } from 'react';
import { ImageDisplayStyle, ImageDisplayContainer } from './styled';
import { Layer } from 'Photoshop'; // Credit to Hans Otto Wirtz for the Typescript library

interface Props {
  exportLayer: Layer;
}

export const ImageDisplay: FC<Props> = (props: Props) => {
  const { exportLayer } = props;
  const [loading, setLoading] = useState(false);
  const [imageUrl, setImageUrl] = useState(null);

 useEffect(() => {
    if(exportLayer) {
       setLoading(true);
       const url = await exportLayerAsPngAndLoad(exportLayer);
       setLoading(false);
       setImageUrl(url);
    }
 }, [exportLayer])

  return (
    <ImageDisplayStyle>
      {(imageUrl && (
        <ImageDisplayContainer>
          <img className="background" src="/images/BackGround.png" alt="Background" />
          {(!loading && <img className="image" src={imageUrl} alt="BaseLayer" />) || (
            <img className="loading" src="LOADING.gif" alt="loading" />
          )}
        </ImageDisplayContainer>
      )) || <ImageDisplayContainer>Select a Choice</ImageDisplayContainer>}
    </ImageDisplayStyle>
  );
};

export default ImageDisplay;
3 Likes

There is also a way to do that without saving the layer as a file in the first place. But that API is not documented and could change at any time. So I am not sure whether it would be good for production use.
And with Canvas support, we could even edit image before being shown in panel.

3 Likes

@pkrishna another user who needs Canvas :eyes:

1 Like

Could you please point me in the right direction for this implementation?

Thank you :blush:

JB, do you mean the API for the scripted trees etc.? :slight_smile:

1 Like

I am talking about require("photoshop").exportmanager.getPreviewAsync and his “brother” exportImageAsync

3 Likes

Thank you, I will require some information regarding the usage, variables and so on :sweat_smile:

Yes, that is the thing. Everybody here wants that feature. And it is working in Photoshop for at least year. There were a few documentation updates but nothing about this feature. And it could be intentional and if so then I don’t know why. And that raises the question of whether we should use it or not. @kerrishotts @Barkin

exportmanager should be considered private for now. (It’s there to support “Export As”).

2 Likes

“Export As”… isn’t it supported by batchplay these days? :smiley:

Interesting… so any way to do the reverse and import modified data back into the layer? So it could be used to get/set pixel data?

Just found out that there are some bugs in my code.
It is now saved in a temporary folder that gets deleted after load.

import { action, core, Layer } from 'photoshop';
import { storage } from 'uxp';
import { selectLayerByID } from '../layer/selectLayer';
import loadImageFromFolder from './loadImage';

export const exportLayerAsPngAndLoad = async (layer: Layer): Promise<string> => {
  const { id, name } = layer;
  const tempFolder = await storage.localFileSystem.getTemporaryFolder();
  const destFolder = await tempFolder.createFolder(`${Math.round(Math.random() * 1000000)}`);
  await core.executeAsModal(
    async () => {
      await selectLayerByID(id);
      const exportCommand = {
        _obj: 'exportSelectionAsFileTypePressed',
        _target: { _ref: 'layer', _enum: 'ordinal', _value: 'targetEnum' },
        fileType: 'png',
        quality: 32,
        metadata: 0,
        destFolder: destFolder.nativePath,
        sRGB: true,
        openWindow: false,
        _options: { dialogOptions: 'dontDisplay' },
      };
      await action.batchPlay([exportCommand], { modalBehavior: 'execute' });
    },
    { commandName: `Export ${layer.name} Layer As PNG` }
  );
  try {
    const image = await loadImageFromFolder(destFolder, name, 10);
    return image;
  } catch (e) {
    console.error(`Error loading image: ${e}`);
  }
  await destFolder.delete();
  return '';
};

The load now has multiple loading attempts

import { storage } from 'uxp';

const getImageData = async (image: storage.Entry) => {
  if (!image) return null;
  if (!image.isFile) throw new Error(`${image.name} is not a file`);

  const file = image as storage.File;
  const data = (await file.read({ format: storage.formats.binary })) as ArrayBuffer;
  console.log('bytelength', data.byteLength);
  if (data.byteLength === 0) return null;
  return data;
};

export const loadImageFromFolder = async (
  folder: storage.Folder,
  imageName: string,
  attempts = 100
): Promise<string> => {
  const name = imageName.concat('.png');

  let image: storage.Entry;
  let attemptsLeft = attempts;
  let data: ArrayBuffer | false;

  console.log((await folder.getEntries()).map((entry) => entry.name));

  while ((!image || !data) && attemptsLeft > 0) {
    const entriesArray = await folder.getEntries();
    image = entriesArray.find((entry) => entry.name === name);
    data = await getImageData(image);
    await new Promise((res) => setTimeout(res, 50));
    attemptsLeft -= 1;
  }

  console.warn('loaded file with', attemptsLeft, 'attempts left');

  if (!image) throw new Error(`Image ${imageName}.png not found`);
  if (!data) throw new Error(`No data in image: ${imageName}.png`);

  const uInt8Array = new Uint8Array(data);
  const blob = new Blob([uInt8Array]);
  const url = URL.createObjectURL(blob);

  await image.delete();

  return url;
};

Also Great help from Hans Otto Wirtz

question: What is the maximum value of image quality? I found it only supports 32% picture quality :innocent:

To be brutally honest, I found that function from another post.
I have tried other values to the quality attribute, but it never exported aside from 32.

Ps has many different ways to export content, and all of these have widely varying ranges for quality ranges. 32 may be the maximum range for PNGs using this method, but if you check w/ a manually exported version at best quality, they should hopefully match. (That is, 32 may be equivalent to 100%.)

I thought PNG was always lossless compression and the only difference was higher compression was higher time, but image quality was the same regardless as the images always unpacked the same, pixel for pixel. So I guess having the quality parameter for PNG is confusing to me. Is the value actually a quality setting? Or is it tied to PNG compression and just called “quality”?.. so it is actually comparable to quick / small / medium / large compression?

It is not lossless if you save 32bit PNG into 8bit (color table)

Ah, I see, that make sense now. The quality setting in this case is bit depth. So in that setting you would set that value to either 8 or 32 but nothing else. Quality 32 is actually RGBA 8 bit per channel. Quality 8 would be 8 bit PNG which is actually indexed like a GIF with 256 colors with 1 bit transparency, but not 8 bit per channel.