Create a new pixel layer with data over the selection

Hi!
I’m trying to generate a XMLHttpRequest to select a region of a RGB image (background layer, locked) to a Flask server that processes it and returns a mask back to the client.
The client then creates a new pixel layer (opacity: 50%, fill: 100%) and puts the returned mask over the selected region.

The input image is this:
input_image

and has the following characteristics:

  W: 192 H: 263, Type: image/uncompressed
  Colour space: RGB, profile: , has alpha: false
  Pixel format: RGB
  Components per pixel: 3, component size (bits): 8
  Is chunky: false

The returned mask appears like this when saved to a file:
mask

and it is sent back to the client side as a Base64 string appended to the data:image/png;base64, tag in JSON format:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAAEHAQAAAADQPXQSAAACD0lEQVR4nO3YMY4UMRCF4TcGCZA22JCQABFzBI6yEnfYdH0QAo7iayASH8GhA2MTdPdqZ1x/9xQiGm2H88n1XE89SetpmE8PgucGIBN8BRg0qhM0960qQSHI7oxEEL2jBkEnaHgrgkpQCDJBIojeWw0CPNHXTSZoO6Pem1Dd4YUg060SnYje8EHQRQvSiSa47tbIBIUAMzJBIojejEHQ8VYEjaAS4KhCkAkSQfSGD4JO0AgwoxIU96hMkAiiN2MQdIJGUAkwvBBkguTOiAAvF79uVCdoBJUAM4r7RCZIBBHgrJGrwjtBI8BRlaC4R/10n4gA541cM6oTNIJKgBmFILtHJYLoHTUIOkEjwIxKUNyjMkFyj4oAl408w2UjxxmXVT3DZSPHowpBJkgE0Rs+NbLB1MjhqKmRDaZGNijujEyQCKI3Y25khbmRFebFV5gXX6EQZALjOYBEEL2jjEYWMBpZwGjkIMOoaoFCkN0ZCIkgnv/WNrhs5M/pdK8gs5FK4U1BdlUKshvZ36MQZPeoRBC9o6yXRwr2y7M/yqxKAapSgEYUoBEFWHz3VhHAXnxvlN2IAiyuAIsrwOJ74dl9IrlP+OCtgr5Z8FFBnyx4kJ6W2j/8Hr/GGEmS3q0fM4ckPX7WF62f+dYUeEf/2x6v8Aq3Bb4vd5J0b8CdJD1Iy3/wxfNmnL7/kAH/8u311uAvGVaxWXY9M+cAAAAASUVORK5CYII=

The problem is that if I try to put the generated data as a RGB image in the pixel layer, it will be displayed in the “wrong” area.
For example, if I select the whole image in Photoshop, this is the final result (with the background layer invisible):

mask_layer

As you can see, the number 4 is not centered and it appears like this when the background layer visible:
mask_layer_as_overlay

If instead I put the data as a grayscale image, the generated layer is blank:
grayscale_mask
grayscale_mask_as_overlay

How can I create the new layer and put the received JSON data on it, so it overlaps the number 4 from the background layer?

Thanks!


The client code is as follows:

const photoshop = require("photoshop");
const app = photoshop.app;
const core = photoshop.core;
const imaging = photoshop.imaging;

