Search code examples
javaimagejpegjavax.imageiograyscale

Exporting 8 bit-dpeth JPEG with ImageIO


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.


Solution

  • 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.