ImageIO.read fails to read ByteArrayInputStream for image

Hi there,

I am trying to print an image onto a Paintable Canvas component (named “tape_image”) by passing the image bytes (called “imageData”) as a string type custom property on it. Converting the String to a ByteArrayInputStream works, but when calling ImageIO.read on that returns null. Why could this be happening?

Paintable Canvas repaint script:

[code]from javax.imageio import ImageIO
from java.io import ByteArrayInputStream

width = event.source.getWidth()
height = event.source.getHeight()

bais = ByteArrayInputStream(event.source.imageData)
print bais.available()
image = ImageIO.read(bais) #error here, image = null
print image
bais.close()

finalWidth = width
finalHeight = height

imgRatio = float(image.getWidth())/float(image.getHeight())

	#-If Image Ratio exists, determine final width and height to fit canvas-

if (imgRatio > 0):
finalWidth = int(height * imgRatio)
if (finalWidth > width):
finalWidth=width
finalHeight = int(finalWidth / imgRatio)

	#Center the picture on the canvas

y = (height-finalHeight) / 2
x = (width-finalWidth) / 2

g = event.graphics
#-Draw the Image on the canvas-
g.drawImage(image,x,y,finalWidth,finalHeight,event.source)
[/code]

Button that uploads the image data script:

path = system.file.openFile() if path != None: bytes = system.file.readFileAsBytes(path) event.source.parent.getComponent("tape_image").imageData = bytes

print results and error from testing (uploading image of strawberry):

7748437 (result from printing bais.available()) None (result from printing "image") 09:25:21.300 [AWT-EventQueue-2] ERROR com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter - <HTML>Error executing script for event:&nbsp;<code><b>repaint</b></code><BR>on component:&nbsp;<code><b>tape_image</b></code>. com.inductiveautomation.ignition.common.script.JythonExecException: Traceback (most recent call last): File "<event:repaint>", line 18, in <module> AttributeError: 'NoneType' object has no attribute 'getWidth' ... ... ... ...

If you wrap the call to system.file.readFileAsBytes in a try-catch block, you can print the error that’s returned and get some small clue about why it’s failing.

Hi Kathy, thanks for your reply.

I have tried that but the it seems that the call to readFileAsBytes succeeds as the image data is successfully transferred to the custom property I made on the paintable canvas. I also tried surrounding my call to ImageIO.read with a try-catch but no errors were thrown inside (the read simply creates a null image). Is there any way I can get some sort of debug or trace info about the execution of ImageIO.read?

No, there are no loggers to turn on. It pretty much succeeds or fails.

I’m a bit confused, though, because what you described in your reply doesn’t match up with the error you posted in your first message, where you said you got a null value.

Sorry about that, I’ve edited the code to be more clear. The error happens in the first script I posted, on the call ImageIO.read(bais), where bais is my byteArrayInputStream created from the custom string property that is passed the bytes from the call to readFileAsBytes. It seems that readFileAsBytes succeeds, ByteArrayInputStream succeeds (since I am able to print bais.available() which tells me how many more bytes are available in the stream), but ImageIO.read returns null when it should be creating a BufferedImage object.
I posted the additional script just in case my method of passing data (via custom properties) is incorrect.

Is there a size limit on custom string properties? perhaps some trailing data has been cut off?

I’ve checked, and there does seem to be some issue with the data sizes. The size of the image is 5,735,855 bytes. The size of the byte array returned by readFileAsBytes is 5,735,855 bytes, so all seems well until I pass the byte array to the custom property imageData. When I call the length of imageData, the size is 7,748,437 bytes. Really strange…

So as you guessed, the string property has an upper limit (edit: I stand corrected on this, see pturmel’s response below). The way to get around this is to create a custom method which has a parameter that accepts the bytes directly. I have un-creatively called this “redraw”. Put the following code in “redraw” Note, I modified the “g.drawImage” for testing purposes, you’ll have to modify this to fit your scaling needs.

Name: redraw
Parameters: bytes

[code]from java.io import ByteArrayInputStream
from javax.imageio import ImageIO

width   = self.getWidth()
height  = self.getHeight()


bais = ByteArrayInputStream(bytes)
print bais.available()
image = ImageIO.read(bais) #error here, image = null
print image
bais.close()   

finalWidth = width
finalHeight = height
    

imgRatio = float(image.getWidth())/float(image.getHeight())
     
      #-If Image Ratio exists, determine final width and height to fit canvas-
if (imgRatio > 0):
   finalWidth = int(height * imgRatio)
   if (finalWidth > width):
      finalWidth=width
      finalHeight = int(finalWidth / imgRatio)                 
      
      #Center the picture on the canvas
y = (height-finalHeight) / 2
x = (width-finalWidth) / 2
     
g = self.graphics
#-Draw the Image on the canvas-
g.drawImage(image,0,0,100,100,self)
    #store the file you uploaded into the paintable canvas so it won't disappear when it repaints itself
self.putClientProperty("img-bytes-cached",bytes) 

[/code]

Then put the following in the repaint function, again this is just the basic form, make sure to edit it so you get the current height/width and pass that back into the drawImage method or else the image will scale to its full size when it is repainted.

