    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}