© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. Sharan, P. SpäthLearn JavaFX 17https://doi.org/10.1007/978-1-4842-7848-2_21

21. Understanding the Image API

Kishori Sharan1   and Peter Späth2
(1)
Montgomery, AL, USA
(2)
Leipzig, Sachsen, Germany
 
In this chapter, you will learn:
  • What the Image API is

  • How to load an image

  • How to view an image in an ImageView node

  • How to perform image operations such as reading/writing pixels, creating an image from scratch, and saving the image to the file system

  • How to take the snapshot of nodes and scenes

The examples of this chapter lie in the com.jdojo.image package. In order for them to work, you must add a corresponding line to the module-info.java file:
...
opens com.jdojo.image to javafx.graphics, javafx.base;
...

What Is the Image API?

JavaFX provides the Image API that lets you load and display images and read/write raw image pixels. A class diagram for the classes in the Image API is shown in Figure 21-1. All classes are in the javafx.scene.image package. The API lets you
  • Load an image in memory

  • Display an image as a node in a scene graph

  • Read pixels from an image

  • Write pixels to an image

  • Convert a node in a scene graph to an image and save it to the local file system

Figure 21-1

A class diagram for classes in the Image API

An instance of the Image class represents an image in memory. You can construct an image in a JavaFX application by supplying pixels to a WritableImage instance.

An ImageView is a Node. It is used to display an Image in a scene graph. If you want to display an image in an application, you need to load the image in an Image and display the Image in an ImageView.

Images are constructed from pixels. Data for pixels in an image may be stored in different formats. A PixelFormat defines how the data for a pixel for a given format is stored. A WritablePixelFormat represents a destination format to write pixels with full pixel color information.

The PixelReader and PixelWriter interfaces define methods to read from an Image and write data to a WritableImage. Besides an Image, you can read pixels from and write pixels to any surface that contains pixels.

I will cover examples of using these classes in the sections to follow.

Loading an Image

An instance of the Image class is an in-memory representation of an image. The class supports BMP, PNG, JPEG, and GIF image formats. It loads an image from a source, which can be specified as a string URL or an InputStream. It can also scale the original image while loading.

The Image class contains several constructors that let you specify the properties for the loaded image:
  • Image(InputStream is)

  • Image(InputStream is, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth)

  • Image(String url)

  • Image(String url, boolean backgroundLoading)

  • Image(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth)

  • Image(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading)

There is no ambiguity of the source of the image if an InputStream is specified as the source. If a string URL is specified as the source, it could be a valid URL or a valid path in the CLASSPATH. If the specified URL is not a valid URL, it is used as a path, and the image source will be searched on the path in the CLASSPATH:
// Load an image from local machine using an InputStream
String sourcePath = "C:\mypicture.png";
Image img = new Image(new FileInputStream(sourcePath));
// Load an image from an URL
Image img = new Image("http://jdojo.com/wp-content/uploads/2013/03/randomness.jpg");
// Load an image from the CLASSPATH. The image is located in the resources.picture package
Image img = new Image("resources/picture/randomness.jpg");

In the preceding statement, the specified URL resources/picture/randomness.jpg is not a valid URL. The Image class will treat it as a path expecting it to exist in the CLASSPATH. It treats the resource.picture as a package and the randomness.jpg as a resource in that package.

Tip

Make sure to add valid URLs if you want to test the code snippets in this chapter. Either you make sure you use relative URLs like resources/picture/randomness.jpg that are in the CLASSPATH or specify absolute URLs like http://path/to/my/server/resources/picture/randomness.jpg or file://some/absolute/path/resources/picture/randomness.jpg .

Specifying the Image-Loading Properties

Some constructors let you specify some image-loading properties to control the quality of the image and the loading process:
  • requestedWidth

  • requestedHeight

  • preserveRatio

  • smooth

  • backgroundLoading

The requestedWidth and requestedHeight properties specify the scaled width and height of the image. By default, an image is loaded in its original size.

The preserveRatio property specifies whether to preserve the aspect ratio of the image while scaling. By default, it is false.

The smooth property specifies the quality of the filtering algorithm to be used in scaling. By default, it is false. If it is set to true, a better quality filtering algorithm is used, which slows down the image-loading process a bit.

The backgroundLoading property specifies whether to load the image asynchronously. By default, the property is set to false, and the image is loaded synchronously. The loading process starts when the Image object is created. If this property is set to true, the image is loaded asynchronously in a background thread.

Reading the Loaded Image Properties

The Image class contains the following read-only properties:
  • width

  • height

  • progress

  • error

  • exception

The width and height properties are the width and height of the loaded image, respectively. They are zero if the image failed to load.

