0

Using Java ImageIO, is it possible to export a jpeg image that has a bit-depth of 8? How would I do this? Even when exporting a BufferedImage of TYPE_BYTE_BINARY, which is a grayscale image, the result is a JPEG with bit-depth of 24.

This is what I have so far.

public void testJpegBitDepth() throws Exception{
    Path pIn = Paths.get("testing/jpg/box1.jpg"), pOut;
    BufferedImage bi;

    //*******************************************
    //Write 8 bit jpg

    //Init ImageWriter
    Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("jpg");
    ImageWriter writer = null;
    while(it.hasNext()) {
        try {
            writer = it.next();

            //Read input
            bi = ImageIO.read(pIn.toFile());
            if(bi == null)
                throw new Exception("Failed to read input file: " + pIn);

            //Convert to gray
            bi = AWTImaging.convertToGray(bi);
            log.debug("Num bands from the image raster: " + bi.getRaster().getNumBands());

            pOut = test.outputDir.resolve("jpegBitDepth-8-"
                    + pIn.getFileName().toString() + ".jpg");

            //Init ImageTypeSpecifier
            ImageTypeSpecifier imageType = ImageTypeSpecifier.createGrayscale(
                    8,                      //8 bits per pixel 
                    DataBuffer.TYPE_BYTE,   //stored in a byte
                    false);                 //unsigned

            //Init WriteParam
            ImageWriteParam param = writer.getDefaultWriteParam();
            param.setDestinationType(imageType);
           //Not sure if this is required or not, but the same Exception occurs either way
           //param.setSourceBands(new int[] {0});

            //Init meta
            IIOMetadata meta = writer.getDefaultImageMetadata(imageType, param);

            String metadataFormat = "javax_imageio_jpeg_image_1.0";
            IIOMetadataNode root = new IIOMetadataNode(metadataFormat);
            IIOMetadataNode jpegVariety = new IIOMetadataNode("JPEGvariety");
            IIOMetadataNode markerSequence = new IIOMetadataNode("markerSequence");

            //I think we want app0JFIF metadata here, as it can specify a grayscale image https://docs.oracle.com/javase/10/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
            IIOMetadataNode app0JFIF = new IIOMetadataNode("app0JFIF");

            root.appendChild(jpegVariety);
            root.appendChild(markerSequence);
            jpegVariety.appendChild(app0JFIF);

            meta.mergeTree(metadataFormat, root);

            //Export jpg
            Files.deleteIfExists(pOut);
            ImageOutputStream ios = ImageIO.createImageOutputStream(pOut.toFile());
            writer.setOutput(ios);
            writer.write(meta, new IIOImage(bi, null, meta), param);
            log.debug("Succeded writing jpeg with writer: " + writer.getClass().toString());
            break;
        }catch(Exception e) {
            log.error("Failed writing jpeg with writer: " + (writer != null ? writer.getClass().toString():"null"));
            log.error("Ex: " + e);              
        }
    }

}

I'm getting an Exception thrown from JpegImageWriter, here is the relevant stack trace:

    Ex: javax.imageio.IIOException: Metadata components != number of destination bands
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=checkSOFBands,Line=-1
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=writeOnThread,Line=-1
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=write,Line=-1

Also I know that the Buffered Image is a TYPE_BYTE_BINARY, and the raster has 1 band (I printed this in a debug message above). So the Exception message would make me think that I need to define in the app0JFIF metadata that we are exporting 1 band. I don't know how to define this though, does anyone have any experience with this? This metadata is difficult to work with, or is it just me?

Thanks in advance.

Eric
  • 43
  • 5
  • I think the JPEG format is always 24 bit. – VGR Apr 11 '20 at 05:15
  • That is false. JPEG can use pixel bit depths of 8, 16, or 24. https://en.wikipedia.org/wiki/JPEG mentions this. – Eric Apr 11 '20 at 15:13
  • Can you point me to the exact section on the page which states this? I looked at it, and at a number of pages that talked about the JPEG format, and also at [Java’s JPEG metadata documentation](https://docs.oracle.com/en/java/javase/14/docs/api/java.desktop/javax/imageio/metadata/doc-files/jpeg_metadata.html), and none of them mention bit depths other than 8 per channel. – VGR Apr 11 '20 at 15:28
  • Right, perhaps it is always 8 bits per channel, but it is possible to use only one channel for a grayscale image. Check this out: https://stackoverflow.com/questions/51008883/is-there-a-grayscale-jpg-format Also, I have several JPG files on my machine that were created by another process that say bit-depth 8 when I right click and look at the Properties. Also if you do a search on the above Wiki document on "8-bit" there are several hits. – Eric Apr 11 '20 at 15:41

1 Answers1

0

You are correct about needing one band. Here’s how I did it:

if (bi.getSampleModel().getNumBands() != 1) {
    ColorModel colorModel = new ComponentColorModel(
        ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false,
        Transparency.OPAQUE, DataBuffer.TYPE_BYTE);

    BufferedImage oneBandedImage = new BufferedImage(colorModel,
        colorModel.createCompatibleWritableRaster(
            bi.getWidth(), bi.getHeight()),
        false, new Properties());

    Graphics g = oneBandedImage.createGraphics();
    g.drawImage(bi, 0, 0, null);
    g.dispose();

    bi = oneBandedImage;
}

After doing that, I didn’t need to directly obtain an ImageWriter and I didn’t need to set any metadata; ImageIO.write(bi, "JPEG", file) was sufficient.

I ran /usr/bin/file on the result, and got this:

JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 315x180, components 1

I assume the components 1 part means that it has only one channel.

VGR
  • 40,506
  • 4
  • 48
  • 63
  • Wow, it does work. I can't believe it is really that simple. Thank you for giving it a shot! That really helps me out. I have been dancing around this solution for a while, but haven't quite made contact yet. I think the only main difference is how you create the ColorModel first, then use it to create the BufferedImage, and the raster within. Anyways many many thanks, and I will definitely be using this approach now. – Eric Apr 11 '20 at 19:19