/* Copyright (c) 2006, Pepijn Van Eeckhoudt All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.inet.gradle.setup.image.icns; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import javax.imageio.ImageIO; public class IcnsCodec { private static final String ICNS = "icns"; private static final String ICS_BW = "ics#"; private static final int ICS_BW_SIZE = 16; private static final String ICN_BW = "ICN#"; private static final int ICN_BW_SIZE = 32; private static final String SMALL_32_BIT_RGB = "is32"; private static final String SMALL_8_BIT_MASK = "s8mk"; public static final int SMALL_SIZE = 16; private static final String LARGE_32_BIT_RGB = "il32"; private static final String LARGE_8_BIT_MASK = "l8mk"; public static final int LARGE_SIZE = 32; private static final String HUGE_32_BIT_RGB = "ih32"; private static final String HUGE_8_BIT_MASK = "h8mk"; public static final int HUGE_SIZE = 48; private static final String THUMBNAIL_32_BIT_RGB = "it32"; private static final String THUMBNAIL_8_BIT_MASK = "t8mk"; public static final int THUMBNAIL_SIZE = 128; public void encode(IconSuite suite, OutputStream outputStream) throws IOException { byte[] small = encode32bitIcon(suite.getSmallIcon(), SMALL_32_BIT_RGB, SMALL_8_BIT_MASK); byte[] large = encode32bitIcon(suite.getLargeIcon(), LARGE_32_BIT_RGB, LARGE_8_BIT_MASK); byte[] huge = encode32bitIcon(suite.getHugeIcon(), HUGE_32_BIT_RGB, HUGE_8_BIT_MASK); byte[] thumbnail = encode32bitIcon(suite.getThumbnailIcon(), THUMBNAIL_32_BIT_RGB, IOSupport.LONG_INT_SIZE, THUMBNAIL_8_BIT_MASK); byte[] icsBW = encodeIcsBW(suite); byte[] icnBW = encodeIcnBW(suite); int totalSize = icsBW.length + icnBW.length + small.length + huge.length + large.length + thumbnail.length; DataOutputStream stream = new DataOutputStream(outputStream); IOSupport.writeLiteralLongInt(stream, ICNS); IOSupport.writeLongInt(stream, totalSize + 2 * IOSupport.LONG_INT_SIZE); stream.write(icsBW); stream.write(small); stream.write(icnBW); stream.write(large); stream.write(huge); stream.write(thumbnail); } private byte[] encodeIcsBW(IconSuite suite) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] data = encodeAsBWData(suite, ICS_BW_SIZE, ICS_BW_SIZE); IOSupport.writeLiteralLongInt(out, ICS_BW); IOSupport.writeLongInt(out, data.length + 2 * IOSupport.LONG_INT_SIZE); out.write(data); return out.toByteArray(); } private byte[] encodeIcnBW(IconSuite suite) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] data = encodeAsBWData(suite, ICN_BW_SIZE, ICN_BW_SIZE); IOSupport.writeLiteralLongInt(out, ICN_BW); IOSupport.writeLongInt(out, 2 * data.length + 2 * IOSupport.LONG_INT_SIZE); out.write(data); // Reuse the pixel data for the mask out.write(data); return out.toByteArray(); } private byte[] encodeAsBWData(IconSuite suite, int width, int height) { BufferedImage icon = suite.getBestMatchingIcon(width, height); if (icon == null) { return new byte[0]; } BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY); Graphics graphics = scaledImage.getGraphics(); graphics.drawImage(icon, 0, 0, width, height, 0, 0, icon.getWidth(), icon.getHeight(), null); graphics.dispose(); DataBufferByte dataBuffer = ((DataBufferByte) scaledImage.getData().getDataBuffer()); byte[] data = dataBuffer.getData(); assert data.length == (width * height / 8) : "Incorrect data size [actual:" + data.length + ",expected:" + (width * height / 8) + "]"; return data; } private byte[] encode32bitIcon(BufferedImage image, String rgbHeader, String maskHeader) throws IOException { return encode32bitIcon(image, rgbHeader, 0, maskHeader); } private byte[] encode32bitIcon(BufferedImage image, String rgbHeader, int rgbPrefixSize, String maskHeader) throws IOException { if (image == null) { return new byte[0]; } int width = image.getWidth(); int height = image.getHeight(); byte[] bytesR = new byte[width * height]; byte[] bytesG = new byte[width * height]; byte[] bytesB = new byte[width * height]; byte[] mask = new byte[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int pixel = image.getRGB(x, y); byte a = (byte) ((pixel & 0xFF000000) >> 24); byte r = (byte) ((pixel & 0x00FF0000) >> 16); byte g = (byte) ((pixel & 0x0000FF00) >> 8); byte b = (byte) ((pixel & 0x000000FF)); int pixelOffset = (y * width) + x; mask[pixelOffset] = a; bytesR[pixelOffset] = r; bytesG[pixelOffset] = g; bytesB[pixelOffset] = b; } } ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] packedR = RunLengthEncoding.packIconData(bytesR); byte[] packedG = RunLengthEncoding.packIconData(bytesG); byte[] packedB = RunLengthEncoding.packIconData(bytesB); int packedLength = packedR.length + packedG.length + packedB.length; int resourceSize = rgbPrefixSize + packedLength + 2 * IOSupport.LONG_INT_SIZE; IOSupport.writeLiteralLongInt(out, rgbHeader); IOSupport.writeLongInt(out, resourceSize); // The rgbPrefixSize allows the unknown value at the beginning of // the thumbnail icons to be added. out.write(new byte[rgbPrefixSize]); out.write(packedR); out.write(packedG); out.write(packedB); IOSupport.writeLiteralLongInt(out, maskHeader); IOSupport.writeLongInt(out, mask.length + 2 * IOSupport.LONG_INT_SIZE); out.write(mask); return out.toByteArray(); } /** * Decode a icns file stream and returns a list of BufferedImages. * * @param inputStream the data * @return a list of images, never null * @throws IOException if any IO/Error occur */ public ArrayList<BufferedImage> decode(InputStream inputStream) throws IOException { /* Literal: 4 bytes, each byte is an ASCII character Long_int: 4 bytes representing a 32-bit big endian integer Icns: 'ICNS' Literal size Long_int; includes 'ICNS', size and all resources resource* variable Resource: type Literal size Long_int; includes type, size and data data Variable, dependes on type ics#: 16x16 black and white icon 64 bytes Each bit is a pixel ICN#: 32x32 black and white icon + 32x32 1-bit mask 256 bytes Each bit is a pixel is32: 16x16 32-bit icon Variable size Data is compressed with PackBits variant Uncompressed data contains r, g and b channel; each byte is a value s8mk: 16x16 mask 256 bytes Each byte is an alpha value for the alpha channel of the is32 icon resource il32: 32x32 32-bit icon see is32 l8mk: 32x32 mask 1024 bytes Each byte is an alpha value for the alpha channel of the il32 icon resource ih32: 48x48 32-bit icon see is32 h8mk: 48x48 mask 2304 bytes Each byte is an alpha value for the alpha channel of the ih32 icon resource it32: 128x128 32-bit icon Contains an extra Long_int before the rgb data that is always set to 0. The purpose of this value is unknown. see is32 t8mk: 128x128 mask 16384 bytes Each byte is an alpha value for the alpha channel of the it32 icon resource */ String header = IOSupport.readLiteralLongInt(inputStream); if (!header.equals(ICNS)) { throw new IOException("Unexpected header encountered: " + header); } ArrayList<BufferedImage> images = new ArrayList<>(); int[] small = null; int[] large = null; int[] huge = null; int[] thumb = null; int fileSize = IOSupport.readLongInt(inputStream); int bytesLeft = fileSize - (2 * IOSupport.LONG_INT_SIZE); while (bytesLeft > 0) { String elementType = IOSupport.readLiteralLongInt(inputStream); int elementSize = IOSupport.readLongInt(inputStream); int elementDataSize = elementSize - (2 * IOSupport.LONG_INT_SIZE); if (elementType.equals(SMALL_32_BIT_RGB)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); small = decode32bitIcon(elementData, small, SMALL_SIZE); } else if (elementType.equals(LARGE_32_BIT_RGB)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); large = decode32bitIcon(elementData, large, LARGE_SIZE); } else if (elementType.equals(HUGE_32_BIT_RGB)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); huge = decode32bitIcon(elementData, huge, HUGE_SIZE); } else if (elementType.equals(THUMBNAIL_32_BIT_RGB)) { // The thumbnail icons contain an extra 4 bytes which // always seem to be set to 0. I don't know what this // data means, so for now simply skip it. IOSupport.skip(inputStream, IOSupport.LONG_INT_SIZE); byte[] elementData = new byte[elementDataSize - IOSupport.LONG_INT_SIZE]; IOSupport.readFully(inputStream, elementData); thumb = decode32bitIcon(elementData, thumb, THUMBNAIL_SIZE); } else if (elementType.equals(SMALL_8_BIT_MASK)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); small = decode8bitMask(elementData, small, SMALL_SIZE); } else if (elementType.equals(LARGE_8_BIT_MASK)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); large = decode8bitMask(elementData, large, LARGE_SIZE); } else if (elementType.equals(HUGE_8_BIT_MASK)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); huge = decode8bitMask(elementData, huge, HUGE_SIZE); } else if (elementType.equals(THUMBNAIL_8_BIT_MASK)) { byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); thumb = decode8bitMask(elementData, thumb, THUMBNAIL_SIZE); } else { // for all other formats we try if it is a PNG format that can be read with ImageIO byte[] elementData = new byte[elementDataSize]; IOSupport.readFully(inputStream, elementData); BufferedImage img = ImageIO.read( new ByteArrayInputStream(elementData) ); if( img != null ) { images.add( img ); } //IOSupport.skip(inputStream, elementDataSize); } bytesLeft -= elementSize; } if (small != null) { images.add(createImage(SMALL_SIZE, small)); } if (large != null) { images.add(createImage(LARGE_SIZE, large)); } if (huge != null) { images.add(createImage(HUGE_SIZE, huge)); } if (thumb != null) { images.add(createImage(THUMBNAIL_SIZE, thumb)); } return images; } private BufferedImage createImage(int size, int[] pixels) { BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); image.setRGB(0, 0, size, size, pixels, 0, size); return image; } private int[] decode32bitIcon(byte[] packedData, int[] destination, int size) { int nbPixels = size * size; byte[] unpackedData; if (packedData.length == nbPixels * 4) { unpackedData = packedData; } else { unpackedData = new byte[nbPixels * 3]; RunLengthEncoding.unpackIconData(packedData, unpackedData); } int[] pixels; if (destination == null) { pixels = new int[nbPixels]; // Fill in the alpha channel to make all pixels opaque for (int i = 0; i < pixels.length; i++) { pixels[i] = 0xFF000000; } } else { pixels = destination; } assert pixels.length == size * size : "Incorrect pixel buffer size"; int unpackedIndex = 0; for (int i = 0; i < pixels.length; i++) { pixels[i] |= (unpackedData[unpackedIndex++] & 0xFF) << 16; } for (int i = 0; i < pixels.length; i++) { pixels[i] |= (unpackedData[unpackedIndex++] & 0xFF) << 8; } for (int i = 0; i < pixels.length; i++) { pixels[i] |= (unpackedData[unpackedIndex++] & 0xFF); } return pixels; } private int[] decode8bitMask(byte[] data, int[] destination, int size) { int[] pixels; int arraySize = size * size; if (destination == null) { pixels = new int[arraySize]; } else { pixels = destination; } assert pixels.length == arraySize : "Incorrect pixel buffer size [actual:" + pixels.length + ",expected:" + arraySize + "]"; assert data.length == arraySize : "Incorrect data buffer size [actual:" + data.length + ",expected:" + arraySize + "]"; for (int i = 0; i < pixels.length; i++) { // Clear old alpha value pixels[i] &= 0x00FFFFFF; // Write new alpha value pixels[i] |= (data[i] & 0xFF) << 24; } return pixels; } }