Можно ли читать несколько изображений из InputStream, используя Java ImageIO? - PullRequest
0 голосов
/ 26 ноября 2018

Я пытаюсь создать поток 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) возвращает ноль.

Возможно ли прочитать несколько изображений из InputStream?Что я делаю не так?

--- EDIT ---

Я попробовал вариант, в котором из потока декодируется только одно изображение (это делается правильно).После этого я попытался сохранить остальное содержимое потока в двоичный файл, не пытаясь декодировать его как изображение.Этот второй двоичный файл пуст, это означает, что первый ImageIO.read, кажется, потребляет весь поток.

Ответы [ 3 ]

0 голосов
/ 26 ноября 2018

Обновление:

Прокрутите вниз до последнего фрагмента кода для обновления этого ответа.

Это не удовлетворительный ответ, а ответна вопрос:

Нет, это (почти наверняка) невозможно.

При передаче InputStream в ImageIO он будет внутреннебыть завернутым в ImageInputStream.Этот поток затем передается в ImageReader.Точная реализация будет зависеть от типа данных изображения.(Обычно это определяется из «магического заголовка», т. Е. Первых нескольких байтов входных данных).

Теперь поведение этих реализаций ImageReader не может быть разумно изменено или проконтролировано.(Для некоторых из них фактическое чтение происходит даже в native методах).

Ниже приведен пример, демонстрирующий различные варианты поведения:

  • Сначала создается поток ввода, содержащий одно изображение JPG и одно изображение PNG.Выходные данные показывают, что входной поток считывается полностью до возвращения изображения JPG.

  • Затем он генерирует входной поток, содержащий одно изображение 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

Обратите внимание, что хотя он не читает полный поток во второйВ этом случае не обязательно означает, что входной поток находится в «начале данных JPG».Это только означает, что он не читает полный поток!

Я также попытался углубиться в это. Если , то можно быть уверенным, что изображения всегда являются только PNG-изображениями, можно попытаться вручную создать экземпляр PNGImageReader и подключиться к его процессу чтения, чтобы проверить, когда он фактически закончил первое изображение.Но опять же, входной поток внутренне упакован в несколько других (буферизованных и дефлируемых) входных потоков, и нет никакого способа разумно определить, был ли определенный набор байтов уже «использован» для изображения.

Поэтому я думаю, что единственное разумное решение здесь - это закрыть поток после того, как изображение было прочитано, и открыть новый поток для следующего изображения.


Обходной путь, который обсуждался в комментарияхэто добавить информацию о длине в поток.Это означает, что производитель данных изображения сначала записывает 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);
        });
    }
}

Обновление

Это основано на ответе от haraldK (подтвердите его ответ, а не этот!)

Пример реализации, показывающий подход, предложенный haraldK.Ему удается прочитать последовательность изображений, хотя есть некоторые ограничения:

  • Кажется, ему нужно прочитать "больше" байтов, чем это строго необходимо, прежде чем он доставит первое изображение.
  • Он не может загружать различные типы изображений (то есть он не может прочитать последовательность смешанных изображений 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 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);
        });
    }
}
0 голосов
/ 27 ноября 2018

Да, можно считывать несколько изображений из (одного) InputStream.

. Я считаю, что наиболее очевидным решением является использование формата файла, который уже имеет широкую поддержку нескольких изображений, например TIFF.,API javax.imageio имеет хорошую поддержку для чтения и записи файлов с несколькими изображениями, хотя класс ImageIO не имеет для этого каких-либо удобных методов, таких как методы ImageIO.read(...)/ImageIO.write(...) для чтения / записи одного изображения.Это означает, что вам нужно написать немного больше кода (примеры кода приведены ниже).

Однако, если ввод создается сторонней организацией вне вашего контроля, использование другого формата может оказаться невозможным.Из комментариев объясняется, что ваши входные данные на самом деле представляют собой поток объединенных файлов 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();
}   

Все, что ниже этой линии, является дополнительной дополнительной информацией.

Вот как записать файлы с несколькими изображениями, используя API ImageIO (в примере кода используется 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, для чтения / записи TIFF с использованием ImageIO.


Другой вариант, если вам действительно не нравится писать этот подробный код, - обернуть изображения в ваш собственный минимальный формат контейнера, который включает (как минимум) длину каждого изображения.Затем вы можете писать, используя ImageIO.write(...), и читать, используя ImageIO.read(...), но вам нужно реализовать некоторую простую потоковую логику.И главный аргумент против этого, конечно же, заключается в том, что он будет полностью закрытым.

Но, если вы читаете / пишете асинхронно в настройке типа клиент / сервер (как я подозреваю, из вашего вопроса), это может иметь смысл и может быть приемлемым компромиссом.

Что-то вроде:

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 для этого.Вместо этого используйте обычный ввод-вывод (предполагая формат из предыдущего примера):

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();
        }
    }
}
0 голосов
/ 26 ноября 2018

Это хорошо известная «особенность» входных потоков.

Входной поток может быть прочитан только один раз (хорошо, есть mark () и reset (), но не каждая реализация поддерживает это (check markSupported)() в Javadoc), и ИМХО это не очень удобно использовать), вы должны либо сохранить свое изображение и передать путь в качестве аргумента, либо вы должны прочитать его в байтовый массив и создать 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...