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}