Version 3.1.6: fix Bridge, Serialiser, Progress:
[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 }
11f9e5f3 104
b771aed5
NR
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
4b7d32e7
NR
123 /**
124 * A shorthand method to create an {@link ImageText} and return its output.
125 *
126 * @param image
127 * the source {@link Image}
128 * @param size
129 * the final text size to target
130 * @param mode
131 * the mode of conversion
132 * @param invert
133 * TRUE to invert colours rendering
134 *
135 * @return the text image
136 */
137 static public String toAscii(Image image, Dimension size, Mode mode,
138 boolean invert) {
139 return new ImageText(image, size, mode, invert).toString();
140 }
141
b771aed5
NR
142 /**
143 * Convert the given {@link InputStream} (which should allow calls to
144 * {@link InputStream#reset()} for better perfs) into an {@link Image}
145 * object, respecting the EXIF transformations if any.
146 *
147 * @param in
4b7d32e7 148 * the {@link InputStream}
b771aed5
NR
149 *
150 * @return the {@link Image} object
151 *
152 * @throws IOException
153 * in case of IO error
154 */
155 static public BufferedImage fromStream(InputStream in) throws IOException {
156 MarkableFileInputStream tmpIn = null;
157 File tmp = null;
11f9e5f3 158
4b7d32e7
NR
159 boolean resetable = in.markSupported();
160 if (resetable) {
11f9e5f3
NR
161 try {
162 in.reset();
163 } catch (IOException e) {
4b7d32e7 164 resetable = false;
11f9e5f3
NR
165 }
166 }
167
4b7d32e7
NR
168 if (resetable) {
169 return fromResetableStream(in);
170 }
171
172 tmp = File.createTempFile(".tmp-image", ".tmp");
173 try {
b771aed5
NR
174 IOUtils.write(in, tmp);
175 tmpIn = new MarkableFileInputStream(new FileInputStream(tmp));
4b7d32e7
NR
176 return fromResetableStream(tmpIn);
177 } finally {
178 try {
179 if (tmpIn != null) {
180 tmpIn.close();
181 }
182 } finally {
183 tmp.delete();
184 }
b771aed5 185 }
4b7d32e7
NR
186 }
187
188 /**
189 * Convert the given resetable {@link InputStream} into an {@link Image}
190 * object, respecting the EXIF transformations if any.
191 *
192 * @param in
193 * the 'resetable' (this is mandatory) {@link InputStream}
194 *
195 * @return the {@link Image} object
196 *
197 * @throws IOException
198 * in case of IO error
199 */
200 static private BufferedImage fromResetableStream(InputStream in)
201 throws IOException {
b771aed5
NR
202
203 int orientation;
204 try {
205 orientation = getExifTransorm(in);
206 } catch (Exception e) {
207 // no EXIF transform, ok
208 orientation = -1;
209 }
210
211 in.reset();
212 BufferedImage image = ImageIO.read(in);
213
214 if (image == null) {
b771aed5
NR
215 throw new IOException("Failed to convert input to image");
216 }
217
218 // Note: this code has been found on Internet;
219 // thank you anonymous coder.
220 int width = image.getWidth();
221 int height = image.getHeight();
222 AffineTransform affineTransform = new AffineTransform();
223
224 switch (orientation) {
225 case 1:
226 affineTransform = null;
227 break;
228 case 2: // Flip X
229 affineTransform.scale(-1.0, 1.0);
230 affineTransform.translate(-width, 0);
231 break;
232 case 3: // PI rotation
233 affineTransform.translate(width, height);
234 affineTransform.rotate(Math.PI);
235 break;
236 case 4: // Flip Y
237 affineTransform.scale(1.0, -1.0);
238 affineTransform.translate(0, -height);
239 break;
240 case 5: // - PI/2 and Flip X
241 affineTransform.rotate(-Math.PI / 2);
242 affineTransform.scale(-1.0, 1.0);
243 break;
244 case 6: // -PI/2 and -width
245 affineTransform.translate(height, 0);
246 affineTransform.rotate(Math.PI / 2);
247 break;
248 case 7: // PI/2 and Flip
249 affineTransform.scale(-1.0, 1.0);
250 affineTransform.translate(-height, 0);
251 affineTransform.translate(0, width);
252 affineTransform.rotate(3 * Math.PI / 2);
253 break;
254 case 8: // PI / 2
255 affineTransform.translate(0, width);
256 affineTransform.rotate(3 * Math.PI / 2);
257 break;
258 default:
259 affineTransform = null;
260 break;
261 }
262
263 if (affineTransform != null) {
264 AffineTransformOp affineTransformOp = new AffineTransformOp(
265 affineTransform, AffineTransformOp.TYPE_BILINEAR);
266
267 BufferedImage transformedImage = new BufferedImage(width, height,
268 image.getType());
269 transformedImage = affineTransformOp
270 .filter(image, transformedImage);
271
272 image = transformedImage;
273 }
274 //
275
b771aed5
NR
276 return image;
277 }
278
b771aed5
NR
279 /**
280 * Return the EXIF transformation flag of this image if any.
281 *
282 * <p>
283 * Note: this code has been found on internet; thank you anonymous coder.
284 * </p>
285 *
286 * @param in
287 * the data {@link InputStream}
288 *
289 * @return the transformation flag if any
290 *
291 * @throws IOException
292 * in case of IO error
293 */
294 static private int getExifTransorm(InputStream in) throws IOException {
295 int[] exif_data = new int[100];
296 int set_flag = 0;
297 int is_motorola = 0;
298
299 /* Read File head, check for JPEG SOI + Exif APP1 */
300 for (int i = 0; i < 4; i++)
301 exif_data[i] = in.read();
302
303 if (exif_data[0] != 0xFF || exif_data[1] != 0xD8
304 || exif_data[2] != 0xFF || exif_data[3] != 0xE1)
305 return -2;
306
307 /* Get the marker parameter length count */
308 int length = (in.read() << 8 | in.read());
309
310 /* Length includes itself, so must be at least 2 */
311 /* Following Exif data length must be at least 6 */
312 if (length < 8)
313 return -1;
314 length -= 8;
315 /* Read Exif head, check for "Exif" */
316 for (int i = 0; i < 6; i++)
317 exif_data[i] = in.read();
318
319 if (exif_data[0] != 0x45 || exif_data[1] != 0x78
320 || exif_data[2] != 0x69 || exif_data[3] != 0x66
321 || exif_data[4] != 0 || exif_data[5] != 0)
322 return -1;
323
324 /* Read Exif body */
325 length = length > exif_data.length ? exif_data.length : length;
326 for (int i = 0; i < length; i++)
327 exif_data[i] = in.read();
328
329 if (length < 12)
330 return -1; /* Length of an IFD entry */
331
332 /* Discover byte order */
333 if (exif_data[0] == 0x49 && exif_data[1] == 0x49)
334 is_motorola = 0;
335 else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D)
336 is_motorola = 1;
337 else
338 return -1;
339
340 /* Check Tag Mark */
341 if (is_motorola == 1) {
342 if (exif_data[2] != 0)
343 return -1;
344 if (exif_data[3] != 0x2A)
345 return -1;
346 } else {
347 if (exif_data[3] != 0)
348 return -1;
349 if (exif_data[2] != 0x2A)
350 return -1;
351 }
352
353 /* Get first IFD offset (offset to IFD0) */
354 int offset;
355 if (is_motorola == 1) {
356 if (exif_data[4] != 0)
357 return -1;
358 if (exif_data[5] != 0)
359 return -1;
360 offset = exif_data[6];
361 offset <<= 8;
362 offset += exif_data[7];
363 } else {
364 if (exif_data[7] != 0)
365 return -1;
366 if (exif_data[6] != 0)
367 return -1;
368 offset = exif_data[5];
369 offset <<= 8;
370 offset += exif_data[4];
371 }
372 if (offset > length - 2)
373 return -1; /* check end of data segment */
374
375 /* Get the number of directory entries contained in this IFD */
376 int number_of_tags;
377 if (is_motorola == 1) {
378 number_of_tags = exif_data[offset];
379 number_of_tags <<= 8;
380 number_of_tags += exif_data[offset + 1];
381 } else {
382 number_of_tags = exif_data[offset + 1];
383 number_of_tags <<= 8;
384 number_of_tags += exif_data[offset];
385 }
386 if (number_of_tags == 0)
387 return -1;
388 offset += 2;
389
390 /* Search for Orientation Tag in IFD0 */
391 for (;;) {
392 if (offset > length - 12)
393 return -1; /* check end of data segment */
394 /* Get Tag number */
395 int tagnum;
396 if (is_motorola == 1) {
397 tagnum = exif_data[offset];
398 tagnum <<= 8;
399 tagnum += exif_data[offset + 1];
400 } else {
401 tagnum = exif_data[offset + 1];
402 tagnum <<= 8;
403 tagnum += exif_data[offset];
404 }
405 if (tagnum == 0x0112)
406 break; /* found Orientation Tag */
407 if (--number_of_tags == 0)
408 return -1;
409 offset += 12;
410 }
411
412 /* Get the Orientation value */
413 if (is_motorola == 1) {
414 if (exif_data[offset + 8] != 0)
415 return -1;
416 set_flag = exif_data[offset + 9];
417 } else {
418 if (exif_data[offset + 9] != 0)
419 return -1;
420 set_flag = exif_data[offset + 8];
421 }
422 if (set_flag > 8)
423 return -1;
424
425 return set_flag;
426 }
427}