The progress property indicates the progress in loading the image data. It is useful to know the progress when the backgroundLoading property is set to true. Its value is between 0.0 and 1.0 where 0.0 indicates 0% loading and 1.0 indicates 100% loading. When the backgroundLoading property is set to false (the default), its value is 1.0. You can add a ChangeListener to the progress property to know the progress in image loading. You may display a text as a placeholder for an image while it is loading and update the text with the current progress in the ChangeListener :
// Load an image in the background
String imagePath = "resources/picture/randomness.jpg";
Boolean backgroundLoading = true;
Image image = new Image(imagePath, backgroundLoading);
// Print the loading progress on the standard output
image.progressProperty().addListener((prop, oldValue, newValue) -> {
        System.out.println("Loading:" +
               Math.round(newValue.doubleValue() * 100.0) + "%");
});
The error property indicates whether an error occurred while loading the image. If it is true, the exception property specifies the Exception that caused the error. At the time of this writing, TIFF image format is not supported on Windows. The following snippet of code attempts to load a TIFF image on Windows XP, and it produces an error. The code contains an error handling logic that adds a ChangeListener to the error property if backgroundLoading is true. Otherwise, it checks for the value of the error property :
String imagePath = "resources/picture/test.tif";
Boolean backgroundLoading = false;
Image image = new Image(imagePath, backgroundLoading);
// Add a ChangeListener to the error property for background loading and
// check its value for non-background loading
if (image.isBackgroundLoading()) {
    image.errorProperty().addListener((prop, oldValue, newValue) -> {
           if (newValue) {
               System.out.println(
                        "An error occurred while loading the image. " +
                        "Error message: " +
                        image.getException().getMessage());
           }
    });
}
else if (image.isError()) {
    System.out.println("An error occurred while loading the image. " +
                  "Error message: " +
                        image.getException().getMessage());
}
An error occurred while loading the image.
Error message: No loader for image data

Viewing an Image

An instance of the ImageView class is used to display an image loaded in an Image object. The ImageView class inherits from the Node class, which makes an ImageView suitable to be added to a scene graph. The class contains several constructors:
  • ImageView()

  • ImageView(Image image)

  • ImageView(String url)

The no-args constructor creates an ImageView without an image. Use the image property to set an image. The second constructor accepts the reference of an Image. The third constructor lets you specify the URL of the image source. Internally, it creates an Image using the specified URL:
// Create an empty ImageView and set an Image for it later
ImageView imageView = new ImageView();
imageView.setImage(new Image("resources/picture/randomness.jpg"));
// Create an ImageView with an Image
ImageView imageView = new ImageView(new Image("resources/picture/randomness.jpg"));
// Create an ImageView with the URL of the image source
ImageView imageView = new ImageView("resources/picture/randomness.jpg");
The program in Listing 21-1 shows how to display an image in a scene. It loads an image in an Image object. The image is scaled without preserving the aspect ratio. The Image object is added to an ImageView, which is added to an HBox. Figure 21-2 shows the window.
// ImageTest.java
package com.jdojo.image;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ImageTest extends Application {
        public static void main(String[] args) {
               Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
           String imagePath =
                   ResourceUtil.getResourceURLStr("picture/randomness.jpg");
           // Scale the image to 200 X 100
           double requestedWidth = 200;
           double requestedHeight = 100;
           boolean preserveRatio = false;
           boolean smooth = true;
           Image image = new Image(imagePath,
                             requestedWidth,
                             requestedHeight,
                             preserveRatio,
                             smooth);
           ImageView imageView = new ImageView(image);
           HBox root = new HBox(imageView);
           Scene scene = new Scene(root);
           stage.setScene(scene);
           stage.setTitle("Displaying an Image");
           stage.show();
        }
}
Listing 21-1

Displaying an Image in an ImageView Node

Figure 21-2

A window with an image

Multiple Views of an Image

An Image loads an image in memory from its source. You can have multiple views of the same Image. An ImageView provides one of the views.

You have an option to resize the original image while loading, displaying, or at both times. Which option you choose to resize an image depends on the requirement at hand:
  • Resizing an image in an Image object resizes the image permanently in memory, and all views of the image will use the resized image. Once an Image is resized, its size cannot be altered. You may want to reduce the size of an image in an Image object to save memory.

  • Resizing an image in an ImageView resizes the image only for this view. You can resize the view of an image in an ImageView even after the image has been displayed.

We have already discussed how to resize an image in an Image object. In this section, we will discuss resizing an image in an ImageView.

Similar to the Image class, the ImageView class contains the following four properties to control the resizing of view of an image:
  • fitWidth

  • fitHeight

  • preserveRatio

  • smooth

The fitWidth and fitHeight properties specify the resized width and height of the image, respectively. By default, they are zero, which means that the ImageView will use the width and height of the loaded image in the Image.

The preserveRatio property specifies whether to preserve the aspect ratio of the image while resizing. By default, it is false.

The smooth property specifies the quality of the filtering algorithm to be used in resizing. Its default value is platform dependent. If it is set to true, a better quality filtering algorithm is used.

The program in Listing 21-2 loads an image in an Image object in original size. It creates three ImageView objects of the Image specifying different sizes. Figure 21-3 shows the three images. The image shows a junk school bus and a junk car. The image is used with a permission from Richard Castillo (www.digitizedchaos.com).
// MultipleImageViews.java
// ...find in the book's download area.
Listing 21-2

Displaying the Same Image in Different ImageView in Different Sizes

Figure 21-3

Three views of the same image

Viewing an Image in a Viewport

A viewport is a rectangular region to view part of a graphics. It is common to use scrollbars in conjunction with a viewport. As the scrollbars are scrolled, the viewport shows different parts of the graphics.

An ImageView lets you define a viewport for an image. In JavaFX, a viewport is an instance of the javafx.geometry.Rectangle2D object. A Rectangle2D is immutable. It is defined in terms of four properties: minX, minY, width, and height. The (minX, minY) value defines the location of the upper-left corner of the rectangle. The width and height properties specify its size. You must specify all properties in the constructor:
// Create a viewport located at (0, 0) and of size 200 X 100
Rectangle2D viewport = new Rectangle2D(0, 0, 200,100);

The ImageView class contains a viewport property, which provides a viewport into the image displayed in the ImageView. The viewport defines a rectangular region in the image. The ImageView shows only the region of the image that falls inside the viewport. The location of the viewport is defined relative to the image, not the ImageView. By default, the viewport of an ImageView is null, and the ImageView shows the whole image.

The following snippet of code loads an image in its original size in an Image. The Image is set as the source for an ImageView. A viewport 200 X 100 in size is set for the ImageView. The viewport is located at (0, 0). This shows in the ImageView the top-left 200 X 100 region of the image:
String imagePath = "resources/picture/school_bus.jpg";
Image image = new Image(imagePath);
imageView = new ImageView(image);
Rectangle2D viewport = new Rectangle2D(0, 0, 200, 100);
imageView.setViewport(viewport);
The following snippet of code will change the viewport to show the 200 X 100 lower-right region of the image:
double minX = image.getWidth() - 200;
double minY = image.getHeight() - 100;
Rectangle2D viewport2 = new Rectangle2D(minX, minY, 200, 100);
imageView.setViewport(viewport2);
Tip

The Rectangle2D class is immutable. Therefore, you need to create a new viewport every time you want to move the viewport into the image.

The program in Listing 21-3 loads an image into an ImageView. It sets a viewport for the ImageView. You can drag the mouse, while pressing the left, right, or both buttons, to scroll to the different parts of the image into the view.
// ImageViewPort.java
// ...find in the book's download area.
Listing 21-3

Using a Viewport to View Part of an Image

The program declares a few class and instance variables. The VIEWPORT_WIDTH and VIEWPORT_HEIGHT are constants holding the width and height of the viewport. The startX and startY instance variables will hold the x and y coordinates of the mouse when the mouse is pressed or dragged. The ImageView instance variable holds the reference of the ImageView. We need this reference in the mouse-dragged event handler.

The starting part of the start() method is simple. It creates an Image, an ImageView, and sets a viewport for the ImageView. Then, it sets the mouse-pressed and mouse-dragged event handlers to the ImageView:
// Set the mouse pressed and mouse dragged event handlers
imageView.setOnMousePressed(this::handleMousePressed);
imageView.setOnMouseDragged(this::handleMouseDragged);
In the handleMousePressed() method , we store the coordinates of the mouse in the startX and startY instance variables. The coordinates are relative to the ImageView:
startX = e.getX();
startY = e.getY();
The handleMousePressed() method computes the new location of the viewport inside the image because of the mouse drag and sets a new viewport at the new location. First, it computes the dragged distance for the mouse along the x-axis and y-axis:
// How far the mouse was dragged
double draggedDistanceX = e.getX() - startX;
double draggedDistanceY = e.getY() - startY;
You reset the startX and startY values to the mouse location that triggered the current mouse-dragged event. This is important to get the correct dragged distance when the user keeps the mouse pressed, drags it, stops without releasing the mouse, and drags it again:
// Reset the starting point for the next drag
// if the user keeps the mouse pressed and drags again
startX = e.getX();
startY = e.getY();
You compute the new location of the upper-left corner of the viewport. You always have a viewport in the ImageView. The new viewport will be located at the dragged distance from the old location:
// Get the minX and minY of the current viewport
double curMinX = imageView.getViewport().getMinX();
double curMinY = imageView.getViewport().getMinY();
// Move the new viewport by the dragged distance
double newMinX = curMinX + draggedDistanceX;
double newMinY = curMinY + draggedDistanceY;
It is fine to place the viewport outside the region of the image. The viewport simply displays an empty area when it falls outside the image area. To restrict the viewport inside the image area, we clamp the location of the viewport:
// Make sure the viewport does not fall outside the image area
newMinX = clamp(newMinX, 0, imageView.getImage().getWidth() - VIEWPORT_WIDTH);
newMinY = clamp(newMinY, 0, imageView.getImage().getHeight() - VIEWPORT_HEIGHT);
Finally , we set a new viewport using the new location:
// Set a new viewport
imageView.setViewport(new Rectangle2D(newMinX, newMinY, VIEWPORT_WIDTH, VIEWPORT_HEIGHT));
Tip

It is possible to scale or rotate the ImageView and set a viewport to view the region of the image defined by the viewport.

Understanding Image Operations

JavaFX supports reading pixels from an image, writing pixels to an image, and creating a snapshot of the scene. It supports creating an Image from scratch. If an image is writable, you can also modify the image in memory and save it to the file system. The image API provides access to each pixel in the image. It supports reading and writing one pixel or a chunk of pixels at a time. This section will discuss operations supported by the Image API with simple examples.

