Read PNG/JPEG/BMP/GIF dimensions without loading image data


   public static Dimension getImageDimension(File file) throws IOException
   {
      RandomAccessFile raf = new RandomAccessFile(file, "r");
      
      try
      {
         int header = raf.readUnsignedShort();

         if (header == 0x8950)
         {
            // PNG!
            raf.seek(8 + 4 + 4); // thanks, Abuse

            return new Dimension(raf.readInt(), raf.readInt());
         }

         if (header == 0xffd8)
         {
            // JPG!

            // (see below)
         }
         else if (header == 0x424D)
         {
            // BMP!
            raf.seek(0x0012);

            int w = raf.read() | (raf.read() << 8) | (raf.read() << 16) | (raf.read() << 24);
            int h = raf.read() | (raf.read() << 8) | (raf.read() << 16) | (raf.read() << 24);
            return new Dimension(w, h);
         }
         else if (header == (('G' << 8) | ('I' << 0)))
         {
            // GIF!
            raf.seek(0x0006);
            int w = raf.read() | (raf.read() << 8);
            int h = raf.read() | (raf.read() << 8);
            return new Dimension(w, h);
         }
         else
         {
            throw new IllegalStateException("unexpected header: " + Integer.toHexString(header));
         }

         while (true) // JPG is not so easy...
         {
            int marker = raf.readUnsignedShort();

            switch (marker)
            {
               case 0xffd8: // SOI
               case 0xffd0: // RST0
               case 0xffd1: // RST1
               case 0xffd2: // RST2
               case 0xffd3: // RST3
               case 0xffd4: // RST4
               case 0xffd5: // RST5
               case 0xffd6: // RST6
               case 0xffd7: // RST7
               case 0xffd9: // EOI
                  break;

               case 0xffdd: // DRI
                  raf.readUnsignedShort();
                  break;

               case 0xffe0: // APP0
               case 0xffe1: // APP1
               case 0xffe2: // APP2
               case 0xffe3: // APP3
               case 0xffe4: // APP4
               case 0xffe5: // APP5
               case 0xffe6: // APP6
               case 0xffe7: // APP7
               case 0xffe8: // APP8
               case 0xffe9: // APP9
               case 0xffea: // APPa
               case 0xffeb: // APPb
               case 0xffec: // APPc
               case 0xffed: // APPd
               case 0xffee: // APPe
               case 0xffef: // APPf
               case 0xfffe: // COM
               case 0xffdb: // DQT
               case 0xffc4: // DHT
               case 0xffda: // SOS
                  raf.readFully(new byte[raf.readUnsignedShort() - 2]);
                  break;

               case 0xffc0: // SOF0
               case 0xffc2: // SOF2
                  raf.readUnsignedShort();
                  raf.readByte();
                  int height = raf.readUnsignedShort();
                  int width = raf.readUnsignedShort();
                  return new Dimension(width, height);

               default:
                  throw new IllegalStateException("invalid jpg marker: " + Integer.toHexString(marker));
            }
         }
      }
      finally
      {
         raf.close();
      }
   }
}

This version works on InputStreams too, which is required for loading your images through a classloader.


   public static Dimension getImageDimension(File file) throws IOException
   {
      return ImageUtil.getImageDimension(new FileInputStream(file));
   }

   public static Dimension getImageDimension(InputStream in) throws IOException
   {
      DataInputStream dis = new DataInputStream(in);

      try
      {
         int header = dis.readUnsignedShort();

         if (header == 0x8950)
         {
            // PNG!
            dis.readFully(new byte[(8 - 2) + 4 + 4]); // thanks Abuse

            return new Dimension(dis.readInt(), dis.readInt());
         }

         if (header == 0xffd8)
         {
            // JPG!

            // (see below)
         }
         else if (header == 0x424D)
         {
            // BMP!
            dis.readFully(new byte[16]);

            int w = dis.read() | (dis.read() << 8) | (dis.read() << 16) | (dis.read() << 24);
            int h = dis.read() | (dis.read() << 8) | (dis.read() << 16) | (dis.read() << 24);
            return new Dimension(w, h);
         }
         else if (header == (('G' << 8) | ('I' << 0))) // GIF
         {
            // GIF!
            dis.readFully(new byte[4]);
            int w = dis.read() | (dis.read() << 8);
            int h = dis.read() | (dis.read() << 8);
            return new Dimension(w, h);
         }
         else
         {
            throw new IllegalStateException("unexpected header: " + Integer.toHexString(header));
         }

         while (true)
         {
            int marker = dis.readUnsignedShort();

            switch (marker)
            {
               case 0xffd8: // SOI
               case 0xffd0: // RST0
               case 0xffd1: // RST1
               case 0xffd2: // RST2
               case 0xffd3: // RST3
               case 0xffd4: // RST4
               case 0xffd5: // RST5
               case 0xffd6: // RST6
               case 0xffd7: // RST7
               case 0xffd9: // EOI
                  break;

               case 0xffdd: // DRI
                  dis.readUnsignedShort();
                  break;

               case 0xffe0: // APP0
               case 0xffe1: // APP1
               case 0xffe2: // APP2
               case 0xffe3: // APP3
               case 0xffe4: // APP4
               case 0xffe5: // APP5
               case 0xffe6: // APP6
               case 0xffe7: // APP7
               case 0xffe8: // APP8
               case 0xffe9: // APP9
               case 0xffea: // APPa
               case 0xffeb: // APPb
               case 0xffec: // APPc
               case 0xffed: // APPd
               case 0xffee: // APPe
               case 0xffef: // APPf
               case 0xfffe: // COM
               case 0xffdb: // DQT
               case 0xffc4: // DHT
               case 0xffda: // SOS
                  dis.readFully(new byte[dis.readUnsignedShort() - 2]);
                  break;

               case 0xffc0: // SOF0
               case 0xffc2: // SOF2
                  dis.readUnsignedShort();
                  dis.readByte();
                  int height = dis.readUnsignedShort();
                  int width = dis.readUnsignedShort();
                  return new Dimension(width, height);

               default:
                  throw new IllegalStateException("invalid jpg marker: " + Integer.toHexString(marker));
            }
         }
      }
      finally
      {
         dis.close();
      }
   }

