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