Mike Cantrell
Mike Cantrell

Reputation: 726

BufferedImage reports incorrect color model type

I'm using some third party software to convert images to PDF and I noticed some inflated file sizes. After some digging, I confirmed that the color model for the images was not preserved. Black and white (1bit) images were being converted to RGB color models.

Digging through the library shows some color model detection:

switch (awtColorSpace.getType()) {
    case ColorSpace.TYPE_RGB:
        return PDDeviceRGB.INSTANCE;
    case ColorSpace.TYPE_GRAY:
        return PDDeviceGray.INSTANCE;
    case ColorSpace.TYPE_CMYK:
        return PDDeviceCMYK.INSTANCE;
    default:
        throw new UnsupportedOperationException("color space not implemented: "
                + awtColorSpace.getType());
}

These images were always coming back as RGB. I decided to write some tests and they appear to confirm this:

package com.acme;

import org.junit.Test;

import javax.imageio.ImageIO;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

import static org.junit.Assert.*;

public class ImageColorDetectionTest {

    @Test
    public void colorImage() throws Exception {
        // Colorspace: sRGB, Depth: 8-bit, Channel depth: Red: 8-bit Green: 8-bit Blue: 8-bit
        BufferedImage image = readImage("/color.png");
        assertEquals(ColorSpace.TYPE_RGB, image.getColorModel().getColorSpace().getType());
    }

    @Test
    public void greyscaleImage() throws Exception {
        // Colorspace: Gray, Depth: 8-bit, Channel depth: Gray: 8-bit
        BufferedImage image = readImage("/greyscale.png");
        assertEquals(ColorSpace.TYPE_GRAY, image.getColorModel().getColorSpace().getType());
    }

    @Test
    public void blackAndWhiteImage() throws Exception {
        // Colorspace: Gray, Depth: 8/1-bit, Channel depth: Gray: 1-bit
        BufferedImage image = readImage("/bw.png");
        assertEquals(ColorSpace.TYPE_GRAY, image.getColorModel().getColorSpace().getType());
    }

    protected BufferedImage readImage(String path) throws IOException {
        try (InputStream content = this.getClass().getResourceAsStream(path)) {
            return ImageIO.read(content);
        }
    }

}

The blackAndWhiteImage test always fails. The color space type is 5 (RGB). Is this a bug in the JDK or am I missing something fundamental here?

Test images:

Colorspace: sRGB, Depth: 8-bit, Channel depth: Red: 8-bit Green: 8-bit Blue: 8-bit color.png

Colorspace: Gray, Depth: 8-bit, Channel depth: Gray: 8-bit greyscale.png

Colorspace: Gray, Depth: 8/1-bit, Channel depth: Gray: 1-bit bw.png

Imagemagick Identification:

magick identify -verbose bw.png
Image: bw.png
  Format: PNG (Portable Network Graphics)
  Mime type: image/png
  Class: PseudoClass
  Geometry: 329x247+0+0
  Units: Undefined
  Type: Bilevel
  Base type: Palette
  Endianess: Undefined
  Colorspace: Gray
  Depth: 8/1-bit
  Channel depth:
    Gray: 1-bit
  Channel statistics:
    Pixels: 81263
    Gray:
      min: 0 (0)
      max: 255 (1)
      mean: 110.66 (0.433961)
      standard deviation: 126.384 (0.495623)
      kurtosis: -1.92901
      skewness: 0.266484
      entropy: 0.98738
  Colors: 2
  Histogram:
     45998: (  0,  0,  0) #000000 gray(0)
     35265: (255,255,255) #FFFFFF gray(255)
  Colormap entries: 2
  Colormap:
         0: (  0,  0,  0,255) #000000FF graya(0,1)
         1: (255,255,255,255) #FFFFFFFF graya(255,1)
  Rendering intent: Undefined
  Gamma: 0.45455
  Chromaticity:
    red primary: (0.64,0.33)
    green primary: (0.3,0.6)
    blue primary: (0.15,0.06)
    white point: (0.3127,0.329)
  Matte color: grey74
  Background color: white
  Border color: srgb(223,223,223)
  Transparent color: none
  Interlace: None
  Intensity: Undefined
  Compose: Over
  Page geometry: 329x247+0+0
  Dispose: Undefined
  Iterations: 0
  Compression: Zip
  Orientation: Undefined
  Properties:
    date:create: 2017-06-22T09:33:09-05:00
    date:modify: 2017-06-22T09:33:09-05:00
    png:bKGD: chunk was found (see Background color, above)
    png:cHRM: chunk was found (see Chromaticity, above)
    png:gAMA: gamma=0.45455 (See Gamma, above)
    png:IHDR.bit-depth-orig: 1
    png:IHDR.bit_depth: 1
    png:IHDR.color-type-orig: 0
    png:IHDR.color_type: 0 (Grayscale)
    png:IHDR.interlace_method: 0 (Not interlaced)
    png:IHDR.width,height: 329, 247
    png:text: 2 tEXt/zTXt/iTXt chunks were found
    png:tIME: 2017-06-21T10:12:36Z
    signature: 689d59f57ef9b4d58011f92e26f937d9d58cf1ca1ccbcaad6bad7bdd0552fcfa
  Artifacts:
    verbose: true
  Tainted: False
  Filesize: 3.69KB
  Number pixels: 81.3K
  User time: 0.000u
  Elapsed time: 0:01.000
  Version: ImageMagick 7.0.5-4 Q16 x86_64 2017-03-25 http://www.imagemagick.org

