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.AbstractBinaryOutputStream;
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              final 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     /**
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 }