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}