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