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