Pixel Formats

The Image API in JavaFX gives you access to each pixel in an image. A pixel stores information about its color (red, green, blue) and opacity (alpha). The pixel information can be stored in several formats.

An instance the PixelFormat<T extends Buffer> represents the layout of data for a pixel. You need to know the pixel format when you read the pixels from an image. You need to specify the pixel format when you write pixels to an image. The WritablePixelFormat class inherits from the PixelFormat class, and its instance represents a pixel format that can store full color information. An instance of the WritablePixelFormat class is used when writing pixels to an image.

Both class PixelFormat and its subclass WritablePixelFormat are abstract. The PixelFormat class provides several static methods to obtain instances to PixelFormat and WritablePixelFormat abstract classes. Before we discuss how to get an instance of the PixelFormat, let us discuss types of storage formats available for storing the pixel data.

A PixelFormat has a type that specifies the storage format for a single pixel. The constants of the PixelFormat.Type enum represent different types of storage formats:
  • BYTE_RGB

  • BYTE_BGRA

  • BYTE_BGRA_PRE

  • BYTE_INDEXED

  • INT_ARGB

  • INT_ARGB_PRE

In the BYTE_RGB format, the pixels are assumed opaque. The pixels are stored in adjacent bytes as red, green, and blue, in order.

In the BYTE_BGRA format, pixels are stored in adjacent bytes as blue, green, red, and alpha in order. The color values (red, green, and blue) are not pre-multiplied with the alpha value.

The BYTE_BGRA_PRE type format is similar to BYTE_BGRA, except that in BYTE_BGRA_PRE the stored color component values are pre-multiplied by the alpha value.

In the BYTE_INDEXED format, a pixel is as a single byte. A separate lookup list of colors is provided. The single byte value for the pixel is used as an index in the lookup list to get the color value for the pixel.

In the INT_ARGB format , each pixel is stored in a 32-bit integer. Bytes from the most significant byte (MSB) to the least significant byte (LSB) store alpha, red, green, and blue values. The color values (red, green, and blue) are not pre-multiplied with the alpha value. The following snippet of code shows how to extract components from a pixel value in this format:
int pixelValue = get the value for a pixel...
int alpha = (pixelValue >> 24) & 0xff;
int red   = (pixelValue >> 16) & 0xff;
int green = (pixelValue >>  8) & 0xff;
int blue  = pixelValue & 0xff;

The INT_ARGB_PRE format is similar to the INT_ARGB format, except that INT_ARGB_PRE stores the color values (red, green, and blue) pre-multiplied with the alpha value.

Typically, you need to create a WritablePixelFormat when you write pixels to create a new image. When you read pixels from an image, the pixel reader will provide you a PixelFormat instance that will tell you how the color information in the pixels are stored. The following snippet of code creates some instances of the WritablePixelFormat class:
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritablePixelFormat;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
...
// BYTE_BGRA Format type
WritablePixelFormat<ByteBuffer> format1 = PixelFormat.getByteBgraInstance();
// BYTE_BGRA_PRE Format type
WritablePixelFormat<ByteBuffer> format2 =
    PixelFormat.getByteBgraPreInstance();
// INT_ARGB Format type
WritablePixelFormat<IntBuffer> format3 = PixelFormat.getIntArgbInstance();
// INT_ARGB_PRE Format type
WritablePixelFormat<IntBuffer> format4 = PixelFormat.getIntArgbPreInstance();

Pixel format classes are not useful without pixel information. After all, they describe the layout of information in a pixel! We will use these classes when we read and write image pixels in the sections to follow. Their use will be obvious in the examples.

Reading Pixels from an Image

An instance of the PixelReader interface is used to read pixels from an image. Use the getPixelReader() method of the Image class to obtain a PixelReader. The PixelReader interface contains the following methods:
  • int getArgb(int x, int y)

  • Color getColor(int x, int y)

  • Void getPixels(int x, int y, int w, int h, WritablePixelFormat<ByteBuffer> pixelformat, byte[] buffer, int offset, int scanlineStride)

  • void getPixels(int x, int y, int w, int h, WritablePixelFormat<IntBuffer> pixelformat, int[] buffer, int offset, int scanlineStride)

  • <T extends Buffer> void getPixels(int x, int y, int w, int h, WritablePixelFormat<T> pixelformat, T buffer, int scanlineStride)

  • PixelFormat getPixelFormat()

The PixelReader interface contains methods to read one pixel or multiple pixels at a time. Use the getArgb() and getColor() methods to read the pixel at the specified (x, y) coordinate. Use the getPixels() method to read pixels in bulk. Use the getPixelFormat() method to get the PixelFormat that best describes the storage format for the pixels in the source.

