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.nio.ByteOrder; 029import java.util.ArrayList; 030import java.util.List; 031 032import org.apache.commons.imaging.ImageFormat; 033import org.apache.commons.imaging.ImageFormats; 034import org.apache.commons.imaging.ImageInfo; 035import org.apache.commons.imaging.ImageParser; 036import org.apache.commons.imaging.ImageReadException; 037import org.apache.commons.imaging.ImageWriteException; 038import org.apache.commons.imaging.common.BinaryOutputStream; 039import org.apache.commons.imaging.common.ImageMetadata; 040import org.apache.commons.imaging.common.bytesource.ByteSource; 041 042public class IcnsImageParser extends ImageParser<IcnsImagingParameters> { 043 static final int ICNS_MAGIC = IcnsType.typeAsInt("icns"); 044 private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension(); 045 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.ICNS.getExtensions(); 046 047 public IcnsImageParser() { 048 super.setByteOrder(ByteOrder.BIG_ENDIAN); 049 } 050 051 @Override 052 public IcnsImagingParameters getDefaultParameters() { 053 return new IcnsImagingParameters(); 054 } 055 056 @Override 057 public String getName() { 058 return "Apple Icon Image"; 059 } 060 061 @Override 062 public String getDefaultExtension() { 063 return DEFAULT_EXTENSION; 064 } 065 066 @Override 067 protected String[] getAcceptedExtensions() { 068 return ACCEPTED_EXTENSIONS; 069 } 070 071 @Override 072 protected ImageFormat[] getAcceptedTypes() { 073 return new ImageFormat[] { ImageFormats.ICNS }; 074 } 075 076 // FIXME should throw UOE 077 @Override 078 public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params) 079 throws ImageReadException, IOException { 080 return null; 081 } 082 083 @Override 084 public ImageInfo getImageInfo(final ByteSource byteSource, IcnsImagingParameters params) 085 throws ImageReadException, IOException { 086 final IcnsContents contents = readImage(byteSource); 087 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements); 088 if (images.isEmpty()) { 089 throw new ImageReadException("No icons in ICNS file"); 090 } 091 final BufferedImage image0 = images.get(0); 092 return new ImageInfo("Icns", 32, new ArrayList<>(), 093 ImageFormats.ICNS, "ICNS Apple Icon Image", 094 image0.getHeight(), "image/x-icns", images.size(), 0, 0, 0, 0, 095 image0.getWidth(), false, true, false, 096 ImageInfo.ColorType.RGB, 097 ImageInfo.CompressionAlgorithm.UNKNOWN); 098 } 099 100 @Override 101 public Dimension getImageSize(final ByteSource byteSource, IcnsImagingParameters params) 102 throws ImageReadException, IOException { 103 final IcnsContents contents = readImage(byteSource); 104 final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements); 105 if (images.isEmpty()) { 106 throw new ImageReadException("No icons in ICNS file"); 107 } 108 final BufferedImage image0 = images.get(0); 109 return new Dimension(image0.getWidth(), image0.getHeight()); 110 } 111 112 @Override 113 public byte[] getICCProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params) 114 throws ImageReadException, IOException { 115 return null; 116 } 117 118 private static class IcnsHeader { 119 public final int magic; // Magic literal (4 bytes), always "icns" 120 public final int fileSize; // Length of file (4 bytes), in bytes. 121 122 IcnsHeader(final int magic, final int fileSize) { 123 this.magic = magic; 124 this.fileSize = fileSize; 125 } 126 127 public void dump(final PrintWriter pw) { 128 pw.println("IcnsHeader"); 129 pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" 130 + IcnsType.describeType(magic) + ")"); 131 pw.println("FileSize: " + fileSize); 132 pw.println(""); 133 } 134 } 135 136 private IcnsHeader readIcnsHeader(final InputStream is) 137 throws ImageReadException, IOException { 138 final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder()); 139 final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder()); 140 141 if (magic != ICNS_MAGIC) { 142 throw new ImageReadException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic)); 143 } 144 145 return new IcnsHeader(magic, fileSize); 146 } 147 148 static class IcnsElement { 149 public final int type; 150 public final int elementSize; 151 public final byte[] data; 152 153 IcnsElement(final int type, final int elementSize, final byte[] data) { 154 this.type = type; 155 this.elementSize = elementSize; 156 this.data = data; 157 } 158 159 public void dump(final PrintWriter pw) { 160 pw.println("IcnsElement"); 161 final IcnsType icnsType = IcnsType.findAnyType(type); 162 String typeDescription; 163 if (icnsType == null) { 164 typeDescription = ""; 165 } else { 166 typeDescription = " " + icnsType.toString(); 167 } 168 pw.println("Type: 0x" + Integer.toHexString(type) + " (" 169 + IcnsType.describeType(type) + ")" + typeDescription); 170 pw.println("ElementSize: " + elementSize); 171 pw.println(""); 172 } 173 } 174 175 private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException { 176 // Icon type (4 bytes) 177 final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder()); 178 // Length of data (4 bytes), in bytes, including this header 179 final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder()); 180 if (elementSize > remainingSize) { 181 throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than " 182 + "remaining size %d", elementSize, remainingSize)); 183 } 184 final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file"); 185 186 return new IcnsElement(type, elementSize, data); 187 } 188 189 private static class IcnsContents { 190 public final IcnsHeader icnsHeader; 191 public final IcnsElement[] icnsElements; 192 193 IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) { 194 this.icnsHeader = icnsHeader; 195 this.icnsElements = icnsElements; 196 } 197 } 198 199 private IcnsContents readImage(final ByteSource byteSource) 200 throws ImageReadException, IOException { 201 try (InputStream is = byteSource.getInputStream()) { 202 final IcnsHeader icnsHeader = readIcnsHeader(is); 203 204 final List<IcnsElement> icnsElementList = new ArrayList<>(); 205 for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) { 206 final IcnsElement icnsElement = readIcnsElement(is, remainingSize); 207 icnsElementList.add(icnsElement); 208 remainingSize -= icnsElement.elementSize; 209 } 210 211 final IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()]; 212 for (int i = 0; i < icnsElements.length; i++) { 213 icnsElements[i] = icnsElementList.get(i); 214 } 215 216 return new IcnsContents(icnsHeader, icnsElements); 217 } 218 } 219 220 @Override 221 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 222 throws ImageReadException, IOException { 223 final IcnsContents icnsContents = readImage(byteSource); 224 icnsContents.icnsHeader.dump(pw); 225 for (final IcnsElement icnsElement : icnsContents.icnsElements) { 226 icnsElement.dump(pw); 227 } 228 return true; 229 } 230 231 @Override 232 public final BufferedImage getBufferedImage(final ByteSource byteSource, 233 final IcnsImagingParameters params) throws ImageReadException, IOException { 234 final IcnsContents icnsContents = readImage(byteSource); 235 final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements); 236 if (!result.isEmpty()) { 237 return result.get(0); 238 } 239 throw new ImageReadException("No icons in ICNS file"); 240 } 241 242 @Override 243 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) 244 throws ImageReadException, IOException { 245 final IcnsContents icnsContents = readImage(byteSource); 246 return IcnsDecoder.decodeAllImages(icnsContents.icnsElements); 247 } 248 249 @Override 250 public void writeImage(final BufferedImage src, final OutputStream os, IcnsImagingParameters params) 251 throws ImageWriteException, IOException { 252 IcnsType imageType; 253 if (src.getWidth() == 16 && src.getHeight() == 16) { 254 imageType = IcnsType.ICNS_16x16_32BIT_IMAGE; 255 } else if (src.getWidth() == 32 && src.getHeight() == 32) { 256 imageType = IcnsType.ICNS_32x32_32BIT_IMAGE; 257 } else if (src.getWidth() == 48 && src.getHeight() == 48) { 258 imageType = IcnsType.ICNS_48x48_32BIT_IMAGE; 259 } else if (src.getWidth() == 128 && src.getHeight() == 128) { 260 imageType = IcnsType.ICNS_128x128_32BIT_IMAGE; 261 } else { 262 throw new ImageWriteException("Invalid/unsupported source width " 263 + src.getWidth() + " and height " + src.getHeight()); 264 } 265 266 try (BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.BIG_ENDIAN)) { 267 bos.write4Bytes(ICNS_MAGIC); 268 bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() 269 * imageType.getHeight() + 4 + 4 + imageType.getWidth() 270 * imageType.getHeight()); 271 272 bos.write4Bytes(imageType.getType()); 273 bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() 274 * imageType.getHeight()); 275 for (int y = 0; y < src.getHeight(); y++) { 276 for (int x = 0; x < src.getWidth(); x++) { 277 final int argb = src.getRGB(x, y); 278 bos.write(0); 279 bos.write(argb >> 16); 280 bos.write(argb >> 8); 281 bos.write(argb); 282 } 283 } 284 285 final IcnsType maskType = IcnsType.find8BPPMaskType(imageType); 286 bos.write4Bytes(maskType.getType()); 287 bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth()); 288 for (int y = 0; y < src.getHeight(); y++) { 289 for (int x = 0; x < src.getWidth(); x++) { 290 final int argb = src.getRGB(x, y); 291 bos.write(argb >> 24); 292 } 293 } 294 } 295 } 296}