Невозможно загрузить JPEG с помощью java, созданного телефоном Samsung

У меня возникли проблемы с загрузкой изображения JPEG, снятого краем Samsung Galaxy S7 с javafx (изображение доступно на https://www.dropbox.com/s/w6lvdnqwcgw321s/20171122_140732.jpg?dl=0). Я использую класс Image для загрузки изображения.

import java.io.FileInputStream;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class JPEGProblem extends Application {

   public static void main(String[] args) {
      launch(args);
   }

   @Override
   public void start(Stage window) throws Exception {
      Image img = new Image(new FileInputStream("/path/to/image.jpg"));
      if (img.getException() != null)
         throw img.getException();

      ImageView imgView = new ImageView(img);
      window.setScene(new Scene(new Pane(imgView)));
      window.show();
   }

}

Вызов конструктора, который пытается загрузить изображение, печатает следующее сообщение об ошибке в потоке ошибок:

Feb 04, 2018 11:48:23 PM com.sun.javafx.tk.quantum.PrismImageLoader2 $ PrismLoadListener imageLoadWarning ПРЕДУПРЕЖДЕНИЕ: Недействительные параметры SOS для последовательного JPEG

Исключением, которое я получаю из объекта изображения, является сообщение IOException с сообщением:

Неподдерживаемый маркер типа 0x65

Я сделал некоторые исследования, и выясняется, что это известная проблема с панорамными изображениями, снятыми телефоном Samsung. Как указано в этом потоке: https://forums.adobe.com/thread/2131432, некоторые из байтов 0xFF, которые указывают, что следующий байт будет метаинформацией, а не фактическими данными, не сбрасываются путем добавления следующего 0x00 байт после 0xFF.
Однако я попытался написать код, который манипулирует данными изображения, чтобы добавить отсутствующие 0x00 байт, но это оказалось намного сложнее, чем ожидалось, и я не хочу писать свой собственный парсер/загрузчик JPEG.
Есть несколько программ, которые могут отображать эти недопустимые изображения JPEG, например, Microsoft Fotos или Paint. Похоже, они терпят эти недопустимые образы, рассматривая эти ложные маркеры как данные контента.
Есть ли способ загрузить эти недопустимые изображения с помощью java, не имея дело с одиночными байтами?

Ответ 1

Ваш вопрос потрясающий, действительно заставил меня думать и искать пару часов)

Вот немного хакерское решение (без сторонних библиотек), в котором я закончил:

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.PixelGrabber;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

public class JPEGProblem extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage window) throws Exception {
        BufferedImage bi = getBufferedImage();
        ImageView imgView = getFinalImageView(bi);
        window.setScene(new Scene(new Pane(imgView)));
        window.show();
    }

    private BufferedImage getBufferedImage() throws InterruptedException {
        final java.awt.Image image = Toolkit.getDefaultToolkit().createImage("path\to\file");

        final int[] RGB_MASKS = {0xFF0000, 0xFF00, 0xFF};
        final ColorModel RGB_OPAQUE =
                new DirectColorModel(32, RGB_MASKS[0], RGB_MASKS[1], RGB_MASKS[2]);

        PixelGrabber pg = new PixelGrabber(image, 0, 0, -1, -1, true);
        pg.grabPixels();
        int width = pg.getWidth(), height = pg.getHeight();
        DataBuffer buffer = new DataBufferInt((int[]) pg.getPixels(), pg.getWidth() * pg.getHeight());
        WritableRaster raster = Raster.createPackedRaster(buffer, width, height, width, RGB_MASKS, null);
        return new BufferedImage(RGB_OPAQUE, raster, false, null);
    }

    private ImageView getFinalImageView(BufferedImage bi) throws Exception {
        ImageView imgView = new ImageView(SwingFXUtils.toFXImage(bi, null));
        imgView.setFitWidth(1024);
        imgView.setFitHeight(756);
        imgView.setRotate(180);
        return imgView;
    }
}

Проблема здесь в том, что стандартное Image api не может читать "сломанные" изображения, поэтому нам нужно как-то почитать его. Для этого метода Toolkit.getDefaultToolkit() может использоваться метод createImage(). На самом деле, часть getBufferedImage была взята из этого ответа, поэтому все кредиты для этого идут туда)

В методе getFinalImageView мы просто преобразуем этот BufferedImage в javafx Image а затем в ImageView с использованием класса ImageIO.

Результат: enter image description here

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

Ответ 2

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

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;


public class ReadImage {

    public static void main(String[] argv) {
        try {
            BufferedImage img = ImageIO.read(new File("/path/to/20171122_140732.jpg"));

            System.out.println( "image is " + img.getHeight() + " pixels in height and " + img.getWidth() + " pixels wide");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Если я запустил его на исходном изображении, я получил:

javax.imageio.IIOException: Bogus DQT index 14
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImage(Native Method)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1247)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:1050)
    at javax.imageio.ImageIO.read(ImageIO.java:1448)
    at javax.imageio.ImageIO.read(ImageIO.java:1308)
    at com.hotjoe.so.imagereader.ReadImage.main(ReadImage.java:15)

Поэтому я тогда побежал (на Ubuntu, но ImageMagick - кросс-платформа)

convert-im6 -rotate 360 20171122_140732.jpg blah.jpg

convert-im6 - это исполняемое имя под Ubuntu - оно может отличаться на разных O/S.

Это дало мне ошибку:

convert: Invalid SOS parameters for sequential JPEG '20171122_140732.jpg' @ warning/jpeg.c/JPEGWarningHandler/352.
convert: Corrupt JPEG data: 61 extraneous bytes before marker 0x65 '20171122_140732.jpg' @ warning/jpeg.c/JPEGWarningHandler/352.
convert: Unsupported marker type 0x65 '20171122_140732.jpg' @ warning/jpeg.c/JPEGErrorHandler/319.

но он все еще работал:

image is 3760 pixels in height and 11888 pixels wide

И напомните мне добраться до Новой Зеландии - это красивая картина.