001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: *//*
041: * CacheWriter.java
042: *
043: * Created on February 16, 2004, 8:12 PM
044: */
045:
046: package org.netbeans.imagecache;
047:
048: import java.awt.Graphics;
049: import java.awt.Image;
050: import java.awt.MediaTracker;
051: import java.awt.Point;
052: import java.awt.Toolkit;
053: import java.awt.image.BufferedImage;
054: import java.awt.image.DataBuffer;
055: import java.awt.image.ImageConsumer;
056: import java.awt.image.SampleModel;
057: import java.awt.image.SinglePixelPackedSampleModel;
058: import java.awt.image.WritableRaster;
059: import java.io.File;
060: import java.io.FileFilter;
061: import java.io.FileOutputStream;
062: import java.io.FilenameFilter;
063: import java.io.IOException;
064: import java.nio.BufferOverflowException;
065: import java.nio.ByteBuffer;
066: import java.nio.IntBuffer;
067: import java.nio.LongBuffer;
068: import java.nio.channels.FileChannel;
069: import java.util.*;
070: import javax.imageio.ImageIO;
071: import javax.swing.ImageIcon;
072:
073: /** Writes an image cache. It will create two files in the directory set in
074: * setDir: images.cache and images.metadata. The first is a flat binary
075: * cache of image data; the second is metadata about the image names, offsets
076: * into the file, etc.
077: * <p>
078: * File formats:<br>
079: * The image cache is a big blob of rgb pixel data with no markers, format
080: * information or anything else. The metadata file is a binary file consisting
081: * of a series of 255 character records. Each record is in the format:
082: * <ul>
083: * <li>4 bytes: integer length of the id (image path in its jar)</li>
084: * <li>4 bytes: integer width of the image</li>
085: * <li>4 bytes: integer height of the image</li>
086: * <li>8 bytes: long offset into the cache file for data start</li>
087: * <li>8 bytes: long offset into the cache file for data end</li>
088: * </ul>
089: * While the width/height of the image could be derived from only one
090: * parameter and the data length, these are included separately to enable
091: * future use of data compression (such as RLE to reduce repeated transparent
092: * pixel data).
093: * <p>
094: * As long as the output filename is not user-settable, it should be possible
095: * to indicate future format changes by altering the file name.
096: *
097: *
098: * @author Tim Boudreau
099: */
100: public class CacheWriter {
101: private File file = null;
102: private String dir = null;
103: private File metafile = null;
104: static final String filename = "images"; //NOI18N
105: /** Fixed length of a single entry of metadata */
106: static final int METAENTRY_LENGTH = 256;
107: /** Offset from the beginning of a metadata entry where the id starts */
108: static final int ID_OFFSET = 28;
109:
110: /** Creates a new instance of CacheWriter */
111: public CacheWriter() {
112: }
113:
114: public void setDir(String dir, boolean clean) throws IOException {
115: StringBuffer sb = new StringBuffer(dir);
116: if (sb.charAt(sb.length() - 1) != File.pathSeparatorChar) {
117: sb.append(File.separatorChar);
118: }
119: this .dir = sb.toString();
120: File f = new File(dir);
121: if (!f.exists()) {
122: f.mkdir();
123: }
124: if (clean) {
125: File mf = new File(this .dir + filename + ".metadata");
126: File fc = new File(this .dir + filename + ".cache");
127: if (mf.exists()) {
128: mf.delete();
129: }
130: if (fc.exists()) {
131: fc.delete();
132: }
133: }
134: }
135:
136: /** Recursively writes all files to the image cache */
137: public void writeDir(String dirname, boolean append)
138: throws IOException {
139: File f = new File(dirname);
140: if (!f.exists()) {
141: throw new IOException("Directory " + dirname
142: + " does not exist");
143: }
144: if (!f.isDirectory()) {
145: throw new IOException("File " + dirname
146: + " is not a directory");
147: }
148: //Force existing files to be deleted if !append
149: if (!append) {
150: getOutfile(append);
151: getMetafile(append);
152: }
153: String[] filenames = findImageFiles(f);
154: for (int i = 0; i < filenames.length; i++) {
155: write(filenames[i], true);
156: }
157: }
158:
159: private String[] findImageFiles(File root) {
160: String[] formats = ImageIO.getReaderFormatNames();
161: Set set = new HashSet();
162: findImageFiles(root, set);
163: String[] result = new String[set.size()];
164: result = (String[]) set.toArray(result);
165: return result;
166: }
167:
168: private void findImageFiles(File root, Set set) {
169: String[] files = root.list(new FormatFilenameFilter());
170: for (int i = 0; i < files.length; i++) {
171: set.add(root.getPath() + File.separatorChar + files[i]);
172: }
173: File[] children = root.listFiles(new DirectoryFilter());
174: for (int i = 0; i < children.length; i++) {
175: findImageFiles(children[i], set);
176: }
177: }
178:
179: public void write(String filename, boolean append)
180: throws IOException {
181: try {
182: BufferedImage img;
183: //Force the gif decoder to get registered
184: Object o = com.sun.imageio.plugins.gif.GIFImageReader.class;
185: ImageIO.scanForPlugins();
186:
187: img = ImageIO.read(new File(filename));
188: write(img, append, filename);
189:
190: } catch (Exception ioe) {
191: System.err.println("Could not write " + filename + " - "
192: + ioe.getMessage());
193: ioe.printStackTrace();
194: }
195: }
196:
197: public void write(BufferedImage img, boolean append, String id)
198: throws IOException {
199: File out = getOutfile(append);
200: File meta = getMetafile(append);
201: System.err.println("Writing to " + out + " and " + meta);
202:
203: int width = img.getWidth();
204: int height = img.getHeight();
205:
206: ByteBuffer buf = ByteBuffer.allocate(width * height * 4);
207: IntBuffer ibuf = buf.asIntBuffer();
208:
209: for (int y = 0; y < height; y++) {
210: for (int x = 0; x < width; x++) {
211: int pixel = img.getRGB(x, y);
212: ibuf.put(pixel);
213: }
214: }
215: FileOutputStream fileOut = new FileOutputStream(out, append);
216: FileOutputStream metaOut = new FileOutputStream(meta, append);
217: FileChannel fileChannel = fileOut.getChannel();
218:
219: if (append) {
220: fileChannel.position(out.length());
221: }
222:
223: //Check the size of the file we're creating - nio bytebuffers are
224: //limited to dealing with files < Integer.MAX_VALUE large
225: if (fileChannel.position() + buf.limit() > Integer.MAX_VALUE) {
226: //Can handle this and create a second cache file in the unlikely
227: //event this comes to pass
228: throw new BufferOverflowException();
229: }
230:
231: long start = fileChannel.position();
232:
233: fileChannel.write(buf);
234:
235: long end = fileChannel.position();
236:
237: fileChannel.force(true);
238: fileChannel.close();
239:
240: FileChannel metaChannel = metaOut.getChannel();
241: if (append) {
242: metaChannel.position(meta.length());
243: }
244:
245: metaChannel.write(getMetadata(img, id, start, end));
246: metaChannel.force(true);
247: metaChannel.close();
248: }
249:
250: private ByteBuffer getMetadata(BufferedImage img, String id,
251: long start, long end) throws IOException {
252: byte[] bytes = new byte[METAENTRY_LENGTH];
253: Arrays.fill(bytes, (byte) '-'); //XXX
254:
255: ByteBuffer result = ByteBuffer.wrap(bytes);
256: result.position(0);
257:
258: int width = img.getWidth();
259: int height = img.getHeight();
260:
261: id = convertPathSeparators(id);
262:
263: //First write the id length, width and height as ints
264:
265: IntBuffer ibuf = result.asIntBuffer();
266: ibuf.put(id.length()).put(width).put(height);
267:
268: result.position(result.position() + (ibuf.position() * 4));
269:
270: //Then write the start and end positions in the cache file as longs
271:
272: LongBuffer lbuf = result.asLongBuffer();
273: lbuf.put(start).put(end);
274:
275: result.position(result.position() + (lbuf.position() * 8));
276:
277: //We are intentionally stripping the high eight bits - unless we start
278: //having modules with katakana pathnames, 16 bits clean filenames will
279: //not be needed
280:
281: char[] chars = id.toCharArray();
282: if (chars.length + result.position() > METAENTRY_LENGTH) {
283: throw new IOException("ID " + id + " too long. Limit is "
284: + (METAENTRY_LENGTH - 8));
285: }
286:
287: //Now write the id text
288: for (int i = 0; i < chars.length; i++) {
289: result.put((byte) chars[i]);
290: }
291:
292: result.position(METAENTRY_LENGTH);
293: result.flip();
294: return result;
295: }
296:
297: private String convertPathSeparators(String id) {
298: String sep = File.separator;
299: if (File.separatorChar == '/') {
300: return id;
301: } else {
302: StringBuffer sb = new StringBuffer(id);
303: while (sb.indexOf(sep) != -1) {
304: int idx = sb.indexOf(sep);
305: sb.replace(idx, idx + sep.length() - 1, "/");
306: }
307: return sb.toString();
308: }
309: }
310:
311: private File getOutfile(boolean append) throws IOException {
312: if (file == null) {
313: String outname = filename + ".cache";
314: file = getOrCreateFile(outname, append);
315: }
316: return file;
317: }
318:
319: private File getMetafile(boolean append) throws IOException {
320: if (metafile == null) {
321: String metaname = filename + ".metadata";
322: metafile = getOrCreateFile(metaname, append);
323: }
324: return metafile;
325: }
326:
327: private File getOrCreateFile(String filename, boolean append)
328: throws IOException {
329: if (dir == null) {
330: throw new IOException("Output directory not set");
331: }
332: File pdir = new File(dir);
333: if (!pdir.exists()) {
334: throw new IOException("Directory " + pdir
335: + " does not exist");
336: }
337: if (!pdir.isDirectory()) {
338: throw new IOException("File " + pdir
339: + " is not a directory");
340: }
341: File result = new File(dir + filename);
342: if (result.exists() && !append) {
343: result.delete();
344: result.createNewFile();
345: } else {
346: result.createNewFile();
347: }
348: return result;
349: }
350:
351: private static class FormatFilenameFilter implements FilenameFilter {
352: private String[] formats;
353:
354: public FormatFilenameFilter() {
355: formats = ImageIO.getReaderFormatNames();
356: String[] s = new String[formats.length + 2];
357: System.arraycopy(formats, 0, s, 2, formats.length);
358: s[0] = "GIF";
359: s[1] = "gif";
360: formats = s;
361: }
362:
363: public boolean accept(File dir, String name) {
364: if (name.startsWith(".xvpics")) {
365: //Goofy non-GIF .xvpicsNNN.gif files that confuse the decoder
366: return false;
367: }
368: for (int i = 0; i < formats.length; i++) {
369: if (name.endsWith(formats[i])) {
370: return true;
371: }
372: }
373: return false;
374: }
375: }
376:
377: private static class DirectoryFilter implements FileFilter {
378: public boolean accept(File pathname) {
379: return pathname.isDirectory();
380: }
381: }
382: }
|