Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to read multiple images from an InputStream using Java ImageIO?

I'm trying to have a Kotlin thread which simply reads multiple images from a single InputStream.

For testing, I have an input stream that receives the content of two small image files in a separate thread. This seems to be working correctly as if I write the content of this input stream to disk, the resulting file is identical to the concatenation of the two source image files.

The problem occurs when reading images from the input stream with ImageIO:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import javax.imageio.ImageIO;

class ImgReader {

    InputStream input;

    ImgReader(InputStream input) {
        this.input = input;
    }

    public void run() {
        ImageIO.setUseCache(false);
        System.out.println("read start");
        int counter = 1;
        try {
            BufferedImage im = ImageIO.read(input);
            System.out.println("read: " + counter + " " + (im != null));

            if (im != null)
                ImageIO.write(im, "jpg", new File("pics/out/" + (counter++) +".jpeg"));

        } catch (Exception e){
            System.out.println("error while reading stream");
            e.printStackTrace(System.out);
        }

        System.out.println("read done");
    }
}

This works for the first image, which is received and saved to file correctly. However, the second image is not read: ImageIO.read(input) returns null.

Is it possible to read multiple images from an InputStream? What am I doing wrong?

--- EDIT ---

I tried a variation, where only one image is decoded from the stream (this is done correctly). After this, I tried saving the rest of the stream content into a binary file, without trying to decode it as an image. This second binary file is empty, meaning that the first ImageIO.read seems to consume the whole stream.

like image 442
Eloy Villasclaras Avatar asked Jan 28 '23 02:01

Eloy Villasclaras


2 Answers

Yes, it is possible to read multiple images from a (single) InputStream.

I believe the most obvious solution is to use a file format that already has widespread support for multiple images, like TIFF. The javax.imageio API has good support for reading and writing multi-image files, even though the ImageIO class doesn't have any convenience methods for it, like the ImageIO.read(...)/ImageIO.write(...) methods for reading/writing a single image. This means the you need to write a bit more code (code samples below).

However, if the input is created by a third-party outside of your control, using a different format may not be an option. From the comments, it is explained that your input is actually a stream of concatenated Exif JPEGs. The good news is that Java's JPEGImageReader/Writer does allow multiple JPEGs in the same stream, even though this is not a very common format.

To read multiple JPEGs from the same stream, you can use the following example (note that the code is completely generic, and will work for reading other multi-image files, like TIFF too):

File file = ...; // May also use InputStream here
List<BufferedImage> images = new ArrayList<>();

try (ImageInputStream in = ImageIO.createImageInputStream(file)) {
    Iterator<ImageReader> readers = ImageIO.getImageReaders(in);

    if (!readers.hasNext()) {
        throw new AssertionError("No reader for file " + file);
    }

    ImageReader reader = readers.next();

    reader.setInput(in);

    // It's possible to use reader.getNumImages(true) and a for-loop here.
    // However, for many formats, it is more efficient to just read until there's no more images in the stream.
    try {
        int i = 0;
        while (true) {
            images.add(reader.read(i++));
        }
    }
    catch (IndexOutOfBoundsException expected) {
        // We're done
    }

    reader.dispose();
}   

Anything below this line is just bonus extra-information.

Here's how to write multi-image files using the ImageIO API (the code example uses TIFF, but it is quite generic, and should in theory also work for other formats, except for the compression type parameter).

File file = ...; // May also use OutputStream/InputStream here
List<BufferedImage> images = new ArrayList<>(); // Just add images...

Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("TIFF");

if (!writers.hasNext()) {
    throw new AssertionError("Missing plugin");
}

ImageWriter writer = writers.next();

if (!writer.canWriteSequence()) {
    throw new AssertionError("Plugin doesn't support multi page file");       
}

ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType("JPEG"); // The allowed compression types may vary from plugin to plugin
// The most common values for TIFF, are NONE, LZW, Deflate or Zip, or JPEG

try (ImageOutputStream out = ImageIO.createImageOutputStream(file)) {
    writer.setOutput(out);

    writer.prepareWriteSequence(null); // No stream metadata needed for TIFF

    for (BufferedImage image : images) {
        writer.writeToSequence(new IIOImage(image, null, null), param);
    }

    writer.endWriteSequence();
}

writer.dispose();

Note that before Java 9, you will also need a third party TIFF plugin, like JAI or my own TwelveMonkeys ImageIO, to read/write TIFF using ImageIO.


Another option, if you really don't like to write this verbose code, is to wrap the images in your own minimal container format, that includes (at least) the length of each image. Then you can write using ImageIO.write(...) and read using ImageIO.read(...), but you need to implement some simple stream logic around it. And the main argument against it, of course, is that it will be entirely proprietary.

But, if you are reading/writing asynchronously in a client/server-like setup (as I suspect, from your question), this may make perfect sense, and could be an acceptable trade-off.

Something like:

File file = new File(args[0]);
List<BufferedImage> images = new ArrayList<>();

try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024 * 1024); // Use larger buffer for large images

    for (BufferedImage image : images) {
        buffer.reset();

        ImageIO.write(image, "JPEG", buffer); // Or PNG or any other format you like, really

        out.writeInt(buffer.size());
        buffer.writeTo(out);
        out.flush();
    }

    out.writeInt(-1); // EOF marker (alternatively, catch EOFException while reading)
}

// And, reading back:
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
    int size;

    while ((size = in.readInt()) != -1) {
        byte[] buffer = new byte[size];
        in.readFully(buffer); // May be more efficient to create a FilterInputStream that counts bytes read, with local EOF after size

        images.add(ImageIO.read(new ByteArrayInputStream(buffer)));
    }
}

PS: If all you want to do is to write the images you receive to disk, you should not use ImageIO for this. Instead, use plain I/O (assuming format from the previous example):

try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
    int counter = 0;

    int size;        
    while ((size = in.readInt()) != -1) {
        byte[] buffer = new byte[size];
        in.readFully(buffer);

        try (FileOutputStream out = new FileOutputStream(new File("pics/out/" + (counter++) +".jpeg"))) {
            out.write(buffer);
            out.flush();
        }
    }
}
like image 86
Harald K Avatar answered Jan 31 '23 10:01

Harald K


This is a well known "feature" of the inputstreams.

An inputstream can be read only once (ok, there is mark() and reset(), but not every implementation supports it (check markSupported() in Javadoc), and IMHO it is not so convinient to use), you should either persist your image and pass the path as an argument, or you should read it to a byte array and create a ByteArrayInputStream for every call where you are trying to read it:

// read your original stream once (e.g. with commons IO, just the sake of shortness)
byte[] imageByteArray = IOUtils.toByteArray(input);
...
// and create new input stream every time
InputStream newInput = new ByteArrayInputStream(imageByteArray);
...
// and call your reader in this way:
new ImgReader(newInput);
like image 27
m4gic Avatar answered Jan 31 '23 11:01

m4gic