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