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
40 Throwables.propagateIfInstanceOf(e, IOException.class);
41 throw new IOException(e);
42 }
43 }
44
45
46
47
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
62
63
64
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
100
101
102
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
141
142
143
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 }