1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.tools; |
17 | |
18 | import java.awt.Color; |
19 | import java.awt.Dimension; |
20 | import java.awt.image.RenderedImage; |
21 | import java.io.File; |
22 | import java.io.IOException; |
23 | import java.util.HashMap; |
24 | import java.util.HashSet; |
25 | import java.util.Iterator; |
26 | import java.util.Map; |
27 | import java.util.Set; |
28 | |
29 | import javax.imageio.IIOException; |
30 | |
31 | import org.apache.commons.logging.Log; |
32 | import org.apache.commons.logging.LogFactory; |
33 | |
34 | /** |
35 | * Cache to store RenderedImages in memory. When the memory usage exceeds a maximum size, the least |
36 | * recent images are removed. Note that the cache holds at least one image, so if this is bigger |
37 | * than the maximum size, so is the whole cache. |
38 | */ |
39 | public class ImageCache implements CacheInfo |
40 | { |
41 | private static final int BROKEN_IMAGE_HEIGHT = 800; |
42 | private static final int BROKEN_IMAGE_WIDTH = 600; |
43 | private static final int DEFAULT_BROKEN_BACKGROUND = 0xf0f0f0; |
44 | private static final int DEFAULT_BROKEN_FOREGROUND = 0xe04040; |
45 | private static final int DEFAULT_BUSY_BACKGROUND = 0xf0f0f0; |
46 | private static final int DEFAULT_BUSY_FOREGROUND = 0xd0d0d0; |
47 | private static final double ONE_HUNDRED = 100.0; |
48 | private Color brokenBackgroundColor; |
49 | private Color brokenForegroundColor; |
50 | private Color busyBackgroundColor; |
51 | private Color busyForegroundColor; |
52 | private boolean disposed; |
53 | private Map entries; |
54 | private ImageTools imageTools; |
55 | private Log logger; |
56 | private long maxMemorySize; |
57 | private long memorySize; |
58 | private String name; |
59 | private ImageCacheRenderThread renderer; |
60 | private Set unrenderableImageFiles; |
61 | |
62 | public ImageCache(String newName, long newMaxMemorySize) { |
63 | assert newName != null; |
64 | assert newName.length() > 0; |
65 | assert newMaxMemorySize >= 0; |
66 | logger = LogFactory.getLog(ImageCache.class); |
67 | imageTools = ImageTools.instance(); |
68 | name = newName; |
69 | entries = new HashMap(); |
70 | // TODO: Collect paths of unrenderable images so they won't be attempted to be rendered again. |
71 | unrenderableImageFiles = new HashSet(); |
72 | maxMemorySize = newMaxMemorySize; |
73 | brokenBackgroundColor = new Color(DEFAULT_BROKEN_BACKGROUND); |
74 | brokenForegroundColor = new Color(DEFAULT_BROKEN_FOREGROUND); |
75 | busyBackgroundColor = new Color(DEFAULT_BUSY_BACKGROUND); |
76 | busyForegroundColor = new Color(DEFAULT_BUSY_FOREGROUND); |
77 | renderer = new ImageCacheRenderThread(this); |
78 | renderer.start(); |
79 | } |
80 | |
81 | public RenderedImage get(File imageFile) |
82 | throws IOException { |
83 | assert !disposed; |
84 | assert imageFile != null; |
85 | ImageCacheEntry entry = (ImageCacheEntry) entries.get(imageFile); |
86 | |
87 | if (entry == null) { |
88 | RenderedImage image = obtainImage(imageFile); |
89 | |
90 | entry = new ImageCacheEntry(imageFile, image); |
91 | |
92 | long entryMemorySize = entry.getImageSize(); |
93 | |
94 | memorySize += entryMemorySize; |
95 | if (logger.isInfoEnabled()) { |
96 | logger.info("memory size increased by " + entryMemorySize + " bytes to " + memorySize); |
97 | } |
98 | while ((memorySize > maxMemorySize) && (entries.size() > 0)) { |
99 | ImageCacheEntry leastRecentlyAccessedEntry = findLeastRecentlyAccessedEntry(); |
100 | |
101 | logger.info("removing least recently used image to meet memory size requirement"); |
102 | remove(leastRecentlyAccessedEntry); |
103 | } |
104 | entries.put(imageFile, entry); |
105 | if (logger.isInfoEnabled()) { |
106 | double percentUsed = (ONE_HUNDRED * getUsedSize() / getMaxSize()); |
107 | |
108 | if (logger.isInfoEnabled()) { |
109 | logger.info("entry added: " + imageFile); |
110 | logger.info("current cache statistics: " + getEntryCount() + " entries use " |
111 | + getUsedSize() + " of " + getMaxSize() + " bytes (" |
112 | + StringTools.instance().getPercentText(percentUsed) + "%)"); |
113 | } |
114 | } |
115 | } |
116 | |
117 | assert entry != null; |
118 | entry.updateLastAccessed(); |
119 | |
120 | RenderedImage result = entry.getImage(); |
121 | |
122 | assert result != null; |
123 | return result; |
124 | } |
125 | |
126 | public RenderedImage get(File imageFile, ImageInCacheListener listener) |
127 | throws IOException { |
128 | assert !disposed; |
129 | assert imageFile != null; |
130 | assert listener != null; |
131 | RenderedImage result; |
132 | |
133 | synchronized (entries) { |
134 | if (has(imageFile)) { |
135 | result = get(imageFile); |
136 | } else { |
137 | Dimension imageDimension = obtainImageDimension(imageFile); |
138 | |
139 | result = imageTools.createBusyImage(imageDimension.width, imageDimension.height, |
140 | busyBackgroundColor, busyForegroundColor); |
141 | |
142 | renderer.addTask(imageFile, listener, ImageCacheRenderThread.PRIORITY_SOON); |
143 | } |
144 | } |
145 | return result; |
146 | } |
147 | |
148 | public Dimension getDimension(File imageFile) { |
149 | assert !disposed; |
150 | Dimension result; |
151 | |
152 | try { |
153 | result = obtainImageDimension(imageFile); |
154 | } catch (Exception error) { |
155 | result = getDefaultImageDimension(); |
156 | logger.warn("cannot get dimension of image " + StringTools.instance().sourced(imageFile) |
157 | + ", using default: " + result, error); |
158 | } |
159 | return result; |
160 | } |
161 | |
162 | public int getEntryCount() { |
163 | assert !disposed; |
164 | return entries.size(); |
165 | } |
166 | |
167 | /** |
168 | * Get number of bytes the cache may fill up before entries get thrown out. Note that the cache |
169 | * will hold at least 1 image, so if this is bigger than the maximum size, the actual size may |
170 | * exceed the maximum in this case. |
171 | */ |
172 | public long getMaxSize() { |
173 | assert !disposed; |
174 | return maxMemorySize; |
175 | } |
176 | |
177 | /** |
178 | * Get the approximate number of bytes currently used by all images in the cache. |
179 | */ |
180 | public long getUsedSize() { |
181 | assert !disposed; |
182 | return memorySize; |
183 | } |
184 | |
185 | protected Dimension getDefaultImageDimension() { |
186 | assert !disposed; |
187 | |
188 | return new Dimension(BROKEN_IMAGE_WIDTH, BROKEN_IMAGE_HEIGHT); |
189 | } |
190 | |
191 | String getName() { |
192 | return name; |
193 | } |
194 | |
195 | /** |
196 | * Remove all entries from cache. |
197 | */ |
198 | public void clear() { |
199 | assert !disposed; |
200 | synchronized (entries) { |
201 | entries.clear(); |
202 | memorySize = 0; |
203 | } |
204 | } |
205 | |
206 | /** |
207 | * Create a broken image using the cache's default dimension. |
208 | */ |
209 | public RenderedImage createBrokenImage() { |
210 | assert !disposed; |
211 | Dimension brokenImageDimension = getDefaultImageDimension(); |
212 | RenderedImage result = imageTools.createBrokenImage( |
213 | brokenImageDimension.width, brokenImageDimension.height, |
214 | brokenBackgroundColor, brokenForegroundColor); |
215 | |
216 | return result; |
217 | } |
218 | |
219 | public void dispose() { |
220 | assert !disposed; |
221 | |
222 | if (entries != null) { |
223 | if (renderer != null) { |
224 | renderer.dispose(); |
225 | } |
226 | clear(); |
227 | } |
228 | disposed = true; |
229 | } |
230 | |
231 | /** |
232 | * Is the image for <code>imageFile</code> already stored in the cache? |
233 | */ |
234 | public boolean has(File imageFile) { |
235 | assert !disposed; |
236 | |
237 | assert imageFile != null; |
238 | return entries.containsKey(imageFile); |
239 | } |
240 | |
241 | protected RenderedImage obtainImage(File imageFile) |
242 | throws IOException { |
243 | assert !disposed; |
244 | assert imageFile != null; |
245 | RenderedImage result; |
246 | |
247 | try { |
248 | result = imageTools.readImage(imageFile); |
249 | } catch (IIOException error) { |
250 | result = createBrokenImage(); |
251 | } |
252 | return result; |
253 | } |
254 | |
255 | protected Dimension obtainImageDimension(File imageFile) |
256 | throws IOException { |
257 | assert !disposed; |
258 | assert imageFile != null; |
259 | Dimension result; |
260 | |
261 | synchronized (entries) { |
262 | if (has(imageFile)) { |
263 | RenderedImage image = get(imageFile); |
264 | |
265 | result = new Dimension(image.getWidth(), image.getHeight()); |
266 | } else { |
267 | result = imageTools.getImageDimension(imageFile); |
268 | } |
269 | } |
270 | return result; |
271 | } |
272 | |
273 | private ImageCacheEntry findLeastRecentlyAccessedEntry() { |
274 | assert !disposed; |
275 | |
276 | ImageCacheEntry result; |
277 | |
278 | synchronized (entries) { |
279 | Iterator rider = entries.values().iterator(); |
280 | |
281 | result = (ImageCacheEntry) rider.next(); |
282 | |
283 | while (rider.hasNext()) { |
284 | ImageCacheEntry nextEntry = (ImageCacheEntry) rider.next(); |
285 | long leastRecentlyAccessed = result.getLastAccessed(); |
286 | long nextAccessed = nextEntry.getLastAccessed(); |
287 | |
288 | if (leastRecentlyAccessed > nextAccessed) { |
289 | result = nextEntry; |
290 | } |
291 | } |
292 | } |
293 | return result; |
294 | } |
295 | |
296 | /** |
297 | * Remove entry and update the memory size. |
298 | */ |
299 | private void remove(ImageCacheEntry entryToRemove) { |
300 | assert !disposed; |
301 | |
302 | synchronized (entries) { |
303 | if (logger.isInfoEnabled()) { |
304 | logger.info("removing entry: " + entryToRemove.getImageFile()); |
305 | } |
306 | |
307 | long entryMemorySize = entryToRemove.getImageSize(); |
308 | Object removedEntry = entries.remove(entryToRemove.getImageFile()); |
309 | |
310 | assert entryToRemove == removedEntry |
311 | : "entryToRemove=" + entryToRemove + ", removedEntry=" + removedEntry; |
312 | |
313 | entryToRemove.dispose(); |
314 | memorySize -= entryMemorySize; |
315 | if (logger.isInfoEnabled()) { |
316 | logger.info("memory size reduced by " + entryMemorySize + " bytes to " + memorySize); |
317 | } |
318 | } |
319 | } |
320 | } |