async function execQuery()
{
  const doc = app.activeDocument;
  const docSize = { "height": doc.height, "width": doc.width };

  let selection = null;
  let selectionPixels = null;
  let jpegData = "";

  await core.executeAsModal(
    async () =>
    {
      selection = await imaging.getSelection({});

      if (selection.imageData === undefined)
      {
        throw "There is no selection.";
      }

      selectionPixels = await imaging.getPixels(
        {
          "sourceBounds": selection.sourceBounds,
          "colorSpace": "RGB",
          "targetSize": {
            "height": selection.sourceBounds.bottom - selection.sourceBounds.top,
            "width": selection.sourceBounds.right - selection.sourceBounds.left
          }
        }
      );

      let docPixels = await imaging.getPixels({
        "documentID": doc.id,
        "targetSize": docSize,
        "componentSize": selectionPixels.imageData.componentSize,
        "applyAlpha": selectionPixels.imageData.hasAlpha,
        "colorProfile": "sRGB IEC61966-2.1"
      });
      jpegData = await imaging.encodeImageData({ "imageData": docPixels.imageData, "base64": true });
    }
  );

  function connection(imageUrl, imageEncoded, options)
  {
    const url = "http://127.0.0.1:5000";
    return new Promise((resolve, reject) =>
    {
      const req = new XMLHttpRequest();
      req.onload = () =>
      {
        if (req.status === 200)
        {
          try
          {
            const json = JSON.parse(req.response);
            addOverlay(json.masks, options);
          }
          catch (err)
          {
            reject(`Couldnt parse response. ${err.message}, ${req.response}`);
          }
        }
        else
        {
          reject(`Request had an error: ${req.status}`);
        }
      };

      // Generate request
      req.onerror = reject;
      req.onabort = reject;
      req.open("POST", url);
      req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
      req.send(JSON.stringify({
        "imageEncoded": imageEncoded,
        "bounds": options.bounds
      }));
    });
  }

  const options = {
    "bounds": selection.sourceBounds,
    "components": selectionPixels.imageData.components,
    "componentSize": selectionPixels.imageData.componentSize,
    "height": selection.sourceBounds.bottom - selection.sourceBounds.top,
    "width": selection.sourceBounds.right - selection.sourceBounds.left
  };

  await connection(doc.name, jpegData, options);
}

async function addOverlay(data, options)
{
    await window.require('photoshop').core.executeAsModal(
      async () =>
      {
        let buffer = null

        switch (options.componentSize)
        {
          case 16:
            buffer = Uint16Array.from(data);
            break;
          case 32:
            buffer = Uint32Array.from(data);
            break;
          default:
            buffer = Uint8Array.from(data);
            break;
        }

        /* AS GRAYSCALE */
        // let image = await imaging.createImageDataFromBuffer(buffer, {
        //   "width": options.width,
        //   "height": options.height,
        //   "components": 1,
        //   "chunky": false,
        //   "colorProfile": "Gray Gamma 2.2",
        //   "colorSpace": "Grayscale"
        // });
        /**/

        /* AS RGB */
        let image = await imaging.createImageDataFromBuffer(buffer, {
          "width": options.width,
          "height": options.height,
          "components": options.components,
          "chunky": false,
          "colorProfile": "sRGB IEC61966-2.1",
          "colorSpace": "RGB"
        });
        /**/

        let doc = app.activeDocument;
        let newLayer = await doc.createPixelLayer({
          "name": "Overlay",
          "opacity": 50
        });

        await imaging.putPixels({
          "layerID": newLayer.id,
          "imageData": image,
          "replace": true,
          "targetBounds": { "left": options.bounds.left, "top": options.bounds.top },
          "commandName": "Add overlay layer"
        });

        image.dispose();
      }
    );
}

In the course of debugging, have you double-checked what values are passed in options.bounds in your call to putPixels? Also what’s the number of components in your mask returned? Seems like you expect it to be the same as your original input image, i.e. 3, in your call to createImageDataFromBuffer.

Hi!
The bounds are the same as the selection (left: 0, top: 0, height: 23, width: 192), which in this case is the whole image, as expected.

Regarding the mask, I’ve recovered some info from the server.
It has a size of 192x163 and has a single channel of 1-bit pixels, black and white, stored with one pixel per byte.

I tried to generate the image data for Photoshop as follows:

let image = await imaging.createImageDataFromBuffer(buffer, {
        "width": options.width,       // 192
        "height": options.height,    // 263
        "components": 1,
        "chunky": false,
        "colorProfile": "Gray Gamma 2.2",
        "colorSpace": "Grayscale"
        });

but the result seems to be a black layer:

  1. Overlay at 50% opacity

  2. Overlay at 100% opacity with the background invisible:

This might be the case since you’re dropping grayscale data into an RGB document, not sure if Photoshop is happy about that. But it would definitely be wrong to attempt passing a 1d buffer with options.components being set to 3 in createImageDataFromBuffer, like in your original code, I’m surprised that didn’t crash Photoshop. For debugging purposes it might be worth trying the grayscale case first, i.e. starting out in a grayscale document or try dropping your mask in a grayscale document and see if that works?

I think you need to convert 1-bit into 8-bit in advance. Imaging API does not support 1-bit.