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.common.BinaryFunctions.read4Bytes;
020import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
021
022import java.awt.Dimension;
023import java.awt.image.BufferedImage;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.io.PrintWriter;
028import java.util.ArrayList;
029import java.util.List;
030
031import org.apache.commons.imaging.AbstractImageParser;
032import org.apache.commons.imaging.ImageFormat;
033import org.apache.commons.imaging.ImageFormats;
034import org.apache.commons.imaging.ImageInfo;
035import org.apache.commons.imaging.ImagingException;
036import org.apache.commons.imaging.bytesource.ByteSource;
037import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
038import org.apache.commons.imaging.common.ImageMetadata;
039
040public class IcnsImageParser extends AbstractImageParser<IcnsImagingParameters> {
041    private static final class IcnsContents {
042        public final IcnsHeader icnsHeader;
043        public final IcnsElement[] icnsElements;
044
045        IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
046            this.icnsHeader = icnsHeader;
047            this.icnsElements = icnsElements;
048        }
049    }
050
051    static class IcnsElement {
052        static final IcnsElement[] EMPTY_ARRAY = {};
053        public final int type;
054        public final int elementSize;
055        public final byte[] data;
056
057        IcnsElement(final int type, final int elementSize, final byte[] data) {
058            this.type = type;
059            this.elementSize = elementSize;
060            this.data = data;
061        }
062
063        public void dump(final PrintWriter pw) {
064            pw.println("IcnsElement");
065            final IcnsType icnsType = IcnsType.findAnyType(type);
066            final String typeDescription;
067            if (icnsType == null) {
068                typeDescription = "";
069            } else {
070                typeDescription = " " + icnsType.toString();
071            }
072            pw.println("Type: 0x" + Integer.toHexString(type) + " (" + IcnsType.describeType(type) + ")" + typeDescription);
073            pw.println("ElementSize: " + elementSize);
074            pw.println("");
075        }
076    }
077
078    private static final class IcnsHeader {
079        public final int magic; // Magic literal (4 bytes), always "icns"
080        public final int fileSize; // Length of file (4 bytes), in bytes.
081
082        IcnsHeader(final int magic, final int fileSize) {
083            this.magic = magic;
084            this.fileSize = fileSize;
085        }
086
087        public void dump(final PrintWriter pw) {
088            pw.println("IcnsHeader");
089            pw.println("Magic: 0x" + Integer.toHexString(magic) + " (" + IcnsType.describeType(magic) + ")");
090            pw.println("FileSize: " + fileSize);
091            pw.println("");
092        }
093    }
094
095    static final int ICNS_MAGIC = IcnsType.typeAsInt("icns");
096
097    private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension();
098
099    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}