8274735: javax.imageio.IIOException: Unsupported Image Type  while processing a valid JPEG image

Backport-of: f8a164915fff5e9e8f3c9c1996b51e7e4fe5d68d
diff --git a/src/java.desktop/share/classes/com/sun/imageio/plugins/common/SimpleCMYKColorSpace.java b/src/java.desktop/share/classes/com/sun/imageio/plugins/common/SimpleCMYKColorSpace.java
index 95c4d9e..26a318c 100644
--- a/src/java.desktop/share/classes/com/sun/imageio/plugins/common/SimpleCMYKColorSpace.java
+++ b/src/java.desktop/share/classes/com/sun/imageio/plugins/common/SimpleCMYKColorSpace.java
@@ -63,7 +63,7 @@
     }
 
     public int hashCode() {
-        return theInstance.hashCode();
+        return System.identityHashCode(theInstance);
     }
 
     public float[] toRGB(float[] colorvalue) {
diff --git a/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageReader.java b/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageReader.java
index 9d8cd29..717af6d 100644
--- a/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageReader.java
+++ b/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageReader.java
@@ -35,6 +35,7 @@
 import javax.imageio.plugins.jpeg.JPEGImageReadParam;
 import javax.imageio.plugins.jpeg.JPEGQTable;
 import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
+import com.sun.imageio.plugins.common.SimpleCMYKColorSpace;
 
 import java.awt.Point;
 import java.awt.Rectangle;
@@ -164,6 +165,12 @@
     /** If we need to post-convert in Java, convert with this op */
     private ColorConvertOp convert = null;
 
+    /** If reading CMYK as an Image, flip the bytes */
+    private boolean invertCMYK = false;
+
+    /** Whether to read as a raster */
+    private boolean readAsRaster = false;
+
     /** The image we are going to fill */
     private BufferedImage image = null;
 
@@ -938,6 +945,32 @@
         ArrayList<ImageTypeProducer> list = new ArrayList<ImageTypeProducer>(1);
 
         switch (colorSpaceCode) {
+        case JPEG.JCS_YCCK:
+        case JPEG.JCS_CMYK:
+            // There's no standard CMYK ColorSpace in JDK so raw.getType()
+            // will return null so skip that.
+            // And we can't add RGB because the number of bands is different.
+            // So need to create our own special that is 4 channels and uses
+            // the iccCS ColorSpace based on profile data in the image, and
+            // if there is none, on the internal CMYKColorSpace class
+            if (iccCS == null) {
+                iccCS = SimpleCMYKColorSpace.getInstance();
+            }
+            if (iccCS != null) {
+                list.add(new ImageTypeProducer(colorSpaceCode) {
+                    @Override
+                    protected ImageTypeSpecifier produce() {
+                        int [] bands = {0, 1, 2, 3};
+                        return ImageTypeSpecifier.createInterleaved
+                         (iccCS,
+                          bands,
+                          DataBuffer.TYPE_BYTE,
+                          false,
+                          false);
+                    }
+                });
+            }
+            break;
         case JPEG.JCS_GRAYSCALE:
             list.add(raw);
             list.add(getImageType(JPEG.JCS_RGB));
@@ -1019,6 +1052,8 @@
         int csType = cs.getType();
         convert = null;
         switch (outColorSpaceCode) {
+        case JPEG.JCS_CMYK:  // Its CMYK in the file
+            break;
         case JPEG.JCS_GRAYSCALE:  // Its gray in the file
             if  (csType == ColorSpace.TYPE_RGB) { // We want RGB
                 // IJG can do this for us more efficiently
@@ -1144,6 +1179,8 @@
     private Raster readInternal(int imageIndex,
                                 ImageReadParam param,
                                 boolean wantRaster) throws IOException {
+
+        readAsRaster = wantRaster;
         readHeader(imageIndex, false);
 
         WritableRaster imRas = null;
@@ -1186,6 +1223,16 @@
             image = null;
         }
 
+         // Adobe seems to have decided that the bytes in CMYK JPEGs
+         // should be stored inverted. So we need some extra logic to
+         // flip them in that case. Don't flip for the raster case
+         // so code that is reading these as rasters today won't
+         // see a change in behaviour.
+         invertCMYK =
+             (!wantRaster &&
+              ((colorSpaceCode == JPEG.JCS_YCCK) ||
+               (colorSpaceCode == JPEG.JCS_CMYK)));
+
         // Create an intermediate 1-line Raster that will hold the decoded,
         // subsampled, clipped, band-selected image data in a single
         // byte-interleaved buffer.  The above transformations
@@ -1364,6 +1411,21 @@
      * After the copy, we notify update listeners.
      */
     private void acceptPixels(int y, boolean progressive) {
+
+        /*
+         * CMYK JPEGs seems to be universally inverted at the byte level.
+         * Fix this here before storing.
+         * For "compatibility" don't do this if the target is a raster.
+         * Need to do this here in case the application is listening
+         * for line-by-line updates to the image.
+         */
+        if (invertCMYK) {
+            byte[] data = ((DataBufferByte)raster.getDataBuffer()).getData();
+            for (int i = 0, len = data.length; i < len; i++) {
+                data[i] = (byte)(0x0ff - (data[i] & 0xff));
+            }
+        }
+
         if (convert != null) {
             convert.filter(raster, raster);
         }
diff --git a/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageWriter.java b/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageWriter.java
index 720e970..86825a4 100644
--- a/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageWriter.java
+++ b/src/java.desktop/share/classes/com/sun/imageio/plugins/jpeg/JPEGImageWriter.java
@@ -132,6 +132,7 @@
     private int newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
     private boolean writeDefaultJFIF = false;
     private boolean writeAdobe = false;
+    private boolean invertCMYK = false;
     private JPEGMetadata metadata = null;
 
     private boolean sequencePrepared = false;
@@ -655,6 +656,7 @@
         newAdobeTransform = JPEG.ADOBE_IMPOSSIBLE;  // Change if needed
         writeDefaultJFIF = false;
         writeAdobe = false;
+        invertCMYK = false;
 
         // By default we'll do no conversion:
         int inCsType = JPEG.JCS_UNKNOWN;
@@ -808,6 +810,14 @@
                                 }
                             }
                             break;
+                         case ColorSpace.TYPE_CMYK:
+                             outCsType = JPEG.JCS_CMYK;
+                             if (jfif != null) {
+                                 ignoreJFIF = true;
+                                 warningOccurred
+                                 (WARNING_IMAGE_METADATA_JFIF_MISMATCH);
+                             }
+                             break;
                         }
                     }
                 } // else no dest, metadata, not an image.  Defaults ok
@@ -1014,6 +1024,11 @@
             System.out.println("outCsType: " + outCsType);
         }
 
+        invertCMYK =
+            (!rasterOnly &&
+             ((outCsType == JPEG.JCS_YCCK) ||
+              (outCsType == JPEG.JCS_CMYK)));
+
         // Note that getData disables acceleration on buffer, but it is
         // just a 1-line intermediate data transfer buffer that does not
         // affect the acceleration of the source image.
@@ -1724,6 +1739,12 @@
                                         srcBands);
         }
         raster.setRect(sourceLine);
+        if (invertCMYK) {
+            byte[] data = ((DataBufferByte)raster.getDataBuffer()).getData();
+            for (int i = 0, len = data.length; i < len; i++) {
+                data[i] = (byte)(0x0ff - (data[i] & 0xff));
+            }
+        }
         if ((y > 7) && (y%8 == 0)) {  // Every 8 scanlines
             cbLock.lock();
             try {
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/CMYKJPEGTest.java b/test/jdk/javax/imageio/plugins/jpeg/CMYK/CMYKJPEGTest.java
new file mode 100644
index 0000000..db4588d
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/CMYKJPEGTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ *
+ * This test verifies that using the built-in ImageI/O JPEG plugin that JPEG images
+ * that are in a CMYK ColorSpace can be read into a BufferedImage using the convemience
+ * APIS and that and the colours are properly interpreted.
+ * Since there is no standard JDK CMYK ColorSpace, this requires that either the image
+ * contain an ICC_Profile which can be used by the plugin to create an ICC_ColorSpace
+ * or that the plugin provides a suitable default CMYK ColorSpace instance by some other means.
+ *
+ * The test further verifies that the resultant BufferedImage will be re-written as a CMYK
+ * BufferedImage. It can do this so long as the BufferedImage has that CMYK ColorSpace
+ * used by its ColorModel.
+ *
+ * The verification requires re-reading again the re-written image and checking the
+ * re-read image still has a CMYK ColorSpace and the same colours.
+ *
+ * Optionally - not for use in the test harness - the test can be passed a parameter
+ * -display to create a UI which renders all the images the test is
+ * verifying so it can be manually verified
+ */
+
+/*
+ * @test
+ * @bug 8274735
+ * @summary Verify CMYK JPEGs can be read and written
+ */
+
+import java.awt.Color;
+import static java.awt.Color.*;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.GridLayout;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+public class CMYKJPEGTest {
+
+    static String[] fileNames = {
+        "black_cmyk.jpg",
+        "white_cmyk.jpg",
+        "gray_cmyk.jpg",
+        "red_cmyk.jpg",
+        "blue_cmyk.jpg",
+        "green_cmyk.jpg",
+        "cyan_cmyk.jpg",
+        "magenta_cmyk.jpg",
+        "yellow_cmyk.jpg",
+    };
+
+    static Color[] colors = {
+         black,
+         white,
+         gray,
+         red,
+         blue,
+         green,
+         cyan,
+         magenta,
+         yellow,
+    };
+
+    static boolean display;
+
+    static BufferedImage[] readImages;
+    static BufferedImage[] writtenImages;
+    static int imageIndex = 0;
+
+    public static void main(String[] args) throws Exception {
+
+        if (args.length > 0) {
+            display = "-display".equals(args[0]);
+        }
+
+        String sep = System.getProperty("file.separator");
+        String dir = System.getProperty("test.src", ".");
+        String prefix = dir+sep;
+
+        readImages = new BufferedImage[fileNames.length];
+        writtenImages = new BufferedImage[fileNames.length];
+
+        for (String fileName : fileNames) {
+            String color = fileName.replace("_cmyk.jpg", "");
+            test(prefix+fileName, color, imageIndex++);
+        }
+        if (display) {
+            SwingUtilities.invokeAndWait(() -> createUI());
+        }
+    }
+
+    static void test(String fileName, String color, int index)
+                 throws IOException {
+
+        readImages[index] = ImageIO.read(new File(fileName));
+        verify(readImages[index], color, colors[index]);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ImageIO.write(readImages[index], "jpg", baos);
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        writtenImages[index] = ImageIO.read(bais);
+        verify(writtenImages[index], color, colors[index]);
+    }
+
+    static void verify(BufferedImage img, String colorName, Color c) {
+        ColorModel cm = img.getColorModel();
+        int tc = cm.getNumComponents();
+        int cc = cm.getNumColorComponents();
+        if (cc != 4 || tc != 4) {
+            throw new RuntimeException("Unexpected num comp for " + img);
+        }
+
+        int rgb = img.getRGB(0,0);
+        int c_red = c.getRed();
+        int c_green = c.getGreen();
+        int c_blue = c.getBlue();
+        int i_red =   (rgb & 0x0ff0000) >> 16;
+        int i_green = (rgb & 0x000ff00) >> 8;
+        int i_blue =  (rgb & 0x00000ff);
+        int tol = 16;
+        if ((Math.abs(i_red - c_red) > tol) ||
+            (Math.abs(i_green - c_green) > tol) ||
+            (Math.abs(i_blue - c_blue) > tol))
+        {
+           System.err.println("red="+i_red+" green="+i_green+" blue="+i_blue);
+           throw new RuntimeException("Too different " + img + " " + colorName + " " + c);
+        }
+    }
+
+    static class ImageComp extends JComponent {
+
+        BufferedImage img;
+
+        ImageComp(BufferedImage img) {
+           this.img = img;
+        }
+
+        public Dimension getPreferredSize() {
+            return new Dimension(img.getWidth(), img.getHeight());
+        }
+
+        public Dimension getMinimumSize() {
+           return getPreferredSize();
+        }
+
+        public void paintComponent(Graphics g) {
+           super.paintComponent(g);
+           g.drawImage(img, 0, 0, null);
+        }
+    }
+
+    static void createUI() {
+        JFrame f = new JFrame("CMYK JPEG Test");
+        JPanel p = new JPanel();
+        p.setLayout(new GridLayout(3, colors.length, 10, 10));
+        for (String s :  fileNames) {
+           p.add(new JLabel(s.replace("_cmyk.jpg", "")));
+        }
+        for (BufferedImage i : readImages) {
+            p.add(new ImageComp(i));
+        }
+        for (BufferedImage i : writtenImages) {
+            p.add(new ImageComp(i));
+        }
+        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        f.add(p);
+        f.pack();
+        f.setVisible(true);
+    }
+}
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/black_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/black_cmyk.jpg
new file mode 100644
index 0000000..1446f06
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/black_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/blue_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/blue_cmyk.jpg
new file mode 100644
index 0000000..74aa676
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/blue_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/cyan_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/cyan_cmyk.jpg
new file mode 100644
index 0000000..4e97acc
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/cyan_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/gray_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/gray_cmyk.jpg
new file mode 100644
index 0000000..a519437
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/gray_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/green_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/green_cmyk.jpg
new file mode 100644
index 0000000..c5c4af3
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/green_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/magenta_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/magenta_cmyk.jpg
new file mode 100644
index 0000000..9209e1f
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/magenta_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/red_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/red_cmyk.jpg
new file mode 100644
index 0000000..594f2ac
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/red_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/white_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/white_cmyk.jpg
new file mode 100644
index 0000000..319bbb3
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/white_cmyk.jpg
Binary files differ
diff --git a/test/jdk/javax/imageio/plugins/jpeg/CMYK/yellow_cmyk.jpg b/test/jdk/javax/imageio/plugins/jpeg/CMYK/yellow_cmyk.jpg
new file mode 100644
index 0000000..4a7a338
--- /dev/null
+++ b/test/jdk/javax/imageio/plugins/jpeg/CMYK/yellow_cmyk.jpg
Binary files differ