Merge commit '868c706b81e33e99c5f57fb9fc200808e061c4bb'
[fanfix.git] / 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 return new MarkableFileInputStream(data);
120 }
121
122 /**
123 * <b>Read</b> the actual image data, as a byte array.
124 *
125 * @deprecated if possible, prefer the {@link Image#newInputStream()}
126 * method, as it can be more efficient
127 *
128 * @return the image data
129 */
130 @Deprecated
131 public byte[] getData() {
132 try {
133 InputStream in = newInputStream();
134 try {
135 return IOUtils.toByteArray(in);
136 } finally {
137 in.close();
138 }
139 } catch (IOException e) {
140 throw new RuntimeException(e);
141 }
142 }
143
144 /**
145 * Convert the given {@link Image} object into a Base64 representation of
146 * the same {@link Image} object.
147 *
148 * @deprecated Please use {@link Image#newInputStream()} instead, it is more
149 * efficient
150 *
151 * @return the Base64 representation
152 */
153 @Deprecated
154 public String toBase64() {
155 try {
156 Base64InputStream stream = new Base64InputStream(newInputStream(),
157 true);
158 try {
159 return IOUtils.readSmallStream(stream);
160 } finally {
161 stream.close();
162 }
163 } catch (IOException e) {
164 return null;
165 }
166 }
167
168 /**
169 * Closing the {@link Image} will delete the associated temporary file on
170 * disk.
171 * <p>
172 * Note that even if you don't, the program will still <b>try</b> to delete
173 * all the temporary files at JVM termination.
174 */
175 @Override
176 public void close() throws IOException {
177 synchronized (instanceLock) {
178 if (size >= 0) {
179 size = -1;
180 data.delete();
181 data = null;
182
183 synchronized (lock) {
184 count--;
185 if (count <= 0) {
186 count = 0;
187 tmpRepository.close();
188 tmpRepository = null;
189 }
190 }
191 }
192 }
193 }
194
195 @Override
196 protected void finalize() throws Throwable {
197 try {
198 close();
199 } finally {
200 super.finalize();
201 }
202 }
203
204 /**
205 * Return a newly created temporary file to work on.
206 *
207 * @return the file
208 *
209 * @throws IOException
210 * in case of I/O error
211 */
212 private File getTemporaryFile() throws IOException {
213 synchronized (lock) {
214 if (tmpRepository == null) {
215 tmpRepository = new TempFiles(tempRoot, "images");
216 count = 0;
217 }
218
219 count++;
220
221 return tmpRepository.createTempFile("image");
222 }
223 }
224
225 /**
226 * Write this {@link Image} for serialization purposes; that is, write the
227 * content of the backing temporary file.
228 *
229 * @param out
230 * the {@link OutputStream} to write to
231 *
232 * @throws IOException
233 * in case of I/O error
234 */
235 private void writeObject(ObjectOutputStream out) throws IOException {
236 InputStream in = newInputStream();
237 try {
238 IOUtils.write(in, out);
239 } finally {
240 in.close();
241 }
242 }
243
244 /**
245 * Read an {@link Image} written by
246 * {@link Image#writeObject(java.io.ObjectOutputStream)}; that is, create a
247 * new temporary file with the saved content.
248 *
249 * @param in
250 * the {@link InputStream} to read from
251 * @throws IOException
252 * in case of I/O error
253 * @throws ClassNotFoundException
254 * will not be thrown by this method
255 */
256 @SuppressWarnings("unused")
257 private void readObject(ObjectInputStream in) throws IOException,
258 ClassNotFoundException {
259 data = getTemporaryFile();
260 IOUtils.write(in, data);
261 }
262
263 /**
264 * Change the temporary root directory used by the program.
265 * <p>
266 * Caution: the directory will be <b>owned</b> by the system, all its files
267 * now belong to us (and will most probably be deleted).
268 * <p>
269 * Note: it may take some time until the new temporary root is used, we
270 * first need to make sure the previous one is not used anymore (i.e., we
271 * must reach a point where no unclosed {@link Image} remains in memory) to
272 * switch the temporary root.
273 *
274 * @param root
275 * the new temporary root, which will be <b>owned</b> by the
276 * system
277 */
278 public static void setTemporaryFilesRoot(File root) {
279 tempRoot = root;
280 }
281 }