Upvotes: 3

Views: 2557

Answers (2)

Harald K
Harald K

Reputation: 27094

I see you already have an accepted answer, but I'll try to explain this anyway.. This is not a bug. But I agree, it is sometimes counter-intuitive.

Your test uses the color space of the decoded in-memory representation of the image, and compares that with the expected color space from the encoded file. When a file is decoded (using ImageIO.read in your example), the ImageReader plugin will typically convert the image to an in-memory representation that is fast to paint on screen. This may be quite different from the representation that is most space efficient when stored on disk.

As an example, a grayscale image using less than 8 bits per sample, is usually converted to IndexColorModel, even if the PNG file did not contain an PLTE chunk. And, IndexColorModel always uses sRGB color space (RGB type), even if it only contains gray values. This doesn't matter for the displayed pixels, which will be black and white regardless, but it does matter for your test.

It is possible to get the color space that was actually encoded in the file, using the ImageIO API:

try (ImageInputStream content = ImageIO.createImageInputStream(this.getClass().getResourceAsStream(path))) {
    ImageReader reader = ImageIO.getImageReaders(input).next(); // Assumes PNGImageReader is always there
    reader.setInput(input);

    IIOMetadata metadata = reader.getImageMetadata(0);
    Node nativeTree = metadata.getAsTree(metadata.getNativeMetadataFormatName());
    Node standardTree = metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);

    // ... Get color space information as needed using DOM traversal
}

I skipped reading the actual values as it gets quite verbose, but it's pretty straight forward. All values are Strings. See the IIOMetadata class documentation for details.

The metadata for the bw.png file contains the following, in two different output representations.

Native meta data:

<javax_imageio_png_1.0>
        <IHDR width="329" height="247" bitDepth="1" colorType="Grayscale" compressionMethod="deflate" filterMethod="adaptive" interlaceMethod="none"/>
        <bKGD>
                <bKGD_Grayscale gray="1"/>
        </bKGD>
        <cHRM whitePointX="31270" whitePointY="32900" redX="64000" redY="33000" greenX="30000" greenY="60000" blueX="15000" blueY="6000"/>
        <gAMA value="45455"/>
        <tIME year="2017" month="6" day="21" hour="10" minute="12" second="36"/>
</javax_imageio_png_1.0>

Standard "plugin-neutral" meta data (skipping irrelevant values):

<javax_imageio_1.0>
        <Chroma>
                <ColorSpaceType name="GRAY"/>
                <NumChannels value="1"/>
                <Gamma value="0.45455"/>
                <BlackIsZero value="TRUE"/>
                <BackgroundColor red="1" green="1" blue="1"/>
        </Chroma>
        <Compression ... />
        <Data>
                <PlanarConfiguration value="PixelInterleaved"/>
                <SampleFormat value="UnsignedIntegral"/>
                <BitsPerSample value="1"/>
        </Data>
        <Dimension ... />
        <Document ... />
        <Transparency>
                <Alpha value="none"/>
        </Transparency>
</javax_imageio_1.0>

If your actual images are TIFFs or multiple other formats, it's probably best to use the standard metadata format, by getting the ColorSpaceType name from the Chroma node.

Upvotes: 6

TheCoolah
TheCoolah

Reputation: 529

I think this is coming from your bw.png file. As I understand it, 1-bit PNG's are either grayscale or indexed (palette) and indexed will use the RGB space, so you'll have 2 colors (#000000 and #ffffff). Check whatever tool you're using to create the PNG and see if it gives you a choice between grayscale and indexed. You might also want to look at the PNG chunks to verify the file is created as you expect it to be.

This might be useful: TweakPNG

Upvotes: 3

Related Questions