Search code examples
javajavafxjpeg

Cannot load JPEG with java created by Samsung phone


I am having trouble loading a JPEG image shot by a Samsung Galaxy S7 edge with javafx (image available at https://www.dropbox.com/s/w6lvdnqwcgw321s/20171122_140732.jpg?dl=0). I am using the Image class to load the 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();
   }

}

The constructor call, which tries to load the image prints the following error message on the error stream:

Feb 04, 2018 11:48:23 PM com.sun.javafx.tk.quantum.PrismImageLoader2$PrismLoadListener imageLoadWarning WARNING: Invalid SOS parameters for sequential JPEG

The exception, that I get from the image object is an IOException with the message:

Unsupported marker type 0x65

I've done some research and it turns out, that it is a known issue with panorama images shot by a samsung phone. As pointed out in this thread: https://forums.adobe.com/thread/2131432, some of the 0xFF bytes, that indicate the following byte to be meta information rather than actual data are not escaped by adding a following 0x00 byte after the 0xFF.
However I tried to write code that manipulates the image data in order to add the missing 0x00 bytes, but that turned out to be far more complicated, than expected and I don't want to write my own JPEG parser/loader.
There are some programs, that can display those invalid JPEG images e.g. Microsoft Fotos or Paint. It seems like they tolerate these invalid images, treating those spurious markers as content data.
Is there any way to load these invalid images with java, without dealing with the single bytes myself?


Solution

  • Your question is awesome, really made me think and search for a couple of hours)

    Here is a bit hacky solution (without 3rd party libs) that I ended up with:

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

    The problem here is that standard Image api cannot read "broken" images, so we need to read it somehow differently. For this Toolkit.getDefaultToolkit().createImage() method can be used. Actually, the getBufferedImage part was taken from this answer, so, all credits for this go there)

    In getFinalImageView method we simply transforms this BufferedImage into javafx Image and then into ImageView using ImageIO class.

    Result: enter image description here

    Note! I can still observe some exceptions in logs, but they don't prevent this code from successful execution.