    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}