The getPixelReader() method of the Image class returns a PixelReader only if the image is readable. Otherwise, it returns null. An image may not be readable if it is not fully loaded yet, it had an error during loading, or its format does not support reading pixels:
Image image = new Image("file://.../resources/picture/ksharan.jpg");
// Get the pixel reader
PixelReader pixelReader = image.getPixelReader();
if (pixelReader == null) {
        System.out.println("Cannot read pixels from the image");
} else {
        // Read image pixels
}
Once you have a PixelReader, you can read pixels invoking one of its methods. The program in Listing 21-4 shows how to read pixels from an image. The code is self-explanatory:
  • The start() method creates an Image. The Image is loaded synchronously.

  • The logic to read the pixels is in the readPixelsInfo() method. The method receives a fully loaded Image. It uses the getColor() method of the PixelReader to get the pixel at a specified location. It prints the colors for all pixels. At the end, it prints the pixel format, which is BYTE_RGB.

// ReadPixelInfo.java
// ...find in the book's download area.
Color at (0, 0) = 0xb5bb41ff
Color at (1, 0) = 0xb0b53dff
...
Color at (233, 287) = 0x718806ff
Color at (234, 287) = 0x798e0bff
Pixel format type: BYTE_RGB
Listing 21-4

Reading Pixels from an Image

Reading pixels in bulk is a little more difficult than reading one pixel at a time. The difficulty arises from the setup information that you have to provide to the getPixels() method . We will repeat the preceding example by reading all pixels in bulk using the following method of the PixelReader:
void getPixels(int x, int y,
               int width, int height,
               WritablePixelFormat<ByteBuffer> pixelformat,
               byte[] buffer,
               int offset,
               int scanlineStride)

The method reads the pixels from rows in order. The pixels in the first row are read, then the pixels from the second row, and so on. It is important that you understand the meaning of all parameters to the method.

The method reads the pixels of a rectangular region in the source.

The x and y coordinates of the upper-left corner of the rectangular region are specified in the x and y arguments.

The width and height arguments specify the width and height of the rectangular region.

The pixelformat specifies the format of the pixel that should be used to store the read pixels in the specified buffer.

The buffer is a byte array in which the PixelReader will store the read pixels. The length of the array must be big enough to store all read pixels.

The offset specifies the starting index in the buffer array to store the first pixel data. Its value of zero indicates that the data for the first pixel will start at index 0 in the buffer.

The scanlineStride specifies the distance between the start of one row of data in the buffer and the start of the next row of data. Suppose you have two pixels in a row, and you want to read in the BYTE_BGRA format taking 4 bytes for a pixel. One row of data can be stored in 8 bytes. If you specify 8 as the argument value, the data for the next row will start in the buffer just after the data for the previous row data ends. If you specify the argument value 10, the last 2 bytes will be empty for each row of data. The first row pixels will be stored from index 0 to 7. The indexes 8 and 9 will be empty (or not written). Indexes 10 to 17 will store pixel data for the second row leaving indexes 18 and 19 empty. You may want to specify a bigger value for the argument than needed to store one row of pixel data if you want to fill the empty slots with your own values later. Specifying a value less than needed will overwrite part of the data in the previous row.

The following snippet of code shows how to read all pixels from an image in a byte array, in BYTE_BGRA format:
Image image = ...
PixelReader pixelReader = image.getPixelReader();
int x = 0;
int y = 0;
int width = (int)image.getWidth();
int height = (int)image.getHeight();
int offset = 0;
int scanlineStride = width * 4;
byte[] buffer = new byte[width * height * 4];
// Get a WritablePixelFormat for the BYTE_BGRA format type
WritablePixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraInstance();
// Read all pixels at once
pixelReader.getPixels(x, y,
                width, height,
                pixelFormat,
                buffer,
                offset,
                scanlineStride);

The x and y coordinates of the upper-left corner of the rectangular region to be read are set to zero. The width and height of the region are set to the width and height of the image. This sets up the arguments to read the entire image.

You want to read the pixel data into the buffer starting at index 0, so you set the offset argument to 0.

You want to read the pixel data in BYTE_BGRA format type, which takes 4 bytes to store data for one pixel. We have set the scanlineStride argument value, which is the length of a row data, to width * 4, so a row data starts at the next index from where the previous row data ended.

You get an instance of the WritablePixelFormat to read the data in the BYTE_BGRA format type. Finally, we call the getPixels() method of the PixelReader to read the pixel data. The buffer will be filled with the pixel data when the getPixels() method returns.

Tip

Setting the value for the scanlineStride argument and the length of the buffer array depends on the pixelFormat argument. Other versions of the getPixels() method allow reading pixel data in different formats.

The program in Listing 21-5 has the complete source code to read pixels in bulk. After reading all pixels, it decodes the color components in the byte array for the pixel at (0, 0). It reads the pixel at (0, 0) using the getColor() method. The pixel data at (0, 0) obtained through both methods are printed on the standard output.
// BulkPixelReading.java
// ...find in the book's download area.
red=181, green=187, blue=65, alpha=255
red=181, green=187, blue=65, alpha=255
Listing 21-5

Reading Pixels from an Image in Bulk

Writing Pixels to an Image

You can write pixels to an image or any surface that supports writing pixels. For example, you can write pixels to a WritableImage and a Canvas.

Tip

An Image is a read-only pixel surface. You can read pixels from an Image. However, you cannot write pixels to an Image. If you want to write to an image or create an image from scratch, use a WritableImage.

