Accessing clipboard in Perspective

Bringing this topic to the surface.
I have the same usecase, where I need user to upload a screenshot to a database.
Instead of saving the screenshot, and using File Upload component, to select the saved screenshot.

It will really bump up efficiency if user can do a 'Ctrl+v' on a Text Area, similar to what would you do in Chat interface..

Can you guys remind me, what does it take to do this?

In any supported manner, the requirement is "a custom module".

In hacky unsupported ways, JS injection. @victordcq might have something readily available.

1 Like

What skill set do I need to have to create a custom module?
Any direction where do I start?

Meanwhile, hoping @victordcq has an idea..

I’ll throw this in here:

1 Like

so the intention is to use javascript, to extract images bytes from clipboard via 'Ctrl+V' (or a button click) and pass the bytes to perspective?

Exactly.

Here's a simple example, reading the clipboard as text.

# Get Clipboard Button
def runAction(self, event):
	
	def withClipboard(clipboard):
		self.view.custom.clipboard = clipboard
		
	
	system.perspective.runJavaScriptAsync('''async () => {
		return await navigator.clipboard.readText()
	}''', {}, withClipboard)

View JSON
{
  "custom": {
    "clipboard": ""
  },
  "params": {},
  "propConfig": {
    "custom.clipboard": {
      "persistent": true
    }
  },
  "props": {},
  "root": {
    "children": [
      {
        "events": {
          "dom": {
            "onClick": {
              "config": {
                "script": "\t\n\tdef withClipboard(clipboard):\n\t\tself.view.custom.clipboard \u003d clipboard\n\t\t\n\t\n\tsystem.perspective.runJavaScriptAsync(\u0027\u0027\u0027async () \u003d\u003e {\n\t\treturn await navigator.clipboard.readText()\n\t}\u0027\u0027\u0027, {}, withClipboard)"
              },
              "scope": "G",
              "type": "script"
            }
          }
        },
        "meta": {
          "name": "Button"
        },
        "position": {
          "height": 34,
          "width": 193,
          "x": 499,
          "y": 167
        },
        "props": {
          "text": "Get Clipboard"
        },
        "type": "ia.input.button"
      },
      {
        "children": [
          {
            "meta": {
              "name": "Label"
            },
            "props": {
              "text": "Clipboard Contents"
            },
            "type": "ia.display.label"
          },
          {
            "meta": {
              "name": "Markdown"
            },
            "position": {
              "grow": 1
            },
            "propConfig": {
              "props.source": {
                "binding": {
                  "config": {
                    "path": "view.custom.clipboard"
                  },
                  "type": "property"
                }
              }
            },
            "type": "ia.display.markdown"
          }
        ],
        "meta": {
          "name": "FlexContainer"
        },
        "position": {
          "height": 200,
          "width": 200,
          "x": 499,
          "y": 211
        },
        "props": {
          "direction": "column"
        },
        "type": "ia.container.flex"
      },
      {
        "children": [
          {
            "meta": {
              "name": "Label"
            },
            "props": {
              "text": "This"
            },
            "type": "ia.display.label"
          },
          {
            "meta": {
              "name": "Label_0"
            },
            "props": {
              "text": "That"
            },
            "type": "ia.display.label"
          },
          {
            "meta": {
              "name": "Label_1"
            },
            "props": {
              "text": "Those"
            },
            "type": "ia.display.label"
          },
          {
            "meta": {
              "name": "Label_2"
            },
            "props": {
              "text": "Other"
            },
            "type": "ia.display.label"
          }
        ],
        "meta": {
          "name": "FlexContainer_0"
        },
        "position": {
          "height": 200,
          "width": 200,
          "x": 200,
          "y": 211
        },
        "props": {
          "direction": "column",
          "style": {
            "gap": "1rem"
          }
        },
        "type": "ia.container.flex"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}
5 Likes

Thanks..
I installed the periscope module..

And ran into this error in gateway log:
Exception occurred executing client-side JavaScript.

in Firefox Console:
store.Channel: Uh oh, received message for unknown protocol "periscope-js-run"

What did I missed?

Is it possible your client session is out of date (i.e. the session was created before the module was installed)?

If so, try closing and reopening your browser.

I am getting this error:

CompletionException: com.mussonindustrial.embr.perspective.common.exceptions.JavaScriptExecutionException: Error running client-side JavaScript.
	caused by JavaScriptExecutionException: Error running client-side JavaScript.
	caused by JavaScriptException: TypeError: Cannot read properties of undefined (reading 'readText')
	caused by JavaScriptException: TypeError: Cannot read properties of undefined (reading 'readText')
    at AsyncFunction.eval [as runNamed] (eval at ks (http://10.10.10.100:8088/res/embr-periscope/embr-periscope-client.js:1:1071), <anonymous>:4:36)
    at http://10.10.10.100:8088/res/embr-periscope/embr-periscope-client.js:31:6434
    at new Promise (<anonymous>)
    at http://10.10.10.100:8088/res/embr-periscope/embr-periscope-client.js:31:6387
    at z.processMessage (http://10.10.10.100:8088/res/perspective/js/PerspectiveClient.4c66478bafda3caeebe1.js:2:473641)

Ignition v8.1.41 (b2024052809)
Java: Azul Systems, Inc. 17.0.10

I notice that the browser does not prompt to allow clipboard access, could this be the issue?

According to MDN, the navigator object is only usable in secure contexts (over HTTPS). Edit: localhost also counts as a secure context.

Also, the first time I ran the script I got a pop-up from Chrome asking to allow clipboard access. I’d assume it’d be the same for Firefox, but I can’t say for sure.

Same error displayed by both Firefox and Chrome,
store.Channel: Uh oh, received message for unknown protocol "periscope-js-run

Seems to me that, browser does not recognize the instruction coming from periscope module..? Why is this?

  1. What version of Ignition, and what version of Periscope?
  2. Anything in the gateway logs when you restart the module?
  3. What happens if you run window.__client.connection.handlers.get("periscope-js-run") in Firefox/Chrome's console window?

Also, can you show your system.perspective.runJavaScriptAsync/Blocking code? Where are you calling it from?

Ignition Version 8.1.37
Periscope Module 0.5.1

There is no window.__client

The only error in gateway is this:

java.util.concurrent.CompletionException: java.util.concurrent.TimeoutException
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture.postComplete(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(Unknown Source)
at java.base/java.util.concurrent.CompletableFuture$Timeout.run(Unknown Source)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.util.concurrent.TimeoutException: null
... 7 common frames omitted

Which relates to client timing out, because no response was received.

I exactly use the "view's" json you posted.

My bad, this command exist in chrome:
image

It looks like you ran window.__client.connection.handlers.get("periscope-js-run") on a non-Perspective tab, correct?

Either way, I can reproduce the issue on 8.1.37.

It looks like you must have at least one of Periscope's components on a view for the periscope-js-run handler to be installed. It doesn't have to be on every view, just a view.

So, to work around this for now, create a new view in your project and add an Embedded View +, Flex Repeater +, or Swiper component. After saving, the JavaScript page handlers should now install themselves when the client starts.

Worked now as per your video..
I added a component from the module.
Thank you for helping me troubleshoot..

Now.. The real thing.
Do you have direction, how to handle images on the clipboard..?
How do I get the byte from navigator.clipboard.read()

I saw this sample code from navigator clipboard doc online:

const destinationImage = document.querySelector("#destination");
destinationImage.addEventListener("click", pasteImage);

async function pasteImage() {
  try {
    const clipboardContents = await navigator.clipboard.read();
    for (const item of clipboardContents) {
      if (!item.types.includes("image/png")) {
        throw new Error("Clipboard does not contain PNG image data.");
      }
      const blob = await item.getType("image/png");
      destinationImage.src = URL.createObjectURL(blob);
    }
  } catch (error) {
    log(error.message);
  }
}

This is a good start.
Appreciate if someone can marinate this with Jython. :beer:

Great! I’ll get this fixed in the next release.

Unfortunately I don’t, maybe some else around here can help.

You should be able to return the image bytes from runJavaScriptAsync, but depending on the size of the images it’s probably not a good idea. A better idea would be to POST the image bytes to a WebDev endpoint, then return the response of the POST.

1 Like

I hit a wall.

From google, cannot convert image data from clipboard to byte array.

1 Like