Wouldn’t it be nice if DataInput defined methods for reading primitives in LSB order as well as Java’s chosen convention of MSB.

It seems such a simple yet incredibly useful thing to have missed off; and given the choice of zip for code package distribution it must have been an I/O necessity from the very beginning! (DEFLATE stores block lengths in LSB order)

Yup. Judging by how the naming convetions at Sun are (LayoutManager => LayoutManager2, NIO => NIO2) we could expect DataInput2 any day now.

You could use a FileChannel. Then when you create your ByteBuffer for reading from the channel you set the order to ByteOrder.LITTLE_ENDIAN or ByteOrder.BIG_ENDIAN. Then from the ByteBuffer getFloat,getInt,getLong etc…

Yeah… but I’m working with InputStreams…

This is very useful, thanks. May I ask, what are you using it for?

I have this webservice, that must scale huge images (8K * 4K pixels) to thumbnails. I use this code to determine whether the webservice (which has a small heap to keep FullGC down to a minimum) can handle it, or it should launch a new JVM to do it – which might run out of memory, without taking down the webservice.

Sounds like what you really want is a progressive decoder for all of said image formats :wink:

Indeed. Me lazy.

BMP and PNG (RGB24/ARGB32) are trivial. JPG not so simple. JAI did not do it (dispite the promise), and I didn’t want to spend more time on it, so this was a quick hack, that will probably be used for a few years. :persecutioncomplex:

Feed the bytes into a ByteBuffer yourself. Then you can still take advantage of the ByteOrder of a ByteBuffer.

Interesting. Clever solution. How annoying that JAI api can’t do it.

And the resulting code would have been… better?

JAI does it for TIFFs - I’ve used it to process a 22k x 22k image without exceeding 64MB heap. Not sure how you were going about it, but what I used was

ImageReader ir=ImageIO.getImageReadersByFormatName("tiff").next(); // Hacky
ImageInputStream iis=new FileImageInputStream(new RandomAccessFile(file,"r"));
ir.setInput(iis);
for (y) {
    for (x) {
        ImageReadParam irp=new ImageReadParam();
        irp.setSourceRegion(new Rectangle(x,y,w,h));
        BufferedImage bi=ir.read(0,irp);
        // Do stuff with bi
    }
}

If Sun’s JPG reader doesn’t handle that then there must be a third party one which does, although finding and getting it approved for use will, of course, be much hassle.

Thanks, turned out I used JAI totally different, and appearantly that resulted in loading the whole image.

Anyway, I got it to work with JPG (trivial, with your code sample), but man… it’s slow!

the image is 8K*4K

ImageIO.read(new File("something.jpg")); // 3sec

the code below takes almost 40 (!!) seconds. Just the loading of the tiles, not even scaling! So I’m going to stick with my current (external JVM) solution.


      ImageReader ir = ImageIO.getImageReadersByFormatName("jpeg").next();
      ImageInputStream iis = new FileImageInputStream(new RandomAccessFile(file, "r"));
      ir.setInput(iis);

      int w = ir.getWidth(0);
      int h = ir.getHeight(0);

      int dim = 512;

      for (int y = 0; y < h - dim; y += dim)
      {
         for (int x = 0; x < w - dim; x += dim)
         {
            ImageReadParam irp = new ImageReadParam();
            irp.setSourceRegion(new Rectangle(x, y, dim, dim));
            BufferedImage bi = ir.read(0, irp);
            bi.flush();
         }
      }