You upload an image, the Paintable Canvas reports 7748437 bytes available, and ImageIO.read returns None. The repaint script then fails with AttributeError: 'NoneType' object has no attribute 'getWidth'. Start here: stop routing binary image data through a String custom property, pass the byte array directly to a custom method, and reject a null decode before calculating dimensions.
Read the failure in the right order
The first failure is the null result from ImageIO.read. The later getWidth exception is only a consequence of using that null reference.
| Observed symptom | What it means | First check |
|---|---|---|
bais.available() prints 7748437
|
The stream contains that many unread units at that moment. It does not prove that they are the original file bytes or that an installed image reader recognizes them. | Compare the data type and byte count before and after the component-property boundary. |
ImageIO.read(bais) returns None
|
No registered image reader decoded the supplied stream as a supported image. | Decode the byte array immediately after system.file.readFileAsBytes, before assigning it to a String property. |
image.getWidth() raises a NoneType error |
The script continued after decoding failed. | Add an explicit null check before every image operation. |
'instancemethod' object has no attribute 'put' |
getDynamicProps was referenced as a method object instead of being called. |
Use getDynamicProps() if diagnosing that separate script. |
| The image appears once but disappears after covering, resizing, or repainting the window | Drawing obtained outside the repaint event is transient. | Retain decodable image state or trigger the same draw operation whenever the component repaints. |
Do not start by changing the aspect-ratio calculation. That code runs only after decoding succeeds. Canvas dimensions also cannot make ImageIO.read return null.
Trace the bytes across the property boundary
system.file.readFileAsBytes(path) produces binary data. A String custom property represents character data. Assigning the byte array to that property introduces a conversion boundary where the original byte values can be replaced by a textual representation or transformed through character encoding.
ByteArrayInputStream accepting the resulting value proves only that Jython found a usable conversion or constructor path. It does not certify a lossless round trip. Likewise, available() reports quantity, not validity. A large stream can still contain altered, truncated, unsupported, or non-image data.
ImageIO.read inspects the stream and selects an installed decoder. If no decoder recognizes the content, it returns null. Treat null as a normal decode failure that needs its own branch, not as a usable image reference.
The decisive isolation test is simple: call ImageIO.read(ByteArrayInputStream(imageBytes)) immediately after readFileAsBytes. If that succeeds but decoding after the String-property assignment fails, the property conversion is the fault. If the immediate decode also returns null, test a known-good image format and inspect the selected file rather than changing the canvas.
Move binary data without converting it to text
Create a custom method on tape_image named redraw with one parameter named bytes. A method parameter can accept the byte array directly, so the upload script does not need a String property or manual DynamicPropertyDescriptor construction.
- Open the file from the button event.
- Read it with
system.file.readFileAsBytes. - Get the
tape_imagecomponent. - Call its
redrawmethod with the byte array. - Decode inside that method before performing any geometry calculation.
path = system.file.openFile()
if path is not None:
imageBytes = system.file.readFileAsBytes(path)
canvas = event.source.parent.getComponent('tape_image')
canvas.redraw(imageBytes)
This removes the String conversion from the path:
file -> byte array -> redraw(bytes) -> ByteArrayInputStream -> ImageIO.read
Do not encode the bytes as text merely to fit the existing property unless the complete encode/decode pair is deliberately designed and tested. It adds memory use and another failure point. Direct byte transfer is the shorter diagnostic and execution path.
Decode before drawing
Put the decode guard at the top of redraw. Close the stream in a finally block, and return immediately when the decoder produces null.
from java.io import ByteArrayInputStream
from javax.imageio import ImageIO
bais = ByteArrayInputStream(bytes)
try:
image = ImageIO.read(bais)
finally:
bais.close()
if image is None:
print 'Image decode failed: unsupported or invalid image data'
return
width = self.getWidth()
height = self.getHeight()
if width <= 0 or height <= 0:
return
imageWidth = image.getWidth()
imageHeight = image.getHeight()
if imageWidth <= 0 or imageHeight <= 0:
return
scale = min(float(width) / imageWidth,
float(height) / imageHeight)
finalWidth = int(imageWidth * scale)
finalHeight = int(imageHeight * scale)
x = (width - finalWidth) / 2
y = (height - finalHeight) / 2
g = self.getGraphics()
if g is None:
return
try:
g.drawImage(image, x, y, finalWidth, finalHeight, self)
finally:
g.dispose()
The scaling calculation performs an aspect-fit operation. It selects the smaller of the horizontal and vertical scale factors, so neither final dimension exceeds the canvas. Subtracting the final dimensions from the component dimensions centers the image.
The original ratio calculation is not the reason for the null image, but it must remain below the null check. Any call to getWidth, getHeight, or drawImage before that guard turns a useful decode failure into a secondary exception.
Verify the repair at each boundary
Run the checks in this order. Stop as soon as one boundary changes the result.
- Confirm file selection. Cancel the dialog and verify that no read or redraw call runs. Select a known-good image and continue.
-
Decode at the button. Temporarily decode the value returned directly by
system.file.readFileAsBytes. A successful result proves that the selected file and the localImageIOreader can work together. -
Compare byte counts. Print the byte-array length at the button and print
bais.available()at the start ofredraw. Matching counts detect truncation, although they do not prove byte-for-byte equality. -
Check the decode result. Print whether
imageis null before reading its dimensions. Do not rely on the later exception as the decode indicator. - Check geometry. Print the canvas size, source-image size, and calculated final size after decoding succeeds. Each dimension must be positive.
- Exercise repaint conditions. Resize, cover, uncover, minimize, and restore the window. A persistent display must survive these events.
Repeat the test with the original strawberry image only after a known-good image decodes through the complete method path. The reported 7748437 value is useful as a boundary measurement, not as proof that the image is too large or intact.
Respect the repaint lifecycle
A Paintable Canvas normally draws from its repaint event because the user interface can repaint at any time. Calling getGraphics() in redraw is a direct way to prove that byte transfer and decoding work, but that drawing can be erased by the next repaint.
For persistent display, keep the decoded image or lossless binary payload in storage that does not coerce it to String, then let the repaint script draw from that retained state. The exact storage choice depends on which object-valued properties the installed Ignition environment exposes. If no suitable component property exists, keep the custom method as the controlled transfer boundary and use a component intended to retain image content.
Do not repeatedly read the file or decode a multi-megabyte image inside every repaint callback. Repaint frequency is controlled by the UI, not by the file workflow. Decode when the selected image changes, then redraw the decoded object as needed.
Avoid the fixes that waste time
- Changing canvas scaling first: Scaling occurs after decoding and cannot repair a null decoder result.
-
Using
available()as a validity test: It reports unread quantity, not image format, byte identity, or decoder compatibility. -
Continuing after
ImageIO.readreturns null: This guarantees the laterNoneTypeexception and hides the useful failure point. - Assuming a String size ceiling caused the failure: The observed count alone does not identify a property limit. Test direct decoding and compare the data on both sides of the property assignment.
- Building a dynamic-property descriptor to move one image: It adds component-internal state handling when a byte-array method parameter already provides the required boundary.
-
Writing
getDynamicPropswithout parentheses: That expression returns the bound method. OnlygetDynamicProps()invokes it and returns its result. -
Fixing the missing parentheses and expecting the image to decode: That removes the
instancemethoderror but does not repair bytes already coerced through a String property. - Drawing once and treating it as retained content: Swing can discard direct drawing during any repaint. Test window repaint behavior before accepting the repair.
FAQ
Can I store image bytes in an Ignition String custom property?
Not as raw binary. A String property introduces character conversion, so pass the byte array directly to redraw(bytes) or use storage that retains the binary or decoded image object without coercion.
Does ByteArrayInputStream.available prove the image is valid?
No. The reported 7748437 only shows how much data remained readable in that stream; verify validity by decoding the original byte array and checking that ImageIO.read returns a non-null image.
Can I fix the instancemethod error by adding parentheses?
Use getDynamicProps() rather than getDynamicProps to call the method. That fixes the put error path, but it does not make a String property safe for raw image bytes.
When should I stop troubleshooting and contact support?
Stop when a known-good image decodes immediately after readFileAsBytes but fails after a verified byte-preserving transfer, or when the component cannot retain the decoded image across repaint events. Escalate to Inductive Automation through its official support channel with the minimal button script, custom method, selected file format, byte counts at both boundaries, and the complete exception trace.