0

I need to convert JPG images provided by customers to sRGB format (sRGB IEC61966-2.1) to make them ready for web.

I can do it successfully with ImageIO and BufferedImage but this operation is really slow:

val srgbSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB)
val colorConversionOperation = new ColorConvertOp(srgbSpace, null)
val converted = colorConversionOperation.filter(inputImage, null)

(already tried providing RenderingHints to ColorConvertOp - it doesn't help)

As far as I understand, the culprit is BufferedImage and I need to work on Rasters to speed things up:

val iccProfile = ICC_Profile.getInstance(ColorSpace.CS_sRGB)
val iccColorSpace = new ICC_ColorSpace(iccProfile)
val sourceColorSpace = inputImage.getColorModel.getColorSpace

val colorConversionOperation = new ColorConvertOp(sourceColorSpace, iccColorSpace, null)
val converted = colorConversionOperation.filter(inputImage.getRaster, null)

This indeed gives a huge performance boost, but I don't know how to save the Raster so that the final image contain appropriate Color Space and Color Profile information.

Currently I create the BufferedImage from Raster in the following way:

val outImage = new BufferedImage(ColorModel.getRGBdefault, converted, false, null);

When I work with BufferedImage, the final JPG is saved as:

Color space: RGB
Color profile: sRGB IEC61966-2.1

which is correct in this case.

When I work with Rasters, the final JPG is saved just as:

Color space: RGB

Which means I have lost Color profile information. I think it's because I do ColorModel.getRGBdefault when transforming Raster to BufferedImage but I have no idea how to get an instance of ColorModel for sRGB IEC61966-2.1.

omnomnom
  • 8,911
  • 4
  • 41
  • 50
  • 1
    `ColorModel.getRGBdefault` is always in sRGB color space (as stated in the API doc). But I agree, it's strange that the output JPEG is different... The thing is, (assuming you are using `ImageIO` and its `JPEGImageWriter` to write the images) the ICC color profile is *not* written by default if the color space is sRGB. For some reason, the color space in the `converted` image is identical to sRGB, but the `isCS_sRGB()` method returns `false` (not sure why), and this is why it's written. I think you can force the ICC profile to be written, by passing it in the metadata (`IIOMetadata` class). – Harald K Mar 20 '17 at 15:12
  • @haraldK Are you aware if any example of passing ICC profile to IIOMedatada is available somewhere? I am looking at IIOMetadata/JPEGMetadata and I have no idea how to pass ICC profile there. I have twelvemonkeys ImageIO on my classpath if that can be any help here. – omnomnom Mar 20 '17 at 15:26

1 Answers1

1

Here's a "short" sample code that shows how to "force" the JPEGImageWriter to output an ICC profile segment in the output JPEG file, even if the image is in sRGB color space (it happens automatically if the image isn't in sRGB color space, as you saw from your testing).

public class ICCTest {
    public static void main(String[] args) throws IOException {
        // Fist let's assume this isn't already in sRGB (it is...)
        BufferedImage inputImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR);

        // Conversion similar to yours
        ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
        WritableRaster raster = new ColorConvertOp(inputImage.getColorModel().getColorSpace(), sRGB, null)
            .filter(inputImage.getRaster(), null);

        ComponentColorModel cm = new ComponentColorModel(sRGB, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
        BufferedImage converted = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);

        // Just for comparison    
        ImageIO.write(converted, "JPEG", new File("converted.jpg"));

        // Write image with forced ICC profile
        ImageWriter writer = ImageIO.getImageWritersByFormatName("JPEG").next();
        try (ImageOutputStream out = ImageIO.createImageOutputStream(new File("converted_icc.jpg"))) {
            writer.setOutput(out);

            ImageWriteParam param = writer.getDefaultWriteParam();
            IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(converted2), param);
            metadata.mergeTree("javax_imageio_jpeg_image_1.0", createICCTree((ICC_ColorSpace) sRGB));

            writer.write(null, new IIOImage(converted, null, metadata), param);
        }
        writer.dispose();
    }

    // Create a minimal IIOMetadata tree, containing an ICC profile
    private static IIOMetadataNode createICCTree(ColorSpace cs) {
        IIOMetadataNode root = new IIOMetadataNode("javax_imageio_jpeg_image_1.0");

        IIOMetadataNode jpegVariety = new IIOMetadataNode("JPEGvariety");
        root.appendChild(jpegVariety);
        root.appendChild(new IIOMetadataNode("markerSequence")); // Must be present, even if empty...

        IIOMetadataNode app0JFIF = new IIOMetadataNode("app0JFIF");
        jpegVariety.appendChild(app0JFIF);

        IIOMetadataNode icc = new IIOMetadataNode("app2ICC");
        app0JFIF.appendChild(icc);

        icc.setUserObject(cs.getProfile());

        return root;
    }
}

You can read more about the JPEG metadata format in the documentation.

PS: I think most modern web browsers will treat a JPEG with no profile as if it was already in sRGB (although this is wrong, according to the JFIF specification). So it might be that you don't need to actually output the ICC profile, as long as you do the colorspace conversion.

Harald K
  • 26,314
  • 7
  • 65
  • 111
  • Thanks a bunch. I am not familiar with image manipulation in Java and getting to the moment when `Color Profile` is properly added to metadata would take me a month without your help. Cheers! – omnomnom Mar 20 '17 at 22:17