Commit | Line | Data |
---|---|---|
b771aed5 NR |
1 | package be.nikiroo.utils; |
2 | ||
3 | import java.awt.Dimension; | |
4 | import java.awt.Image; | |
5 | import java.awt.geom.AffineTransform; | |
6 | import java.awt.image.AffineTransformOp; | |
7 | import java.awt.image.BufferedImage; | |
8 | import java.io.ByteArrayInputStream; | |
9 | import java.io.ByteArrayOutputStream; | |
10 | import java.io.File; | |
11 | import java.io.FileInputStream; | |
12 | import java.io.IOException; | |
13 | import java.io.InputStream; | |
14 | ||
15 | import javax.imageio.ImageIO; | |
16 | ||
17 | import be.nikiroo.utils.ImageText.Mode; | |
18 | ||
19 | /** | |
20 | * This class offer some utilities based around images. | |
21 | * | |
22 | * @author niki | |
23 | */ | |
24 | public 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 | } |