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      public PnmImageParser() {
71          super(ByteOrder.LITTLE_ENDIAN);
72      }
73  
74      private void check(final boolean value, final String type) throws ImagingException {
75          if (!value) {
76              throw new ImagingException("PAM header has no " + type + " value");
77          }
78      }
79  
80      private void checkFound(final int value, final String type) throws ImagingException {
81          check(value != -1, type);
82      }
83  
84      private String checkNextTokens(final StringTokenizer tokenizer, final String type) throws ImagingException {
85          check(tokenizer.hasMoreTokens(), type);
86          return tokenizer.nextToken();
87      }
88  
89      private int checkNextTokensAsInt(final StringTokenizer tokenizer, final String type) throws ImagingException {
90          return Integer.parseInt(checkNextTokens(tokenizer, type));
91      }
92  
93      @Override
94      public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
95          pw.println("pnm.dumpImageFile");
96  
97          final ImageInfo imageData = getImageInfo(byteSource);
98          if (imageData == null) {
99              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 }