Version 1.2.3: getVersion, openResource, fixes
[nikiroo-utils.git] / src / be / nikiroo / utils / IOUtils.java
1 package be.nikiroo.utils;
2
3 import java.awt.Image;
4 import java.awt.geom.AffineTransform;
5 import java.awt.image.AffineTransformOp;
6 import java.awt.image.BufferedImage;
7 import java.io.BufferedReader;
8 import java.io.ByteArrayOutputStream;
9 import java.io.File;
10 import java.io.FileInputStream;
11 import java.io.FileOutputStream;
12 import java.io.FileReader;
13 import java.io.FileWriter;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.OutputStream;
17 import java.util.zip.ZipEntry;
18 import java.util.zip.ZipOutputStream;
19
20 import javax.imageio.ImageIO;
21
22 /**
23 * This class offer some utilities based around Streams.
24 *
25 * @author niki
26 */
27 public class IOUtils {
28 /**
29 * Write the data to the given {@link File}.
30 *
31 * @param in
32 * the data source
33 * @param target
34 * the target {@link File}
35 *
36 * @throws IOException
37 * in case of I/O error
38 */
39 public static void write(InputStream in, File target) throws IOException {
40 OutputStream out = new FileOutputStream(target);
41 try {
42 write(in, out);
43 } finally {
44 out.close();
45 }
46 }
47
48 /**
49 * Write the data to the given {@link OutputStream}.
50 *
51 * @param in
52 * the data source
53 * @param target
54 * the target {@link OutputStream}
55 *
56 * @throws IOException
57 * in case of I/O error
58 */
59 public static void write(InputStream in, OutputStream out)
60 throws IOException {
61 byte buffer[] = new byte[4069];
62 for (int len = 0; (len = in.read(buffer)) > 0;) {
63 out.write(buffer, 0, len);
64 }
65 }
66
67 /**
68 * Recursively Add a {@link File} (which can thus be a directory, too) to a
69 * {@link ZipOutputStream}.
70 *
71 * @param zip
72 * the stream
73 * @param base
74 * the path to prepend to the ZIP info before the actual
75 * {@link File} path
76 * @param target
77 * the source {@link File} (which can be a directory)
78 * @param targetIsRoot
79 * FALSE if we need to add a {@link ZipEntry} for base/target,
80 * TRUE to add it at the root of the ZIP
81 *
82 * @throws IOException
83 * in case of I/O error
84 */
85 public static void zip(ZipOutputStream zip, String base, File target,
86 boolean targetIsRoot) throws IOException {
87 if (target.isDirectory()) {
88 if (!targetIsRoot) {
89 if (base == null || base.isEmpty()) {
90 base = target.getName();
91 } else {
92 base += "/" + target.getName();
93 }
94 zip.putNextEntry(new ZipEntry(base + "/"));
95 }
96 for (File file : target.listFiles()) {
97 zip(zip, base, file, false);
98 }
99 } else {
100 if (base == null || base.isEmpty()) {
101 base = target.getName();
102 } else {
103 base += "/" + target.getName();
104 }
105 zip.putNextEntry(new ZipEntry(base));
106 FileInputStream in = new FileInputStream(target);
107 try {
108 IOUtils.write(in, zip);
109 } finally {
110 in.close();
111 }
112 }
113 }
114
115 /**
116 * Zip the given source into dest.
117 *
118 * @param src
119 * the source {@link File} (which can be a directory)
120 * @param dest
121 * the destination <tt>.zip</tt> file
122 * @param srctIsRoot
123 * FALSE if we need to add a {@link ZipEntry} for src, TRUE to
124 * add it at the root of the ZIP
125 *
126 * @throws IOException
127 * in case of I/O error
128 */
129 public static void zip(File src, File dest, boolean srcIsRoot)
130 throws IOException {
131 OutputStream out = new FileOutputStream(dest);
132 try {
133 ZipOutputStream zip = new ZipOutputStream(out);
134 try {
135 IOUtils.zip(zip, "", src, srcIsRoot);
136 } finally {
137 zip.close();
138 }
139 } finally {
140 out.close();
141 }
142 }
143
144 /**
145 * Write the {@link String} content to {@link File}.
146 *
147 * @param dir
148 * the directory where to write the {@link File}
149 * @param filename
150 * the {@link File} name
151 * @param content
152 * the content
153 *
154 * @throws IOException
155 * in case of I/O error
156 */
157 public static void writeSmallFile(File dir, String filename, String content)
158 throws IOException {
159 if (!dir.exists()) {
160 dir.mkdirs();
161 }
162
163 FileWriter writerVersion = new FileWriter(new File(dir, filename));
164 try {
165 writerVersion.write(content);
166 } finally {
167 writerVersion.close();
168 }
169 }
170
171 /**
172 * Read the whole {@link File} content into a {@link String}.
173 *
174 * @param file
175 * the {@link File}
176 *
177 * @return the content
178 *
179 * @throws IOException
180 * in case of I/O error
181 */
182 public static String readSmallFile(File file) throws IOException {
183 BufferedReader reader = new BufferedReader(new FileReader(file));
184 try {
185 StringBuilder builder = new StringBuilder();
186 for (String line = reader.readLine(); line != null; line = reader
187 .readLine()) {
188 builder.append(line);
189 }
190 return builder.toString();
191 } finally {
192 reader.close();
193 }
194 }
195
196 /**
197 * Recursively delete the given {@link File}, which may of course also be a
198 * directory.
199 * <p>
200 * Will silently continue in case of error.
201 *
202 * @param target
203 * the target to delete
204 */
205 public static void deltree(File target) {
206 for (File file : target.listFiles()) {
207 if (file.isDirectory()) {
208 deltree(file);
209 } else {
210 if (!file.delete()) {
211 System.err.println("Cannot delete file: "
212 + file.getAbsolutePath());
213 }
214 }
215 }
216
217 if (!target.delete()) {
218 System.err.println("Cannot delete file: "
219 + target.getAbsolutePath());
220 }
221 }
222
223 /**
224 * Convert the given {@link InputStream} (which should allow calls to
225 * {@link InputStream#reset() for better perfs}) into an {@link Image}
226 * object, respecting the EXIF transformations if any.
227 *
228 * @param in
229 * the 'resetable' {@link InputStream}
230 *
231 * @return the {@link Image} object
232 *
233 * @throws IOException
234 * in case of IO error
235 */
236 public static BufferedImage toImage(InputStream in) throws IOException {
237 MarkableFileInputStream tmpIn = null;
238 File tmp = null;
239 try {
240 in.reset();
241 } catch (IOException e) {
242 tmp = File.createTempFile("fanfic-tmp-image", ".tmp");
243 tmp.deleteOnExit();
244 IOUtils.write(in, tmp);
245 tmpIn = new MarkableFileInputStream(new FileInputStream(tmp));
246 }
247
248 int orientation;
249 try {
250 orientation = getExifTransorm(in);
251 } catch (Exception e) {
252 // no EXIF transform, ok
253 orientation = -1;
254 }
255
256 in.reset();
257 BufferedImage image = ImageIO.read(in);
258
259 if (image == null) {
260 if (tmp != null) {
261 tmp.delete();
262 tmpIn.close();
263 }
264 throw new IOException("Failed to convert input to image");
265 }
266
267 // Note: this code has been found on internet;
268 // thank you anonymous coder.
269 int width = image.getWidth();
270 int height = image.getHeight();
271 AffineTransform affineTransform = new AffineTransform();
272
273 switch (orientation) {
274 case 1:
275 affineTransform = null;
276 break;
277 case 2: // Flip X
278 affineTransform.scale(-1.0, 1.0);
279 affineTransform.translate(-width, 0);
280 break;
281 case 3: // PI rotation
282 affineTransform.translate(width, height);
283 affineTransform.rotate(Math.PI);
284 break;
285 case 4: // Flip Y
286 affineTransform.scale(1.0, -1.0);
287 affineTransform.translate(0, -height);
288 break;
289 case 5: // - PI/2 and Flip X
290 affineTransform.rotate(-Math.PI / 2);
291 affineTransform.scale(-1.0, 1.0);
292 break;
293 case 6: // -PI/2 and -width
294 affineTransform.translate(height, 0);
295 affineTransform.rotate(Math.PI / 2);
296 break;
297 case 7: // PI/2 and Flip
298 affineTransform.scale(-1.0, 1.0);
299 affineTransform.translate(-height, 0);
300 affineTransform.translate(0, width);
301 affineTransform.rotate(3 * Math.PI / 2);
302 break;
303 case 8: // PI / 2
304 affineTransform.translate(0, width);
305 affineTransform.rotate(3 * Math.PI / 2);
306 break;
307 default:
308 affineTransform = null;
309 break;
310 }
311
312 if (affineTransform != null) {
313 AffineTransformOp affineTransformOp = new AffineTransformOp(
314 affineTransform, AffineTransformOp.TYPE_BILINEAR);
315
316 BufferedImage transformedImage = new BufferedImage(width, height,
317 image.getType());
318 transformedImage = affineTransformOp
319 .filter(image, transformedImage);
320
321 image = transformedImage;
322 }
323 //
324
325 if (tmp != null) {
326 tmp.delete();
327 tmpIn.close();
328 }
329
330 return image;
331 }
332
333 /**
334 * Return the version of the program if it follows the VERSION convention
335 * (i.e., if it has a file called VERSION containing the version as a
336 * {@link String} on its binary root).
337 *
338 * @return the version, or NULL
339 */
340 public static String getVersion() {
341 String version = null;
342
343 InputStream in = openResource("VERSION");
344 if (in != null) {
345 try {
346 ByteArrayOutputStream ba = new ByteArrayOutputStream();
347 write(in, ba);
348 in.close();
349
350 version = ba.toString("UTF-8");
351 } catch (IOException e) {
352 }
353 }
354
355 return version;
356 }
357
358 /**
359 * Open the given /-separated resource (from the binary root).
360 *
361 * @param name
362 * the resource name
363 *
364 * @return the opened resource if found, NLL if not
365 */
366 public static InputStream openResource(String name) {
367 ClassLoader loader = IOUtils.class.getClassLoader();
368 if (loader == null) {
369 loader = ClassLoader.getSystemClassLoader();
370 }
371
372 return loader.getResourceAsStream(name);
373 }
374
375 /**
376 * Return the EXIF transformation flag of this image if any.
377 *
378 * <p>
379 * Note: this code has been found on internet; thank you anonymous coder.
380 * </p>
381 *
382 * @param in
383 * the data {@link InputStream}
384 *
385 * @return the transformation flag if any
386 *
387 * @throws IOException
388 * in case of IO error
389 */
390 private static int getExifTransorm(InputStream in) throws IOException {
391 int[] exif_data = new int[100];
392 int set_flag = 0;
393 int is_motorola = 0;
394
395 /* Read File head, check for JPEG SOI + Exif APP1 */
396 for (int i = 0; i < 4; i++)
397 exif_data[i] = in.read();
398
399 if (exif_data[0] != 0xFF || exif_data[1] != 0xD8
400 || exif_data[2] != 0xFF || exif_data[3] != 0xE1)
401 return -2;
402
403 /* Get the marker parameter length count */
404 int length = (in.read() << 8 | in.read());
405
406 /* Length includes itself, so must be at least 2 */
407 /* Following Exif data length must be at least 6 */
408 if (length < 8)
409 return -1;
410 length -= 8;
411 /* Read Exif head, check for "Exif" */
412 for (int i = 0; i < 6; i++)
413 exif_data[i] = in.read();
414
415 if (exif_data[0] != 0x45 || exif_data[1] != 0x78
416 || exif_data[2] != 0x69 || exif_data[3] != 0x66
417 || exif_data[4] != 0 || exif_data[5] != 0)
418 return -1;
419
420 /* Read Exif body */
421 length = length > exif_data.length ? exif_data.length : length;
422 for (int i = 0; i < length; i++)
423 exif_data[i] = in.read();
424
425 if (length < 12)
426 return -1; /* Length of an IFD entry */
427
428 /* Discover byte order */
429 if (exif_data[0] == 0x49 && exif_data[1] == 0x49)
430 is_motorola = 0;
431 else if (exif_data[0] == 0x4D && exif_data[1] == 0x4D)
432 is_motorola = 1;
433 else
434 return -1;
435
436 /* Check Tag Mark */
437 if (is_motorola == 1) {
438 if (exif_data[2] != 0)
439 return -1;
440 if (exif_data[3] != 0x2A)
441 return -1;
442 } else {
443 if (exif_data[3] != 0)
444 return -1;
445 if (exif_data[2] != 0x2A)
446 return -1;
447 }
448
449 /* Get first IFD offset (offset to IFD0) */
450 int offset;
451 if (is_motorola == 1) {
452 if (exif_data[4] != 0)
453 return -1;
454 if (exif_data[5] != 0)
455 return -1;
456 offset = exif_data[6];
457 offset <<= 8;
458 offset += exif_data[7];
459 } else {
460 if (exif_data[7] != 0)
461 return -1;
462 if (exif_data[6] != 0)
463 return -1;
464 offset = exif_data[5];
465 offset <<= 8;
466 offset += exif_data[4];
467 }
468 if (offset > length - 2)
469 return -1; /* check end of data segment */
470
471 /* Get the number of directory entries contained in this IFD */
472 int number_of_tags;
473 if (is_motorola == 1) {
474 number_of_tags = exif_data[offset];
475 number_of_tags <<= 8;
476 number_of_tags += exif_data[offset + 1];
477 } else {
478 number_of_tags = exif_data[offset + 1];
479 number_of_tags <<= 8;
480 number_of_tags += exif_data[offset];
481 }
482 if (number_of_tags == 0)
483 return -1;
484 offset += 2;
485
486 /* Search for Orientation Tag in IFD0 */
487 for (;;) {
488 if (offset > length - 12)
489 return -1; /* check end of data segment */
490 /* Get Tag number */
491 int tagnum;
492 if (is_motorola == 1) {
493 tagnum = exif_data[offset];
494 tagnum <<= 8;
495 tagnum += exif_data[offset + 1];
496 } else {
497 tagnum = exif_data[offset + 1];
498 tagnum <<= 8;
499 tagnum += exif_data[offset];
500 }
501 if (tagnum == 0x0112)
502 break; /* found Orientation Tag */
503 if (--number_of_tags == 0)
504 return -1;
505 offset += 12;
506 }
507
508 /* Get the Orientation value */
509 if (is_motorola == 1) {
510 if (exif_data[offset + 8] != 0)
511 return -1;
512 set_flag = exif_data[offset + 9];
513 } else {
514 if (exif_data[offset + 9] != 0)
515 return -1;
516 set_flag = exif_data[offset + 8];
517 }
518 if (set_flag > 8)
519 return -1;
520
521 return set_flag;
522 }
523 }