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.nio.ByteOrder;
029import java.util.ArrayList;
030import java.util.List;
031
032import org.apache.commons.imaging.ImageFormat;
033import org.apache.commons.imaging.ImageFormats;
034import org.apache.commons.imaging.ImageInfo;
035import org.apache.commons.imaging.ImageParser;
036import org.apache.commons.imaging.ImageReadException;
037import org.apache.commons.imaging.ImageWriteException;
038import org.apache.commons.imaging.common.BinaryOutputStream;
039import org.apache.commons.imaging.common.ImageMetadata;
040import org.apache.commons.imaging.common.bytesource.ByteSource;
041
042public class IcnsImageParser extends ImageParser<IcnsImagingParameters> {
043    static final int ICNS_MAGIC = IcnsType.typeAsInt("icns");
044    private static final String DEFAULT_EXTENSION = ImageFormats.ICNS.getDefaultExtension();
045    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.ICNS.getExtensions();
046
047    public IcnsImageParser() {
048        super.setByteOrder(ByteOrder.BIG_ENDIAN);
049    }
050
051    @Override
052    public IcnsImagingParameters getDefaultParameters() {
053        return new IcnsImagingParameters();
054    }
055
056    @Override
057    public String getName() {
058        return "Apple Icon Image";
059    }
060
061    @Override
062    public String getDefaultExtension() {
063        return DEFAULT_EXTENSION;
064    }
065
066    @Override
067    protected String[] getAcceptedExtensions() {
068        return ACCEPTED_EXTENSIONS;
069    }
070
071    @Override
072    protected ImageFormat[] getAcceptedTypes() {
073        return new ImageFormat[] { ImageFormats.ICNS };
074    }
075
076    // FIXME should throw UOE
077    @Override
078    public ImageMetadata getMetadata(final ByteSource byteSource, final IcnsImagingParameters params)
079            throws ImageReadException, IOException {
080        return null;
081    }
082
083    @Override
084    public ImageInfo getImageInfo(final ByteSource byteSource, IcnsImagingParameters params)
085            throws ImageReadException, IOException {
086        final IcnsContents contents = readImage(byteSource);
087        final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
088        if (images.isEmpty()) {
089            throw new ImageReadException("No icons in ICNS file");
090        }
091        final BufferedImage image0 = images.get(0);
092        return new ImageInfo("Icns", 32, new ArrayList<>(),
093                ImageFormats.ICNS, "ICNS Apple Icon Image",
094                image0.getHeight(), "image/x-icns", images.size(), 0, 0, 0, 0,
095                image0.getWidth(), false, true, false,
096                ImageInfo.ColorType.RGB,
097                ImageInfo.CompressionAlgorithm.UNKNOWN);
098    }
099
100    @Override
101    public Dimension getImageSize(final ByteSource byteSource, IcnsImagingParameters params)
102            throws ImageReadException, IOException {
103        final IcnsContents contents = readImage(byteSource);
104        final List<BufferedImage> images = IcnsDecoder.decodeAllImages(contents.icnsElements);
105        if (images.isEmpty()) {
106            throw new ImageReadException("No icons in ICNS file");
107        }
108        final BufferedImage image0 = images.get(0);
109        return new Dimension(image0.getWidth(), image0.getHeight());
110    }
111
112    @Override
113    public byte[] getICCProfileBytes(final ByteSource byteSource, final IcnsImagingParameters params)
114            throws ImageReadException, IOException {
115        return null;
116    }
117
118    private static class IcnsHeader {
119        public final int magic; // Magic literal (4 bytes), always "icns"
120        public final int fileSize; // Length of file (4 bytes), in bytes.
121
122        IcnsHeader(final int magic, final int fileSize) {
123            this.magic = magic;
124            this.fileSize = fileSize;
125        }
126
127        public void dump(final PrintWriter pw) {
128            pw.println("IcnsHeader");
129            pw.println("Magic: 0x" + Integer.toHexString(magic) + " ("
130                    + IcnsType.describeType(magic) + ")");
131            pw.println("FileSize: " + fileSize);
132            pw.println("");
133        }
134    }
135
136    private IcnsHeader readIcnsHeader(final InputStream is)
137            throws ImageReadException, IOException {
138        final int magic = read4Bytes("Magic", is, "Not a Valid ICNS File", getByteOrder());
139        final int fileSize = read4Bytes("FileSize", is, "Not a Valid ICNS File", getByteOrder());
140
141        if (magic != ICNS_MAGIC) {
142            throw new ImageReadException("Not a Valid ICNS File: " + "magic is 0x" + Integer.toHexString(magic));
143        }
144
145        return new IcnsHeader(magic, fileSize);
146    }
147
148    static class IcnsElement {
149        public final int type;
150        public final int elementSize;
151        public final byte[] data;
152
153        IcnsElement(final int type, final int elementSize, final byte[] data) {
154            this.type = type;
155            this.elementSize = elementSize;
156            this.data = data;
157        }
158
159        public void dump(final PrintWriter pw) {
160            pw.println("IcnsElement");
161            final IcnsType icnsType = IcnsType.findAnyType(type);
162            String typeDescription;
163            if (icnsType == null) {
164                typeDescription = "";
165            } else {
166                typeDescription = " " + icnsType.toString();
167            }
168            pw.println("Type: 0x" + Integer.toHexString(type) + " ("
169                    + IcnsType.describeType(type) + ")" + typeDescription);
170            pw.println("ElementSize: " + elementSize);
171            pw.println("");
172        }
173    }
174
175    private IcnsElement readIcnsElement(final InputStream is, final int remainingSize) throws IOException {
176        // Icon type (4 bytes)
177        final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
178        // Length of data (4 bytes), in bytes, including this header
179        final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
180        if (elementSize > remainingSize) {
181            throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than "
182                    + "remaining size %d", elementSize, remainingSize));
183        }
184        final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
185
186        return new IcnsElement(type, elementSize, data);
187    }
188
189    private static class IcnsContents {
190        public final IcnsHeader icnsHeader;
191        public final IcnsElement[] icnsElements;
192
193        IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
194            this.icnsHeader = icnsHeader;
195            this.icnsElements = icnsElements;
196        }
197    }
198
199    private IcnsContents readImage(final ByteSource byteSource)
200            throws ImageReadException, IOException {
201        try (InputStream is = byteSource.getInputStream()) {
202            final IcnsHeader icnsHeader = readIcnsHeader(is);
203
204            final List<IcnsElement> icnsElementList = new ArrayList<>();
205            for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
206                final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
207                icnsElementList.add(icnsElement);
208                remainingSize -= icnsElement.elementSize;
209            }
210
211            final IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()];
212            for (int i = 0; i < icnsElements.length; i++) {
213                icnsElements[i] = icnsElementList.get(i);
214            }
215
216            return new IcnsContents(icnsHeader, icnsElements);
217        }
218    }
219
220    @Override
221    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
222            throws ImageReadException, IOException {
223        final IcnsContents icnsContents = readImage(byteSource);
224        icnsContents.icnsHeader.dump(pw);
225        for (final IcnsElement icnsElement : icnsContents.icnsElements) {
226            icnsElement.dump(pw);
227        }
228        return true;
229    }
230
231    @Override
232    public final BufferedImage getBufferedImage(final ByteSource byteSource,
233            final IcnsImagingParameters params) throws ImageReadException, IOException {
234        final IcnsContents icnsContents = readImage(byteSource);
235        final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
236        if (!result.isEmpty()) {
237            return result.get(0);
238        }
239        throw new ImageReadException("No icons in ICNS file");
240    }
241
242    @Override
243    public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource)
244            throws ImageReadException, IOException {
245        final IcnsContents icnsContents = readImage(byteSource);
246        return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
247    }
248
249    @Override
250    public void writeImage(final BufferedImage src, final OutputStream os, IcnsImagingParameters params)
251            throws ImageWriteException, IOException {
252        IcnsType imageType;
253        if (src.getWidth() == 16 && src.getHeight() == 16) {
254            imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
255        } else if (src.getWidth() == 32 && src.getHeight() == 32) {
256            imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
257        } else if (src.getWidth() == 48 && src.getHeight() == 48) {
258            imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
259        } else if (src.getWidth() == 128 && src.getHeight() == 128) {
260            imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
261        } else {
262            throw new ImageWriteException("Invalid/unsupported source width "
263                    + src.getWidth() + " and height " + src.getHeight());
264        }
265
266        try (BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.BIG_ENDIAN)) {
267            bos.write4Bytes(ICNS_MAGIC);
268            bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth()
269            * imageType.getHeight() + 4 + 4 + imageType.getWidth()
270            * imageType.getHeight());
271
272            bos.write4Bytes(imageType.getType());
273            bos.write4Bytes(4 + 4 + 4 * imageType.getWidth()
274            * imageType.getHeight());
275            for (int y = 0; y < src.getHeight(); y++) {
276                for (int x = 0; x < src.getWidth(); x++) {
277                    final int argb = src.getRGB(x, y);
278                    bos.write(0);
279                    bos.write(argb >> 16);
280                    bos.write(argb >> 8);
281                    bos.write(argb);
282                }
283            }
284
285            final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
286            bos.write4Bytes(maskType.getType());
287            bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
288            for (int y = 0; y < src.getHeight(); y++) {
289                for (int x = 0; x < src.getWidth(); x++) {
290                    final int argb = src.getRGB(x, y);
291                    bos.write(argb >> 24);
292                }
293            }
294        }
295    }
296}