Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / utils / Image.java
1 package be.nikiroo.utils;
2
3 import java.io.ByteArrayInputStream;
4 import java.io.Closeable;
5 import java.io.File;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.io.ObjectInputStream;
9 import java.io.ObjectOutputStream;
10 import java.io.OutputStream;
11 import java.io.Serializable;
12
13 import be.nikiroo.utils.streams.Base64InputStream;
14 import be.nikiroo.utils.streams.MarkableFileInputStream;
15
16 /**
17 * This class represents an image data.
18 *
19 * @author niki
20 */
21 public class Image implements Closeable, Serializable {
22 static private final long serialVersionUID = 1L;
23
24 static private File tempRoot;
25 static private TempFiles tmpRepository;
26 static private long count = 0;
27 static private Object lock = new Object();
28
29 private Object instanceLock = new Object();
30 private File data;
31 private long size;
32
33 /**
34 * Do not use -- for serialisation purposes only.
35 */
36 @SuppressWarnings("unused")
37 private Image() {
38 }
39
40 /**
41 * Create a new {@link Image} with the given data.
42 *
43 * @param data
44 * the data
45 */
46 public Image(byte[] data) {
47 ByteArrayInputStream in = new ByteArrayInputStream(data);
48 try {
49 this.data = getTemporaryFile();
50 size = IOUtils.write(in, this.data);
51 } catch (IOException e) {
52 throw new RuntimeException(e);
53 } finally {
54 try {
55 in.close();
56 } catch (IOException e) {
57 throw new RuntimeException(e);
58 }
59 }
60 }
61
62 /**
63 * Create an image from Base64 encoded data.
64 *
65 * <p>
66 * Please use {@link Image#Image(InputStream)} when possible instead, with a
67 * {@link Base64InputStream}; it can be much more efficient.
68 *
69 * @param base64EncodedData
70 * the Base64 encoded data as a String
71 *
72 * @throws IOException
73 * in case of I/O error or badly formated Base64
74 */
75 public Image(String base64EncodedData) throws IOException {
76 this(new Base64InputStream(new ByteArrayInputStream(
77 StringUtils.getBytes(base64EncodedData)), false));
78 }
79
80 /**
81 * Create a new {@link Image} from a stream.
82 *
83 * @param in
84 * the stream
85 *
86 * @throws IOException
87 * in case of I/O error
88 */
89 public Image(InputStream in) throws IOException {
90 data = getTemporaryFile();
91 size = IOUtils.write(in, data);
92 }
93
94 /**
95 * The size of the enclosed image in bytes.
96 *
97 * @return the size
98 */
99 public long getSize() {
100 return size;
101 }
102
103 /**
104 * Generate an {@link InputStream} that you can {@link InputStream#reset()}
105 * for this {@link Image}.
106 * <p>
107 * This {@link InputStream} will (always) be a new one, and <b>you</b> are
108 * responsible for it.
109 * <p>
110 * Note: take care that the {@link InputStream} <b>must not</b> live past
111 * the {@link Image} life time!
112 *
113 * @return the stream
114 *
115 * @throws IOException
116 * in case of I/O error
117 */
118 public InputStream newInputStream() throws IOException {
119 synchronized (instanceLock) {
120 if (data == null) {
121 throw new IOException("Image was close()d");
122 }
123
124 return new MarkableFileInputStream(data);
125 }
126 }
127
128 /**
129 * <b>Read</b> the actual image data, as a byte array.
130 *
131 * @deprecated if possible, prefer the {@link Image#newInputStream()}
132 * method, as it can be more efficient
133 *
134 * @return the image data
135 */
136 @Deprecated
137 public byte[] getData() {
138 try {
139 InputStream in = newInputStream();
140 try {
141 return IOUtils.toByteArray(in);
142 } finally {
143 in.close();
144 }
145 } catch (IOException e) {
146 throw new RuntimeException(e);
147 }
148 }
149
150 /**
151 * Convert the given {@link Image} object into a Base64 representation of
152 * the same {@link Image} object.
153 *
154 * @deprecated Please use {@link Image#newInputStream()} instead, it is more
155 * efficient
156 *
157 * @return the Base64 representation
158 */
159 @Deprecated
160 public String toBase64() {
161 try {
162 Base64InputStream stream = new Base64InputStream(newInputStream(),
163 true);
164 try {
165 return IOUtils.readSmallStream(stream);
166 } finally {
167 stream.close();
168 }
169 } catch (IOException e) {
170 return null;
171 }
172 }
173
174 /**
175 * Closing the {@link Image} will delete the associated temporary file on
176 * disk.
177 * <p>
178 * Note that even if you don't, the program will still <b>try</b> to delete
179 * all the temporary files at JVM termination.
180 */
181 @Override
182 public void close() throws IOException {
183 synchronized (instanceLock) {
184 new Exception().printStackTrace();
185 if (size >= 0) {
186 size = -1;
187 data.delete();
188 data = null;
189
190 synchronized (lock) {
191 count--;
192 if (count <= 0) {
193 count = 0;
194 tmpRepository.close();
195 tmpRepository = null;
196 }
197 }
198 }
199 }
200 }
201
202 @Override
203 protected void finalize() throws Throwable {
204 try {
205 close();
206 } finally {
207 super.finalize();
208 }
209 }
210
211 /**
212 * Return a newly created temporary file to work on.
213 *
214 * @return the file
215 *
216 * @throws IOException
217 * in case of I/O error
218 */
219 private File getTemporaryFile() throws IOException {
220 synchronized (lock) {
221 if (tmpRepository == null) {
222 tmpRepository = new TempFiles(tempRoot, "images");
223 count = 0;
224 }
225
226 count++;
227
228 return tmpRepository.createTempFile("image");
229 }
230 }
231
232 /**
233 * Write this {@link Image} for serialization purposes; that is, write the
234 * content of the backing temporary file.
235 *
236 * @param out
237 * the {@link OutputStream} to write to
238 *
239 * @throws IOException
240 * in case of I/O error
241 */
242 private void writeObject(ObjectOutputStream out) throws IOException {
243 InputStream in = newInputStream();
244 try {
245 IOUtils.write(in, out);
246 } finally {
247 in.close();
248 }
249 }
250
251 /**
252 * Read an {@link Image} written by
253 * {@link Image#writeObject(java.io.ObjectOutputStream)}; that is, create a
254 * new temporary file with the saved content.
255 *
256 * @param in
257 * the {@link InputStream} to read from
258 * @throws IOException
259 * in case of I/O error
260 * @throws ClassNotFoundException
261 * will not be thrown by this method
262 */
263 @SuppressWarnings("unused")
264 private void readObject(ObjectInputStream in) throws IOException,
265 ClassNotFoundException {
266 data = getTemporaryFile();
267 IOUtils.write(in, data);
268 }
269
270 /**
271 * Change the temporary root directory used by the program.
272 * <p>
273 * Caution: the directory will be <b>owned</b> by the system, all its files
274 * now belong to us (and will most probably be deleted).
275 * <p>
276 * Note: it may take some time until the new temporary root is used, we
277 * first need to make sure the previous one is not used anymore (i.e., we
278 * must reach a point where no unclosed {@link Image} remains in memory) to
279 * switch the temporary root.
280 *
281 * @param root
282 * the new temporary root, which will be <b>owned</b> by the
283 * system
284 */
285 public static void setTemporaryFilesRoot(File root) {
286 tempRoot = root;
287 }
288 }