package org.lateralgm.file.iconio; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.lateralgm.file.StreamDecoder; import org.lateralgm.file.StreamEncoder; /** * <p> * ICO file with one or more embedded bitmaps representing icons in various resolutions. An ICO file * essentially is a format that glues together a couple of bitmaps into a single file. * </p> * <p> * This code uses file format information gleaned from: * </p> * <p> * winicontoppm.c - read a MS Windows .ico file and write portable pixmap(s) * </p> * <p> * Copyright (C) 2000 by Lee Benfield - lee@recoil.org * </p> * <p> * Permission to use, copy, modify, and distribute this software and its documentation for any * purpose and without fee is hereby granted, provided that the above copyright notice appear in all * copies and that both that copyright notice and this permission notice appear in supporting * documentation. This software is provided "as is" without express or implied warranty. * </p> * <p> * Addendum for Java code by Christian Treber: * </p> * <p> * The rules for the Java adaption are the same as stated above for the C version. * </p> * <p> * Notes: All code in 1:10h. Realized stuff is little endian. At 2:30h: Debugged, got stuck on wrong * determination of row length in 8 BPP images. Got black images until I set the alpha channel... * Put some more effort into supporting mask; all in all I would say this took me 4:00h. Had to add * another 4:00h for research into 24 and 32 BPP. * </p> * * @author © Christian Treber, ct@ctreber.com */ public class ICOFile implements Comparable<ICOFile> { /** Source file name. */ private String fileName; /** Unspecified purpose. */ private int reserved; private int type; /** Number of contained images. */ private int imageCount; private final List<BitmapDescriptor> descriptors = new ArrayList<BitmapDescriptor>(); /** * Create ICOFile object from an ICO file. Use {@link #getDescriptors()}to access the icon(s). * Yes, ICO files might contain more than one icon). * * @param pFileName Name of the file to read (derived decoder automatically closed). * @throws IOException */ public ICOFile(final String pFileName) throws IOException { this(pFileName,new StreamDecoder(pFileName)); } /** * Create ICO file from an input stream. * * @param pInput (automatically closed) * @throws IOException */ public ICOFile(final InputStream pInput) throws IOException { this("[from stream]",new StreamDecoder(pInput)); } /** * Create ICO file from an URL. * * @param pURL * @throws IOException */ public ICOFile(final URL pURL) throws IOException { this(pURL.toString(),new StreamDecoder(pURL.openStream())); } /** * Create ICOFile from a byte array. * * @param pBuffer * @throws IOException */ public ICOFile(final byte[] pBuffer) throws IOException { this("[from buffer]",new StreamDecoder(new ByteArrayInputStream(pBuffer))); } /** * Create ICO file. * * @param pFileName Just serves as information for toString() output; input is obtained through * pFileDecoder. * @param pFileDecoder Decoder to read from (will remain unclosed). * @throws IOException If anything goes wrong with reading from the decoder. */ // @PMD:REVIEWED:CallSuperInConstructor: by Chris on 06.03.06 10:32 public ICOFile(final String pFileName, final StreamDecoder pFileDecoder) throws IOException { fileName = pFileName; read(pFileDecoder); } public int compareTo(final ICOFile pOther) { return pOther.getFileName().compareTo(getFileName()); } public String toString() { final StringBuffer lSB = new StringBuffer(100); lSB.append(fileName + ", type: " + type + ", image count: " + getImageCount()); // Iterator lIt = _entries.iterator(); // while (lIt.hasNext()) // { // BitmapDescriptor lBitmapDescriptor = (BitmapDescriptor) lIt.next(); // lSB.append(lBitmapDescriptor); // } return lSB.toString(); } /** * Read the ICO file. The file consists of a header (with type and image count), a list of image * entries (describing image properties and offsets into the ICO file), and the image data itself. * The image data for each image consists of a header (describing some more image properties) and * the bitmap. * * @param pDec Decoder to read from. * @throws IOException */ private void read(final StreamDecoder pDec) throws IOException { readHeader(pDec); final BitmapDescriptor[] lDescriptors = readDescriptors(pDec); fillDescriptors(pDec,lDescriptors); } /** * @param pDec The decoder. * @throws IOException */ private void readHeader(final StreamDecoder pDec) throws IOException { reserved = pDec.read2(); type = pDec.read2(); imageCount = pDec.read2(); if (type != 1) { throw new IllegalArgumentException("Unknown ICO type " + type); } if (imageCount == 0) { // Yes, I found some ICO files say "0" images, but they contain one. imageCount = 1; } } /** * @param pDec The decoder. * @throws IOException */ private void fillDescriptors(final StreamDecoder pDec, final BitmapDescriptor[] pDescriptors) throws IOException { for (final BitmapDescriptor lDescriptor : pDescriptors) { fillDescriptor(pDec,lDescriptor); descriptors.add(lDescriptor); } } /** * @param pDec The decoder. * @param pDescriptor * @throws IOException */ private void fillDescriptor(final StreamDecoder pDec, final BitmapDescriptor pDescriptor) throws IOException { if (pDec.getPos() != pDescriptor.getOffset()) { pDec.seek(pDescriptor.getOffset()); } pDescriptor.setHeader(new BitmapHeader(pDec)); pDescriptor.setBitmap(readBitmap(pDec,pDescriptor)); doSomeChecks(pDescriptor); } /** * @param pDec The decoder. * @return * @throws IOException */ private BitmapDescriptor[] readDescriptors(final StreamDecoder pDec) throws IOException { final BitmapDescriptor[] lEntries = new BitmapDescriptor[imageCount]; for (int lImageNo = 0; lImageNo < imageCount; lImageNo++) { lEntries[lImageNo] = readDescriptor(pDec); } return lEntries; } /** * @param pDec The decoder. * @return * @throws IOException */ private static BitmapDescriptor readDescriptor(final StreamDecoder pDec) throws IOException { return new BitmapDescriptor(pDec); } /** * Perform some sanity checks. */ private void doSomeChecks(final BitmapDescriptor pDescriptor) { if (pDescriptor.getHeader().getWidth() * 2 != pDescriptor.getHeader().getHeight()) { System.out.println(this + ": In header, height is not twice the width"); } } /** * @param pDec The decoder. * @return Bitmap, type depends on BPP * @throws IOException */ private static AbstractBitmap readBitmap(final StreamDecoder pDec, final BitmapDescriptor pDescriptor) throws IOException { final int lBitsPerPixel = pDescriptor.getHeader().getBPP(); AbstractBitmap lBitmap = null; if (pDescriptor.getHeader().getCompression() == TypeCompression.BI_PNG) lBitmap = new BitmapPNG(pDescriptor); else switch (lBitsPerPixel) { // Palette style case 1: lBitmap = new BitmapIndexed1BPP(pDescriptor); break; case 4: lBitmap = new BitmapIndexed4BPP(pDescriptor); break; case 8: lBitmap = new BitmapIndexed8BPP(pDescriptor); break; // RGB style case 16: return null; case 24: lBitmap = new BitmapRGB24BPP(pDescriptor); break; case 32: lBitmap = new BitmapRGB32BPP(pDescriptor); break; default: throw new IllegalArgumentException("Unsupported bit count " + lBitsPerPixel); } lBitmap.read(pDec); return lBitmap; } public byte[] getDigest(String method) throws IOException,NoSuchAlgorithmException { byte[][] bitmaps = getBitmaps(); MessageDigest md5 = MessageDigest.getInstance(method); for (byte[] bitmap : bitmaps) md5.update(bitmap); return md5.digest(); } /** * Get all contained images (comfort method). * * @return Images (type Image). */ public List<BufferedImage> getImages() { final List<BufferedImage> lImages = new ArrayList<BufferedImage>(); final Iterator<BitmapDescriptor> lItDesc = getDescriptors().iterator(); while (lItDesc.hasNext()) { final BitmapDescriptor lDesc = lItDesc.next(); lImages.add(lDesc.getBitmap().createImageRGB()); } return lImages; } /** * Get the list of BitmapDescriptors contained in the ICO file. * * @return List of {@link BitmapDescriptor}in same order as in the ICO file (use methods on * ICOEntry to get the actual images). */ public List<BitmapDescriptor> getDescriptors() { return descriptors; } /** * Get the speicified BitmapDescriptor. * * @param pDescriptorNo Number of the descriptor to get. * @return BitmapDescriptor. */ public BitmapDescriptor getDescriptor(final int pDescriptorNo) { return descriptors.get(pDescriptorNo); } /** * Get the image type. * * @return The image type (any ideas what that is?). */ public int getType() { return type; } /** * Get the number of contained images. * * @return Number of contained images. */ public int getImageCount() { return descriptors == null ? imageCount : descriptors.size(); } /** * @return Source file name. */ public String getFileName() { return fileName; } /** * @return Returns the "reserved" value. */ public int getReserved() { return reserved; } private static final int HEADER_SIZE = 6; private static final int DESCRIPTOR_SIZE = 16; public void write(OutputStream out) throws IOException { if (out instanceof StreamEncoder) write((StreamEncoder) out); else { StreamEncoder se = new StreamEncoder(out); write(se); se.flush(); } } public void write(StreamEncoder out) throws IOException { writeHeader(out); byte[][] imageData = getBitmaps(); int offset = HEADER_SIZE + DESCRIPTOR_SIZE * descriptors.size(); for (int i = 0; i < descriptors.size(); i++) { BitmapDescriptor bmd = descriptors.get(i); bmd.setOffset(offset); bmd.setSize(imageData[i].length); offset += imageData[i].length; } writeDescriptors(out); for (byte[] dat : imageData) out.write(dat); } private void writeHeader(StreamEncoder out) throws IOException { out.write2(reserved); out.write2(type); out.write2(getImageCount()); } private void writeDescriptors(StreamEncoder out) throws IOException { for (BitmapDescriptor bmd : descriptors) bmd.write(out); } private byte[][] getBitmaps() throws IOException { byte[][] res = new byte[descriptors.size()][]; int i = 0; for (BitmapDescriptor bmd : descriptors) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamEncoder o = new StreamEncoder(baos); if (bmd.getBitmap() instanceof BitmapPNG) { BitmapPNG png = (BitmapPNG) bmd.getBitmap(); png.write(o); } else { bmd.getHeader().write(o); bmd.getBitmap().write(o); } o.flush(); res[i] = baos.toByteArray(); o.close(); i++; } return res; } public Image getDisplayImage() { BitmapDescriptor desc = descriptors.get(descriptors.size() - 1); if (desc != null) { return desc.getImageRGB(); } else { return null; } } }