Version 2.0.0 (small API change)
[nikiroo-utils.git] / src / be / nikiroo / utils / ImageUtils.java
CommitLineData
b771aed5
NR
1package be.nikiroo.utils;
2
3import java.awt.Dimension;
4import java.awt.Image;
5import java.awt.geom.AffineTransform;
6import java.awt.image.AffineTransformOp;
7import java.awt.image.BufferedImage;
8import java.io.ByteArrayInputStream;
9import java.io.ByteArrayOutputStream;
10import java.io.File;
11import java.io.FileInputStream;
12import java.io.IOException;
13import java.io.InputStream;
14
15import javax.imageio.ImageIO;
16
17import be.nikiroo.utils.ImageText.Mode;
18
19/**
20 * This class offer some utilities based around images.
21 *
22 * @author niki
23 */
24public class ImageUtils {
25 /**
26 * Convert the given {@link Image} object into a Base64 representation of
27 * the same {@link Image} object.
28 *
29 * @param image
30 * the {@link Image} object to convert
31 *
32 * @return the Base64 representation
33 *
34 * @throws IOException
35 * in case of IO error
36 */
37 static public String toBase64(BufferedImage image) throws IOException {
38 return toBase64(image, null);
39 }
40
41 /**
42 * Convert the given {@link Image} object into a Base64 representation of
43 * the same {@link Image}. object.
44 *
45 * @param image
46 * the {@link Image} object to convert
47 * @param format
48 * the image format to use to serialise it (default is PNG)
49 *
50 * @return the Base64 representation
51 *
52 * @throws IOException
53 * in case of IO error
54 */
55 static public String toBase64(BufferedImage image, String format)
56 throws IOException {
57 if (format == null) {
58 format = "png";
59 }
60
61 String imageString = null;
62 ByteArrayOutputStream out = new ByteArrayOutputStream();
63
64 ImageIO.write(image, format, out);
65 byte[] imageBytes = out.toByteArray();
66
67 imageString = new String(Base64.encodeBytes(imageBytes));
68
69 out.close();
70
71 return imageString;
72 }
73
74 /**
75 * Convert the given image into a Base64 representation of the same
76 * {@link File}.
77 *
78 * @param in
79 * the image to convert
80 *
81 * @return the Base64 representation
82 *
83 * @throws IOException
84 * in case of IO error
85 */
86 static public String toBase64(InputStream in) throws IOException {
87 String fileString = null;
88 ByteArrayOutputStream out = new ByteArrayOutputStream();
89
90 byte[] buf = new byte[8192];
91
92 int c = 0;
93 while ((c = in.read(buf, 0, buf.length)) > 0) {
94 out.write(buf, 0, c);
95 }
96 out.flush();
97 in.close();
98
99 fileString = new String(Base64.encodeBytes(out.toByteArray()));
100 out.close();
101
102 return fileString;
103 }
104
105 /**
106 * Convert the given Base64 representation of an image into an {@link Image}
107 * object.
108 *
109 * @param b64data
110 * the {@link Image} in Base64 format
111 *
112 * @return the {@link Image} object
113 *
114 * @throws IOException
115 * in case of IO error
116 */
117 static public BufferedImage fromBase64(String b64data) throws IOException {
118 ByteArrayInputStream in = new ByteArrayInputStream(
119 Base64.decode(b64data));
120 return fromStream(in);
121 }
122
123 /**
124 * Convert the given {@link InputStream} (which should allow calls to
125 * {@link InputStream#reset()} for better perfs) into an {@link Image}
126 * object, respecting the EXIF transformations if any.
127 *
128 * @param in
129 * the 'resetable' {@link InputStream}
130 *
131 * @return the {@link Image} object
132 *
133 * @throws IOException
134 * in case of IO error
135 */
136 static public BufferedImage fromStream(InputStream in) throws IOException {
137 MarkableFileInputStream tmpIn = null;
138 File tmp = null;
139 try {
140 in.reset();
141 } catch (IOException e) {
142 tmp = File.createTempFile(".tmp-image", ".tmp");
143 tmp.deleteOnExit();
144 IOUtils.write(in, tmp);
145 tmpIn = new MarkableFileInputStream(new FileInputStream(tmp));
146 }
147
148 int orientation;
149 try {
150 orientation = getExifTransorm(in);
151 } catch (Exception e) {
152 // no EXIF transform, ok
153 orientation = -1;
154 }
155
156 in.reset();
157 BufferedImage image = ImageIO.read(in);
158
159 if (image == null) {
160 if (tmp != null) {
161 tmp.delete();
162 tmpIn.close();
163 }
164 throw new IOException("Failed to convert input to image");
165 }
166
167 // Note: this code has been found on Internet;
168 // thank you anonymous coder.
169 int width = image.getWidth();
170 int height = image.getHeight();
171 AffineTransform affineTransform = new AffineTransform();
172
173 switch (orientation) {
174 case 1:
175 affineTransform = null;
176 break;
177 case 2: // Flip X
178 affineTransform.scale(-1.0, 1.0);
179 affineTransform.translate(-width, 0);
180 break;
181 case 3: // PI rotation
182 affineTransform.translate(width, height);
183 affineTransform.rotate(Math.PI);
184 break;
185 case 4: // Flip Y
186 affineTransform.scale(1.0, -1.0);
187 affineTransform.translate(0, -height);
188 break;
189 case 5: // - PI/2 and Flip X
190 affineTransform.rotate(-Math.PI / 2);
191 affineTransform.scale(-1.0, 1.0);
192 break;
193 case 6: // -PI/2 and -width
194 affineTransform.translate(height, 0);
195 affineTransform.rotate(Math.PI / 2);
196 break;
197 case 7: // PI/2 and Flip
198 affineTransform.scale(-1.0, 1.0);
199 affineTransform.translate(-height, 0);
200 affineTransform.translate(0, width);
201 affineTransform.rotate(3 * Math.PI / 2);
202 break;
203 case 8: // PI / 2
204 affineTransform.translate(0, width);
205 affineTransform.rotate(3 * Math.PI / 2);
206 break;
207 default:
208 affineTransform = null;
209 break;
210 }
211
212 if (affineTransform != null) {
213 AffineTransformOp affineTransformOp = new AffineTransformOp(
214 affineTransform, AffineTransformOp.TYPE_BILINEAR);
215
216 BufferedImage transformedImage = new BufferedImage(width, height,
217 image.getType());
218 transformedImage = affineTransformOp
219 .filter(image, transformedImage);
220
221 image = transformedImage;
222 }
223 //
224
225 if (tmp != null) {
226 tmp.delete();
227 tmpIn.close();
228 }
229
230 return image;
231 }
232
233 /**
234 * A shorthand method to create an {@link ImageText} and return its output.
235 *
236 * @param image
237 * the source {@link Image}
238 * @param size
239 * the final text size to target
240 * @param mode
241 * the mode of conversion
242 * @param invert
243 * TRUE to invert colours rendering
244 *
245 * @return the text image
246 */
247 static public String toAscii(Image image, Dimension size, Mode mode,
248 boolean invert) {
249 return new ImageText(image, size, mode, invert).toString();
250 }
251
252 /**
253 * Return the EXIF transformation flag of this image if any.
254 *
255 * <p>
256 * Note: this code has been found on internet; thank you anonymous coder.
257 * </p>
258 *
259 * @param in
260 * the data {@link InputStream}
261 *
262 * @return the transformation flag if any
263 *
264 * @throws IOException
265 * in case of IO error
266 */
267 static private int getExifTransorm(InputStream in) throws IOException {
268 int[] exif_data = new int[100];
269 int set_flag = 0;
270 int is_motorola = 0;
271
272 /* Read File head, check for JPEG SOI + Exif APP1 */
273 for (int i = 0; i < 4; i++)
274 exif_data[i] = in.read();
275
276 if (exif_data[0] != 0xFF || exif_data[1] != 0xD8
277 || exif_data[2] != 0xFF || exif_data[3] != 0xE1)
278 return -2;
279
280 /* Get the marker parameter length count */
281 int length = (in.read() << 8 | in.read());
282
283 /* Length includes itself, so must be at least 2 */
284 /* Following Exif data length must be at least 6 */
285 if (length < 8)
286 return -1;
287 length -= 8;
288 /* Read Exif head, check for "Exif" */
289 for (int i = 0; i < 6; i++)
290 exif_data[i] = in.read();
291
292 if (exif_data[0] != 0x45 || exif_data[1] != 0x78
293 || exif_data[2] != 0x69 || exif_data[3] != 0x66
294 || exif_data[4] != 0 || exif_data[5] != 0)
295 return -1;
296
297 /* Read Exif body */
298 length = length > exif_data.length ? exif_data.length : length;
299 for (int i = 0; i < length; i++)
300 exif_data[i] = in.read();
301
302 if (length < 12)
303 return -1; /* Length of an IFD entry */
304
305 /* Discover byte order */
306 if (exif_data[0] == 0x49 && exif_data[1] == 0x49)
307 is_motorola = 0;
308 else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D)
309 is_motorola = 1;
310 else
311 return -1;
312
313 /* Check Tag Mark */
314 if (is_motorola == 1) {
315 if (exif_data[2] != 0)
316 return -1;
317 if (exif_data[3] != 0x2A)
318 return -1;
319 } else {
320 if (exif_data[3] != 0)
321 return -1;
322 if (exif_data[2] != 0x2A)
323 return -1;
324 }
325
326 /* Get first IFD offset (offset to IFD0) */
327 int offset;
328 if (is_motorola == 1) {
329 if (exif_data[4] != 0)
330 return -1;
331 if (exif_data[5] != 0)
332 return -1;
333 offset = exif_data[6];
334 offset <<= 8;
335 offset += exif_data[7];
336 } else {
337 if (exif_data[7] != 0)
338 return -1;
339 if (exif_data[6] != 0)
340 return -1;
341 offset = exif_data[5];
342 offset <<= 8;
343 offset += exif_data[4];
344 }
345 if (offset > length - 2)
346 return -1; /* check end of data segment */
347
348 /* Get the number of directory entries contained in this IFD */
349 int number_of_tags;
350 if (is_motorola == 1) {
351 number_of_tags = exif_data[offset];
352 number_of_tags <<= 8;
353 number_of_tags += exif_data[offset + 1];
354 } else {
355 number_of_tags = exif_data[offset + 1];
356 number_of_tags <<= 8;
357 number_of_tags += exif_data[offset];
358 }
359 if (number_of_tags == 0)
360 return -1;
361 offset += 2;
362
363 /* Search for Orientation Tag in IFD0 */
364 for (;;) {
365 if (offset > length - 12)
366 return -1; /* check end of data segment */
367 /* Get Tag number */
368 int tagnum;
369 if (is_motorola == 1) {
370 tagnum = exif_data[offset];
371 tagnum <<= 8;
372 tagnum += exif_data[offset + 1];
373 } else {
374 tagnum = exif_data[offset + 1];
375 tagnum <<= 8;
376 tagnum += exif_data[offset];
377 }
378 if (tagnum == 0x0112)
379 break; /* found Orientation Tag */
380 if (--number_of_tags == 0)
381 return -1;
382 offset += 12;
383 }
384
385 /* Get the Orientation value */
386 if (is_motorola == 1) {
387 if (exif_data[offset + 8] != 0)
388 return -1;
389 set_flag = exif_data[offset + 9];
390 } else {
391 if (exif_data[offset + 9] != 0)
392 return -1;
393 set_flag = exif_data[offset + 8];
394 }
395 if (set_flag > 8)
396 return -1;
397
398 return set_flag;
399 }
400}