    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.dcx;
    018
    019import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
    020import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_PIXEL_DENSITY;
    021import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
    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.PixelDensity;
    042import org.apache.commons.imaging.common.BinaryOutputStream;
    043import org.apache.commons.imaging.common.ImageMetadata;
    044import org.apache.commons.imaging.common.bytesource.ByteSource;
    045import org.apache.commons.imaging.common.bytesource.ByteSourceInputStream;
    046import org.apache.commons.imaging.formats.pcx.PcxConstants;
    047import org.apache.commons.imaging.formats.pcx.PcxImageParser;
    048
    049public class DcxImageParser extends ImageParser {
    050    // See http://www.fileformat.fine/format/pcx/egff.htm for documentation
    051    private static final String DEFAULT_EXTENSION = ".dcx";
    052    private static final String[] ACCEPTED_EXTENSIONS = { ".dcx", };
    053
    054    public DcxImageParser() {
    055        super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
    056    }
    057
    058    @Override
    059    public String getName() {
    060        return "Dcx-Custom";
    061    }
    062
    063    @Override
    064    public String getDefaultExtension() {
    065        return DEFAULT_EXTENSION;
    066    }
    067
    068    @Override
    069    protected String[] getAcceptedExtensions() {
    070        return ACCEPTED_EXTENSIONS;
    071    }
    072
    073    @Override
    074    protected ImageFormat[] getAcceptedTypes() {
    075        return new ImageFormat[] {
    076                ImageFormats.DCX, //
    077        };
    078    }
    079
    080    // FIXME should throw UOE
    081    @Override
    082    public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
    083            throws ImageReadException, IOException {
    084        return null;
    085    }
    086
    087    // FIXME should throw UOE
    088    @Override
    089    public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
    090            throws ImageReadException, IOException {
    091        return null;
    092    }
    093
    094    // FIXME should throw UOE
    095    @Override
    096    public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params)
    097            throws ImageReadException, IOException {
    098        return null;
    099    }
    100
    101    // FIXME should throw UOE
    102    @Override
    103    public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
    104            throws ImageReadException, IOException {
    105        return null;
    106    }
    107
    108    private static class DcxHeader {
    109
    110        public static final int DCX_ID = 0x3ADE68B1;
    111        public final int id;
    112        public final long[] pageTable;
    113
    114        DcxHeader(final int id, final long[] pageTable) {
    115            this.id = id;
    116            this.pageTable = pageTable;
    117        }
    118
    119        public void dump(final PrintWriter pw) {
    120            pw.println("DcxHeader");
    121            pw.println("Id: 0x" + Integer.toHexString(id));
    122            pw.println("Pages: " + pageTable.length);
    123            pw.println();
    124        }
    125    }
    126
    127    private DcxHeader readDcxHeader(final ByteSource byteSource)
    128            throws ImageReadException, IOException {
    129        try (InputStream is = byteSource.getInputStream()) {
    130            final int id = read4Bytes("Id", is, "Not a Valid DCX File", getByteOrder());
    131            final List<Long> pageTable = new ArrayList<>(1024);
    132            for (int i = 0; i < 1024; i++) {
    133                final long pageOffset = 0xFFFFffffL & read4Bytes("PageTable", is,
    134                        "Not a Valid DCX File", getByteOrder());
    135                if (pageOffset == 0) {
    136                    break;
    137                }
    138                pageTable.add(pageOffset);
    139            }
    140
    141            if (id != DcxHeader.DCX_ID) {
    142                throw new ImageReadException(
    143                        "Not a Valid DCX File: file id incorrect");
    144            }
    145            if (pageTable.size() == 1024) {
    146                throw new ImageReadException(
    147                        "DCX page table not terminated by zero entry");
    148            }
    149
    150            final Object[] objects = pageTable.toArray();
    151            final long[] pages = new long[objects.length];
    152            for (int i = 0; i < objects.length; i++) {
    153                pages[i] = ((Long) objects[i]);
    154            }
    155
    156            final DcxHeader ret = new DcxHeader(id, pages);
    157            return ret;
    158        }
    159    }
    160
    161    @Override
    162    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
    163            throws ImageReadException, IOException {
    164        readDcxHeader(byteSource).dump(pw);
    165        return true;
    166    }
    167
    168    @Override
    169    public final BufferedImage getBufferedImage(final ByteSource byteSource,
    170            final Map<String, Object> params) throws ImageReadException, IOException {
    171        final List<BufferedImage> list = getAllBufferedImages(byteSource);
    172        if (list.isEmpty()) {
    173            return null;
    174        }
    175        return list.get(0);
    176    }
    177
    178    @Override
    179    public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource)
    180            throws ImageReadException, IOException {
    181        final DcxHeader dcxHeader = readDcxHeader(byteSource);
    182        final List<BufferedImage> images = new ArrayList<>();
    183        final PcxImageParser pcxImageParser = new PcxImageParser();
    184        for (final long element : dcxHeader.pageTable) {
    185            try (InputStream stream = byteSource.getInputStream(element)) {
    186                final ByteSourceInputStream pcxSource = new ByteSourceInputStream(
    187                        stream, null);
    188                final BufferedImage image = pcxImageParser.getBufferedImage(
    189                        pcxSource, new HashMap<String, Object>());
    190                images.add(image);
    191            }
    192        }
    193        return images;
    194    }
    195
    196    @Override
    197    public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
    198            throws ImageWriteException, IOException {
    199        // make copy of params; we'll clear keys as we consume them.
    200        params = (params == null) ? new HashMap<>() : new HashMap<>(params);
    201
    202        final HashMap<String, Object> pcxParams = new HashMap<>();
    203
    204        // clear format key.
    205        if (params.containsKey(PARAM_KEY_FORMAT)) {
    206            params.remove(PARAM_KEY_FORMAT);
    207        }
    208
    209        if (params.containsKey(PcxConstants.PARAM_KEY_PCX_COMPRESSION)) {
    210            final Object value = params.remove(PcxConstants.PARAM_KEY_PCX_COMPRESSION);
    211            pcxParams.put(PcxConstants.PARAM_KEY_PCX_COMPRESSION, value);
    212        }
    213
    214        if (params.containsKey(PARAM_KEY_PIXEL_DENSITY)) {
    215            final Object value = params.remove(PARAM_KEY_PIXEL_DENSITY);
    216            if (value != null) {
    217                if (!(value instanceof PixelDensity)) {
    218                    throw new ImageWriteException(
    219                            "Invalid pixel density parameter");
    220                }
    221                pcxParams.put(PARAM_KEY_PIXEL_DENSITY, value);
    222            }
    223        }
    224
    225
    226        if (!params.isEmpty()) {
    227            final Object firstKey = params.keySet().iterator().next();
    228            throw new ImageWriteException("Unknown parameter: " + firstKey);
    229        }
    230
    231        final int headerSize = 4 + 1024 * 4;
    232
    233        final BinaryOutputStream bos = new BinaryOutputStream(os,
    234                ByteOrder.LITTLE_ENDIAN);
    235        bos.write4Bytes(DcxHeader.DCX_ID);
    236        // Some apps may need a full 1024 entry table
    237        bos.write4Bytes(headerSize);
    238        for (int i = 0; i < 1023; i++) {
    239            bos.write4Bytes(0);
    240        }
    241        final PcxImageParser pcxImageParser = new PcxImageParser();
    242        pcxImageParser.writeImage(src, bos, pcxParams);
    243    }
    244}