6

Similar question has been asked many times. But I still don't get why I get too dark output after I converted a picture with ICC_Profile. I've tried many profiles: from Adobe site, and from the picture itself.

Before Image

Before Image

After Image

After Image

Code

Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpeg");
ImageReader reader = null;
while (readers.hasNext()){
      reader = readers.next();
      if (reader.canReadRaster()){
          break;
      }
}
// read
ImageInputStream ios = ImageIO.createImageInputStream(new FileInputStream(new File(myPic.jpg)));
reader.setInput(ios);
Raster r = reader.readRaster(0, null);

BufferedImage result = new BufferedImage(r.getWidth(), r.getHeight(), bufferedImage.TYPE_INT_RGB);
WritableRaster resultRaster = result.getRaster();
ICC_Profile iccProfile = ICC_Profile.getInstance(new File("profile_name.icc");
ColorSpace cs = new ICC_ColorSpace(iccProfile);
ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null);
cmykToRgb.filter(r, resultRaster);

// write
ImageIo.write(resul, "jpg", new File("myPic.jpg"));

Do I have to do something else after I have converted the picture?

codingbadger
  • 42,678
  • 13
  • 95
  • 110
nixspirit
  • 509
  • 1
  • 7
  • 19
  • Excuse me, but your question is not full. Where do you get the image, where do you put it, how looks the profile_name.icc file... – Gangnus Nov 14 '11 at 10:08
  • hm. I got this image from a designer. It was created using CMYK profile. This profile is builtin in the picture itself. I have tried 2 ways: 1. downloaded a list of icc_profiles from Adobe site and use the code above; 2. extracted the picture's profile with Sanselan and use the code above. Both those ways produce the same result, you can see it here "before" and "after". I hope it'll make the problem clear – nixspirit Nov 14 '11 at 10:30
  • Sorry for incorrect questions. I mean not where YOU presonally had got the image :-), but where your code takes it. Is it r? How it is read? Where is its definition? The same about the output. – Gangnus Nov 14 '11 at 10:40
  • @nixspirit: Though your question is several months old, I've posted a new answer that explains most of the underlying problems and includes a working solution. The dark colors are mainly due to an old Photoshop bug (CMYK values are inverted) that has now become a defacto standard that's handled by most JPEG software (except Java). – Codo Aug 26 '12 at 18:04

4 Answers4

24

This question isn't exactly new. But since I spent a lot of time on the problem and came up with a working solution, I thought I'll post it here. The solution 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.

The basic problem is that Java - out of the box - can only read JPEG files in RGB. If you have a CMYK file, 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).

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

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

I'm not quite satisfied with my solution. While the colors are mostly good, dark areas are slightly too bright, in particular black isn't fully black. If anyone knows what I could improve, I'd be glad to hear it.

Update:

I've figured out how to fix the brightness issues. Or rather: the people from the twelvemonkeys-imageio project have (see this post). It has to do with the color rendering intent.

There fix is to add the following lines which nicely work for me. Basically, the color profile is modified because there seems to be no other way to tell the ColorConvertOp class to use a perceptual color render intent.

    if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY) {
        byte[] profileData = cmykProfile.getData(); // Need to clone entire profile, due to a JDK 7 bug

        if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual) {
            intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass); // Header is first

            cmykProfile = ICC_Profile.getInstance(profileData);
        }
    }

...

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);
}
Codo
  • 75,595
  • 17
  • 168
  • 206
  • 2
    I've tried your code with this image: http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg I have used the latest Sanselan without providing any color profile (how should I provide it?). The code works but resulting colors are brighter than the original ones. It's not enough to go to production for me. Thank you, anyway. – Pino Sep 06 '12 at 10:30
  • I am aware of this (see the last paragraph of my answer). If you find out what the cause is (I might have a mistake in my code), please let me know. – Codo Sep 06 '12 at 11:42
  • 1
    @Pino: I've discovered a fix for the brightness issue on the net. See my update. – Codo Sep 06 '12 at 19:41
  • Interesting, but you have missed the intToBigEndian() method. – Pino Sep 07 '12 at 13:26
  • I've added the missing method. – Codo Sep 09 '12 at 15:50
  • @Codo Can I confirm that the update code listing should be inserted just after cmykProfile is initialised in the convertCmykToRgb() method? – andyroberts Oct 16 '12 at 21:53
  • Thank you so much for this! Any pointers on where/how to save JPEG CMYK images with the right headers? – jedierikb Dec 13 '12 at 16:06
  • Recent JpegImageParser now returns App14Segment instead of UnknownSegment – Alex K. Feb 16 '21 at 05:57
