简体   繁体   中英

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.

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();
        }
    }
}

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);

Update:

Scroll down to the last code snippet for an update to this answer.

It is not a satisfactory answer, but an answer to the question:

No, this is (almost certainly) not possible.

When passing an InputStream to ImageIO , it will internally be wrapped into an ImageInputStream . This stream is then passed to an ImageReader . The exact implementation will depend on the type of the image data. (This is usually determined from the "magic header", ie the first few bytes of the input data).

Now, the behavior of these ImageReader implementations cannot be altered or controlled sensibly. (For some of them the actual reading even takes place in native methods).

The following is an example showing the different behaviors:

  • First, it generates an input stream containing one JPG image and one PNG image. The output shows that the input stream is read fully before the JPG image is returned.

  • Then, it generates an input stream containing one PNG and one JPG image. One can see that it only reads a few bytes, until it can decode the result of the first PNG image.

_

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.imageio.ImageIO;

public class MultipleImagesFromSingleStream
{
    public static void main(String[] args) throws IOException
    {
        readJpgAndPng();
        readPngAndJpg();
    }

    private static void readJpgAndPng() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "jpg", baos);
        ImageIO.write(createDummyImage("Image 1", 60), "png", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = ImageIO.read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = ImageIO.read(inputStream);
        System.out.println("Read " + image1);
    }

    private static void readPngAndJpg() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "png", baos);
        ImageIO.write(createDummyImage("Image 1", 60), "jpg", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = ImageIO.read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = ImageIO.read(inputStream);
        System.out.println("Read " + image1);
    }

    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }
}

The output is as follows:

Read 100 of 1519 bytes
Read 200 of 1519 bytes
Read 300 of 1519 bytes
Read 400 of 1519 bytes
Read 500 of 1519 bytes
Read 600 of 1519 bytes
Read 700 of 1519 bytes
Read 800 of 1519 bytes
Read 900 of 1519 bytes
Read 1000 of 1519 bytes
Read 1100 of 1519 bytes
Read 1200 of 1519 bytes
Read 1300 of 1519 bytes
Read 1400 of 1519 bytes
Read 1500 of 1519 bytes
Read BufferedImage@3eb07fd3: type = 0 DirectColorModel: rmask=ff000000 gmask=ff0000 bmask=ff00 amask=ff IntegerInterleavedRaster: width = 100 height = 50 #Bands = 4 xOff = 0 yOff = 0 dataOffset[0] 0
Read null
Read 100 of 1499 bytes
Read 200 of 1499 bytes
Read BufferedImage@42110406: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@531d72ca transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 100 height = 50 #numDataElements 4 dataOff[0] = 3
Read null

Note that although it does not read the full stream in the second case, this still does not necessarily mean that the input stream then is located at the "beginning of the JPG data". It only means that it does not read the full stream!

I also tried to dive deeper into this. Iff one could be sure that the images are always only PNG images, one could try to manually create a PNGImageReader instance and hook into its reading process, to check when it actually finished the first image. But again, the input stream is wrapped internally into several other (buffered- and deflating) input streams, and there is no way to sensibly detect whether a certain set of bytes already has been "used" for an image.

So I think that the only sensible solution here is to close the stream after the image has been read, and open a new stream for the next image.


A workaround that has been discussed in the comments is to add length information to the stream. This means that the producer of the image data first writes an int into the stream, describing the length of the image data. Then it writes the byte[length] data with the actual image data.

The receiver can then use this information to load individual images.

