View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.icns;
18  
19  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
20  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_VERBOSE;
21  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
22  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
23  
24  import java.awt.Dimension;
25  import java.awt.image.BufferedImage;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.io.PrintWriter;
30  import java.nio.ByteOrder;
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  
36  import org.apache.commons.imaging.ImageFormat;
37  import org.apache.commons.imaging.ImageFormats;
38  import org.apache.commons.imaging.ImageInfo;
39  import org.apache.commons.imaging.ImageParser;
40  import org.apache.commons.imaging.ImageReadException;
41  import org.apache.commons.imaging.ImageWriteException;
42  import org.apache.commons.imaging.common.BinaryOutputStream;
43  import org.apache.commons.imaging.common.ImageMetadata;
44  import org.apache.commons.imaging.common.bytesource.ByteSource;
45  
46  public class IcnsImageParser extends ImageParser {
47      static final int ICNS_MAGIC = IcnsType.typeAsInt("icns");
48      private static final String DEFAULT_EXTENSION = ".icns";
49      private static final String[] ACCEPTED_EXTENSIONS = { ".icns", };
50  
51      public IcnsImageParser() {
52          super.setByteOrder(ByteOrder.BIG_ENDIAN);
53      }
54  
55      @Override
56      public String getName() {
57          return "Apple Icon Image";
58      }
59  
60      @Override
61      public String getDefaultExtension() {
62          return DEFAULT_EXTENSION;
63      }
64  
65      @Override
66      protected String[] getAcceptedExtensions() {
67          return ACCEPTED_EXTENSIONS;
68      }
69  
70      @Override
71      protected ImageFormat[] getAcceptedTypes() {
72          return new ImageFormat[] { ImageFormats.ICNS };
73      }
74  
75      // FIXME should throw UOE
76      @Override
77      public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
78              throws ImageReadException, IOException {
79          return null;
80      }
81  
82      @Override
83      public ImageInfo getImageInfo(final ByteSource byteSource, Map<String, Object> params)
84              throws ImageReadException, IOException {
85          // make copy of params; we'll clear keys as we consume them.
86          params = params == null ? new HashMap<String, Object>() : new HashMap<>(params);
87  
88          if (params.containsKey(PARAM_KEY_VERBOSE)) {
89              params.remove(PARAM_KEY_VERBOSE);
90          }
91  
92          if (!params.isEmpty()) {
93              final Object firstKey = params.keySet().iterator().next();
94              throw new ImageReadException("Unknown parameter: " + firstKey);
95          }
96  
97          final IcnsContents contents = readImage(byteSource);
98          final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
99          if (images.isEmpty()) {
100             throw new ImageReadException("No icons in ICNS file");
101         }
102         final BufferedImage image0 = images.get(0);
103         return new ImageInfo("Icns", 32, new ArrayList<String>(),
104                 ImageFormats.ICNS, "ICNS Apple Icon Image",
105                 image0.getHeight(), "image/x-icns", images.size(), 0, 0, 0, 0,
106                 image0.getWidth(), false, true, false,
107                 ImageInfo.ColorType.RGB,
108                 ImageInfo.CompressionAlgorithm.UNKNOWN);
109     }
110 
111     @Override
112     public Dimension getImageSize(final ByteSource byteSource, Map<String, Object> params)
113             throws ImageReadException, IOException {
114         // make copy of params; we'll clear keys as we consume them.
115         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
116 
117         if (params.containsKey(PARAM_KEY_VERBOSE)) {
118             params.remove(PARAM_KEY_VERBOSE);
119         }
120 
121         if (!params.isEmpty()) {
122             final Object firstKey = params.keySet().iterator().next();
123             throw new ImageReadException("Unknown parameter: " + firstKey);
124         }
125 
126         final IcnsContents contents = readImage(byteSource);
127         final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
128         if (images.isEmpty()) {
129             throw new ImageReadException("No icons in ICNS file");
130         }
131         final BufferedImage image0 = images.get(0);
132         return new Dimension(image0.getWidth(), image0.getHeight());
133     }
134 
135     @Override
136     public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
137             throws ImageReadException, IOException {
138         return null;
139     }
140 
141     private static class IcnsHeader {
142         public final int magic; // Magic literal (4 bytes), always "icns"
143         public final int fileSize; // Length of file (4 bytes), in bytes.
144 
145         public IcnsHeader(final int magic, final int fileSize) {
146             this.magic = magic;
147             this.fileSize = fileSize;
148         }
149 
150         public void dump(final PrintWriter pw) {
151             pw.println("IcnsHeader");
152             pw.println("Magic: 0x" + Integer.toHexString(magic) + " ("
153                     + IcnsType.describeType(magic) + ")");
154             pw.println("FileSize: " + fileSize);
155             pw.println("");
156         }
157     }
158 
159     private IcnsHeader readIcnsHeader(final InputStream is)
160             throws ImageReadException, IOException {
161         final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder());
162         final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder());
163 
164         if (magic != ICNS_MAGIC) {
165             throw new ImageReadException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic));
166         }
167 
168         return new IcnsHeader(magic, fileSize);
169     }
170 
171     static class IcnsElement {
172         public final int type;
173         public final int elementSize;
174         public final byte[] data;
175 
176         public IcnsElement(final int type, final int elementSize, final byte[] data) {
177             this.type = type;
178             this.elementSize = elementSize;
179             this.data = data;
180         }
181 
182         public void dump(final PrintWriter pw) {
183             pw.println("IcnsElement");
184             final IcnsType icnsType = IcnsType.findAnyType(type);
185             String typeDescription;
186             if (icnsType == null) {
187                 typeDescription = "";
188             } else {
189                 typeDescription = " " + icnsType.toString();
190             }
191             pw.println("Type: 0x" + Integer.toHexString(type) + " ("
192                     + IcnsType.describeType(type) + ")" + typeDescription);
193             pw.println("ElementSize: " + elementSize);
194             pw.println("");
195         }
196     }
197 
198     private IcnsElement readIcnsElement(final InputStream is) throws IOException {
199         final int type = read4Bytes("Type", is, "Not a Valid ICNS File", getByteOrder()); // Icon type
200                                                                     // (4 bytes)
201         final int elementSize = read4Bytes("ElementSize", is, "Not a Valid ICNS File", getByteOrder()); // Length
202                                                                                   // of
203                                                                                   // data
204                                                                                   // (4
205                                                                                   // bytes),
206                                                                                   // in
207                                                                                   // bytes,
208                                                                                   // including
209                                                                                   // this
210                                                                                   // header
211         final byte[] data = readBytes("Data", is, elementSize - 8,
212                 "Not a Valid ICNS File");
213 
214         return new IcnsElement(type, elementSize, data);
215     }
216 
217     private static class IcnsContents {
218         public final IcnsHeader icnsHeader;
219         public final IcnsElement[] icnsElements;
220 
221         public IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
222             super();
223             this.icnsHeader = icnsHeader;
224             this.icnsElements = icnsElements;
225         }
226     }
227 
228     private IcnsContents readImage(final ByteSource byteSource)
229             throws ImageReadException, IOException {
230         try (InputStream is = byteSource.getInputStream()) {
231             final IcnsHeader icnsHeader = readIcnsHeader(is);
232 
233             final List<IcnsElement> icnsElementList = new ArrayList<>();
234             for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
235                 final IcnsElement icnsElement = readIcnsElement(is);
236                 icnsElementList.add(icnsElement);
237                 remainingSize -= icnsElement.elementSize;
238             }
239 
240             final IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()];
241             for (int i = 0; i < icnsElements.length; i++) {
242                 icnsElements[i] = icnsElementList.get(i);
243             }
244 
245             final IcnsContents ret = new IcnsContents(icnsHeader, icnsElements);
246             return ret;
247         }
248     }
249 
250     @Override
251     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
252             throws ImageReadException, IOException {
253         final IcnsContents icnsContents = readImage(byteSource);
254         icnsContents.icnsHeader.dump(pw);
255         for (final IcnsElement icnsElement : icnsContents.icnsElements) {
256             icnsElement.dump(pw);
257         }
258         return true;
259     }
260 
261     @Override
262     public final BufferedImage getBufferedImage(final ByteSource byteSource,
263             final Map<String, Object> params) throws ImageReadException, IOException {
264         final IcnsContents icnsContents = readImage(byteSource);
265         final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
266         if (!result.isEmpty()) {
267             return result.get(0);
268         }
269         throw new ImageReadException("No icons in ICNS file");
270     }
271 
272     @Override
273     public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource)
274             throws ImageReadException, IOException {
275         final IcnsContents icnsContents = readImage(byteSource);
276         return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
277     }
278 
279     @Override
280     public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
281             throws ImageWriteException, IOException {
282         // make copy of params; we'll clear keys as we consume them.
283         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
284 
285         // clear format key.
286         if (params.containsKey(PARAM_KEY_FORMAT)) {
287             params.remove(PARAM_KEY_FORMAT);
288         }
289 
290         if (!params.isEmpty()) {
291             final Object firstKey = params.keySet().iterator().next();
292             throw new ImageWriteException("Unknown parameter: " + firstKey);
293         }
294 
295         IcnsType imageType;
296         if (src.getWidth() == 16 && src.getHeight() == 16) {
297             imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
298         } else if (src.getWidth() == 32 && src.getHeight() == 32) {
299             imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
300         } else if (src.getWidth() == 48 && src.getHeight() == 48) {
301             imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
302         } else if (src.getWidth() == 128 && src.getHeight() == 128) {
303             imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
304         } else {
305             throw new ImageWriteException("Invalid/unsupported source width "
306                     + src.getWidth() + " and height " + src.getHeight());
307         }
308 
309         try (final BinaryOutputStream bos = new BinaryOutputStream(os,
310                 ByteOrder.BIG_ENDIAN)) {
311             bos.write4Bytes(ICNS_MAGIC);
312             bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth()
313             * imageType.getHeight() + 4 + 4 + imageType.getWidth()
314             * imageType.getHeight());
315 
316             bos.write4Bytes(imageType.getType());
317             bos.write4Bytes(4 + 4 + 4 * imageType.getWidth()
318             * imageType.getHeight());
319             for (int y = 0; y < src.getHeight(); y++) {
320                 for (int x = 0; x < src.getWidth(); x++) {
321                     final int argb = src.getRGB(x, y);
322                     bos.write(0);
323                     bos.write(argb >> 16);
324                     bos.write(argb >> 8);
325                     bos.write(argb);
326                 }
327             }
328 
329             final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
330             bos.write4Bytes(maskType.getType());
331             bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
332             for (int y = 0; y < src.getHeight(); y++) {
333                 for (int x = 0; x < src.getWidth(); x++) {
334                     final int argb = src.getRGB(x, y);
335                     bos.write(argb >> 24);
336                 }
337             }
338         }
339     }
340 
341     /**
342      * Extracts embedded XML metadata as XML string.
343      * <p>
344      * 
345      * @param byteSource
346      *            File containing image data.
347      * @param params
348      *            Map of optional parameters, defined in ImagingConstants.
349      * @return Xmp Xml as String, if present. Otherwise, returns null.
350      */
351     @Override
352     public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params)
353             throws ImageReadException, IOException {
354         return null;
355     }
356 }