2

Like I said, the idea was to convert CMYK pictures to RGB, and use them in my application.

But for some reason ConvertOp doesn't do any CMYK to RGB conversion. It reduces numBand numbers to 3 and that's it. And I decided to try CMYKtoRGB algorithms.

i.e. Get an image, recognize its ColorSpace and read it or convert it.

Also another problem was Photoshop. This quote I found on the internet.

In the case of adobe it includes the CMYK profile in the metadata, but then saves the raw image data as inverted YCbCrK colors.

Finally I could achieve my goal with this algorithm below. I don't use icc_profiles so far, the output looks a little bit darker.. I got proper RGB images which looks fine.

pseudocode

BufferedImage result = null;
Raster r = reader.readRaster()
if (r.getNumBands != 4){
    result = reader.read(0);
} else {

   if (isPhotoshopYCCK(reader)){
       result = YCCKtoCMYKtoRGB(r);
   }else{
      result = CMYKtoRGB(r);
   }
}

private boolean isPhotoshopYCCK(reader){
    // read IIOMetadata from reader and according to
    // http://download.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html decide which ColorSpace is used
    // or maybe there is another way to do it
    int transform = ... // 2 or 0 or something else
    return transform;
}    

I doesn't make any sense to show YCCKtoCMYKtoRGB or CMYKtoRGB algorithms. It is easy to find on the internet.

halfer
  • 19,824
  • 17
  • 99
  • 186
nixspirit
  • 509
  • 1
  • 7
  • 19
0

If you had the same problems on two absolutely different ways with the same profile file, I think, the file(profile_name.icc) is not OK.

Gangnus
  • 24,044
  • 16
  • 90
  • 149
  • I downloaded about 20 profiles from [adobe](http://www.adobe.com/support/downloads/detail.jsp?ftpID=3680). Tried each one and nothing is changed. – nixspirit Nov 14 '11 at 10:55
  • Also I extracted a profile from the picture itself with [Sanselan](http://commons.apache.org/sanselan/). and created like this `ICC_Profile iccProfile = Sanselan.getICCProfile(byteArrayStreamFromFile);` Result is the same – nixspirit Nov 14 '11 at 10:58
  • it looks like this code `ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null); cmykToRgb.filter(r, resultRaster);` doesn't convert that raster from CMYK to RGB colorSpace. – nixspirit Nov 15 '11 at 06:47
0
  1. You are writing the result, given in CMYK, as Jpeg, i.e. as if it is written in RGB format. So, the error, as I see it, is OUT of the piece of code you are looking it in :-)

  2. correct resul to result in the last line.

Gangnus
  • 24,044
  • 16
  • 90
  • 149
  • sorry I can't edit my post or I have to remove the pictures. I am a new user here and some of the moderators added those pictures in the post for me – nixspirit Nov 15 '11 at 02:49
  • But the raster suppose to be converted from CMYK to RGB with `ColorConvertOp cmykToRgb = new ColorConvertOp(cs, result.getColorModel().getColorSpace(), null); cmykToRgb.filter(r, resultRaster)` and the result image holds that result raster – nixspirit Nov 15 '11 at 03:02
  • I think, that you had converted it OK, but now, after convertion, it is written in different format - CMYK, 4 color coordinates instead of 3. And you can't show it as usualy. And you can't work with it as with an image in RPG. So, your CONVERSION is OK. But the part after it belongs to the other task altogether. – Gangnus Nov 17 '11 at 10:30
  • I put +1 on your question, so you'll have more points and hopefully, soon you'll can edit. BTW, the question really IS interesting - it appeared before, the answer was as you have put it in the code, but you have found the further problem. – Gangnus Nov 17 '11 at 10:33
  • _But the part after it belongs to the other task altogether_ agreed. When I have done with this question I'll put the complete answer and some explanation – nixspirit Nov 17 '11 at 10:58
  • What are you trying to to REALLY? You are making CMYK, it is for PRINT ONLY. So, the only sensible output format is pdf or some postscript. – Gangnus Nov 17 '11 at 15:08
  • The idea was to convert CMYK picture to RGB one, and use it in my application. i.e. I get either CMYK or RGB pictures from user and convert to RGB – nixspirit Nov 21 '11 at 15:59