使用 ImageIO 导出 8 位深度的 JPEG

Exporting 8 bit-dpeth JPEG with ImageIO

使用 Java ImageIO,是否可以导出位深度为 8 的 jpeg 图像?我该怎么做?即使导出灰度图像 TYPE_BYTE_BINARY 的 BufferedImage,结果也是位深度为 24 的 JPEG。

这就是我目前所拥有的。

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

}

我收到 JpegImageWriter 抛出的异常,这是相关的堆栈跟踪:

    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

我还知道缓冲图像是 TYPE_BYTE_BINARY,光栅有 1 个波段(我在上面的调试消息中打印了它)。所以异常消息会让我认为我需要在我们正在导出 1 个波段的 app0JFIF 元数据中定义。我不知道如何定义这个,有没有人有这方面的经验?这个元数据很难处理,还是只有我这样?

提前致谢。

你说的需要一支乐队是对的。这是我的做法:

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

这样做之后,我不需要直接获取ImageWriter,也不需要设置任何元数据; ImageIO.write(bi, "JPEG", file) 足够了。

我 运行 /usr/bin/file 结果,得到了这个:

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

我假设 components 1 部分意味着它只有一个频道。