This is implemented here, as an example:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class MultipleImagesFromSingleStreamWorkaround
{
    public static void main(String[] args) throws IOException
    {
        workaround();
    }

    private static void workaround() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        write(createDummyImage("Image 0", 50), "jpg", baos);
        write(createDummyImage("Image 1", 60), "png", baos);
        write(createDummyImage("Image 2", 70), "gif", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        BufferedImage image0 = read(inputStream);
        System.out.println("Read " + image0);
        BufferedImage image1 = read(inputStream);
        System.out.println("Read " + image1);
        BufferedImage image2 = read(inputStream);
        System.out.println("Read " + image2);

        showImages(image0, image1, image2);
    }

    private static void write(BufferedImage bufferedImage, 
        String formatName, OutputStream outputStream) throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, formatName, baos);
        byte data[] = baos.toByteArray();
        DataOutputStream dos = new DataOutputStream(outputStream);
        dos.writeInt(data.length);
        dos.write(data);
        dos.flush();
    }

    private static BufferedImage read(
        InputStream inputStream) throws IOException
    {
        DataInputStream dis = new DataInputStream(inputStream);
        int length = dis.readInt();
        byte data[] = new byte[length];
        dis.read(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return ImageIO.read(bais);
    }




    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }


    private static void showImages(BufferedImage ... images)
    {
        SwingUtilities.invokeLater(() -> 
        {
            JFrame f = new JFrame();
            f.getContentPane().setLayout(new GridLayout(1,0));
            for (BufferedImage image : images)
            {
                f.getContentPane().add(new JLabel(new ImageIcon(image)));
            }
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}

Update

This is based on the answer from haraldK (upvote his answer, not this one!)

An example implementation showing the approach that haraldK proposed. It manages to read a sequence of images, although there are some limitations:

  • It seems to have to read "more" bytes than strictly necessary before it delivers the first image.
  • It cannot load different types of images (ie it cannot read a sequence of mixed PNG and JPG images)
  • Specifically, it seemed to work only for JPG images for me. For PNG or GIF, only the first image was read (at least for me...)

However, posting it here for others to easily test it:

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;


public class MultipleImagesFromSingleStreamWorking
{
    public static void main(String[] args) throws IOException
    {
        readExample();
    }

    private static void readExample() throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(createDummyImage("Image 0", 50), "jpg", baos);
        //ImageIO.write(createDummyImage("Image 1", 60), "png", baos);
        ImageIO.write(createDummyImage("Image 2", 70), "jpg", baos);
        ImageIO.write(createDummyImage("Image 3", 80), "jpg", baos);
        ImageIO.write(createDummyImage("Image 4", 90), "jpg", baos);
        ImageIO.write(createDummyImage("Image 5", 100), "jpg", baos);
        ImageIO.write(createDummyImage("Image 6", 110), "jpg", baos);
        ImageIO.write(createDummyImage("Image 7", 120), "jpg", baos);
        byte data[] = baos.toByteArray();
        InputStream inputStream = createSlowInputStream(data);

        List<BufferedImage> images = readImages(inputStream);
        showImages(images);
    }

    private static List<BufferedImage> readImages(InputStream inputStream)
        throws IOException
    {
        // From https://stackoverflow.com/a/53501316/3182664
        List<BufferedImage> images = new ArrayList<BufferedImage>();
        try (ImageInputStream in = ImageIO.createImageInputStream(inputStream))
        {
            Iterator<ImageReader> readers = ImageIO.getImageReaders(in);

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

            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)
                {
                    BufferedImage image = reader.read(i++);
                    System.out.println("Read " + image);
                    images.add(image);
                }
            }
            catch (IndexOutOfBoundsException expected)
            {
                // We're done
            }

            reader.dispose();
        }
        return images;
    }

    private static InputStream createSlowInputStream(byte data[])
    {
        ByteArrayInputStream bais = new ByteArrayInputStream(data);
        return new InputStream()
        {
            private long counter = 0;
            @Override
            public int read() throws IOException
            {
                counter++;
                if (counter % 100 == 0)
                {
                    System.out.println(
                        "Read " + counter + " of " + data.length + " bytes");
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
                return bais.read();
            }
        };
    }

    private static BufferedImage createDummyImage(String text, int h)
    {
        int w = 100;
        BufferedImage image = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(Color.WHITE);
        g.drawString(text, 20, 20);
        g.dispose();
        return image;
    }


    private static void showImages(List<BufferedImage> images)
    {
        SwingUtilities.invokeLater(() -> 
        {
            JFrame f = new JFrame();
            f.getContentPane().setLayout(new GridLayout(1,0));
            for (BufferedImage image : images)
            {
                f.getContentPane().add(new JLabel(new ImageIcon(image)));
            }
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM