View Javadoc

1   package com.atlassian.core.util.thumbnail.loader;
2   
3   import com.atlassian.core.util.ImageInfo;
4   import com.google.common.base.Optional;
5   import com.google.common.base.Throwables;
6   import org.apache.sanselan.ImageReadException;
7   import org.apache.sanselan.Sanselan;
8   import org.apache.sanselan.common.byteSources.ByteSource;
9   import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
10  import org.apache.sanselan.formats.jpeg.JpegImageParser;
11  import org.apache.sanselan.formats.jpeg.segments.UnknownSegment;
12  
13  import javax.imageio.ImageIO;
14  import javax.imageio.ImageReader;
15  import java.awt.color.ColorSpace;
16  import java.awt.color.ICC_ColorSpace;
17  import java.awt.color.ICC_Profile;
18  import java.awt.image.BufferedImage;
19  import java.awt.image.ColorConvertOp;
20  import java.awt.image.Raster;
21  import java.awt.image.WritableRaster;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.ArrayList;
25  import java.util.Iterator;
26  
27  class CMYKImageLoader implements ImageLoader
28  {
29  
30      @Override
31      public Optional<BufferedImage> loadImage(final InputStream inputStream, ImageInfo imageInfo) throws IOException
32      {
33          try
34          {
35              return new JpegWithCMYKReader().readImage(inputStream);
36          }
37          catch (Exception e)
38          {
39              //java.awt classes can throw various of RuntimeExceptions when incorrect
40  			Throwables.propagateIfInstanceOf(e, IOException.class);
41  			throw new IOException(e);
42          }
43      }
44  
45      /**
46       * Standard JDK image utils: javax.imageio cannot read JPEGs with CMYK color profile.
47       * This class inputStream using Sanselan library to workaround Java limitations.
48       */
49      private static class JpegWithCMYKReader
50      {
51  
52          private enum ColorType
53          {
54              CMYK, YCCK
55          }
56  
57          private ColorType colorType = ColorType.CMYK;
58          private boolean isAdobeMarkerPresent = false;
59  
60          /**
61           * Tries to read provided stream as CMYK JPEG and converts it to RGB
62           *
63           * @return Optional with image with RGB colors;
64           * @throws java.io.IOException
65           */
66          public Optional<BufferedImage> readImage(final InputStream inputStream) throws IOException
67          {
68  
69              ICC_Profile iccProfile;
70  
71              try
72              {
73                  checkAdobeMarkerAndYcck(inputStream);
74                  inputStream.reset();
75                  iccProfile = Sanselan.getICCProfile(inputStream, null);
76              } catch (ImageReadException e)
77              {
78                  return Optional.absent();
79              }
80  
81              inputStream.reset();
82              WritableRaster raster = getRasterFromStream(inputStream);
83              if (raster == null)
84                  return Optional.absent();
85  
86              if (colorType == ColorType.YCCK)
87              {
88                  convertYcckToCmyk(raster);
89              }
90              if (isAdobeMarkerPresent)
91              {
92                  convertInvertedColors(raster);
93              }
94  
95              return Optional.of(convertCmykToRgb(raster, iccProfile));
96          }
97  
98          /**
99           * Gets raster data from image stream
100          *
101          * @return WritableRaster from inpusStream
102          * @throws IOException
103          */
104         private WritableRaster getRasterFromStream(InputStream inputStream) throws IOException
105         {
106             Iterator readers = ImageIO.getImageReadersByFormatName("JPEG");
107             ImageReader reader = null;
108             try
109             {
110                 while (readers.hasNext())
111                 {
112                     reader = (ImageReader) readers.next();
113                     if (reader.canReadRaster())
114                     {
115                         break;
116                     }
117                     else
118                     {
119                         reader.dispose();
120                         reader = null;
121                     }
122                 }
123                 if (reader == null)
124                 {
125                     return null;
126                 }
127                 reader.setInput(ImageIO.createImageInputStream(inputStream));
128 				return (WritableRaster) reader.readRaster(0, null);
129             }
130             finally
131             {
132                 if (reader != null)
133                 {
134                     reader.dispose();
135                 }
136             }
137         }
138 
139         /**
140          * Checks if image contains Adobe marker and if image is saved in YCCK color profile
141          *
142          * @throws IOException
143          * @throws ImageReadException
144          */
145         private void checkAdobeMarkerAndYcck(InputStream inputStream) throws IOException, ImageReadException
146         {
147             JpegImageParser parser = new JpegImageParser();
148             ByteSource byteSource = new ByteSourceInputStream(inputStream, null);
149             ArrayList segments = parser.readSegments(byteSource, new int[]{0xffee}, true);
150 
151             if (segments != null && segments.size() >= 1)
152             {
153                 UnknownSegment app14Segment = (UnknownSegment) segments.get(0);
154                 byte[] data = app14Segment.bytes;
155                 if (data.length >= 12 && data[0] == 'A' && data[1] == 'd' && data[2] == 'o' && data[3] == 'b' && data[4] == 'e')
156                 {
157 
158                     isAdobeMarkerPresent = true;
159 
160                     int transform = app14Segment.bytes[11] & 0xff;
161                     if (transform == 2)
162                     {
163                         colorType = ColorType.YCCK;
164                     }
165                 }
166             }
167         }
168 
169         private static void convertYcckToCmyk(WritableRaster raster)
170         {
171             int height = raster.getHeight();
172             int width = raster.getWidth();
173             int stride = width * 4;
174             int[] pixelRow = new int[stride];
175             for (int h = 0; h < height; h++)
176             {
177                 raster.getPixels(0, h, width, 1, pixelRow);
178 
179                 for (int x = 0; x < stride; x += 4)
180                 {
181                     int y = pixelRow[x];
182                     int cb = pixelRow[x + 1];
183                     int cr = pixelRow[x + 2];
184 
185                     int c = (int) (y + 1.402 * cr - 178.956);
186                     int m = (int) (y - 0.34414 * cb - 0.71414 * cr + 135.95984);
187                     y = (int) (y + 1.772 * cb - 226.316);
188 
189                     if (c < 0) c = 0;
190                     else if (c > 255) c = 255;
191                     if (m < 0) m = 0;
192                     else if (m > 255) m = 255;
193                     if (y < 0) y = 0;
194                     else if (y > 255) y = 255;
195 
196                     pixelRow[x] = 255 - c;
197                     pixelRow[x + 1] = 255 - m;
198                     pixelRow[x + 2] = 255 - y;
199                 }
200 
201                 raster.setPixels(0, h, width, 1, pixelRow);
202             }
203         }
204 
205         private static void convertInvertedColors(WritableRaster raster)
206         {
207             int height = raster.getHeight();
208             int width = raster.getWidth();
209             int stride = width * 4;
210             int[] pixelRow = new int[stride];
211             for (int h = 0; h < height; h++)
212             {
213                 raster.getPixels(0, h, width, 1, pixelRow);
214                 for (int x = 0; x < stride; x++)
215                     pixelRow[x] = 255 - pixelRow[x];
216                 raster.setPixels(0, h, width, 1, pixelRow);
217             }
218         }
219 
220         private static BufferedImage convertCmykToRgb(Raster cmykRaster, ICC_Profile cmykProfile) throws IOException
221         {
222 			if(cmykProfile == null) {
223 				cmykProfile = ICC_Profile.getInstance(CMYKImageLoader.class.getResourceAsStream("/ICCProfile/ISOcoated_v2_300_bas.ICC"));
224 			}
225             cmykProfile = fixCmykProfile(cmykProfile);
226 
227             final ICC_ColorSpace cmykCS = new ICC_ColorSpace(cmykProfile);
228             final BufferedImage rgbImage = new BufferedImage(cmykRaster.getWidth(), cmykRaster.getHeight(), BufferedImage.TYPE_INT_RGB);
229             final WritableRaster rgbRaster = rgbImage.getRaster();
230             final ColorSpace rgbCS = rgbImage.getColorModel().getColorSpace();
231             final ColorConvertOp cmykToRgb = new ColorConvertOp(cmykCS, rgbCS, null);
232 
233             cmykToRgb.filter(cmykRaster, rgbRaster);
234             return rgbImage;
235         }
236 
237         private static ICC_Profile fixCmykProfile(ICC_Profile cmykProfile)
238         {
239             if (cmykProfile.getProfileClass() != ICC_Profile.CLASS_DISPLAY)
240             {
241                 byte[] profileData = cmykProfile.getData();
242 
243                 if (profileData[ICC_Profile.icHdrRenderingIntent] == ICC_Profile.icPerceptual)
244                 {
245                     intToBigEndian(ICC_Profile.icSigDisplayClass, profileData, ICC_Profile.icHdrDeviceClass);
246 
247                     cmykProfile = ICC_Profile.getInstance(profileData);
248                 }
249             }
250             return cmykProfile;
251         }
252 
253         private static void intToBigEndian(int value, byte[] array, int index)
254         {
255             array[index] = (byte) (value >> 24);
256             array[index + 1] = (byte) (value >> 16);
257             array[index + 2] = (byte) (value >> 8);
258             array[index + 3] = (byte) (value);
259         }
260     }
261 }