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