是否可以使用 Java ImageIO 从 InputStream 读取多个图像?

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

我正在尝试使用一个 Kotlin 线程,它可以简单地从单个 InputStream 中读取多个图像。

为了测试,我有一个输入流,它在一个单独的线程中接收两个小图像文件的内容。这似乎工作正常,就像我将此输入流的内容写入磁盘一样,生成的文件与两个源图像文件的串联相同。

使用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");
    }
}

这适用于第一张图像,该图像已正确接收并保存到文件中。然而,第二张图片没有被读取:ImageIO.read(input) returns null.

是否可以从 InputStream 中读取多个图像?我做错了什么?

--- 编辑 ---

我尝试了一种变体,其中只有一个图像从流中解码(这是正确完成的)。在此之后,我尝试将其余的流内容保存到二进制文件中,而不是尝试将其解码为图像。第二个二进制文件是空的,这意味着第一个 ImageIO.read 似乎消耗了整个流。

这是众所周知的 "feature" 输入流。

一个输入流只能读取一次(好吧,有mark()和reset(),但不是每个实现都支持它(检查Javadoc中的markSupported()),恕我直言,使用起来不太方便) ,您应该保留您的图像并将路径作为参数传递,或者您应该将其读取为字节数组并为您尝试读取它的每个调用创建一个 ByteArrayInputStream:

// 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.

不是满意的答案,而是问题的答案:

不,这(几乎可以肯定)是不可能的。

InputStream 传递给 ImageIO 时,它会在内部包装成 ImageInputStream。然后将该流传递给 ImageReader。确切的实现将取决于图像数据的类型。 (这通常由"magic header",即输入数据的前几个字节决定)。

现在,这些 ImageReader 实现的行为无法被合理地改变或控制。 (对于其中一些,实际阅读甚至发生在 native 方法中)。

以下是显示不同行为的示例:

  • 首先,它生成一个输入流,其中包含一个 JPG 图像和一个 PNG 图像。输出显示在返回 JPG 图像之前 fully 读取了输入流。

  • 然后,它生成一个包含一张PNG和一张JPG图像的输入流。可以看到它只读取了几个字节,直到它可以解码第一个 PNG 图像的结果。

_

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

输出结果如下:

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

请注意,虽然在第二种情况下它没有读取完整的流,但这仍然并不必然意味着输入流然后位于"beginning of the JPG data" .这只意味着它没有读取完整的流!

我也试图深入研究这个问题。 Iff 可以确定图像始终只是 PNG 图像,可以尝试手动创建一个 PNGImageReader 实例并挂钩其读取过程,以检查它何时实际完成第一张图片。但是同样,输入流在内部被包装到其他几个(缓冲和放气)输入流中,并且没有办法明智地检测图像的特定字节集是否已经"used"。

所以我认为这里唯一明智的解决方案是在读取图像后关闭流,并为下一张图像打开一个新流。


评论中讨论的解决方法是将长度信息添加到流中。这意味着图像数据的生产者首先将 int 写入流中,描述图像数据的长度。然后它用实际图像数据写入 byte[length] 数据。

然后接收方可以使用此信息加载单个图像。

这里实现了,作为例子:

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

这是基于(支持他的回答,而不是这个!)

一个示例实现展示了 haraldK 提出的方法。它设法读取一系列图像,尽管有一些限制:

  • 它似乎必须读取 "more" 个字节才能传送第一张图像。
  • 它无法加载不同类型 的图像(即它无法读取 PNG 和 JPG 混合图像序列)
  • 具体来说,它对我来说似乎只适用于 JPG 图片。对于 PNG 或 GIF,只读取了第一张图片(至少对我而言...)

但是,将它张贴在这里供其他人轻松测试:

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

是的,可以从(单个)InputStream读取多个图像。

我认为最明显的解决方案是使用一种已经广泛支持多图像的文件格式,例如 TIFF。 javax.imageio API 对读写多图像文件有很好的支持,即使 ImageIO class 没有任何方便的方法,比如 ImageIO.read(...)/ImageIO.write(...) 方法用于 reading/writing 单个图像。这意味着您需要编写更多代码(下面的代码示例)。

但是,如果输入是由您无法控制的第三方创建的,则可能无法使用其他格式。从评论中解释说,您的输入实际上是一连串的 Exif JPEG 流。好消息是 Java 的 JPEGImageReader/Writer 确实允许在同一流中使用多个 JPEG,即使这不是一种很常见的格式。

要从同一个流中读取多个 JPEG,您可以使用以下示例(请注意,该代码是完全通用的,也适用于读取其他多图像文件,如 TIFF):

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

此行以下的任何内容都只是额外的额外信息。

以下是如何使用 ImageIO 写入 多图像文件 API(代码示例使用 TIFF,但它非常通用,理论上应该也可以工作对于其他格式,压缩类型参数除外)。

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

请注意,在 Java 9 之前,您还需要第三方 TIFF 插件,如 JAI 或我自己的 TwelveMonkeys ImageIO,以使用 ImageIO read/write TIFF。


如果您真的不喜欢编写这种冗长的代码,另一种选择是将图像包装在您自己的最小容器格式中,其中包括(至少)每个图像的长度。然后你可以使用 ImageIO.write(...) 写入并使用 ImageIO.read(...) 读取,但是你需要围绕它实现一些简单的流逻辑。当然,反对它的主要理由是它将完全专有。

但是,如果您 reading/writing 在 client/server-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:如果您只想将收到的图像写入磁盘,则不应为此使用ImageIO。相反,使用普通的 I/O (假设格式来自上一个示例):

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