Rev 1272 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
936 | dev | 1 | package ak.photoalbum.images; |
2 | |||
3 | import java.util.Map; |
||
4 | import java.util.HashMap; |
||
5 | import java.util.Comparator; |
||
6 | import java.util.Arrays; |
||
7 | import java.io.File; |
||
8 | import java.io.FileFilter; |
||
9 | import java.io.IOException; |
||
10 | import java.io.FileInputStream; |
||
11 | import java.io.OutputStream; |
||
1257 | dev | 12 | import java.io.BufferedReader; |
13 | import java.io.InputStreamReader; |
||
936 | dev | 14 | import java.awt.Image; |
15 | import java.awt.Toolkit; |
||
16 | import java.awt.Graphics; |
||
17 | import java.awt.image.BufferedImage; |
||
18 | import java.awt.image.ImageObserver; |
||
19 | import java.awt.image.PixelGrabber; |
||
20 | import javax.imageio.ImageIO; |
||
21 | import org.apache.log4j.Logger; |
||
22 | import marcoschmidt.image.ImageInfo; |
||
1272 | dev | 23 | import ak.photoalbum.util.TimestampRecipient; |
936 | dev | 24 | import ak.photoalbum.util.FileUtils; |
25 | import ak.photoalbum.util.FileNameComparator; |
||
26 | |||
27 | public class Thumbnailer |
||
28 | { |
||
29 | protected static final String DEFAULT_FORMAT = "jpg"; |
||
30 | protected static final String SMALL_SUFFIX = ".small"; |
||
31 | protected static final String MEDIUM_SUFFIX = ".medium"; |
||
32 | protected static final String DIR_SUFFIX = ".dir"; |
||
1074 | dev | 33 | protected static final String DIR_PART_SUFFIX = ".dir_part"; |
936 | dev | 34 | protected static final int DEFAULT_SMALL_WIDTH = 120; |
35 | protected static final int DEFAULT_SMALL_HEIGHT = 120; |
||
36 | protected static final int DEFAULT_MEDIUM_WIDTH = 800; |
||
37 | protected static final int DEFAULT_MEDIUM_HEIGHT = 800; |
||
38 | |||
1272 | dev | 39 | protected static final int CACHED_NOT_FOUND = 0; |
40 | protected static final int CACHED_NOT_MODIFIED = 1; |
||
41 | protected static final int CACHED_WRITTEN = 2; |
||
42 | |||
1274 | dev | 43 | private static final Logger logger = Logger.getLogger(Thumbnailer.class); |
44 | |||
936 | dev | 45 | protected ImageResizer resizer; |
46 | protected int smallWidth = DEFAULT_SMALL_WIDTH; |
||
47 | protected int smallHeight = DEFAULT_SMALL_HEIGHT; |
||
48 | protected int mediumWidth = DEFAULT_MEDIUM_WIDTH; |
||
49 | protected int mediumHeight = DEFAULT_MEDIUM_HEIGHT; |
||
50 | protected File cacheDir; |
||
51 | protected String format = DEFAULT_FORMAT; |
||
52 | protected Map smallCache = new HashMap(); |
||
53 | protected Map mediumCache = new HashMap(); |
||
54 | protected Map dirCache = new HashMap(); |
||
55 | protected File imagesRoot; |
||
56 | protected FileFilter imagesFilter = null; |
||
57 | protected Comparator fileNameComparator = new FileNameComparator(true); |
||
58 | protected Comparator fileNameComparatorRev = new FileNameComparator(false); |
||
1074 | dev | 59 | protected boolean nativeMode = true; // FIXME config |
936 | dev | 60 | |
61 | protected File dirTemplate; |
||
62 | protected ThumbnailPosition[] dirThumbnailPositions; |
||
63 | protected int[] dirTemplateSize; |
||
64 | protected long dirTemplateTimestamp; |
||
65 | |||
66 | public Thumbnailer() |
||
67 | { |
||
68 | } |
||
69 | |||
70 | public String getMime() |
||
71 | { |
||
72 | return FileUtils.getMime(format); |
||
73 | } |
||
74 | |||
75 | public int[] getDirSize(File origin) |
||
76 | throws IOException |
||
77 | { |
||
78 | if(dirTemplateSize == null |
||
79 | || dirTemplateTimestamp != dirTemplate.lastModified()) |
||
80 | { |
||
81 | dirTemplateSize = getOriginSize(dirTemplate); |
||
82 | dirTemplateTimestamp = dirTemplate.lastModified(); |
||
83 | } |
||
84 | |||
85 | return dirTemplateSize; |
||
86 | } |
||
87 | |||
88 | public int[] getSmallSize(File origin) |
||
89 | throws IOException |
||
90 | { |
||
91 | CachedFile cached = getCached(smallCache, origin); |
||
92 | |||
93 | if(cached == null) { |
||
94 | int[] originSize = getOriginSize(origin); |
||
95 | |||
96 | return calcSizes(originSize[0], originSize[1], smallWidth, smallHeight); |
||
97 | } |
||
98 | else { |
||
99 | int[] originSize = new int[2]; |
||
100 | |||
101 | originSize[0] = cached.getWidth(); |
||
102 | originSize[1] = cached.getHeight(); |
||
103 | |||
104 | return originSize; |
||
105 | } |
||
106 | } |
||
107 | |||
108 | public int[] getMediumSize(File origin) |
||
109 | throws IOException |
||
110 | { |
||
111 | CachedFile cached = getCached(mediumCache, origin); |
||
112 | |||
113 | if(cached == null) { |
||
114 | int[] originSize = getOriginSize(origin); |
||
115 | |||
116 | return calcSizes(originSize[0], originSize[1], mediumWidth, mediumHeight); |
||
117 | } |
||
118 | else { |
||
119 | int[] originSize = new int[2]; |
||
120 | |||
121 | originSize[0] = cached.getWidth(); |
||
122 | originSize[1] = cached.getHeight(); |
||
123 | |||
124 | return originSize; |
||
125 | } |
||
126 | } |
||
127 | |||
128 | protected int[] getOriginSize(File origin) |
||
129 | throws IOException |
||
130 | { |
||
131 | if(logger.isDebugEnabled()) |
||
132 | logger.debug("get size of " + origin.getCanonicalPath()); |
||
133 | |||
134 | ImageInfo ii = new ImageInfo(); |
||
135 | FileInputStream in = null; |
||
136 | int[] res = new int[2]; |
||
137 | |||
138 | try { |
||
139 | in = new FileInputStream(origin); |
||
140 | ii.setInput(in); |
||
141 | |||
1274 | dev | 142 | if(ii.check()) { |
143 | res[0] = ii.getWidth(); |
||
144 | res[1] = ii.getHeight(); |
||
145 | } |
||
146 | else{ |
||
936 | dev | 147 | logger.warn("not supported format of " + origin.getCanonicalPath()); |
148 | res[0] = 0; |
||
149 | res[1] = 0; |
||
150 | } |
||
151 | } |
||
152 | finally { |
||
153 | if(in != null) in.close(); |
||
154 | } |
||
155 | |||
156 | return res; |
||
157 | } |
||
158 | |||
159 | public void rebuildCache() |
||
160 | throws IOException |
||
161 | { |
||
162 | logger.info("rebuild cache"); |
||
163 | |||
164 | deleteCache(); |
||
165 | buildCache(); |
||
166 | } |
||
167 | |||
168 | public void deleteCache() |
||
169 | throws IOException |
||
170 | { |
||
171 | logger.info("delete cache"); |
||
172 | |||
173 | deleteCache(cacheDir); |
||
174 | } |
||
175 | |||
176 | public void buildCache() |
||
177 | throws IOException |
||
178 | { |
||
179 | logger.info("build cache"); |
||
180 | |||
181 | buildCache(imagesRoot); |
||
182 | } |
||
183 | |||
1257 | dev | 184 | public void reloadCache() |
185 | throws IOException |
||
186 | { |
||
187 | logger.info("reload cache"); |
||
188 | |||
189 | smallCache.clear(); |
||
190 | mediumCache.clear(); |
||
191 | dirCache.clear(); |
||
192 | loadCaches(cacheDir); |
||
193 | } |
||
194 | |||
936 | dev | 195 | protected void deleteCache(File dir) |
196 | throws IOException |
||
197 | { |
||
198 | File[] children = dir.listFiles(); |
||
199 | |||
200 | if(children == null) return; // the dir does not exists |
||
201 | |||
202 | Arrays.sort(children, fileNameComparator); |
||
203 | |||
204 | for(int i = 0; i < children.length; i++) { |
||
205 | if(children[i].isDirectory()) |
||
206 | deleteCache(children[i]); |
||
207 | else |
||
208 | children[i].delete(); |
||
209 | } |
||
210 | dir.delete(); |
||
211 | } |
||
212 | |||
213 | protected void buildCache(File dir) |
||
214 | throws IOException |
||
215 | { |
||
216 | File[] children; |
||
217 | |||
218 | if(imagesFilter == null) |
||
219 | children = dir.listFiles(); |
||
220 | else |
||
221 | children = dir.listFiles(imagesFilter); |
||
222 | |||
223 | if(children == null) return; // the dir does not exists |
||
224 | |||
225 | Arrays.sort(children, fileNameComparator); |
||
226 | |||
227 | for(int i = 0; i < children.length; i++) { |
||
228 | if(children[i].isDirectory()) { |
||
1272 | dev | 229 | writeDir(children[i], -1, null, null); |
936 | dev | 230 | buildCache(children[i]); |
231 | } |
||
232 | else { |
||
1272 | dev | 233 | writeSmall(children[i], -1, null, null); |
234 | writeMedium(children[i], -1, null, null); |
||
936 | dev | 235 | } |
236 | } |
||
237 | } |
||
238 | |||
239 | protected CachedFile getCached(Map cache, File imageFile) |
||
240 | throws IOException |
||
241 | { |
||
1272 | dev | 242 | String key = imageFile.getCanonicalPath(); |
243 | CachedFile cached = (CachedFile)cache.get(key); |
||
936 | dev | 244 | |
245 | if(cached == null || !cached.getFile().exists()) { |
||
1272 | dev | 246 | cache.remove(key); |
936 | dev | 247 | logger.debug("not found in cache"); |
248 | return null; |
||
249 | } |
||
250 | |||
251 | if(cached.getOriginTimestamp() != imageFile.lastModified()) { |
||
252 | cached.getFile().delete(); |
||
1272 | dev | 253 | cache.remove(key); |
936 | dev | 254 | logger.debug("timestamps dont match"); |
255 | return null; |
||
256 | } |
||
257 | |||
258 | return cached; |
||
259 | } |
||
260 | |||
1272 | dev | 261 | protected int writeCached(Map cache, File imageFile, long ifModifiedSince, |
262 | OutputStream out, TimestampRecipient timestampRecipient) |
||
936 | dev | 263 | throws IOException |
264 | { |
||
265 | CachedFile cached = getCached(cache, imageFile); |
||
266 | |||
1272 | dev | 267 | if(cached == null) return CACHED_NOT_FOUND; |
936 | dev | 268 | |
1272 | dev | 269 | if(ifModifiedSince >= 0 && ifModifiedSince <= cached.getTimestamp()) |
270 | return CACHED_NOT_MODIFIED; // cached image found and not modified |
||
271 | // since given timestamp - do nothing |
||
272 | |||
936 | dev | 273 | if(logger.isDebugEnabled()) |
274 | logger.debug("write cached " + imageFile.getCanonicalPath()); |
||
275 | |||
1272 | dev | 276 | if(timestampRecipient != null) { |
277 | timestampRecipient.setTimestamp(cached.getTimestamp()); |
||
278 | } |
||
279 | |||
936 | dev | 280 | if(out != null) { |
281 | FileInputStream in = null; |
||
282 | |||
283 | try { |
||
284 | in = new FileInputStream(cached.getFile()); |
||
285 | FileUtils.copyStreams(in, out); |
||
286 | } |
||
287 | finally { |
||
288 | if(in != null) in.close(); |
||
289 | } |
||
290 | } |
||
291 | |||
1272 | dev | 292 | return CACHED_WRITTEN; |
936 | dev | 293 | } |
294 | |||
1074 | dev | 295 | protected File getCacheFile(File dir, File imageFile, String suffix) |
296 | { |
||
297 | return new File(dir, imageFile.getName() + suffix + "." + format); |
||
298 | } |
||
299 | |||
1272 | dev | 300 | // @return last modified timestamp pf the cached thumbnail |
301 | protected long cacheThumbnail(Map cache, File imageFile, |
||
936 | dev | 302 | BufferedImage thumbnail, String suffix, int width, int height) |
303 | throws IOException |
||
304 | { |
||
305 | logger.debug("cache thumbnail " + suffix + " " |
||
306 | + imageFile.getCanonicalPath()); |
||
307 | |||
308 | File dir = getCacheFileDir(imageFile); |
||
1074 | dev | 309 | File cacheFile = getCacheFile(dir, imageFile, suffix); |
936 | dev | 310 | |
311 | dir.mkdirs(); |
||
312 | ImageIO.write(thumbnail, format, cacheFile); |
||
313 | |||
314 | cache.put(imageFile.getCanonicalPath(), |
||
315 | new CachedFile(cacheFile, imageFile, |
||
316 | cacheFile.lastModified(), imageFile.lastModified(), width, height)); |
||
1272 | dev | 317 | |
318 | return cacheFile.lastModified(); |
||
936 | dev | 319 | } |
320 | |||
321 | public void startup() |
||
322 | throws IOException |
||
323 | { |
||
324 | logger.info("startup"); |
||
325 | loadCaches(cacheDir); |
||
326 | logger.info("started"); |
||
327 | } |
||
328 | |||
1074 | dev | 329 | protected void loadFileCache(File file) |
936 | dev | 330 | throws IOException |
1074 | dev | 331 | { |
332 | /* FIXME use this optimization? |
||
936 | dev | 333 | String dirEnd = DIR_SUFFIX + "." + format; |
334 | String smallEnd = SMALL_SUFFIX + "." + format; |
||
335 | String mediumEnd = MEDIUM_SUFFIX + "." + format; |
||
1074 | dev | 336 | */ |
936 | dev | 337 | |
1251 | dev | 338 | File origin = null; |
339 | Map cache = null; |
||
936 | dev | 340 | |
1074 | dev | 341 | if(file.getName().endsWith(SMALL_SUFFIX + "." + format)) { |
342 | origin = getOriginFile(file, SMALL_SUFFIX); |
||
343 | cache = smallCache; |
||
936 | dev | 344 | |
1074 | dev | 345 | if(logger.isDebugEnabled()) |
346 | logger.debug("load cached small " + file.getCanonicalPath() |
||
347 | + " for " + origin.getCanonicalPath()); |
||
348 | } |
||
349 | else if(file.getName().endsWith(MEDIUM_SUFFIX + "." + format)) { |
||
350 | origin = getOriginFile(file, MEDIUM_SUFFIX); |
||
351 | cache = mediumCache; |
||
936 | dev | 352 | |
1074 | dev | 353 | if(logger.isDebugEnabled()) |
354 | logger.debug("load cached medium " + file.getCanonicalPath() |
||
355 | + " for " + origin.getCanonicalPath()); |
||
356 | } |
||
357 | else if(file.getName().endsWith(DIR_SUFFIX + "." + format)) { |
||
358 | origin = getOriginFile(file, DIR_SUFFIX); |
||
359 | cache = dirCache; |
||
936 | dev | 360 | |
1074 | dev | 361 | if(logger.isDebugEnabled()) |
362 | logger.debug("load cached dir " + file.getCanonicalPath() |
||
363 | + " for " + origin.getCanonicalPath()); |
||
364 | } |
||
1251 | dev | 365 | else if(file.getName().endsWith(DIR_PART_SUFFIX + "." + format)) { |
366 | if(logger.isDebugEnabled()) |
||
367 | logger.debug("skip cached dir part " + file.getCanonicalPath()); |
||
368 | } |
||
1074 | dev | 369 | else { |
370 | if(logger.isInfoEnabled()) |
||
371 | logger.warn( |
||
372 | "unknown type of cached " + file.getCanonicalPath()); |
||
373 | } |
||
936 | dev | 374 | |
1274 | dev | 375 | if(origin == null || cache == null) { |
1257 | dev | 376 | file.delete(); |
377 | |||
378 | if(logger.isDebugEnabled()) |
||
379 | logger.debug("deleted (unknown origin)"); |
||
380 | } |
||
1274 | dev | 381 | else { |
382 | loadCacheInfo(file, origin, cache); |
||
383 | } |
||
1074 | dev | 384 | } |
936 | dev | 385 | |
1074 | dev | 386 | protected void loadCacheInfo(File file, File origin, Map cache) |
387 | throws IOException |
||
388 | { |
||
389 | long originTimestamp = origin.lastModified(); |
||
390 | long cachedTimestamp = file.lastModified(); |
||
391 | int[] sizes = getOriginSize(file); |
||
936 | dev | 392 | |
1074 | dev | 393 | if(origin.exists() && cachedTimestamp >= originTimestamp) { |
394 | cache.put(origin.getCanonicalPath(), |
||
395 | new CachedFile(file, origin, cachedTimestamp, |
||
396 | originTimestamp, sizes[0], sizes[1])); |
||
936 | dev | 397 | |
1074 | dev | 398 | logger.debug("added"); |
399 | } |
||
400 | else { |
||
401 | file.delete(); |
||
936 | dev | 402 | |
1074 | dev | 403 | if(logger.isDebugEnabled()) |
404 | logger.debug("deleted: " + origin.exists() |
||
405 | + " " + cachedTimestamp + " " + originTimestamp); |
||
406 | } |
||
407 | } |
||
936 | dev | 408 | |
1074 | dev | 409 | protected void loadCaches(File dir) |
410 | throws IOException |
||
411 | { |
||
412 | if(logger.isDebugEnabled()) |
||
413 | logger.debug("load caches in " + dir.getCanonicalPath()); |
||
414 | |||
415 | File[] children = dir.listFiles(); |
||
416 | |||
417 | if(children == null) return; // the dir does not exists |
||
418 | |||
419 | for(int i = 0; i < children.length; i++) { |
||
420 | if(children[i].isDirectory()) |
||
421 | loadCaches(children[i]); |
||
422 | else |
||
423 | loadFileCache(children[i]); |
||
936 | dev | 424 | } |
425 | } |
||
426 | |||
427 | protected File getOriginFile(File cached, String suffix) |
||
428 | throws IOException |
||
429 | { |
||
430 | String fileEnd = suffix + "." + format; |
||
431 | String fileName = cached.getName(); |
||
432 | String cachedPath = cached.getParentFile().getCanonicalPath(); |
||
433 | String cacheRoot = cacheDir.getCanonicalPath(); |
||
434 | |||
435 | fileName = fileName.substring(0, fileName.length() - fileEnd.length()); |
||
436 | |||
437 | if(!cacheRoot.equals(cachedPath)) |
||
438 | if(!cacheRoot.endsWith(File.separator)) cacheRoot += File.separator; |
||
439 | |||
440 | return new File(imagesRoot, cachedPath.substring(cacheRoot.length()) |
||
441 | + File.separator + fileName); |
||
442 | } |
||
443 | |||
444 | protected File getCacheFileDir(File imageFile) |
||
445 | throws IOException |
||
446 | { |
||
447 | String imagePath = imageFile.getParentFile().getCanonicalPath(); |
||
448 | String rootPath = imagesRoot.getCanonicalPath(); |
||
449 | |||
450 | if(imagePath.equals(rootPath)) return cacheDir; |
||
451 | if(!rootPath.endsWith(File.separator)) rootPath += File.separator; |
||
452 | |||
453 | if(!imagePath.startsWith(rootPath)) |
||
454 | throw new RuntimeException("Image " + imageFile.getCanonicalPath() |
||
455 | + " is not under images root " + imagesRoot.getCanonicalPath()); |
||
456 | |||
457 | return new File(cacheDir, imagePath.substring(rootPath.length())); |
||
458 | } |
||
459 | |||
1272 | dev | 460 | protected boolean writeThumbnail(Map cache, String suffix, int width, int height, |
461 | File imageFile, long ifModifiedSince, OutputStream out, TimestampRecipient timestampRecipient) |
||
936 | dev | 462 | throws IOException |
463 | { |
||
1272 | dev | 464 | int cachedResult = writeCached(cache, imageFile, ifModifiedSince, out, timestampRecipient); |
465 | if(cachedResult != CACHED_NOT_FOUND) return (cachedResult == CACHED_WRITTEN); |
||
936 | dev | 466 | |
1074 | dev | 467 | if(nativeMode) { |
468 | File dir = getCacheFileDir(imageFile); |
||
1272 | dev | 469 | File cacheFile = getCacheFile(dir, imageFile, suffix); |
936 | dev | 470 | |
1272 | dev | 471 | createThumbnailNative(dir, cacheFile, imageFile, width, height); |
472 | loadCacheInfo(cacheFile, imageFile, cache); |
||
473 | writeCached(cache, imageFile, -1, out, timestampRecipient); |
||
1074 | dev | 474 | } |
475 | else { |
||
1272 | dev | 476 | BufferedImage thumbnail = createThumbnail(imageFile, width, height); |
936 | dev | 477 | |
1272 | dev | 478 | if(thumbnail != null) { |
1074 | dev | 479 | // a thumbnail returned - save it into the cache dir |
480 | int sizes[] = calcSizes( |
||
1272 | dev | 481 | thumbnail.getWidth(null), thumbnail.getHeight(null), width, height); |
482 | long lastModified = cacheThumbnail( |
||
483 | cache, imageFile, thumbnail, suffix, sizes[0], sizes[1]); |
||
1074 | dev | 484 | |
1272 | dev | 485 | if(timestampRecipient != null) { |
486 | timestampRecipient.setTimestamp(lastModified); |
||
487 | } |
||
488 | |||
489 | if(out != null) { |
||
490 | ImageIO.write(thumbnail, format, out); |
||
491 | } |
||
1074 | dev | 492 | } |
493 | } |
||
1272 | dev | 494 | |
495 | return true; // image written |
||
936 | dev | 496 | } |
497 | |||
1272 | dev | 498 | public boolean writeSmall(File imageFile, long ifModifiedSince, |
499 | OutputStream out, TimestampRecipient timestampRecipient) |
||
936 | dev | 500 | throws IOException |
501 | { |
||
502 | if(logger.isInfoEnabled()) |
||
1272 | dev | 503 | logger.info("write small " + imageFile.getCanonicalPath()); |
936 | dev | 504 | |
1272 | dev | 505 | return writeThumbnail(smallCache, SMALL_SUFFIX, smallWidth, smallHeight, |
506 | imageFile, ifModifiedSince, out, timestampRecipient); |
||
507 | } |
||
936 | dev | 508 | |
1272 | dev | 509 | public boolean writeMedium(File imageFile, long ifModifiedSince, |
510 | OutputStream out, TimestampRecipient timestampRecipient) |
||
511 | throws IOException |
||
512 | { |
||
513 | if(logger.isInfoEnabled()) |
||
514 | logger.info("write medium " + imageFile.getCanonicalPath()); |
||
936 | dev | 515 | |
1272 | dev | 516 | return writeThumbnail(mediumCache, MEDIUM_SUFFIX, mediumWidth, mediumHeight, |
517 | imageFile, ifModifiedSince, out, timestampRecipient); |
||
936 | dev | 518 | } |
519 | |||
1272 | dev | 520 | public boolean writeDir(File dir, long ifModifiedSince, |
521 | OutputStream out, TimestampRecipient timestampRecipient) |
||
936 | dev | 522 | throws IOException |
523 | { |
||
524 | if(logger.isInfoEnabled()) |
||
525 | logger.info("write dir " + dir.getCanonicalPath()); |
||
526 | |||
1272 | dev | 527 | int cachedResult = writeCached(dirCache, dir, ifModifiedSince, out, timestampRecipient); |
528 | if(cachedResult != CACHED_NOT_FOUND) return (cachedResult == CACHED_WRITTEN); |
||
936 | dev | 529 | |
530 | BufferedImage thumbnail = createDirThumbnail(dir); |
||
531 | |||
532 | if(thumbnail != null) { |
||
533 | if(dirTemplateSize == null |
||
534 | || dirTemplateTimestamp != dirTemplate.lastModified()) |
||
535 | { |
||
536 | dirTemplateSize = getOriginSize(dirTemplate); |
||
537 | dirTemplateTimestamp = dirTemplate.lastModified(); |
||
538 | } |
||
539 | |||
1272 | dev | 540 | long lastModified = cacheThumbnail(dirCache, dir, thumbnail, DIR_SUFFIX, |
936 | dev | 541 | dirTemplateSize[0], dirTemplateSize[1]); |
542 | |||
1272 | dev | 543 | if(timestampRecipient != null) { |
544 | timestampRecipient.setTimestamp(lastModified); |
||
545 | } |
||
546 | |||
547 | if(out != null) { |
||
548 | ImageIO.write(thumbnail, format, out); |
||
549 | } |
||
936 | dev | 550 | } |
1272 | dev | 551 | |
552 | return true; // image written |
||
936 | dev | 553 | } |
554 | |||
1274 | dev | 555 | protected BufferedImage createThumbnail(File imageFile, |
936 | dev | 556 | int width, int height) |
557 | throws IOException |
||
558 | { |
||
1274 | dev | 559 | // FIXME make several instances parallel if we have sevelar processors |
560 | synchronized(this) { |
||
561 | if(logger.isDebugEnabled()) |
||
562 | logger.debug("create thumbnail " + imageFile.getCanonicalPath()); |
||
936 | dev | 563 | |
1274 | dev | 564 | Image image = loadImage(imageFile.getCanonicalPath()); |
565 | // = ImageIO.read(imageFile); |
||
566 | int[] sizes; |
||
936 | dev | 567 | |
1274 | dev | 568 | if(image == null) { // not supported format |
569 | logger.warn("unsupported format for origin or operation interrupted"); |
||
936 | dev | 570 | |
1274 | dev | 571 | return null; |
572 | } |
||
573 | else { |
||
574 | sizes = calcSizes(image.getWidth(null), image.getHeight(null), |
||
575 | width, height); |
||
576 | logger.debug("resize to " + sizes[0] + "x" + sizes[1]); |
||
936 | dev | 577 | |
1274 | dev | 578 | return resizer.resize(image, sizes[0], sizes[1]); |
579 | } |
||
936 | dev | 580 | } |
581 | } |
||
582 | |||
1274 | dev | 583 | protected BufferedImage createThumbnailNative(File dir, File cacheFile, |
1074 | dev | 584 | File imageFile, int width, int height) |
585 | throws IOException |
||
586 | { |
||
1274 | dev | 587 | // FIXME make several instances parallel if we have sevelar processors |
588 | synchronized(this) { |
||
589 | if(logger.isDebugEnabled()) |
||
590 | logger.debug("create thumbnail2 " + imageFile.getCanonicalPath() + " to " |
||
591 | + cacheFile.getCanonicalPath()); |
||
1074 | dev | 592 | |
1274 | dev | 593 | dir.mkdirs(); |
1074 | dev | 594 | |
1274 | dev | 595 | // FIXME: 1) make util path (and params?) configurable |
596 | Process process = Runtime.getRuntime().exec(new String[] { |
||
597 | "/usr/local/bin/convert", |
||
598 | "-size", width + "x" + height, |
||
599 | "-thumbnail", width + "x" + height, |
||
600 | imageFile.getCanonicalPath(), |
||
601 | cacheFile.getCanonicalPath() |
||
602 | }); |
||
1074 | dev | 603 | |
1274 | dev | 604 | // FIXME make it finner |
605 | BufferedReader in = new BufferedReader(new InputStreamReader(process.getErrorStream())); |
||
606 | String line; |
||
607 | while((line = in.readLine()) != null) { |
||
608 | logger.info("EXEC: " + line); |
||
609 | } |
||
1074 | dev | 610 | |
1274 | dev | 611 | try { |
612 | int res = process.waitFor(); |
||
1074 | dev | 613 | |
1274 | dev | 614 | if(logger.isDebugEnabled()) |
615 | logger.debug("process exited with result " + res); |
||
616 | } |
||
617 | catch(InterruptedException ex) { |
||
618 | logger.debug("process interrupted"); |
||
619 | } |
||
1074 | dev | 620 | |
1274 | dev | 621 | return null; |
622 | } |
||
1074 | dev | 623 | } |
624 | |||
1274 | dev | 625 | protected BufferedImage createDirThumbnail(File dir) |
936 | dev | 626 | throws IOException |
627 | { |
||
1274 | dev | 628 | // FIXME make several instances parallel if we have sevelar processors |
629 | synchronized(this) { |
||
630 | long timeStart = System.currentTimeMillis(); |
||
1074 | dev | 631 | |
1274 | dev | 632 | if(logger.isDebugEnabled()) |
633 | logger.debug("create dir thumbnail " + dir.getCanonicalPath()); |
||
936 | dev | 634 | |
1274 | dev | 635 | Image template = loadImage(dirTemplate.getCanonicalPath()); |
636 | BufferedImage dirThumbnail; |
||
637 | Graphics graphics; |
||
638 | int count; |
||
639 | File[] firstFiles; |
||
936 | dev | 640 | |
1274 | dev | 641 | if(template == null) { // not supported format |
642 | logger.warn("unsupported format for template or operation interrupted"); |
||
936 | dev | 643 | |
1274 | dev | 644 | return null; |
645 | } |
||
936 | dev | 646 | |
1274 | dev | 647 | dirThumbnail = createBufferedImage(template); |
936 | dev | 648 | |
1274 | dev | 649 | graphics = dirThumbnail.getGraphics(); |
650 | count = dirThumbnailPositions.length; |
||
651 | firstFiles = new File[count]; |
||
652 | count = getFirstFiles(dir, count, firstFiles, 0); |
||
936 | dev | 653 | |
1274 | dev | 654 | for(int i = 0; i < count; i++) { |
655 | Image image; |
||
656 | BufferedImage thumbnail = null; |
||
657 | int[] sizes; |
||
936 | dev | 658 | |
1274 | dev | 659 | if(nativeMode) { |
660 | File cacheFileDir = getCacheFileDir(firstFiles[i]); |
||
661 | File cacheFile = getCacheFile(cacheFileDir, firstFiles[i], DIR_PART_SUFFIX); |
||
662 | createThumbnailNative(cacheFileDir, cacheFile, firstFiles[i], |
||
663 | dirThumbnailPositions[i].getWidth(), dirThumbnailPositions[i].getHeight()); |
||
664 | image = loadImage(cacheFile.getCanonicalPath()); |
||
665 | thumbnail = createBufferedImage(image); |
||
666 | } |
||
667 | else { |
||
668 | image = loadImage(firstFiles[i].getCanonicalPath()); |
||
669 | } |
||
1074 | dev | 670 | |
1274 | dev | 671 | if(image == null) { // not supported format |
672 | logger.warn("unsupported format for origin or operation interrupted"); |
||
936 | dev | 673 | |
1274 | dev | 674 | return null; |
675 | } |
||
676 | else { |
||
677 | sizes = calcSizes(image.getWidth(null), image.getHeight(null), |
||
678 | dirThumbnailPositions[i].getWidth(), |
||
679 | dirThumbnailPositions[i].getHeight()); |
||
680 | } |
||
681 | |||
682 | if(!nativeMode) { |
||
683 | thumbnail = resizer.resize(image, sizes[0], sizes[1]); |
||
684 | } |
||
685 | |||
686 | graphics.drawImage(thumbnail, |
||
687 | getXPosition(dirThumbnailPositions[i].getX(), |
||
688 | dirThumbnailPositions[i].getWidth(), sizes[0], |
||
689 | dirThumbnailPositions[i].getHorAlign()), |
||
690 | getYPosition(dirThumbnailPositions[i].getY(), |
||
691 | dirThumbnailPositions[i].getHeight(), sizes[1], |
||
692 | dirThumbnailPositions[i].getVertAlign()), |
||
693 | null); |
||
936 | dev | 694 | } |
695 | |||
1274 | dev | 696 | if(logger.isDebugEnabled()) { |
697 | logger.debug("dir thumbnail created in " |
||
698 | + (System.currentTimeMillis() - timeStart) + " ms"); |
||
936 | dev | 699 | } |
1074 | dev | 700 | |
1274 | dev | 701 | return dirThumbnail; |
936 | dev | 702 | } |
703 | } |
||
704 | |||
705 | protected Image loadImage(String fileName) |
||
706 | { |
||
1074 | dev | 707 | long timeStart = System.currentTimeMillis(); |
708 | |||
936 | dev | 709 | // FIXME: probably toolbox reads an image not by every request but |
710 | // caches it |
||
711 | Toolkit toolkit = Toolkit.getDefaultToolkit(); |
||
712 | Image image = toolkit.getImage(fileName); |
||
713 | |||
714 | toolkit.prepareImage(image, -1, -1, null); |
||
715 | |||
716 | while(true) { |
||
717 | int status = toolkit.checkImage(image, -1, -1, null); |
||
718 | |||
719 | if((status & ImageObserver.ALLBITS) != 0) break; |
||
720 | if((status & ImageObserver.ERROR) != 0) return null; |
||
721 | |||
722 | try { |
||
723 | Thread.sleep(100); |
||
724 | } |
||
725 | catch(Exception ex) { |
||
726 | return null; |
||
727 | } |
||
728 | } |
||
729 | |||
1074 | dev | 730 | if(logger.isDebugEnabled()) { |
731 | logger.debug("image " + fileName + " loaded in " |
||
732 | + (System.currentTimeMillis() - timeStart) + " ms"); |
||
733 | } |
||
734 | |||
936 | dev | 735 | return image; |
736 | } |
||
737 | |||
738 | protected int[] calcSizes(int width, int height, int maxWidth, int maxHeight) |
||
739 | { |
||
740 | int[] result = new int[2]; |
||
741 | double xRate; |
||
742 | double yRate; |
||
743 | |||
744 | if(width == 0 || height == 0) { |
||
745 | result[0] = 0; |
||
746 | result[1] = 0; |
||
747 | return result; |
||
748 | } |
||
749 | |||
750 | xRate = (double)maxWidth / (double)width; |
||
751 | yRate = (double)maxHeight / (double)height; |
||
752 | if(xRate >= 1.0 || yRate >= 1.0) { |
||
753 | result[0] = width; |
||
754 | result[1] = height; |
||
755 | } |
||
756 | else if(xRate > yRate) { |
||
757 | result[0] = maxHeight * width / height; |
||
758 | result[1] = maxHeight; |
||
759 | } |
||
760 | else { |
||
761 | result[0] = maxWidth; |
||
762 | result[1] = maxWidth * height / width; |
||
763 | } |
||
764 | |||
765 | return result; |
||
766 | } |
||
767 | |||
768 | protected int getXPosition(int left, int maxWidth, int width, int align) |
||
769 | { |
||
770 | if(align == ThumbnailPosition.ALIGN_HOR_LEFT) |
||
771 | return left; |
||
772 | else if(align == ThumbnailPosition.ALIGN_HOR_RIGHT) |
||
773 | return left + (maxWidth - width); |
||
774 | else if(align == ThumbnailPosition.ALIGN_HOR_CENTER) |
||
775 | return left + (maxWidth - width) / 2; |
||
776 | else |
||
777 | throw new RuntimeException("Unknown align type: " + align); |
||
778 | } |
||
779 | |||
780 | protected int getYPosition(int top, int maxHeight, int height, int align) |
||
781 | { |
||
782 | if(align == ThumbnailPosition.ALIGN_VERT_TOP) |
||
783 | return top; |
||
784 | else if(align == ThumbnailPosition.ALIGN_VERT_BOTTOM) |
||
785 | return top + (maxHeight - height); |
||
786 | else if(align == ThumbnailPosition.ALIGN_VERT_CENTER) |
||
787 | return top + (maxHeight - height) / 2; |
||
788 | else |
||
789 | throw new RuntimeException("Unknown align type: " + align); |
||
790 | } |
||
791 | |||
792 | protected int getFirstFiles(File dir, int count, File[] files, int pos) |
||
793 | { |
||
794 | File[] children; |
||
795 | |||
796 | if(imagesFilter == null) |
||
797 | children = dir.listFiles(); |
||
798 | else |
||
799 | children = dir.listFiles(imagesFilter); |
||
800 | |||
801 | if(children == null) return 0; // the dir does not exists |
||
802 | |||
803 | Arrays.sort(children, fileNameComparatorRev); |
||
804 | |||
1254 | dev | 805 | for(int i = 0; i < children.length && pos < count; i++) { |
1274 | dev | 806 | if(children[i].isDirectory()) { |
807 | //pos = getFirstFiles(children[i], count, files, pos); |
||
808 | } |
||
936 | dev | 809 | else { |
810 | files[pos++] = children[i]; |
||
811 | } |
||
812 | } |
||
813 | |||
814 | return pos; |
||
815 | } |
||
816 | |||
817 | protected BufferedImage createBufferedImage(Image image) |
||
818 | { |
||
819 | if(image == null) return null; |
||
820 | |||
821 | int width = image.getWidth(null); |
||
822 | int height = image.getHeight(null); |
||
823 | |||
824 | if(width < 1 || height < 1) return null; |
||
825 | |||
826 | // get pixels |
||
827 | int[] pixels = new int[width * height]; |
||
828 | PixelGrabber pg = new PixelGrabber(image, |
||
829 | 0, 0, width, height, pixels, 0, width); |
||
830 | |||
831 | try { |
||
832 | pg.grabPixels(); |
||
833 | } |
||
834 | catch(InterruptedException e) { |
||
835 | return null; |
||
836 | } |
||
837 | |||
838 | if((pg.getStatus() & ImageObserver.ABORT) != 0) return null; |
||
839 | |||
840 | // create buffered image |
||
841 | BufferedImage buffered = new BufferedImage(width, height, |
||
842 | BufferedImage.TYPE_INT_RGB); |
||
843 | |||
844 | for(int y = 0; y < height; y++) { |
||
845 | for(int x = 0; x < width; x++) |
||
846 | buffered.setRGB(x, y, pixels[y * width + x]); |
||
847 | } |
||
848 | |||
849 | return buffered; |
||
850 | } |
||
851 | |||
852 | public ImageResizer getResizer() |
||
853 | { |
||
854 | return resizer; |
||
855 | } |
||
856 | |||
857 | public void setResizer(ImageResizer resizer) |
||
858 | { |
||
859 | this.resizer = resizer; |
||
860 | } |
||
861 | |||
862 | public String getFormat() |
||
863 | { |
||
864 | return format; |
||
865 | } |
||
866 | |||
867 | public void setFormat(String format) |
||
868 | { |
||
869 | this.format = format; |
||
870 | } |
||
871 | |||
872 | public File getCacheDir() |
||
873 | { |
||
874 | return cacheDir; |
||
875 | } |
||
876 | |||
877 | public void setCacheDir(File dir) |
||
878 | { |
||
879 | this.cacheDir = dir; |
||
880 | } |
||
881 | |||
882 | public File getImagesRoot() |
||
883 | { |
||
884 | return imagesRoot; |
||
885 | } |
||
886 | |||
887 | public void setImagesRoot(File dir) |
||
888 | { |
||
889 | this.imagesRoot = dir; |
||
890 | } |
||
891 | |||
892 | public int getSmallWidth() |
||
893 | { |
||
894 | return smallWidth; |
||
895 | } |
||
896 | |||
897 | public void setSmallWidth(int width) |
||
898 | { |
||
899 | this.smallWidth = width; |
||
900 | } |
||
901 | |||
902 | public int getSmallHeight() |
||
903 | { |
||
904 | return smallHeight; |
||
905 | } |
||
906 | |||
907 | public void setSmallHeight(int height) |
||
908 | { |
||
909 | this.smallHeight = height; |
||
910 | } |
||
911 | |||
912 | public int getMediumWidth() |
||
913 | { |
||
914 | return mediumWidth; |
||
915 | } |
||
916 | |||
917 | public void setMediumWidth(int width) |
||
918 | { |
||
919 | this.mediumWidth = width; |
||
920 | } |
||
921 | |||
922 | public int getMediumHeight() |
||
923 | { |
||
924 | return mediumHeight; |
||
925 | } |
||
926 | |||
927 | public void setMediumHeight(int height) |
||
928 | { |
||
929 | this.mediumHeight = height; |
||
930 | } |
||
931 | |||
932 | public FileFilter getImagesFilter() |
||
933 | { |
||
934 | return imagesFilter; |
||
935 | } |
||
936 | |||
937 | public void setImagesFilter(FileFilter filter) |
||
938 | { |
||
939 | this.imagesFilter = filter; |
||
940 | } |
||
941 | |||
942 | public File getDirTemplate() |
||
943 | { |
||
944 | return dirTemplate; |
||
945 | } |
||
946 | |||
947 | public void setDirTemplate(File dirTemplate) |
||
948 | { |
||
949 | this.dirTemplate = dirTemplate; |
||
950 | } |
||
951 | |||
952 | public ThumbnailPosition[] getDirThumbnailPositions() |
||
953 | { |
||
954 | return dirThumbnailPositions; |
||
955 | } |
||
956 | |||
957 | public void setDirThumbnailPositions( |
||
958 | ThumbnailPosition[] dirThumbnailPositions) |
||
959 | { |
||
960 | this.dirThumbnailPositions = dirThumbnailPositions; |
||
961 | } |
||
962 | } |