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            }
133        } else if (identifier2 == PnmConstants.PAM_RAW_CODE) {
134            int width = -1;
135            boolean seenWidth = false;
136            int height = -1;
137            boolean seenHeight = false;
138            int depth = -1;
139            boolean seenDepth = false;
140            int maxVal = -1;
141            boolean seenMaxVal = false;
142            final StringBuilder tupleType = new StringBuilder();
143            boolean seenTupleType = false;
144
145            // Advance to next line
146            wsr.readLine();
147            String line;
148            while ((line = wsr.readLine()) != null) {
149                line = line.trim();
150                if (line.charAt(0) == '#') {
151                    continue;
152                }
153                final StringTokenizer tokenizer = new StringTokenizer(line, " ", false);
154                final String type = tokenizer.nextToken();
155                if ("WIDTH".equals(type)) {
156                    seenWidth = true;
157                    if(!tokenizer.hasMoreTokens()) {
158                        throw new ImageReadException("PAM header has no WIDTH value");
159                    }
160                    width = Integer.parseInt(tokenizer.nextToken());
161                } else if ("HEIGHT".equals(type)) {
162                    seenHeight = true;
163                    if(!tokenizer.hasMoreTokens()) {
164                        throw new ImageReadException("PAM header has no HEIGHT value");
165                    }
166                    height = Integer.parseInt(tokenizer.nextToken());
167                } else if ("DEPTH".equals(type)) {
168                    seenDepth = true;
169                    if(!tokenizer.hasMoreTokens()) {
170                        throw new ImageReadException("PAM header has no DEPTH value");
171                    }
172                    depth = Integer.parseInt(tokenizer.nextToken());
173                } else if ("MAXVAL".equals(type)) {
174                    seenMaxVal = true;
175                    if(!tokenizer.hasMoreTokens()) {
176                        throw new ImageReadException("PAM header has no MAXVAL value");
177                    }
178                    maxVal = Integer.parseInt(tokenizer.nextToken());
179                } else if ("TUPLTYPE".equals(type)) {
180                    seenTupleType = true;
181                    if(!tokenizer.hasMoreTokens()) {
182                        throw new ImageReadException("PAM header has no TUPLTYPE value");
183                    }
184                    tupleType.append(tokenizer.nextToken());
185                } else if ("ENDHDR".equals(type)) {
186                    break;
187                } else {
188                    throw new ImageReadException("Invalid PAM file header type " + type);
189                }
190            }
191
192            if (!seenWidth) {
193                throw new ImageReadException("PAM header has no WIDTH");
194            } else if (!seenHeight) {
195                throw new ImageReadException("PAM header has no HEIGHT");
196            } else if (!seenDepth) {
197                throw new ImageReadException("PAM header has no DEPTH");
198            } else if (!seenMaxVal) {
199                throw new ImageReadException("PAM header has no MAXVAL");
200            } else if (!seenTupleType) {
201                throw new ImageReadException("PAM header has no TUPLTYPE");
202            }
203
204            return new PamFileInfo(width, height, depth, maxVal, tupleType.toString());
205        }
206        throw new ImageReadException("PNM file has invalid prefix byte 2");
207    }
208
209    private FileInfo readHeader(final ByteSource byteSource)
210            throws ImageReadException, IOException {
211        try (InputStream is = byteSource.getInputStream()) {
212            return readHeader(is);
213        }
214    }
215
216    @Override
217    public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
218            throws ImageReadException, IOException {
219        return null;
220    }
221
222    @Override
223    public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params)
224            throws ImageReadException, IOException {
225        final FileInfo info = readHeader(byteSource);
226
227        if (info == null) {
228            throw new ImageReadException("PNM: Couldn't read Header");
229        }
230
231        return new Dimension(info.width, info.height);
232    }
233
234    @Override
235    public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
236            throws ImageReadException, IOException {
237        return null;
238    }
239
240    @Override
241    public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
242            throws ImageReadException, IOException {
243        final FileInfo info = readHeader(byteSource);
244
245        if (info == null) {
246            throw new ImageReadException("PNM: Couldn't read Header");
247        }
248
249        final List<String> comments = new ArrayList<>();
250
251        final int bitsPerPixel = info.getBitDepth() * info.getNumComponents();
252        final ImageFormat format = info.getImageType();
253        final String formatName = info.getImageTypeDescription();
254        final String mimeType = info.getMIMEType();
255        final int numberOfImages = 1;
256        final boolean progressive = false;
257
258        // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0);
259        //
260        final int physicalWidthDpi = 72;
261        final float physicalWidthInch = (float) ((double) info.width / (double) physicalWidthDpi);
262        final int physicalHeightDpi = 72;
263        final float physicalHeightInch = (float) ((double) info.height / (double) physicalHeightDpi);
264
265        final String formatDetails = info.getImageTypeDescription();
266
267        final boolean transparent = info.hasAlpha();
268        final boolean usesPalette = false;
269
270        final ImageInfo.ColorType colorType = info.getColorType();
271        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE;
272
273        return new ImageInfo(formatDetails, bitsPerPixel, comments,
274                format, formatName, info.height, mimeType, numberOfImages,
275                physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
276                physicalWidthInch, info.width, progressive, transparent,
277                usesPalette, colorType, compressionAlgorithm);
278    }
279
280    @Override
281    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
282            throws ImageReadException, IOException {
283        pw.println("pnm.dumpImageFile");
284
285        final ImageInfo imageData = getImageInfo(byteSource);
286        if (imageData == null) {
287            return false;
288        }
289
290        imageData.toString(pw, "");
291
292        pw.println("");
293
294        return true;
295    }
296
297    @Override
298    public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params)
299            throws ImageReadException, IOException {
300        try (InputStream is = byteSource.getInputStream()) {
301            final FileInfo info = readHeader(is);
302
303            final int width = info.width;
304            final int height = info.height;
305
306            final boolean hasAlpha = info.hasAlpha();
307            final ImageBuilder imageBuilder = new ImageBuilder(width, height,
308                    hasAlpha);
309            info.readImage(imageBuilder, is);
310
311            final BufferedImage ret = imageBuilder.getBufferedImage();
312            return ret;
313        }
314    }
315
316    @Override
317    public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
318            throws ImageWriteException, IOException {
319        PnmWriter writer = null;
320        boolean useRawbits = true;
321
322        if (params != null) {
323            final Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS);
324            if (useRawbitsParam != null) {
325                if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) {
326                    useRawbits = false;
327                }
328            }
329
330            final Object subtype = params.get(PARAM_KEY_FORMAT);
331            if (subtype != null) {
332                if (subtype.equals(ImageFormats.PBM)) {
333                    writer = new PbmWriter(useRawbits);
334                } else if (subtype.equals(ImageFormats.PGM)) {
335                    writer = new PgmWriter(useRawbits);
336                } else if (subtype.equals(ImageFormats.PPM)) {
337                    writer = new PpmWriter(useRawbits);
338                } else if (subtype.equals(ImageFormats.PAM)) {
339                    writer = new PamWriter();
340                }
341            }
342        }
343
344        if (writer == null) {
345            final boolean hasAlpha = new PaletteFactory().hasTransparency(src);
346            if (hasAlpha) {
347                writer = new PamWriter();
348            } else {
349                writer = new PpmWriter(useRawbits);
350            }
351        }
352
353        // make copy of params; we'll clear keys as we consume them.
354        if (params != null) {
355            params = new HashMap<>(params);
356        } else {
357            params = new HashMap<>();
358        }
359
360        // clear format key.
361        if (params.containsKey(PARAM_KEY_FORMAT)) {
362            params.remove(PARAM_KEY_FORMAT);
363        }
364
365        // clear rawbits key.
366        if (params.containsKey(PARAM_KEY_PNM_RAWBITS)) {
367            params.remove(PARAM_KEY_PNM_RAWBITS);
368        }
369
370        if (!params.isEmpty()) {
371            final Object firstKey = params.keySet().iterator().next();
372            throw new ImageWriteException("Unknown parameter: " + firstKey);
373        }
374
375        writer.writeImage(src, os, params);
376    }
377}