My Java code to convert a CMYK jpeg to RGB results in the output image being far too light - see code below. Can anyone suggest the correct way to do the conversion?
The following code requires Java Advanced Image IO to read the jpeg and example-cmyk.jpg
import java.awt.image.BufferedImage; import java.awt.image.ColorConvertOp; import java.io.File; import javax.imageio.ImageIO; public class TestCmykToRgb { public static void main(String[] args) throws Exception { BufferedImage cmykImage = ImageIO.read(new File( "j:\\temp\\example-cmyk.jpg")); BufferedImage rgbImage = new BufferedImage(cmykImage.getWidth(), cmykImage.getHeight(), BufferedImage.TYPE_INT_RGB); ColorConvertOp op = new ColorConvertOp(null); op.filter(cmykImage, rgbImage); ImageIO.write(rgbImage, "JPEG", new File("j:\\temp\\example-rgb.jpg")); } }
Can you convert RGB to CMYK without losing color? You cannot convert between RGB and CMYK without some amount of color difference of some sort. It's a good idea to do test prints of your work with a high quality printer to see how your colors turn out.
As a graphic designer, doing anything in color requires you to be at least somewhat familiar with the two most common color models: RGB (red, green, blue) and CMYK (cyan, magenta, yellow, black). Fundamentally, RGB is best for websites and digital communications, while CMYK is better for print materials.
There is a lot of good stuff in the existing answers already. But none of them is a complete solution that handles the different kinds of CMYK JPEG images.
For CMYK JPEG images, you need to distinguish between regular CMYK, Adobe CMYK (with inverted values, i.e. 255 for no ink and 0 for maximum ink) and Adobe CYYK (some variant with inverted colors as well).
This solution here requires Sanselan (or Apache Commons Imaging as it's called now) and it requires a reasonable CMYK color profile (.icc file). You can get the later one from Adobe or from eci.org.
public class JpegReader { public static final int COLOR_TYPE_RGB = 1; public static final int COLOR_TYPE_CMYK = 2; public static final int COLOR_TYPE_YCCK = 3; private int colorType = COLOR_TYPE_RGB; private boolean hasAdobeMarker = false; public BufferedImage readImage(File file) throws IOException, ImageReadException { colorType = COLOR_TYPE_RGB; hasAdobeMarker = false; ImageInputStream stream = ImageIO.createImageInputStream(file); Iterator<ImageReader> iter = ImageIO.getImageReaders(stream); while (iter.hasNext()) { ImageReader reader = iter.next(); reader.setInput(stream); BufferedImage image; ICC_Profile profile = null; try { image = reader.read(0); } catch (IIOException e) { colorType = COLOR_TYPE_CMYK; checkAdobeMarker(file); profile = Sanselan.getICCProfile(file); WritableRaster raster = (WritableRaster) reader.readRaster(0, null); if (colorType == COLOR_TYPE_YCCK) convertYcckToCmyk(raster); if (hasAdobeMarker) convertInvertedColors(raster); image = convertCmykToRgb(raster, profile); } return image; } return null; } public void checkAdobeMarker(File file) throws IOException, ImageReadException { JpegImageParser parser = new JpegImageParser(); ByteSource byteSource = new ByteSourceFile(file); @SuppressWarnings("rawtypes") ArrayList segments = parser.readSegments(byteSource, new int[] { 0xffee }, true); if (segments != null && segments.size() >= 1) { UnknownSegment app14Segment = (UnknownSegment) segments.get(0); byte[] data = app14Segment.bytes; if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e') { hasAdobeMarker = true; int transform = app14Segment.bytes[11] & 0xff; if (transform == 2) colorType = COLOR_TYPE_YCCK; } } } public static void convertYcckToCmyk(WritableRaster raster) { int height = raster.getHeight(); int width = raster.getWidth(); int stride = width * 4; int[] pixelRow = new int[stride]; for (int h = 0; h < height; h++) { raster.getPixels(0, h, width, 1, pixelRow); for (int x = 0; x < stride; x += 4) { int y = pixelRow[x]; int cb = pixelRow[x + 1]; int cr = pixelRow[x + 2]; int c = (int) (y + 1.402 * cr - 178.956); int m = (int) (y - 0.34414 * cb - 0.71414 * cr + 135.95984); y = (int) (y + 1.772 * cb - 226.316); if (c < 0) c = 0; else if (c > 255) c = 255; if (m < 0) m = 0; else if (m > 255) m = 255; if (y < 0) y = 0; else if (y > 255) y = 255; pixelRow[x] = 255 - c; pixelRow[x + 1] = 255 - m; pixelRow[x + 2] = 255 - y; } raster.setPixels(0, h, width, 1, pixelRow); } } public static void convertInvertedColors(WritableRaster raster) { int height = raster.getHeight(); int width = raster.getWidth(); int stride = width * 4; int[] pixelRow = new int[stride]; for (int h = 0; h < height; h++) { raster.getPixels(0, h, width, 1, pixelRow); for (int x = 0; x < stride; x++) pixelRow[x] = 255 - pixelRow[x]; raster.setPixels(0, h, width, 1, pixelRow); } } public static BufferedImage convertCmykToRgb(Raster cmykRaster, ICC_Profile cmykProfile) throws IOException { if (cmykProfile == null) cmykProfile = ICC_Profile.getInstance(JpegReader.class.getResourceAsStream("/ISOcoated_v2_300_eci.icc")); if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) { byte[] profileData = cmykProfile.getData(); if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) { intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first cmykProfile = ICC_Profile.getInstance(profileData); } } ICC_ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile); BufferedImage rgbImage = new BufferedImage(cmykRaster.getWidth(), cmykRaster.getHeight(), BufferedImage.TYPE_INT_RGB); WritableRaster rgbRaster = rgbImage.getRaster(); ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace(); ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null); cmykToRgb.filter(cmykRaster, rgbRaster); return rgbImage; } } static void intToBigEndian(int value, byte[] array, int index) { array[index] = (byte) (value >> 24); array[index+1] = (byte) (value >> 16); array[index+2] = (byte) (value >> 8); array[index+3] = (byte) (value); }
The code first tries to read the file using the regular method, which works for RGB files. If it fails, it reads the details of the color model (profile, Adobe marker, Adobe variant). Then it reads the raw pixel data (raster) and does all the necessary conversion (YCCK to CMYK, inverted colors, CMYK to RGB).
Update:
The original code has a slight problem: the result was too bright. The people from the twelvemonkeys-imageio project had the same problem (see this post) and have fixed it by patching the color profile such that Java uses a perceptual color render intent. The fix has been integrated into the above code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With