An instance of the PixelWriter interface is used to write pixels to a surface. A PixelWriter is provided by the writable surface. For example, you can use the getPixelWriter() method of the Canvas and WritableImage to obtain a PixelWriter for them.

The PixelWriter interface contains methods to write pixels to a surface and obtain the pixel format supported by the surface:
  • PixelFormat getPixelFormat()

  • void setArgb(int x, int y, int argb)

  • void setColor(int x, int y, Color c)

  • void setPixels(int x, int y, int w, int h, PixelFormat<ByteBuffer> pixelformat, byte[] buffer, int offset, int scanlineStride)

  • void setPixels(int x, int y, int w, int h, PixelFormat<IntBuffer> pixelformat, int[] buffer, int offset, int scanlineStride)

  • <T extends Buffer> void setPixels(int x, int y, int w, int h, PixelFormat<T> pixelformat, T buffer, int scanlineStride)

  • void setPixels(int dstx, int dsty, int w, int h, PixelReader reader, int srcx, int srcy)

The getPixelFormat() method returns the pixel format in which the pixels can be written to the surface. The setArgb() and setColor() methods allow for writing one pixel at the specified (x, y) location in the destination surface. The setArgb() method accepts the pixel data in an integer in the INT_ARGB format, whereas the setColor() method accepts a Color object. The setPixels() methods allow for bulk pixel writing.

You can use an instance of the WritableImage to create an image from scratch. The class contains three constructors:
  • WritableImage(int width, int height)

  • WritableImage(PixelReader reader, int width, int height)

  • WritableImage(PixelReader reader, int x, int y, int width, int height)

The first constructor creates an empty image of the specified width and height:
// Create a new empty image of 200 X 100
WritableImage newImage = new WritableImage(200, 100);
The second constructor creates an image of the specified width and height. The specified reader is used to fill the image with pixels. An ArrayIndexOutOfBoundsException is thrown if the reader reads from a surface that does not have the necessary number of rows and columns to fill the new image. Use this constructor to copy the whole or part of an image. The following snippet of code creates a copy of an image:
String imagePath = "file://.../resources/picture/ksharan.jpg";
Image image = new Image(imagePath, 200, 100, true, true);
int width = (int)image.getWidth();
int height = (int)image.getHeight();
// Create a copy of the image
WritableImage newImage =
    new WritableImage(image.getPixelReader(), width, height);

The third constructor lets you copy a rectangular region from a surface. The (x, y) value is coordinates of the upper-left corner of the rectangular region. The (width, height) value is the dimension of the rectangular region to be read using the reader and the desired dimension of the new image. An ArrayIndexOutOfBoundsException is thrown if the reader reads from a surface that does not have the necessary number of rows and columns to fill the new image.

The WritableImage is a read-write image. Its getPixelWriter() method returns a PixelWriter to write pixels to the image. It inherits the getPixelReader() method that returns a PixelReader to read data from the image.

The following snippet of code creates an Image and an empty WritableImage. It reads one pixel at a time from the Image, makes the pixel darker, and writes the same pixel to the new WritableImage. At the end, we have created a darker copy of the original image:
Image image = new Image("file://.../resources/picture/ksharan.jpg";);
PixelReader pixelReader = image.getPixelReader();
int width = (int)image.getWidth();
int height = (int)image.getHeight();
// Create a new, empty WritableImage
WritableImage darkerImage = new WritableImage(width, height);
PixelWriter darkerWriter = darkerImage.getPixelWriter();
// Read one pixel at a time from the source and
// write it to the destinations - one darker and one brighter
for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {
               // Read the pixel from the source image
               Color color = pixelReader.getColor(x, y);
               // Write a darker pixel to the new image at the same
                    // location
               darkerWriter.setColor(x, y, color.darker());
        }
}
The program in Listing 21-6 creates an Image. It creates three instances of the WritableImage and copies the pixels from the original image to them. The copied pixels are modified before they are written to the destination. For one destination, pixels are darkened, for one brightened, and for one, made semitransparent. All four images are displayed in ImageViews as shown in Figure 21-4.
// CopyingImage.java
// ...find in the book's download area.
Listing 21-6

Writing Pixels to an Image

Figure 21-4

Original image and modified images

Tip

It is easy to crop an image in JavaFX. Use one of the getPixels() methods of the PixelReader to read the needed area of the image in a buffer and write the buffer to a new image. This gives you a new image that is the cropped version of the original image.

Creating an Image from Scratch

In the previous section, we created new images by copying pixels from another image. We had altered the color and opacity of the original pixels before writing them to the new image. That was easy because we were working on one pixel at a time, and we received a pixel as a Color object. It is also possible to create pixels from scratch and then use them to create a new image. Anyone would admit that creating a new, meaningful image by defining its each pixel in code is not an easy task. However, JavaFX has made the process of doing so easy.

In this section, we will create a new image with a pattern of rectangles placed in a grid-like fashion. Each rectangle will be divided into two parts using the diagonal connecting the upper-left and lower-right corners. The upper triangle is painted in green and the lower in red. A new image will be created and filled with the rectangles.

