ImageIO.write for JPEG can write corrupt JPEG for certain thumbnail dimensions

XMLWordPrintable

    • b19
    • generic
    • generic

        A DESCRIPTION OF THE PROBLEM :
        When calling ImageIO.write(..) for a JPEG with a thumbnail: you may write a corrupt JPEG file depending on the dimensions.

        In the attached test case: dimensions of 100x218 pass, but 100x219 fail.

        (I'm guessing the problem is that 100*219*3 = 65700, which exceeds the ~65536 size limit for a JPEG segment.)

        STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
        Run the attached test code

        EXPECTED VERSUS ACTUAL BEHAVIOR :
        EXPECTED -
        Ideally the console should indicate that both tests pass. This may be tricky, though, because writing a valid JPEG file here may require automatically switching from the RGB-encoded thumbnail to a JPEG-encoded thumbnail.

        Or an alternative expected behavior might be:
        The act of calling `ImageIO.write(..)` should fail with an Exception. (Ideally without writing to the OutputStream at all.) It is important to let the caller know that their image data is NOT safe/saved.
        ACTUAL -
        An exception appears for the larger thumbnail:
        ```
        javax.imageio.IIOException: Unsupported JPEG process: SOF type 0xc8
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(JPEGImageReader.java:739)
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.checkTablesOnly(JPEGImageReader.java:354)
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.gotoImage(JPEGImageReader.java:504)
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.getImageMetadata(JPEGImageReader.java:1136)
        at java.desktop/com.sun.imageio.plugins.jpeg.JPEGImageReader.getNumThumbnails(JPEGImageReader.java:1652)
        at WriteJPEGThumbnailTest.run(WriteJPEGThumbnailTest.java:79)
        at WriteJPEGThumbnailTest.main(WriteJPEGThumbnailTest.java:54)
        ```

        ---------- BEGIN SOURCE ----------
        import javax.imageio.IIOImage;
        import javax.imageio.ImageIO;
        import javax.imageio.ImageReader;
        import javax.imageio.ImageWriter;
        import javax.imageio.stream.ImageInputStream;
        import javax.imageio.stream.ImageOutputStream;
        import java.awt.*;
        import java.awt.geom.AffineTransform;
        import java.awt.image.BufferedImage;
        import java.io.*;
        import java.util.Arrays;
        import java.util.Iterator;

        public class WriteJPEGThumbnailTest {

            private static void assertEquals(int expected, int observed) {
                if (expected != observed) {
                    throw new Error("expected " + expected + ", but observed " + observed);
                }
            }
            public static void main(String[] args) throws Exception {
                boolean b1 = new WriteJPEGThumbnailTest(100, 218).run();
                boolean b2 = new WriteJPEGThumbnailTest(100, 219).run();
                if (!(b1 && b2))
                    System.err.println("Test failed");
            }

            final int thumbWidth, thumbHeight;

            public WriteJPEGThumbnailTest(int thumbWidth, int thumbHeight) {
                this.thumbWidth = thumbWidth;
                this.thumbHeight = thumbHeight;
            }

            public boolean run() throws Exception {
                System.out.println("Testing thumbnail " + thumbWidth + "x" + thumbHeight + "...");
                try {
                    byte[] jpegData;
                    BufferedImage thumbnail;
                    try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream()) {
                        thumbnail = writeImage(byteOut);
                        jpegData = byteOut.toByteArray();
                    }

                    ImageReader reader = getJPEGImageReader();
                    ImageInputStream stream = ImageIO.createImageInputStream(new ByteArrayInputStream(jpegData));
                    reader.setInput(stream);
                    assertEquals(1, reader.getNumThumbnails(0));
                    assertEquals(thumbnail.getWidth(), reader.getThumbnailWidth(0, 0));
                    assertEquals(thumbnail.getHeight(), reader.getThumbnailHeight(0, 0));

                    BufferedImage readThumbnail = reader.readThumbnail(0, 0);
                    for (int y = 0; y < readThumbnail.getHeight(); y++) {
                        for (int x = 0; x < readThumbnail.getWidth(); x++) {
                            assertEquals(thumbnail.getRGB(x, y), readThumbnail.getRGB(x, y));
                        }
                    }
                    System.out.println("\tTest passed");
                } catch(Exception e) {
                    e.printStackTrace();
                    return false;
                }
                return true;
            }

            private BufferedImage writeImage(OutputStream out) throws IOException {
                BufferedImage thumbnail = createImage(thumbWidth, thumbHeight);
                BufferedImage bi = createImage(thumbnail.getWidth() * 10, thumbnail.getHeight() * 10);

                ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();

                try (ImageOutputStream outputStream = ImageIO.createImageOutputStream(out)) {
                    writer.setOutput(outputStream);

                    // Write the main image
                    IIOImage img = new javax.imageio.IIOImage(bi, Arrays.asList(thumbnail), null);
                    writer.write(null, img, null);

                    writer.dispose();
                }
                return thumbnail;
            }

            private static BufferedImage createImage(int width, int height) {
                BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
                Graphics2D g = bi.createGraphics();
                double sx = ((double)width) / 1000.0;
                double sy = ((double)height) / 1000.0;
                g.transform(AffineTransform.getScaleInstance(sx, sy));
                g.setColor(Color.red);
                g.fillRect(0,0,100,100);
                g.setColor(Color.green);
                g.fillRect(900,0,900,100);
                g.setColor(Color.orange);
                g.fillRect(0,900,100,100);
                g.setColor(Color.magenta);
                g.fillRect(900,900,100,100);
                g.dispose();
                return bi;
            }

            private static ImageReader getJPEGImageReader() {
                Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpeg");
                ImageReader reader;
                while(readers.hasNext()) {
                    reader = readers.next();
                    if(reader.canReadRaster()) {
                        return reader;
                    }
                }
                return null;
            }
        }
        ---------- END SOURCE ----------

              Assignee:
              Philip Race
              Reporter:
              Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              6 Start watching this issue

                Created:
                Updated:
                Resolved: