Carlos Moncayo
Carlos Moncayo

Reputation: 11

How to serialize a map that contains Image objects?

I'm creating an image gallery, which contains photo albums and each album contains pictures. These items are stored in a HashMap like this

HashMap<Album, ArrayList<Picture>> albums=new HashMap<>();

the problem starts when trying to serialize the map, because every Picture object contains an Image Object, so I can pick this Image and create an ImageView more easily for my app, the Picture constructor looks like this:

public Picture(String name,String place, String description, Image image)

I always get this exception:

java.io.NotSerializableException: javafx.scene.image.Image

Is there any way to make my Pictures serializable?

Upvotes: 1

Views: 643

Answers (1)

Slaw
Slaw

Reputation: 46136

You need to customize the serialization of Picture. To customize serialization of an object you use the following two methods:

  • void readObject(ObjectInputStream) throws ClassNotFoundException, IOException
  • void writeObject(ObjectOutputStream) throws IOException

These methods can have any access modifier but are typically(?) private.

If you have the following class:

public class Picture implements Serializable {

    private final String name;
    private final String place;
    private final String description;
    private transient Image image;

    public Picture(String name, String place, String description, Image image) {
        this.name = name;
        this.place = place;
        this.description = description;
        this.image = image;
    }

    public String getName() {
        return name;
    }

    public String getPlace() {
        return place;
    }

    public String getDescription() {
        return description;
    }

    public Image getImage() {
        return image;
    }

}

You have at least three options regarding the serialization of the Image.

  1. Serialize the location of the image.

    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
        in.defaultReadObject();
        String url = (String) in.readObject();
        if (url != null) {
            image = new Image(url);
        }
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObjet();
        out.writeObject(image == null ? null : image.getUrl());
    }
    

    The Image#getUrl() method was added in JavaFX 9.

  2. Serialize the pixel data of the Image via the PixelReader. When deserializing you'll use a PixelWriter.

    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
        in.defaultReadObject();
        if (in.readBoolean()) {
            int w = in.readInt();
            int h = in.readInt();
    
            byte[] b = new byte[w * h * 4];
            in.readFully(b);
    
            WritableImage wImage = new WritableImage(w, h);
            wImage.getPixelWriter().setPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(), b, 0, w * 4);
    
            image = wImage;
        }
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeBoolean(image != null);
        if (image != null) {
            int w = (int) image.getWidth();
            int h = (int) image.getHeight();
    
            byte[] b = new byte[w * h * 4];
            image.getPixelReader().getPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(), b, 0, w * 4);
    
            out.writeInt(w);
            out.writeInt(h);
            out.write(b);
        }
    }
    

    WARNING: Serializing the pixel data in this way is saving the image in an uncompressed format. Images can be quite large which may cause problems for you when using this approach.

    This is dependent on the PixelReader being available, which is not always the case. If you read the documentation of Image#getPixelReader() you'll see (emphasis mine):

    This method returns a PixelReader that provides access to read the pixels of the image, if the image is readable. If this method returns null then this image does not support reading at this time. This method will return null if the image is being loaded from a source and is still incomplete {the progress is still <1.0) or if there was an error. This method may also return null for some images in a format that is not supported for reading and writing pixels to.

    Outside still-loading and errors, some non-exhaustive testing indicates animated GIFs do not have an associated PixelReader.

  3. Serialize the actual image file (I don't recommend this one, reasons below).

    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
        in.defaultReadObject();
        if (in.readBoolean()) {
            byte[] bytes = new byte[in.readInt()];
            in.readFully(bytes);
            image = new Image(new ByteArrayInputStream(bytes));
        }
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeBoolean(image != null);
        if (image != null) {
            byte[] bytes;
            try (InputStream is = new URL(image.getUrl()).openStream()) {
                bytes = is.readAllBytes();
            }
            out.writeInt(bytes.length);
            out.write(bytes);
        }
    }
    

    This assumes the Image wasn't loaded from a resource (or at least that the URL has a scheme).

    However, as I said I don't recommend this approach. For one, it only allows you to serialize the Picture instance one time because the original URL is lost after deserialization. You could get around this by storing the location separately but then you might as well use option #1. Then there's also the fact you open up an InputStream and read from it during serialization; this could be quite unexpected behavior for a developer serializing Picture instances.


Some notes:

  • There may be room for optimizations in the code above.

  • Options #1 and #3 don't take the requested width and height of the image into account. This may lead to having a larger image in memory after deserialization. You can modify the code to fix this.

  • Your Picture class seems like a model class. If that's the case, it may be better to simply store the location of the image in a field rather than the Image itself (this can also make customizing the serialization unneeded); then have other code responsible for loading the actual Image (e.g. a cache) based on the location stored in Picture. Either that or allow for lazily loading the Image in the Picture instance.

    The point is to avoid loading Images when you don't need them. For instance, what if part of your UI only wants to display a list of available pictures by name. If you have thousands of Pictures you'll want to avoid loading thousands of Images as you could very easily run out of memory (at even just a few dozen images).

Upvotes: 2

Related Questions