Search code examples
javaswingbufferedimagegraphics2dcmyk

Write image in java with CMYK color space


I want to write an image in java using CMYK color space like this:

BufferedImage image= new BufferedImage(path.getBounds().width, 
path.getBounds().height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
color c = new Color(ColorSpaces.getDeviceCMYKColorSpace(), new float[] 
{1.0f,1.0f,1.0f,1.0f}, 1.0f)
g2d.setPaint(c);
g2d.fill(path);     
g2d.draw(path);
g2d.dispose();

Then I use imageIO to write the image into JPEG, however, the resulted image, when converted to PDF, does no have the CMYK color I provided in the code, instead, it has the following

CMYK Values

My questions are:

  1. can I use BufferedImage to write images with CMYK color space ?
  2. How do I write image using CMYK color space if the components values are 0,0,0,1.0 and alpha is 1 for example

Please note that I have tried EPS and it worked fine, however, I don't feel comfortable working with EPS at this stage of my project.


Solution

    1. Yes, you can. You need to create a BufferedImage in the CMYK color space. Then paint on that. You can't use any of the standard BufferedImage.TYPE_* types, as they are all gray or RGB. See code below.

    2. You can write a CMYK JPEG using ImageIO. However, you need to add some extra details to the meta data and massage the pixel data a little to do this, otherwise the image will be written as either RGBA or inverted CMYK. Again, see below for code.

    Here's a full, runnable, proof of concept code example:

    public class CMYKTest {
    
        public static final String JAVAX_IMAGEIO_JPEG_IMAGE_1_0 = "javax_imageio_jpeg_image_1.0";
    
        public static void main(String[] args) throws IOException {
            // I'm using my own TwelveMonkeys ImageIO library for this, 
            // but I think you can use the one you used above, like:
            // ColorSpace cmykCS = ColorSpaces.getDeviceCMYKColorSpace()
            ColorSpace cmykCS = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
    
            // Create CMYK color model, raster and image
            ColorModel colorModel = new ComponentColorModel(cmykCS, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
            BufferedImage image = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(100, 100), colorModel.isAlphaPremultiplied(), null);
    
            // Paint some sample rectangles on it
            Graphics2D g = image.createGraphics();
            try {
                g.setColor(new Color(cmykCS, new float[] {0, 0, 0, 0}, 1.0f)); // All 0 (White)
                g.fillRect(0, 0, 25, 50);
                g.setColor(new Color(cmykCS, new float[] {0, 0, 0, 1}, 1.0f)); // Key (Black)
                g.fillRect(25, 0, 25, 50);
                g.setColor(new Color(cmykCS, new float[] {1, 0, 0, 0}, 1.0f)); // Cyan
                g.fillRect(50, 0, 50, 50);
                g.setColor(new Color(cmykCS, new float[] {0, 1, 0, 0}, 1.0f)); // Magenta
                g.fillRect(0, 50, 50, 50);
                g.setColor(new Color(cmykCS, new float[] {0, 0, 1, 0}, 1.0f)); // Yellow
                g.fillRect(50, 50, 50, 50);
            }
            finally {
                g.dispose();
            }
    
            // Write it as a JPEG, using ImageIO    
            try (ImageOutputStream stream = ImageIO.createImageOutputStream(new File("cmyk.jpg"))) {
                ImageWriter writer = ImageIO.getImageWritersByFormatName("JPEG").next();
                writer.setOutput(stream);
    
                // We need to massage the image metadata a little to be able to write CMYK
                ImageWriteParam param = writer.getDefaultWriteParam();
                IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(image), param);
    
                IIOMetadataNode jpegMeta = new IIOMetadataNode(JAVAX_IMAGEIO_JPEG_IMAGE_1_0);
                jpegMeta.appendChild(new IIOMetadataNode("JPEGVariety")); // Just leave as default
    
                IIOMetadataNode markerSequence = new IIOMetadataNode("markerSequence");
                jpegMeta.appendChild(markerSequence);
    
                // The APP14 "Adobe" marker acts as a trigger for decoders, to
                // specify 4 channels as CMYK or YCCK (instead of RGBA or YCCA).
                IIOMetadataNode app14Adobe = new IIOMetadataNode("app14Adobe");
                app14Adobe.setAttribute("transform", "0"); // 0 means "unknown"
                markerSequence.appendChild(app14Adobe);
    
                // You could also append an ICC profile as part of the JPEG metadata
                // if you feel adventurous...
    
                // Merge with metadata from the writer
                metadata.mergeTree(JAVAX_IMAGEIO_JPEG_IMAGE_1_0, jpegMeta);
    
                // Also, we need to massage the raster a little, as CMYK data is
                // written in "inverse" form. 
                // We could use image.getRaster() here to get better performance
                // if you don't mind the image being inverted in memory too. 
                // image.getData() creates a copy, and is safe from this side effect.
                Raster raster = image.getData(); 
                byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
                // Inverse the pixel data
                for (int i = 0; i < data.length; i++) {
                    data[i] = (byte) (255 - data[i]);
                }
    
                // Finally, write it all
                writer.write(null, new IIOImage(raster, null, metadata), param);
            }
        }
    }
    

    Note on reading CMYK JPEG images using ImageIO

    The above code will write a JPEG image the standard JPEGImageReader will not be able to read (other than through the readRaster() method).

    For better/easier CMYK JPEG support, I suggest using the TwelveMonkeys ImageIO JPEG plugin (I'm the author of this plugin).

    Small print about CMYK and transparency

    You can also create a CMYK color model/image with transparency (alpha channel), like this:

    ColorModel colorModel = new ComponentColorModel(cmykCS, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
    BufferedImage image = new BufferedImage(colorModel, colorModel.createCompatibleWritableRaster(100, 100), colorModel.isAlphaPremultiplied(), null);
    

    But, unfortunately, the JPEGImageWriter can't handle more than 4 channels of data. I get an IndexArrayOutOfBoundsException in an array that has the comment (IJG is the Independent JPEG Group, who develops libjpeg):

    /** IJG can handle up to 4-channel JPEGs */
    

    So, unfortunately, there's no easy fix for this.