Creating an image from scratch involves three steps:
  • Create an instance of the WritableImage.

  • Create buffer (a byte array, an int array, etc.) and populate it with pixel data depending on the pixel format you want to use for the pixel data.

  • Write the pixels in the buffer to the image.

Let us write the code that creates the pixels for our rectangular region. Let us declare constants for the width and height of the rectangle:
static final int RECT_WIDTH = 20;
static final int RECT_HEIGHT = 20;
We need to define a buffer (a byte array) big enough to hold data for all pixels. Each pixel in BYTE_RGB format takes 2 bytes:
byte[] pixels = new byte[RECT_WIDTH * RECT_HEIGHT * 3];
If the region is rectangular, we need to know the height to width ratio to divide the region into upper and lower rectangles:
double ratio = 1.0 * RECT_HEIGHT/RECT_WIDTH;
The following snippet of code populates the buffer:
// Generate pixel data
for (int y = 0; y < RECT_HEIGHT; y++) {
        for (int x = 0; x < RECT_WIDTH; x++) {
           int i = y * RECT_WIDTH * 3 + x * 3;
           if (x <= y/ratio) {
               // Lower-half
               pixels[i] = -1;  // red -1 means 255 (-1 & 0xff = 255)
               pixels[i+1] = 0; // green = 0
               pixels[i+2] = 0; // blue = 0
           } else {
               // Upper-half
               pixels[i] = 0;    // red = 0
               pixels[i+1] = -1; // Green 255
               pixels[i+2] = 0;  // blue = 0
           }
        }
}

Pixels are stored in the buffer in the row-first order. The variable i inside the loop computes the position in the buffer where the 3-byte data starts for a pixel. For example, the data for the pixel at (0, 0) starts at index 0; the data for the pixel at (0, 1) starts at index 3; etc. The 3 bytes for a pixel store red, green, and blue values in order of increasing index. Encoded values for the color components are stored in the buffer, so that the expression “byteValue & 0xff” will produce the actual color component value between 0 and 255. If you want a red pixel, you need to set –1 for the red component as “-1 & 0xff” produces 255. For a red color, the green and blue components will be set to zero. A byte array initializes all elements to zero. However, we have explicitly set them to zero in our code. For the lower-half triangle, we set the color to green. The condition “x =<= y/ratio” is used to determine the position of a pixel whether it falls in the upper-half triangle or the lower-half triangle. If the y/ratio is not an integer, the division of the rectangle into two triangles may be a little off at the lower-right corner.

Once we get the pixel data, we need to write them to a WritableImage. The following snippet of code writes the pixels for the rectangle, once at the upper-left corner of the image:
WritableImage newImage = new WritableImage(350, 100);
PixelWriter pixelWriter = newImage.getPixelWriter();
byte[] pixels = generate pixel data...
// Our data is in BYTE_RGB format
PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance();
Int xPos 0;
int yPos =0;
int offset = 0;
int scanlineStride = RECT_WIDTH * 3;
pixelWriter.setPixels(xPos, yPos,
                 RECT_WIDTH, RECT_HEIGHT,
                 pixelFormat,
                 pixels, offset,
                 scanlineStride);
The program in Listing 21-7 creates an image from scratch. It creates a pattern by writing row pixels for the rectangular region to fill the image. Figure 21-5 shows the image.
// CreatingImage.java
// ...find in the book's download area.
Listing 21-7

Creating an Image from Scratch

Figure 21-5

An image created from scratch

Saving a New Image to a File System

Saving an Image to the file system is easy:
  • Convert the Image to a BufferedImage using the fromFXImage() method of the SwingFXUtils class.

  • Pass the BufferedImage to the write() method of the ImageIO class.

Notice that we have to use two classes—BufferedImage and ImageIO—that are part of the standard Java library, not the JavaFX library. The following snippet of code shows the outline of the steps involved in saving an image to a file in the PNG format:
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import javax.imageio.ImageIO;
...
Image image = create an image...
BufferedImage bImage = SwingFXUtils.fromFXImage(image, null);
// Save the image to the file
File fileToSave = ...
String imageFormat = "png";
try {
        ImageIO.write(bImage, imageFormat, fileToSave);
}
catch (IOException e) {
        throw new RuntimeException(e);
}
The program in Listing 21-8 has code for a utility class ImageUtil. Its static saveToFile(Image image) method can be used to save an Image to a local file system. The method asks for a file name. The user can select a PNG or a JPEG format for the image.
// ImageUtil.java
// ...find in the book's download area.
Listing 21-8

A Utility Class to Save an Image to a File

The program in Listing 21-9 shows how to save an image to a file. Click the Save Image button to save the picture to a file. It opens a file chooser dialog to let you select a file name. If you cancel the file chooser dialog, the saving process is aborted.
// SaveImage.java
// ...find in the book's download area.
Listing 21-9

Saving an Image to a File

Taking the Snapshot of a Node and a Scene

JavaFX allows you to take a snapshot of a Node and a Scene as they will appear in the next frame. You get the snapshot in a WritableImage, which means you can perform all pixel-level operations after you take the snapshot. The Node and Scene classes contain a snapshot() method to accomplish this.

Taking the Snapshot of a Node

