    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, final int remainingSize) throws IOException {
    190        // Icon type (4 bytes)
    191        final int type = read4Bytes("Type", is, "Not a valid ICNS file", getByteOrder());
    192        // Length of data (4 bytes), in bytes, including this header
    193        final int elementSize = read4Bytes("ElementSize", is, "Not a valid ICNS file", getByteOrder());
    194        if (elementSize > remainingSize) {
    195            throw new IOException(String.format("Corrupted ICNS file: element size %d is greater than "
    196                    + "remaining size %d", elementSize, remainingSize));
    197        }
    198        final byte[] data = readBytes("Data", is, elementSize - 8, "Not a valid ICNS file");
    199
    200        return new IcnsElement(type, elementSize, data);
    201    }
    202
    203    private static class IcnsContents {
    204        public final IcnsHeader icnsHeader;
    205        public final IcnsElement[] icnsElements;
    206
    207        IcnsContents(final IcnsHeader icnsHeader, final IcnsElement[] icnsElements) {
    208            super();
    209            this.icnsHeader = icnsHeader;
    210            this.icnsElements = icnsElements;
    211        }
    212    }
    213
    214    private IcnsContents readImage(final ByteSource byteSource)
    215            throws ImageReadException, IOException {
    216        try (InputStream is = byteSource.getInputStream()) {
    217            final IcnsHeader icnsHeader = readIcnsHeader(is);
    218
    219            final List<IcnsElement> icnsElementList = new ArrayList<>();
    220            for (int remainingSize = icnsHeader.fileSize - 8; remainingSize > 0;) {
    221                final IcnsElement icnsElement = readIcnsElement(is, remainingSize);
    222                icnsElementList.add(icnsElement);
    223                remainingSize -= icnsElement.elementSize;
    224            }
    225
    226            final IcnsElement[] icnsElements = new IcnsElement[icnsElementList.size()];
    227            for (int i = 0; i < icnsElements.length; i++) {
    228                icnsElements[i] = icnsElementList.get(i);
    229            }
    230
    231            final IcnsContents ret = new IcnsContents(icnsHeader, icnsElements);
    232            return ret;
    233        }
    234    }
    235
    236    @Override
    237    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
    238            throws ImageReadException, IOException {
    239        final IcnsContents icnsContents = readImage(byteSource);
    240        icnsContents.icnsHeader.dump(pw);
    241        for (final IcnsElement icnsElement : icnsContents.icnsElements) {
    242            icnsElement.dump(pw);
    243        }
    244        return true;
    245    }
    246
    247    @Override
    248    public final BufferedImage getBufferedImage(final ByteSource byteSource,
    249            final Map<String, Object> params) throws ImageReadException, IOException {
    250        final IcnsContents icnsContents = readImage(byteSource);
    251        final List<BufferedImage> result = IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
    252        if (!result.isEmpty()) {
    253            return result.get(0);
    254        }
    255        throw new ImageReadException("No icons in ICNS file");
    256    }
    257
    258    @Override
    259    public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource)
    260            throws ImageReadException, IOException {
    261        final IcnsContents icnsContents = readImage(byteSource);
    262        return IcnsDecoder.decodeAllImages(icnsContents.icnsElements);
    263    }
    264
    265    @Override
    266    public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
    267            throws ImageWriteException, IOException {
    268        // make copy of params; we'll clear keys as we consume them.
    269        params = (params == null) ? new HashMap<>() : new HashMap<>(params);
    270
    271        // clear format key.
    272        if (params.containsKey(PARAM_KEY_FORMAT)) {
    273            params.remove(PARAM_KEY_FORMAT);
    274        }
    275
    276        if (!params.isEmpty()) {
    277            final Object firstKey = params.keySet().iterator().next();
    278            throw new ImageWriteException("Unknown parameter: " + firstKey);
    279        }
    280
    281        IcnsType imageType;
    282        if (src.getWidth() == 16 && src.getHeight() == 16) {
    283            imageType = IcnsType.ICNS_16x16_32BIT_IMAGE;
    284        } else if (src.getWidth() == 32 && src.getHeight() == 32) {
    285            imageType = IcnsType.ICNS_32x32_32BIT_IMAGE;
    286        } else if (src.getWidth() == 48 && src.getHeight() == 48) {
    287            imageType = IcnsType.ICNS_48x48_32BIT_IMAGE;
    288        } else if (src.getWidth() == 128 && src.getHeight() == 128) {
    289            imageType = IcnsType.ICNS_128x128_32BIT_IMAGE;
    290        } else {
    291            throw new ImageWriteException("Invalid/unsupported source width "
    292                    + src.getWidth() + " and height " + src.getHeight());
    293        }
    294
    295        try (BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.BIG_ENDIAN)) {
    296            bos.write4Bytes(ICNS_MAGIC);
    297            bos.write4Bytes(4 + 4 + 4 + 4 + 4 * imageType.getWidth()
    298            * imageType.getHeight() + 4 + 4 + imageType.getWidth()
    299            * imageType.getHeight());
    300
    301            bos.write4Bytes(imageType.getType());
    302            bos.write4Bytes(4 + 4 + 4 * imageType.getWidth()
    303            * imageType.getHeight());
    304            for (int y = 0; y < src.getHeight(); y++) {
    305                for (int x = 0; x < src.getWidth(); x++) {
    306                    final int argb = src.getRGB(x, y);
    307                    bos.write(0);
    308                    bos.write(argb >> 16);
    309                    bos.write(argb >> 8);
    310                    bos.write(argb);
    311                }
    312            }
    313
    314            final IcnsType maskType = IcnsType.find8BPPMaskType(imageType);
    315            bos.write4Bytes(maskType.getType());
    316            bos.write4Bytes(4 + 4 + imageType.getWidth() * imageType.getWidth());
    317            for (int y = 0; y < src.getHeight(); y++) {
    318                for (int x = 0; x < src.getWidth(); x++) {
    319                    final int argb = src.getRGB(x, y);
    320                    bos.write(argb >> 24);
    321                }
    322            }
    323        }
    324    }
    325}