--- title: "Vision - Paintable Canvas" description: "The Paintable Canvas component can be custom painted using Jython scripting." --- # Vision - Paintable Canvas > The Paintable Canvas component can be custom painted using Jython scripting. ![](paintable-canvas.png) **Component Palette Icon**: ![](paintable-canvas1.png) ## Description The Paintable Canvas component can be custom "painted" using Jython scripting. By responding to the component's **repaint** Event Handler, a designer can use Java2D to draw anything within the component's bounds. Whenever any dynamic properties on the component change, the component is re-painted automatically, making it possible to create dynamic, vector-drawn components that can represent anything. :::note This component is an advanced component for those who are comfortable using scripting. It is not user-friendly, but it is extraordinarily powerful, as your imagination is the only limit with what this component can be. ::: When you first drop a Paintable Canvas onto a window, you'll notice that it looks like a placeholder. If you switch the Designer into preview mode, you'll see an icon of a pump displayed. The pump is displayed because of the pre-loaded code in the repaint Script Editor. By editing the script, you can dissect how the pump was drawn. You will notice that the script uses Java2D. You can read more about Java2D [here](https://docs.oracle.com/javase/tutorial/2d/index.html). You will notice that as you resize the pump, it scales accordingly in preview mode. Java2D is a vector drawing library, enabling you to create components that scale gracefully. ![](paintable-canvas2.png) :::tip Tips * You can add [dynamic properties](ignition-modules/vision/working-with-vision-components/vision-component-customizer/vision-component-customizer.md) to this component and use the [styles](ignition-modules/vision/working-with-vision-components/vision-component-customizer/vision-component-customizer.md#style-customizer) feature. Use the values of dynamic properties in your repaint code to create a dynamic component. The component will repaint automatically when these values change. * You can create an interactive component by responding to mouse and keyboard events. * You can store your custom components on a [custom palette](ignition-modules/vision/working-with-vision-components/creating-vision-components/creating-vision-components.md) and use them like standard components. ::: ## Properties | Name | Description | Property Type | Scripting | Category | |--- |--- |--- |--- |--- | | Background Color | The background color of the component. Can be chosen from color wheel, chosen from color palette, or entered as RGB or HSL value. See [Color Selector](appendix/reference-pages/color-selector-reference/color-selector-reference.mdx). | Color | .background | Appearance | | Border | The border surrounding this component. Options are No border, Etched (Lowered), Etched (Raised), Bevel (Lowered), Bevel (Raised), Bevel (Double), and Field Border.**Note**: The border is unaffected by rotation. | Border | .border | Common | | Cursor | The mouse cursor to use when hovering over this component. Options are Default, Crosshair, Text, Wait, Hand, Move, SW Resize, or SE Resize. | int | .cursorCode | Common | | Focusable| If the component is focusable, it will receive keyboard input and can detect if it is the focus owner. | boolean | .focusable | Behavior | | Font | Font of text on this component. | Font | .font | Appearance | | Foreground Color | The foreground color of the component. See [Color Selector](appendix/reference-pages/color-selector-reference/color-selector-reference.mdx). | Color | .foreground | Appearance | | Mouseover Text | The text that is displayed in the tooltip which pops up on mouseover of this component. | String | .toolTipText | Common | | Name | The name of this component. | String | .name | Common | | Quality | The data quality code for any Tag bindings on this component. | QualityCode | .quality | Data | | Styles | Contains the component's styles. | Dataset | .styles | Appearance | | Visible | If disabled, the component will be hidden. | boolean | .visible | Common | ## Scripting ### Component Functions This component does not have component functions associated with it. ### Extension Functions This component does not have extension functions associated with it. ### Event Handlers Event handlers allow you to run a script based off specific triggers. See the full list of available event handlers on the [Component Events](ignition-modules/vision/scripting-in-vision/component-events/component-events.md) page ## Customizers * [Vision Component Customizers](ignition-modules/vision/working-with-vision-components/vision-component-customizer/vision-component-customizer.md) * [Style Customizer](ignition-modules/vision/working-with-vision-components/vision-component-customizer/vision-component-customizer.md#style-customizer) ## Examples ### Drawing Custom Images It is possible to draw geometric primitives and arbitrary shapes on the Paintable Canvas. This includes points, lines, curves, ellipses, arcs, rectangles, paths, and any combinations of the above to create a completely custom image. Below are script examples illustrating this functionality. ```python title="Script 1: Draw Geometric Primitives - Rectangle" from java.awt import Color from java.awt.geom import Rectangle2D # Specify the rectangle's size in pixels width = 100 height = 50 # Specify its position, in pixels, relative to the top left edge of the component x = 20 y = 30 # Create the rectangle using the specs from steps 1 and 2. rectangle = Rectangle2D.Float( x, y, width, height) # Set the color and draw the rectangle g = event.graphics g.setColor(Color.GREEN) g.draw(rectangle) ``` ```python title="Script 2: Draw an Arbitrary Shape Based on the Coordinates Stored in a Dataset" # This example implies a dataset with each row storing the X and Y coordinates of a single point. # The columns in this case are called "xCoord" and "xCoord", respectively. from java.awt import Color from java.awt.geom import GeneralPath # Get the shape coordinates from a dataset coordinates = event.source.points # Create an arbitrary shape shape = GeneralPath() # Create a starting point from the 0-th row of the dataset shape.moveTo(coordinates.getValueAt(0, "xCoord"), coordinates.getValueAt(0, "yCoord")) # Iterate through the rows of coordinates and draw the lines for rowNdx in range(1, coordinates.rowCount): shape.lineTo(coordinates.getValueAt(rowNdx, "xCoord"), coordinates.getValueAt(rowNdx, "yCoord")) # Close up the shape shape.closePath() # Set the color and draw the shape. g = event.graphics g.setColor(Color.GREEN) g.draw(shape) ``` Additionally, you can specify the line color, stroke style, fill color and gradient for the shape. Refer to the following Oracle tutorials for more infomation: * [Geometric primitives](https://docs.oracle.com/javase/tutorial/2d/geometry/primitives.html) * [Arbitrary shapes](https://docs.oracle.com/javase/tutorial/2d/geometry/arbitrary.html) * [Stroking and Filling Graphics](https://docs.oracle.com/javase/tutorial/2d/geometry/strokeandfill.html) ### Working with Existing Images One of the most common use cases for the Paintable Canvas component is displaying images retrieved either from a database table using a SQL query or from a file server using the image URL. #### Displaying an Image Stored in a Database Table The following script shows how to retrieve an image stored as a BLOB in a SQL database and display it using the Paintable Canvas component. The query in this example relies on a specific SQL table structure. Make sure to adjust the query, including table and column names, as well as any filtering, to match your table structure and needs. ```python title="Displaying an Image Stored in a Database Table Script" from javax.imageio import ImageIO from java.io import ByteArrayInputStream # See if the image is cached; if not, load it from the database bytes = event.source.getClientProperty("img-bytes-cached") if not bytes: query = "SELECT image_blob FROM imagestore LIMIT 1" bytes = system.db.runScalarPrepQuery(query, []) event.source.putClientProperty("img-bytes-cached", bytes) # Read the image from the bytes image = ImageIO.read(ByteArrayInputStream(bytes)) # Draw the image event.graphics.drawImage(image, 0, 0, event.source) ``` #### Displaying an Image from a URL The script below shows how to retrieve and display an image from a URL. The URL can be hard-coded, or read from a custom property. ```python title="Displaying an Image from a URL" from java.net import URL from javax.imageio import ImageIO # Obtain the URL string. imageURL = "your/URL/here" # Create a Java URL object from the URL string url = URL(imageURL) # Read the image at the URL image = ImageIO.read(url) # Display the image, aligned to the top left corner event.graphics.drawImage(image, 0, 0, event.source) ``` :::note Adjusting Image Size When displaying existing images, make sure to consider their original size. If the image does not have the same dimensions as the size of the Paintable Canvas component, it will either be cropped or only partially fill the Paintable Canvas component depending on if it’s larger or smaller than the Paintable Canvas. However, you can scale the image using the `scale` method of the Graphics2D object. Refer to [Scaling, Zooming, and Rotating](#scaling-zooming-and-rotating) for examples. ::: ### Drawing Text If you want to display text on the Paintable canvas by itself or to label images, Java Graphics2D library provides the `drawString` method. The string given to the drawString method can be specified as a literal, obtained from a custom property, or put together programmatically. ``` from java.awt import Color from java.awt.geom import GeneralPath # Specify the string to display string = "Some string to display." # Specify X and Y coordinates of the lower left point for the displayed text startingX = 50 startingY = 10 # Set the color and draw the string g = event.graphics g.setColor(Color.BLUE) g.drawString(string, startingX, startingY) ``` ### Scaling, Zooming, and Rotating You can scale an image by simply defining the scale factor or calculating the width and height values to dynamically match the component size. Dynamically scaling an image is most helpful when the ratio of the image size to the component size is not known in advance. ```python title="Defining the Scale Factor" # Get a reference to the java.awt.Graphics2D object g = event.graphics # Define the scale factor, and scale the image horizontally and vertically scaleFactor = 2.0 g.scale(scaleFactor, scaleFactor) # Draw the image, aligned ot the top left of the component g.drawImage(image,0,0,event.source) ``` ```python title="Dynamically Scale an Image" # Get the width and height of the BufferedImage object returned from ImageIO.read call: imageW = image.getWidth() imageH = image.getHeight() # Get the width and height of the Paintable Canvas component canvasW = event.width canvasH = event.height # Calculate the scale factors; # Account for 1 pixel of the component's border dimensions scaleX = (canvasW-1.0) / imageW scaleY = (canvasH-1.0) / imageH # Apply scaling and draw the image g = event.graphics g.scale(scaleX, scaleY) g.drawImage(image,0,0,event.source) ``` The same dynamic technique as the scaling option above can be used to dynamically control the zoom factor via a custom property. The following example implies that the Paintable Canvas component has an Integer type custom property called `zoom` containing the zoom percentage (200 for zooming by 200%). ```python title="Controlling the Zoom Factor" from javax.imageio import ImageIO from java.io import ByteArrayInputStream # Get the image bytes from the database or cache bytes = event.source.getClientProperty("img-bytes-cached") if not bytes: bytes = system.db.runScalarPrepQuery( "SELECT image_blob FROM imagestore LIMIT 1", [] ) event.source.putClientProperty("img-bytes-cached", bytes) # Read the image from the bytes image = ImageIO.read(ByteArrayInputStream(bytes)) # Get the scale factor and convert the Integer percent value to a Float scale factor scaleFactor = event.source.zoom * 0.01 # Scale and draw the image g = event.graphics g.scale(scaleFactor, scaleFactor) g.drawImage(image, 0, 0, event.source) ``` This last example shows how to rotate the image based on rotation center and angle values. The rotation center coordinates and the rotation angle value are obtained from the component's custom properties. With this, user controls like sliders, can be provided to dynamically control rotation. ```python title="Rotate an Image" from javax.imageio import ImageIO from java.io import ByteArrayInputStream # Get the image bytes from the database or cache bytes = event.source.getClientProperty("img-bytes-cached") if not bytes: bytes = system.db.runScalarPrepQuery( "SELECT image_blob FROM imagestore LIMIT 1", [] ) event.source.putClientProperty("img-bytes-cached", bytes) # Read the image from the bytes image = ImageIO.read(ByteArrayInputStream(bytes)) # Get the center and angle of rotation from custom properties rotationCenterX = event.source.rotationCenterX rotationCenterY = event.source.rotationCenterY rotationAngle = event.source.rotationAngle # Draw the image g = event.graphics g.rotate(rotationAngle, rotationCenterX, rotationCenterY) g.drawImage(image, 0, 0, event.source) ```