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.common.BinaryFunctions.read4Bytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
21  
22  import java.awt.Dimension;
23  import java.awt.image.BufferedImage;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.io.PrintWriter;
28  import java.util.ArrayList;
29  import java.util.List;
30  
31  import org.apache.commons.imaging.AbstractImageParser;
32  import org.apache.commons.imaging.ImageFormat;
33  import org.apache.commons.imaging.ImageFormats;
34  import org.apache.commons.imaging.ImageInfo;
35  import org.apache.commons.imaging.ImagingException;
36  import org.apache.commons.imaging.bytesource.ByteSource;
37  import org.apache.commons.imaging.common.BinaryOutputStream;
38  import org.apache.commons.imaging.common.ImageMetadata;
39  
40  public class IcnsImageParser extends AbstractImageParser<IcnsImagingParameters> {
41      private static final class IcnsContents {
42          public final IcnsHeader icnsHeader;
43          public final IcnsElement[] icnsElements;
44  
45          IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
46              this.icnsHeader = icnsHeader;
47              this.icnsElements = icnsElements;
48          }
49      }
50  
51      static class IcnsElement {
52          static final IcnsElement[] EMPTY_ARRAY = {};
53          public final int type;
54          public final int elementSize;
55          public final byte[] data;
56  
57          IcnsElement(final int type, final int elementSize, final byte[] data) {
58              this.type = type;
59              this.elementSize = elementSize;
60              this.data = data;
61          }
62  
63          public void dump(final PrintWriter pw) {
64              pw.println("IcnsElement");
65              final IcnsType icnsType = IcnsType.findAnyType(type);
66              String typeDescription;
67              if (icnsType == null) {
68                  typeDescription = "";
69              } else {
70                  typeDescription = " " + icnsType.toString();
71              }
72              pw.println("Type: 0x" + Integer.toHexString(type) + " (" + IcnsType.describeType(type) + ")" + typeDescription);
73              pw.println("ElementSize: " + elementSize);
74              pw.println("");
75          }
76      }
77  
78      private static final class IcnsHeader {
79          public final int magic; // Magic literal (4 bytes), always "icns"
80          public final int fileSize; // Length of file (4 bytes), in bytes.
81  
82          IcnsHeader(final int magic, final int fileSize) {
83              this.magic = magic;
84              this.fileSize = fileSize;
85          }
86  
87          public void dump(final PrintWriter pw) {
88              pw.println("IcnsHeader");
89              pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" + IcnsType.describeType(magic) + ")");
90              pw.println("FileSize: " + fileSize);
91              pw.println("");
92          }
93      }
94  
95      static final int ICNS_MAGIC = IcnsType.typeAsInt("icns");
96  
97      private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension();
98  
99      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.ICNS.getExtensions();
100 
101     @Override
102     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
103         final IcnsContents icnsContents = readImage(byteSource);
104         icnsContents.icnsHeader.dump(pw);
105         for (final IcnsElement icnsElement : icnsContents.icnsElements) {
106             icnsElement.dump(pw);
107         }
108         return true;
109     }
110 
111     @Override
112     protected String[] getAcceptedExtensions() {
113         return ACCEPTED_EXTENSIONS;
114     }
115 
116     @Override
117     protected ImageFormat[] getAcceptedTypes() {
118         return new ImageFormat[] { ImageFormats.ICNS };
119     }
120 
121     @Override
122     public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException {
123         final IcnsContents icnsContents = readImage(byteSource);
124         return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
125     }
126 
127     @Override
128     public final BufferedImage getBufferedImage(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
129         final IcnsContents icnsContents = readImage(byteSource);
130         final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
131         if (!result.isEmpty()) {
132             return result.get(0);
133         }
134         throw new ImagingException("No icons in ICNS file");
135     }
136 
137     @Override
138     public String getDefaultExtension() {
139         return DEFAULT_EXTENSION;
140     }
141 
142     @Override
143     public IcnsImagingParameters getDefaultParameters() {
144         return new IcnsImagingParameters();
145     }
146 
147     @Override
148     public byte[] getIccProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
149         return null;
150     }
151 
152     @Override
153     public ImageInfo getImageInfo(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
154         final IcnsContents contents = readImage(byteSource);
155         final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
156         if (images.isEmpty()) {
157             throw new ImagingException("No icons in ICNS file");
158         }
159         final BufferedImage image0 = images.get(0);
160         return new ImageInfo("Icns", 32, new ArrayList<>(), ImageFormats.ICNS, "ICNS Apple Icon Image", image0.getHeight(), "image/x-icns", images.size(), 0, 0,
161                 0, 0, image0.getWidth(), false, true, false, ImageInfo.ColorType.RGB, ImageInfo.CompressionAlgorithm.UNKNOWN);
162     }
163 
164     @Override
165     public Dimension getImageSize(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
166         final IcnsContents contents = readImage(byteSource);
167         final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
168         if (images.isEmpty()) {
169             throw new ImagingException("No icons in ICNS file");
170         }
171         final BufferedImage image0 = images.get(0);
172         return new Dimension(image0.getWidth(), image0.getHeight());
173     }
174 
175     // FIXME should throw UOE
176     @Override
177     public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params) throws ImagingException, IOException {
178         return null;
179     }
180 
181     @Override
182     public String getName() {
183         return "Apple Icon Image";
184     }
185 
186     private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException {
187         // Icon type (4 bytes)
188         final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
189         // Length of data (4 bytes), in bytes, including this header
190         final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
191         if (elementSize > remainingSize) {
192             throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than " + "remaining size %d", elementSize, remainingSize));
193         }
194         final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
195 
196         return new IcnsElement(type, elementSize, data);
197     }
198 
199     private IcnsHeader readIcnsHeader(final InputStream is) throws ImagingException, IOException {
200         final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder());
201         final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder());
202 
203         if (magic != ICNS_MAGIC) {
204             throw new ImagingException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic));
205         }
206 
207         return new IcnsHeader(magic, fileSize);
208     }
209 
210     private IcnsContents readImage(final ByteSource byteSource) throws ImagingException, IOException {
211         try (InputStream is = byteSource.getInputStream()) {
212             final IcnsHeader icnsHeader = readIcnsHeader(is);
213 
214             final List<IcnsElement> icnsElementList = new ArrayList<>();
215             for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
216                 final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
217                 icnsElementList.add(icnsElement);
218                 remainingSize -= icnsElement.elementSize;
219             }
220 
221             return new IcnsContents(icnsHeader, icnsElementList.toArray(IcnsElement.EMPTY_ARRAY));
222         }
223     }
224 
225     @Override
226     public void writeImage(final BufferedImage src, final OutputStream os, final IcnsImagingParameters params) throws ImagingException, IOException {
227         IcnsType imageType;
228         if (src.getWidth() == 16 && src.getHeight() == 16) {
229             imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
230         } else if (src.getWidth() == 32 && src.getHeight() == 32) {
231             imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
232         } else if (src.getWidth() == 48 && src.getHeight() == 48) {
233             imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
234         } else if (src.getWidth() == 128 && src.getHeight() == 128) {
235             imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
236         } else {
237             throw new ImagingException("Invalid/unsupported source width " + src.getWidth() + " and height " + src.getHeight());
238         }
239 
240         try (BinaryOutputStream bos = BinaryOutputStream.bigEndian(os)) {
241             bos.write4Bytes(ICNS_MAGIC);
242             bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth() * imageType.getHeight() + 4 + 4 + imageType.getWidth() * imageType.getHeight());
243 
244             bos.write4Bytes(imageType.getType());
245             bos.write4Bytes(4 + 4 + 4 * imageType.getWidth() * imageType.getHeight());
246             for (int y = 0; y < src.getHeight(); y++) {
247                 for (int x = 0; x < src.getWidth(); x++) {
248                     final int argb = src.getRGB(x, y);
249                     bos.write(0);
250                     bos.write(argb >> 16);
251                     bos.write(argb >> 8);
252                     bos.write(argb);
253                 }
254             }
255 
256             final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
257             bos.write4Bytes(maskType.getType());
258             bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
259             for (int y = 0; y < src.getHeight(); y++) {
260                 for (int x = 0; x < src.getWidth(); x++) {
261                     final int argb = src.getRGB(x, y);
262                     bos.write(argb >> 24);
263                 }
264             }
265         }
266     }
267 }