Reputation: 726
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
Colorspace: Gray, Depth: 8-bit, Channel depth: Gray: 8-bit
Colorspace: Gray, Depth: 8/1-bit, Channel depth: Gray: 1-bit
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
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 String
s. 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
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