/* * Copyright 2006-2012 ICEsoft Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.util; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; /** * The <code>ImageCache</code> class is designed to cache image data to the * file system. The cached file can then be read file the file system at a * later time. The <code>ImageCache</code> class was created to help minimize * the amount of data that has to be kept in memory when opening graphic * heavy PDF files. The <code>ImageCache</code> class also speeds up the * loading of a previously decoded image stream as it is much more effecient * to read the decode image from file then it is to decode the image bit stream * every time it is required. * <p/> * The cached image is writen to the user temporary folder and is identeifed by * "PDFImageStream" followed by seriers of numbers assigned from the objects * hash code. Every time a new image cache is created the full path to the file * is recored in the CacheManager class which allows the CacheManager to delete * a documents cached images when the doucment is closed. * * @since 1.1 */ public class ImageCache { private static final Logger logger = Logger.getLogger(ImageCache.class.toString()); // temp file that is created for the cache private File tempFile = null; // length in bytes of the the image data private long length = 0; // is caching enabled or disabled private boolean isCached = false; // cache manager reference private CacheManager cacheManager = null; // flag for image scaling, only want to scale it once private boolean isScaled = false; // If caching is disabled, it is stored in memory with this var private BufferedImage imageStore; // disable/enable file caching private static boolean isCachingEnabled; // disable/inable file cahcing, overrides fileCachingSize. private static boolean scaleImages; static { // NOTE: currently disabled as file caching tends to slow things down more // then it helps. // sets if file caching is enabled or disabled. isCachingEnabled = Defs.sysPropertyBoolean("org.icepdf.core.imagecache.enabled", true); // deside if large images will be scaled scaleImages = Defs.sysPropertyBoolean("org.icepdf.core.scaleImages", true); } /** * Create new instance of ImageCache * * @param library reference to documents library */ public ImageCache(Library library) { // get pointer to cache manager. cacheManager = library.getCacheManager(); } /** * Writes Buffered imate to this object. If images caching is enabled, the * images is written to disk, otherwise it is kept in memory. * * @param image */ public void setImage(BufferedImage image) { setImage(image, isCached); } /** * Write the <code>image</code> to a temporary file in the users temp * directory. * * @param image image to be cached. * @param useCaching use caching when saving image. */ private void setImage(BufferedImage image, boolean useCaching) { try { if (useCaching && isCached && imageStore == image) return; if (imageStore != null && imageStore != image) { imageStore.flush(); imageStore = null; } // if caching, write the image to file; if (useCaching) {//isCachingEnabled) { // create tmp file and write bytes to it. tempFile = File.createTempFile("PDFImageStream" + this.getClass().hashCode(), ".tmp"); cacheManager.addCachedFile(tempFile.getAbsolutePath()); // Delete temp file on exits, but dispose should do this too tempFile.deleteOnExit(); ImageIO.write(image, "png", tempFile); // clean up the stream length = tempFile.length(); // set cached flag isCached = true; } else { // use the store to keep track of the image. imageStore = image; isCached = false; } } catch (IOException e) { logger.log(Level.FINE, "Error creating ImageCache temporary file.", e); } } /** * Read the cached image file and return the corresponding buffered image. * * @return buffered image contained in the image cache temporary file. */ public BufferedImage readImage() { if (imageStore != null) return imageStore; // if caching, write the image to file; if (isCached) {//isCachingEnabled) { BufferedImage image = null; try { image = ImageIO.read(tempFile); // get length of temp file length = tempFile.length(); } catch (IOException e) { logger.log(Level.FINE, "Error creating ImageCache temporary file ", e); } // imageStore = image; return image; } return null; } /** * Dispose of this objects resources * * @param cache if true, image will be cached to disk, otherwise, image * resources will be released. * @param imageRecoverableElsewise If the ImageCache is not the only way * of getting back at the image later */ public void dispose(boolean cache, boolean imageRecoverableElsewise) { // empty the image store if (imageStore != null) { // We don't look at if( !cache && isCached ) to delete // the temp file, because the CacheManager handles it // cache to disk for fast access at a later time if (cache && isCachingEnabled && !isCached && !imageRecoverableElsewise) { setImage(imageStore, true); } // delete the resources used by the image source if (!cache || isCached || imageRecoverableElsewise) { //imageStore.flush(); imageStore = null; } } } public void scaleImage(int width, int height) { if (scaleImages) { // Calling scaleBufferedImage() causes a spike in memory usage // For example, a source image that's 100KB in size, we spike 5.5MB, // with 2.8MB remaining and 2.7MB getting gc'ed // System.out.println("Mem free: " + Runtime.getRuntime().freeMemory() + ", total:" + Runtime.getRuntime().totalMemory() + ", used: " + (Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory())); setImage(scaleBufferedImage(readImage(), width, height)); isScaled = true; } else { isScaled = false; } } public void setIsScaled(boolean flag) { isScaled = flag; } /** * Return the number of bytes used by this images when it is cached. * * @return number of bytes of the cached file. */ public long getLength() { return length; } public boolean isScaled() { return isScaled; } public boolean isCachedSomehow() { return isCached || (imageStore != null); } public static BufferedImage scaleBufferedImage(BufferedImage bim, int width, int height) { // TODO: Use size of image to determine if will scale /* DataBuffer db = bim.getRaster().getDataBuffer(); int dbNumElements = db.getSize(); int dbBitsPerElement = DataBuffer.getDataTypeSize( db.getDataType() ); int dbBytes = dbNumElements * dbBitsPerElement / 8; */ double imageScale = 1.0; // do a little scaling on a the buffer if ((width >= 500 || height >= 500) && (width < 1000 || height < 1000)) { imageScale = 0.80; } else if ((width >= 1000 || height >= 1000) && (width < 1500 || height < 1500)) { imageScale = 0.70; } else if ((width >= 1500 || height >= 1500) && (width < 2000 || height < 2000)) { imageScale = 0.60; } else if ((width >= 2000 || height >= 2000) && (width < 2500 || height < 2500)) { imageScale = 0.50; } else if ((width >= 2500 || height >= 2500) && (width < 3000 || height < 3000)) { imageScale = 0.40; } else if ((width >= 3000 || height >= 3000)) { imageScale = 0.30; } // scale the image if (imageScale < 1.0) { AffineTransform tx = new AffineTransform(); tx.scale(imageScale, imageScale); AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); BufferedImage sbim = op.filter(bim, null); bim.flush(); bim = sbim; } return bim; } }