001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.icns; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 021 022import java.awt.Dimension; 023import java.awt.image.BufferedImage; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.io.PrintWriter; 028import java.util.ArrayList; 029import java.util.List; 030 031import org.apache.commons.imaging.AbstractImageParser; 032import org.apache.commons.imaging.ImageFormat; 033import org.apache.commons.imaging.ImageFormats; 034import org.apache.commons.imaging.ImageInfo; 035import org.apache.commons.imaging.ImagingException; 036import org.apache.commons.imaging.bytesource.ByteSource; 037import org.apache.commons.imaging.common.AbstractBinaryOutputStream; 038import org.apache.commons.imaging.common.ImageMetadata; 039 040public class IcnsImageParser extends AbstractImageParser<IcnsImagingParameters> { 041 private static final class IcnsContents { 042 public final IcnsHeader icnsHeader; 043 public final IcnsElement[] icnsElements; 044 045 IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) { 046 this.icnsHeader = icnsHeader; 047 this.icnsElements = icnsElements; 048 } 049 } 050 051 static class IcnsElement { 052 static final IcnsElement[] EMPTY_ARRAY = {}; 053 public final int type; 054 public final int elementSize; 055 public final byte[] data; 056 057 IcnsElement(final int type, final int elementSize, final byte[] data) { 058 this.type = type; 059 this.elementSize = elementSize; 060 this.data = data; 061 } 062 063 public void dump(final PrintWriter pw) { 064 pw.println("IcnsElement"); 065 final IcnsType icnsType = IcnsType.findAnyType(type); 066 final String typeDescription; 067 if (icnsType == null) { 068 typeDescription = ""; 069 } else { 070 typeDescription = " " + icnsType.toString(); 071 } 072 pw.println("Type: 0x" + Integer.toHexString(type) + " (" + IcnsType.describeType(type) + ")" + typeDescription); 073 pw.println("ElementSize: " + elementSize); 074 pw.println(""); 075 } 076 } 077 078 private static final class IcnsHeader { 079 public final int magic; // Magic literal (4 bytes), always "icns" 080 public final int fileSize; // Length of file (4 bytes), in bytes. 081 082 IcnsHeader(final int magic, final int fileSize) { 083 this.magic = magic; 084 this.fileSize = fileSize; 085 } 086 087 public void dump(final PrintWriter pw) { 088 pw.println("IcnsHeader"); 089 pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" + IcnsType.describeType(magic) + ")"); 090 pw.println("FileSize: " + fileSize); 091 pw.println(""); 092 } 093 } 094 095 static final int ICNS_MAGIC = IcnsType.typeAsInt("icns"); 096 097 private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension(); 098 099 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.ICNS.getExtensions(); 100 101 /** 102 * Constructs a new instance with the big-endian byte order. 103 */ 104 public IcnsImageParser() { 105 // empty 106 } 107 108 @Override 109 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 110 final IcnsContents icnsContents = readImage(byteSource); 111 icnsContents.icnsHeader.dump(pw); 112 for (final IcnsElement icnsElement : icnsContents.icnsElements) { 113 icnsElement.dump(pw); 114 } 115 return true; 116 } 117 118 @Override 119 protected String[] getAcceptedExtensions() { 120 return ACCEPTED_EXTENSIONS; 121 } 122 123 @Override 124 protected ImageFormat[] getAcceptedTypes() { 125 return new ImageFormat[] { ImageFormats.ICNS }; 126 } 127 128 @Override 129 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException { 130 final IcnsContents icnsContents = readImage(byteSource); 131 return IcnsDecoder.decodeAllImages(icnsContents.icnsElements); 132 } 133 134 @Override 135 public final BufferedImage getBufferedImage(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException { 136 final IcnsContents icnsContents = readImage(byteSource); 137 final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements); 138 if (!result.isEmpty()) { 139 return result.get(0); 140 } 141 throw new ImagingException("No icons in ICNS file"); 142 } 143 144 @Override 145 public String getDefaultExtension() { 146 return DEFAULT_EXTENSION; 147 } 148 149 @Override 150 public IcnsImagingParameters getDefaultParameters() { 151 return new IcnsImagingParameters(); 152 } 153 154 @Override 155 public byte[] getIccProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException { 156 return null; 157 } 158 159 @Override 160 public ImageInfo getImageInfo(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException { 161 final IcnsContents contents = readImage(byteSource); 162 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements); 163 if (images.isEmpty()) { 164 throw new ImagingException("No icons in ICNS file"); 165 } 166 final BufferedImage image0 = images.get(0); 167 return new ImageInfo("Icns", 32, new ArrayList<>(), ImageFormats.ICNS, "ICNS Apple Icon Image", image0.getHeight(), "image/x-icns", images.size(), 0, 0, 168 0, 0, image0.getWidth(), false, true, false, ImageInfo.ColorType.RGB, ImageInfo.CompressionAlgorithm.UNKNOWN); 169 } 170 171 @Override 172 public Dimension getImageSize(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException { 173 final IcnsContents contents = readImage(byteSource); 174 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements); 175 if (images.isEmpty()) { 176 throw new ImagingException("No icons in ICNS file"); 177 } 178 final BufferedImage image0 = images.get(0); 179 return new Dimension(image0.getWidth(), image0.getHeight()); 180 } 181 182 // FIXME should throw UOE 183 @Override 184 public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException { 185 return null; 186 } 187 188 @Override 189 public String getName() { 190 return "Apple Icon Image"; 191 } 192 193 private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException { 194 // Icon type (4 bytes) 195 final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder()); 196 // Length of data (4 bytes), in bytes, including this header 197 final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder()); 198 if (elementSize > remainingSize) { 199 throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than " + "remaining size %d", elementSize, remainingSize)); 200 } 201 final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file"); 202 203 return new IcnsElement(type, elementSize, data); 204 } 205 206 private IcnsHeader readIcnsHeader(final InputStream is) throws ImagingException, IOException { 207 final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder()); 208 final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder()); 209 210 if (magic != ICNS_MAGIC) { 211 throw new ImagingException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic)); 212 } 213 214 return new IcnsHeader(magic, fileSize); 215 } 216 217 private IcnsContents readImage(final ByteSource byteSource) throws ImagingException, IOException { 218 try (InputStream is = byteSource.getInputStream()) { 219 final IcnsHeader icnsHeader = readIcnsHeader(is); 220 221 final List<IcnsElement> icnsElementList = new ArrayList<>(); 222 for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) { 223 final IcnsElement icnsElement = readIcnsElement(is, remainingSize); 224 icnsElementList.add(icnsElement); 225 remainingSize -= icnsElement.elementSize; 226 } 227 228 return new IcnsContents(icnsHeader, icnsElementList.toArray(IcnsElement.EMPTY_ARRAY)); 229 } 230 } 231 232 @Override 233 public void writeImage(final BufferedImage src, final OutputStream os, final IcnsImagingParameters params) throws ImagingException, IOException { 234 final IcnsType imageType; 235 if (src.getWidth() == 16 && src.getHeight() == 16) { 236 imageType = IcnsType.ICNS_16x16_32BIT_IMAGE; 237 } else if (src.getWidth() == 32 && src.getHeight() == 32) { 238 imageType = IcnsType.ICNS_32x32_32BIT_IMAGE; 239 } else if (src.getWidth() == 48 && src.getHeight() == 48) { 240 imageType = IcnsType.ICNS_48x48_32BIT_IMAGE; 241 } else if (src.getWidth() == 128 && src.getHeight() == 128) { 242 imageType = IcnsType.ICNS_128x128_32BIT_IMAGE; 243 } else { 244 throw new ImagingException("Invalid/unsupported source width " + src.getWidth() + " and height " + src.getHeight()); 245 } 246 247 try (AbstractBinaryOutputStream bos = AbstractBinaryOutputStream.bigEndian(os)) { 248 bos.write4Bytes(ICNS_MAGIC); 249 bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() * imageType.getHeight() + 4 + 4 + imageType.getWidth() * imageType.getHeight()); 250 251 bos.write4Bytes(imageType.getType()); 252 bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() * imageType.getHeight()); 253 for (int y = 0; y < src.getHeight(); y++) { 254 for (int x = 0; x < src.getWidth(); x++) { 255 final int argb = src.getRGB(x, y); 256 bos.write(0); 257 bos.write(argb >> 16); 258 bos.write(argb >> 8); 259 bos.write(argb); 260 } 261 } 262 263 final IcnsType maskType = IcnsType.find8BPPMaskType(imageType); 264 bos.write4Bytes(maskType.getType()); 265 bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth()); 266 for (int y = 0; y < src.getHeight(); y++) { 267 for (int x = 0; x < src.getWidth(); x++) { 268 final int argb = src.getRGB(x, y); 269 bos.write(argb >> 24); 270 } 271 } 272 } 273 } 274}