View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.pnm;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
20  
21  import java.awt.Dimension;
22  import java.awt.image.BufferedImage;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.PrintWriter;
27  import java.nio.ByteOrder;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.StringTokenizer;
31  import java.util.stream.Stream;
32  
33  import org.apache.commons.imaging.AbstractImageParser;
34  import org.apache.commons.imaging.ImageFormat;
35  import org.apache.commons.imaging.ImageFormats;
36  import org.apache.commons.imaging.ImageInfo;
37  import org.apache.commons.imaging.ImagingException;
38  import org.apache.commons.imaging.bytesource.ByteSource;
39  import org.apache.commons.imaging.common.ImageBuilder;
40  import org.apache.commons.imaging.common.ImageMetadata;
41  import org.apache.commons.imaging.palette.PaletteFactory;
42  
43  public class PnmImageParser extends AbstractImageParser<PnmImagingParameters> {
44  
45      private static final String TOKEN_ENDHDR = "ENDHDR";
46      private static final String TOKEN_TUPLTYPE = "TUPLTYPE";
47      private static final String TOKEN_MAXVAL = "MAXVAL";
48      private static final String TOKEN_DEPTH = "DEPTH";
49      private static final String TOKEN_HEIGHT = "HEIGHT";
50      private static final String TOKEN_WIDTH = "WIDTH";
51  
52      private static final int DPI = 72;
53      private static final ImageFormat[] IMAGE_FORMATS;
54      private static final String DEFAULT_EXTENSION = ImageFormats.PNM.getDefaultExtension();
55      private static final String[] ACCEPTED_EXTENSIONS;
56  
57      static {
58          IMAGE_FORMATS = new ImageFormat[] {
59                  // @formatter:off
60                  ImageFormats.PAM,
61                  ImageFormats.PBM,
62                  ImageFormats.PGM,
63                  ImageFormats.PNM,
64                  ImageFormats.PPM
65                  // @formatter:on
66          };
67          ACCEPTED_EXTENSIONS = Stream.of(IMAGE_FORMATS).map(ImageFormat::getDefaultExtension).toArray(String[]::new);
68      }
69  
70      /**
71       * Constructs a new instance with the little-endian byte order.
72       */
73      public PnmImageParser() {
74          super(ByteOrder.LITTLE_ENDIAN);
75      }
76  
77      private void check(final boolean value, final String type) throws ImagingException {
78          if (!value) {
79              throw new ImagingException("PAM header has no " + type + " value");
80          }
81      }
82  
83      private void checkFound(final int value, final String type) throws ImagingException {
84          check(value != -1, type);
85      }
86  
87      private String checkNextTokens(final StringTokenizer tokenizer, final String type) throws ImagingException {
88          check(tokenizer.hasMoreTokens(), type);
89          return tokenizer.nextToken();
90      }
91  
92      private int checkNextTokensAsInt(final StringTokenizer tokenizer, final String type) throws ImagingException {
93          return Integer.parseInt(checkNextTokens(tokenizer, type));
94      }
95  
96      @Override
97      public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
98          pw.println("pnm.dumpImageFile");
99  
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 }