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.pnm;
018
019import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
020import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
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.HashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.StringTokenizer;
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.ImageBuilder;
042import org.apache.commons.imaging.common.ImageMetadata;
043import org.apache.commons.imaging.common.bytesource.ByteSource;
044import org.apache.commons.imaging.palette.PaletteFactory;
045
046public class PnmImageParser extends ImageParser {
047    private static final String DEFAULT_EXTENSION = ".pnm";
048    private static final String[] ACCEPTED_EXTENSIONS = { ".pbm", ".pgm",
049            ".ppm", ".pnm", ".pam" };
050    public static final String PARAM_KEY_PNM_RAWBITS = "PNM_RAWBITS";
051    public static final String PARAM_VALUE_PNM_RAWBITS_YES = "YES";
052    public static final String PARAM_VALUE_PNM_RAWBITS_NO = "NO";
053
054    public PnmImageParser() {
055        super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
056        // setDebug(true);
057    }
058
059    @Override
060    public String getName() {
061        return "Pbm-Custom";
062    }
063
064    @Override
065    public String getDefaultExtension() {
066        return DEFAULT_EXTENSION;
067    }
068
069    @Override
070    protected String[] getAcceptedExtensions() {
071        return ACCEPTED_EXTENSIONS;
072    }
073
074    @Override
075    protected ImageFormat[] getAcceptedTypes() {
076        return new ImageFormat[] {
077                ImageFormats.PBM,
078                ImageFormats.PGM,
079                ImageFormats.PPM,
080                ImageFormats.PNM,
081                ImageFormats.PAM
082        };
083    }
084
085    private FileInfo readHeader(final InputStream is) throws ImageReadException,
086            IOException {
087        final byte identifier1 = readByte("Identifier1", is, "Not a Valid PNM File");
088        final byte identifier2 = readByte("Identifier2", is, "Not a Valid PNM File");
089
090        if (identifier1 != PnmConstants.PNM_PREFIX_BYTE) {
091            throw new ImageReadException("PNM file has invalid prefix byte 1");
092        }
093
094        final WhiteSpaceReader wsr = new WhiteSpaceReader(is);
095
096        if (identifier2 == PnmConstants.PBM_TEXT_CODE
097                || identifier2 == PnmConstants.PBM_RAW_CODE
098                || identifier2 == PnmConstants.PGM_TEXT_CODE
099                || identifier2 == PnmConstants.PGM_RAW_CODE
100                || identifier2 == PnmConstants.PPM_TEXT_CODE
101                || identifier2 == PnmConstants.PPM_RAW_CODE) {
102
103            final int width;
104            try {
105              width = Integer.parseInt(wsr.readtoWhiteSpace());
106            } catch (final NumberFormatException e) {
107              throw new ImageReadException("Invalid width specified." , e);
108            }
109            final int height;
110            try {
111              height = Integer.parseInt(wsr.readtoWhiteSpace());
112            } catch (final NumberFormatException e) {
113              throw new ImageReadException("Invalid height specified." , e);
114            }
115
116            if (identifier2 == PnmConstants.PBM_TEXT_CODE) {
117                return new PbmFileInfo(width, height, false);
118            } else if (identifier2 == PnmConstants.PBM_RAW_CODE) {
119                return new PbmFileInfo(width, height, true);
120            } else if (identifier2 == PnmConstants.PGM_TEXT_CODE) {
121                final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace());
122                return new PgmFileInfo(width, height, false, maxgray);
123            } else if (identifier2 == PnmConstants.PGM_RAW_CODE) {
124                final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace());
125                return new PgmFileInfo(width, height, true, maxgray);
126            } else if (identifier2 == PnmConstants.PPM_TEXT_CODE) {
127                final int max = Integer.parseInt(wsr.readtoWhiteSpace());
128                return new PpmFileInfo(width, height, false, max);
129            } else if (identifier2 == PnmConstants.PPM_RAW_CODE) {
130                final int max = Integer.parseInt(wsr.readtoWhiteSpace());
131                return new PpmFileInfo(width, height, true, max);
132            } else {
133                throw new ImageReadException("PNM file has invalid header.");
134            }
135        } else if (identifier2 == PnmConstants.PAM_RAW_CODE) {
136            int width = -1;
137            boolean seenWidth = false;
138            int height = -1;
139            boolean seenHeight = false;
140            int depth = -1;
141            boolean seenDepth = false;
142            int maxVal = -1;
143            boolean seenMaxVal = false;
144            final StringBuilder tupleType = new StringBuilder();
145            boolean seenTupleType = false;
146
147            // Advance to next line
148            wsr.readLine();
149            String line;
150            while ((line = wsr.readLine()) != null) {
151                line = line.trim();
152                if (line.charAt(0) == '#') {
153                    continue;
154                }
155                final StringTokenizer tokenizer = new StringTokenizer(line, " ", false);
156                final String type = tokenizer.nextToken();
157                if ("WIDTH".equals(type)) {
158                    seenWidth = true;
159                    if(!tokenizer.hasMoreTokens()) {
160                        throw new ImageReadException("PAM header has no WIDTH value");
161                    }
162                    width = Integer.parseInt(tokenizer.nextToken());
163                } else if ("HEIGHT".equals(type)) {
164                    seenHeight = true;
165                    if(!tokenizer.hasMoreTokens()) {
166                        throw new ImageReadException("PAM header has no HEIGHT value");
167                    }
168                    height = Integer.parseInt(tokenizer.nextToken());
169                } else if ("DEPTH".equals(type)) {
170                    seenDepth = true;
171                    if(!tokenizer.hasMoreTokens()) {
172                        throw new ImageReadException("PAM header has no DEPTH value");
173                    }
174                    depth = Integer.parseInt(tokenizer.nextToken());
175                } else if ("MAXVAL".equals(type)) {
176                    seenMaxVal = true;
177                    if(!tokenizer.hasMoreTokens()) {
178                        throw new ImageReadException("PAM header has no MAXVAL value");
179                    }
180                    maxVal = Integer.parseInt(tokenizer.nextToken());
181                } else if ("TUPLTYPE".equals(type)) {
182                    seenTupleType = true;
183                    if(!tokenizer.hasMoreTokens()) {
184                        throw new ImageReadException("PAM header has no TUPLTYPE value");
185                    }
186                    tupleType.append(tokenizer.nextToken());
187                } else if ("ENDHDR".equals(type)) {
188                    break;
189                } else {
190                    throw new ImageReadException("Invalid PAM file header type " + type);
191                }
192            }
193
194            if (!seenWidth) {
195                throw new ImageReadException("PAM header has no WIDTH");
196            } else if (!seenHeight) {
197                throw new ImageReadException("PAM header has no HEIGHT");
198            } else if (!seenDepth) {
199                throw new ImageReadException("PAM header has no DEPTH");
200            } else if (!seenMaxVal) {
201                throw new ImageReadException("PAM header has no MAXVAL");
202            } else if (!seenTupleType) {
203                throw new ImageReadException("PAM header has no TUPLTYPE");
204            }
205
206            return new PamFileInfo(width, height, depth, maxVal, tupleType.toString());
207        } else {
208            throw new ImageReadException("PNM file has invalid prefix byte 2");
209        }
210    }
211
212    private FileInfo readHeader(final ByteSource byteSource)
213            throws ImageReadException, IOException {
214        try (InputStream is = byteSource.getInputStream()) {
215            return readHeader(is);
216        }
217    }
218
219    @Override
220    public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
221            throws ImageReadException, IOException {
222        return null;
223    }
224
225    @Override
226    public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params)
227            throws ImageReadException, IOException {
228        final FileInfo info = readHeader(byteSource);
229
230        if (info == null) {
231            throw new ImageReadException("PNM: Couldn't read Header");
232        }
233
234        return new Dimension(info.width, info.height);
235    }
236
237    @Override
238    public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
239            throws ImageReadException, IOException {
240        return null;
241    }
242
243    @Override
244    public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
245            throws ImageReadException, IOException {
246        final FileInfo info = readHeader(byteSource);
247
248        if (info == null) {
249            throw new ImageReadException("PNM: Couldn't read Header");
250        }
251
252        final List<String> comments = new ArrayList<>();
253
254        final int bitsPerPixel = info.getBitDepth() * info.getNumComponents();
255        final ImageFormat format = info.getImageType();
256        final String formatName = info.getImageTypeDescription();
257        final String mimeType = info.getMIMEType();
258        final int numberOfImages = 1;
259        final boolean progressive = false;
260
261        // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0);
262        //
263        final int physicalWidthDpi = 72;
264        final float physicalWidthInch = (float) ((double) info.width / (double) physicalWidthDpi);
265        final int physicalHeightDpi = 72;
266        final float physicalHeightInch = (float) ((double) info.height / (double) physicalHeightDpi);
267
268        final String formatDetails = info.getImageTypeDescription();
269
270        final boolean transparent = info.hasAlpha();
271        final boolean usesPalette = false;
272
273        final ImageInfo.ColorType colorType = info.getColorType();
274        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE;
275
276        return new ImageInfo(formatDetails, bitsPerPixel, comments,
277                format, formatName, info.height, mimeType, numberOfImages,
278                physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
279                physicalWidthInch, info.width, progressive, transparent,
280                usesPalette, colorType, compressionAlgorithm);
281    }
282
283    @Override
284    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
285            throws ImageReadException, IOException {
286        pw.println("pnm.dumpImageFile");
287
288        final ImageInfo imageData = getImageInfo(byteSource);
289        if (imageData == null) {
290            return false;
291        }
292
293        imageData.toString(pw, "");
294
295        pw.println("");
296
297        return true;
298    }
299
300    @Override
301    public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params)
302            throws ImageReadException, IOException {
303        try (InputStream is = byteSource.getInputStream()) {
304            final FileInfo info = readHeader(is);
305
306            final int width = info.width;
307            final int height = info.height;
308
309            final boolean hasAlpha = info.hasAlpha();
310            final ImageBuilder imageBuilder = new ImageBuilder(width, height,
311                    hasAlpha);
312            info.readImage(imageBuilder, is);
313
314            final BufferedImage ret = imageBuilder.getBufferedImage();
315            return ret;
316        }
317    }
318
319    @Override
320    public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
321            throws ImageWriteException, IOException {
322        PnmWriter writer = null;
323        boolean useRawbits = true;
324
325        if (params != null) {
326            final Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS);
327            if (useRawbitsParam != null) {
328                if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) {
329                    useRawbits = false;
330                }
331            }
332
333            final Object subtype = params.get(PARAM_KEY_FORMAT);
334            if (subtype != null) {
335                if (subtype.equals(ImageFormats.PBM)) {
336                    writer = new PbmWriter(useRawbits);
337                } else if (subtype.equals(ImageFormats.PGM)) {
338                    writer = new PgmWriter(useRawbits);
339                } else if (subtype.equals(ImageFormats.PPM)) {
340                    writer = new PpmWriter(useRawbits);
341                } else if (subtype.equals(ImageFormats.PAM)) {
342                    writer = new PamWriter();
343                }
344            }
345        }
346
347        if (writer == null) {
348            final boolean hasAlpha = new PaletteFactory().hasTransparency(src);
349            if (hasAlpha) {
350                writer = new PamWriter();
351            } else {
352                writer = new PpmWriter(useRawbits);
353            }
354        }
355
356        // make copy of params; we'll clear keys as we consume them.
357        if (params != null) {
358            params = new HashMap<>(params);
359        } else {
360            params = new HashMap<>();
361        }
362
363        // clear format key.
364        if (params.containsKey(PARAM_KEY_FORMAT)) {
365            params.remove(PARAM_KEY_FORMAT);
366        }
367
368        // clear rawbits key.
369        if (params.containsKey(PARAM_KEY_PNM_RAWBITS)) {
370            params.remove(PARAM_KEY_PNM_RAWBITS);
371        }
372
373        if (!params.isEmpty()) {
374            final Object firstKey = params.keySet().iterator().next();
375            throw new ImageWriteException("Unknown parameter: " + firstKey);
376        }
377
378        writer.writeImage(src, os, params);
379    }
380
381    /**
382     * Extracts embedded XML metadata as XML string.
383     * <p>
384     *
385     * @param byteSource
386     *            File containing image data.
387     * @param params
388     *            Map of optional parameters, defined in ImagingConstants.
389     * @return Xmp Xml as String, if present. Otherwise, returns null.
390     */
391    @Override
392    public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params)
393            throws ImageReadException, IOException {
394        return null;
395    }
396}