The Node class contains an overloaded snapshot() method:
  • WritableImage snapshot(SnapshotParameters params, WritableImage image)

  • void snapshot(Callback<SnapshotResult,Void> callback, SnapshotParameters params, WritableImage image)

The first version of the snapshot() method is synchronous, whereas the second one is asynchronous. The method lets you specify an instance of the SnapshotParameters class that contains the rendering attributes for the snapshot. If this is null, default values will be used. You can set the following attributes for the snapshot:
  • A fill color

  • A transform

  • A viewport

  • A camera

  • A depth buffer

By default, the fill color is white; no transform and viewport are used; a ParallelCamera is used; and the depth buffer is set to false. Note that these attributes are used on the node only while taking its snapshot.

You can specify a WritableImage in the snapshot() method that will hold the snapshot of the node. If this is null, a new WritableImage is created. If the specified WritableImage is smaller than the node, the node will be clipped to fit the image size.

The first version of the snapshot() method returns the snapshot in a WritableImage. The image is either the one that is passed as the parameter or a new one created by the method.

The second, asynchronous version of the snapshot() method accepts a Callback object whose call() method is called. A SnapshotResult object is passed to the call() method, which can be used to obtain the snapshot image, the source node, and the snapshot parameters using the following methods:
  • WritableImage getImage()

  • SnapshotParameters getSnapshotParameters()

  • Object getSource()

Tip

The snapshot() method takes the snapshot of the node using the boundsInParent property of the node. That is, the snapshot contains all effects and transformations applied to the node. If the node is being animated, the snapshot will include the animated state of the node at the time it is taken.

The program in Listing 21-10 shows how to take a snapshot of a TextField node. It displays a Label, a TextField, and two Buttons in a GridPane. Buttons are used to take the snapshot of the TextField synchronously and asynchronously. Click one of the Buttons to take a snapshot. A file save dialog appears for you to enter the file name for the saved snapshot. The syncSnapshot() and asyncSnapshot() methods contain the logic to take the snapshot. For the snapshot, the fill is set to red, and a Scale and a Rotate transforms are applied. Figure 21-6 shows the snapshot.
// NodeSnapshot.java
// ...find in the book's download area.
Listing 21-10

Taking a Snapshot of a Node

Figure 21-6

The snapshot of a node

Taking the Snapshot of a Scene

The Scene class contains an overloaded snapshot() method:
  • WritableImage snapshot(WritableImage image)

  • void snapshot(Callback<SnapshotResult,Void> callback, WritableImage image)

Compare the snapshot() methods of the Scene class with that of the Node class. The only difference is that the snapshot() method in the Scene class does not contain the SnapshotParameters argument. This means that you cannot customize the scene snapshot. Except this, the method works the same way as it works for the Node class, as discussed in the previous section.

The first version of the snapshot() method is synchronous, whereas the second one is asynchronous. You can specify a WritableImage to the method that will hold the snapshot of the node. If this is null, a new WritableImage is created. If the specified WritableImage is smaller than the scene, the scene will be clipped to fit the image size.

The program in Listing 21-11 shows how to take a snapshot of a scene. The main logic in the program is essentially the same as that of the program in Listing 21-10, except that, this time, it takes a snapshot of a scene. Figure 21-7 shows the snapshot.
// SceneSnapshot.java
// ...find in the book's download area.
Listing 21-11

Taking a Snapshot of a Scene

Figure 21-7

The snapshot of a scene

Summary

JavaFX provides the Image API that lets you load and display images and read/write raw image pixels. All classes in the API are in the javafx.scene.image package. The API lets you perform the following operations on images: load an image in memory, display an image as a node in a scene graph, read pixels from an image, write pixels to an image, and convert a node in a scene graph to an image and save it to the local file system.

An instance of the Image class is an in-memory representation of an image. You can also construct an image in a JavaFX application by supplying pixels to a WritableImage instance. The Image class supports BMP, PNG, JPEG, and GIF image formats. It loads an image from a source, which can be specified as a string URL or an InputStream. It can also scale the original image while loading.

An instance of the ImageView class is used to display an image loaded in an Image object. The ImageView class inherits from the Node class, which makes an ImageView suitable to be added to a scene graph.

Images are constructed from pixels. JavaFX supports reading pixels from an image, writing pixels to an image, and creating a snapshot of the scene. It supports creating an image from scratch. If an image is writable, you can also modify the image in memory and save it to the file system. The Image API provides access to each pixel in the image. It supports reading and writing one pixel or a chunk of pixels at a time.

Data for pixels in an image may be stored in different formats. A PixelFormat defines how the data for a pixel for a given format is stored. A WritablePixelFormat represents a destination format to write pixels with full pixel color information.

The PixelReader and PixelWriter interfaces define methods to read data from an Image and write data to a WritableImage. Besides an Image, you can read pixels from and write pixels to any surface that contains pixels.

JavaFX allows you to take a snapshot of a Node and a Scene as they will appear in the next frame. You get the snapshot in a WritableImage, which means you can perform all pixel-level operations after you take the snapshot. The Node and Scene classes contain a snapshot() method to accomplish this.

The next chapter will discuss how to draw on a canvas using the Canvas API.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.82.83