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}