[code]from javax.imageio import ImageIO
from java.io import ByteArrayInputStream

bytes = event.source.getClientProperty(“img-bytes-cached”)
if bytes:
image = ImageIO.read(ByteArrayInputStream(bytes))
event.graphics.drawImage(image,0,0,event.source)[/code]

Then change your button code to:

path = system.file.openFile() if path != None: bytes = system.file.readFileAsBytes(path) event.source.parent.getComponent("tape_image").redraw(bytes)

1 Like

Unlike python2, jython is more like python3 when dealing with byte arrays. Assigning bytes to a string type will “decode” the bytes into 16-bit unicode characters. Fragments of your array are being extended to 16-bits and fragments that make a valid unicode sequence (probably intepreted as utf8) are being expressed directly. Nearly doubling the size is the natural result.
You simply cannot use a String custom property to hold your raw bytes. Technically, you need a property with java type “array of bytes”. The properties customizer doesn’t offer that data type, but you can programmatically create one in the designer. This isn’t documented and if you store anything in a property that doesn’t serialize, you can break your project and the designer. This post and the this follow-up post might help you.
I personally wouldn’t use abishur’s suggestion, as there’s an awful lot of data manipulation going on in repaint events – events that need to be blistering fast and really should not reference anything outside the component.

Very cool to know about the array of bytes.

The method I recommend was taken from the “Working with the Paintable Canvas Component” page in the IA knowledge base.

To my knowledge, it is not referencing anything outside the component itself in the repaint script I listed. event.source.getClientProperty should be getting a property from the canvas itself, right?

Thanks for the suggestions! The decoding explains why my resulting data was larger. I will try out the DynamicPropertyDescriptor stuff and see how that works out for me.

Hmm. I’m going to have to revisit get/putClientProperty(). That usage is handy. I’d be inclined to stick the ImageIO.read() result in the cache, though, not the raw bytes.

I have been playing around and came across an error I don’t know how to fix. The script for my button now looks like this:

[code]from com.inductiveautomation.factorypmi.application.binding import DynamicPropertyDescriptor
from java.nio import ByteBuffer
import java

path = system.file.openFile()
if path != None:
bytes = system.file.readFileAsBytes(path)
props = event.source.parent.getComponent(“tape_image”).getDynamicProps
print props
print “setting the properties”
property = DynamicPropertyDescriptor(“imageData”, “data for image display”, java.nio.ByteBuffer, ByteBuffer.wrap(bytes))
print property
props.put(“imageData”,property)
event.source.parent.getComponent(“tape_image”).setDynamicProps(props)
[/code]

but this results in the following error:

[code]File “event:actionPerformed”, line 13, in
AttributeError: ‘instancemethod’ object has no attribute ‘put’

at org.python.core.Py.AttributeError(Py.java:173)
at org.python.core.PyObject.noAttributeError(PyObject.java:930)
at org.python.core.PyObject.__getattr__(PyObject.java:925)
at org.python.pycode._pyx94.f$0(<event:actionPerformed>:14)
at org.python.pycode._pyx94.call_function(<event:actionPerformed>)
at org.python.core.PyTableCode.call(PyTableCode.java:165)
at org.python.core.PyCode.call(PyCode.java:18)
at org.python.core.Py.runCode(Py.java:1275)
at com.inductiveautomation.ignition.common.script.ScriptManager.runCode(ScriptManager.java:647)
at com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter.runActions(ActionAdapter.java:183)
at com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter.invoke(ActionAdapter.java:284)
at com.inductiveautomation.factorypmi.application.binding.action.RelayInvocationHandler.invoke(RelayInvocationHandler.java:55)
at com.sun.proxy.$Proxy30.actionPerformed(Unknown Source)
at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.setPressed(Unknown Source)
at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)
at java.awt.Component.processMouseEvent(Unknown Source)
at javax.swing.JComponent.processMouseEvent(Unknown Source)
at java.awt.Component.processEvent(Unknown Source)
at java.awt.Container.processEvent(Unknown Source)
at java.awt.Component.dispatchEventImpl(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Window.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
at java.awt.EventQueue.access$500(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.awt.EventQueue$3.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.awt.EventQueue$4.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
at java.awt.EventQueue.dispatchEvent(Unknown Source)
at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.run(Unknown Source)

Caused by: org.python.core.PyException: Traceback (most recent call last):
File “event:actionPerformed”, line 13, in
AttributeError: ‘instancemethod’ object has no attribute ‘put’

[/code]

but the treemap returned by the call getDynamicProps does have the type <java.lang.String,DynamicPropertyDescriptor> so my “put” should work, shouldn’t it?

getDynamicProps is a method. You run it by including parenthesis on it like a function. Like so:props = event.source.parent.getComponent("tape_image").getDynamicProps()Also, you probably don’t want to use java.nio.ByteBuffer as your property type. Just use bytes.getClass() to use the byte array’s type.

One more thing: creating a dynamic property descriptor should only be done once, in the designer, to add the property to your object. Don’t keep creating and adding the property in your client code. Once you’ve created the property with the type you want, just read it and assign to it like any other custom property.

okay, thanks for all the help :slight_smile: