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