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.ImagingConstants.PARAM_KEY_FORMAT; 020import static org.apache.commons.imaging.common.BinaryFunctions.readByte; 021 022import java.awt.Dimension; 023import java.awt.image.BufferedImage; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.io.PrintWriter; 028import java.nio.ByteOrder; 029import java.util.ArrayList; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.StringTokenizer; 034 035import org.apache.commons.imaging.ImageFormat; 036import org.apache.commons.imaging.ImageFormats; 037import org.apache.commons.imaging.ImageInfo; 038import org.apache.commons.imaging.ImageParser; 039import org.apache.commons.imaging.ImageReadException; 040import org.apache.commons.imaging.ImageWriteException; 041import org.apache.commons.imaging.common.ImageBuilder; 042import org.apache.commons.imaging.common.ImageMetadata; 043import org.apache.commons.imaging.common.bytesource.ByteSource; 044import org.apache.commons.imaging.palette.PaletteFactory; 045 046public class PnmImageParser extends ImageParser { 047 private static final String DEFAULT_EXTENSION = ".pnm"; 048 private static final String[] ACCEPTED_EXTENSIONS = { ".pbm", ".pgm", 049 ".ppm", ".pnm", ".pam" }; 050 public static final String PARAM_KEY_PNM_RAWBITS = "PNM_RAWBITS"; 051 public static final String PARAM_VALUE_PNM_RAWBITS_YES = "YES"; 052 public static final String PARAM_VALUE_PNM_RAWBITS_NO = "NO"; 053 054 public PnmImageParser() { 055 super.setByteOrder(ByteOrder.LITTLE_ENDIAN); 056 // setDebug(true); 057 } 058 059 @Override 060 public String getName() { 061 return "Pbm-Custom"; 062 } 063 064 @Override 065 public String getDefaultExtension() { 066 return DEFAULT_EXTENSION; 067 } 068 069 @Override 070 protected String[] getAcceptedExtensions() { 071 return ACCEPTED_EXTENSIONS; 072 } 073 074 @Override 075 protected ImageFormat[] getAcceptedTypes() { 076 return new ImageFormat[] { 077 ImageFormats.PBM, 078 ImageFormats.PGM, 079 ImageFormats.PPM, 080 ImageFormats.PNM, 081 ImageFormats.PAM 082 }; 083 } 084 085 private FileInfo readHeader(final InputStream is) throws ImageReadException, 086 IOException { 087 final byte identifier1 = readByte("Identifier1", is, "Not a Valid PNM File"); 088 final byte identifier2 = readByte("Identifier2", is, "Not a Valid PNM File"); 089 090 if (identifier1 != PnmConstants.PNM_PREFIX_BYTE) { 091 throw new ImageReadException("PNM file has invalid prefix byte 1"); 092 } 093 094 final WhiteSpaceReader wsr = new WhiteSpaceReader(is); 095 096 if (identifier2 == PnmConstants.PBM_TEXT_CODE 097 || identifier2 == PnmConstants.PBM_RAW_CODE 098 || identifier2 == PnmConstants.PGM_TEXT_CODE 099 || identifier2 == PnmConstants.PGM_RAW_CODE 100 || identifier2 == PnmConstants.PPM_TEXT_CODE 101 || identifier2 == PnmConstants.PPM_RAW_CODE) { 102 103 final int width; 104 try { 105 width = Integer.parseInt(wsr.readtoWhiteSpace()); 106 } catch (final NumberFormatException e) { 107 throw new ImageReadException("Invalid width specified." , e); 108 } 109 final int height; 110 try { 111 height = Integer.parseInt(wsr.readtoWhiteSpace()); 112 } catch (final NumberFormatException e) { 113 throw new ImageReadException("Invalid height specified." , e); 114 } 115 116 if (identifier2 == PnmConstants.PBM_TEXT_CODE) { 117 return new PbmFileInfo(width, height, false); 118 } else if (identifier2 == PnmConstants.PBM_RAW_CODE) { 119 return new PbmFileInfo(width, height, true); 120 } else if (identifier2 == PnmConstants.PGM_TEXT_CODE) { 121 final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); 122 return new PgmFileInfo(width, height, false, maxgray); 123 } else if (identifier2 == PnmConstants.PGM_RAW_CODE) { 124 final int maxgray = Integer.parseInt(wsr.readtoWhiteSpace()); 125 return new PgmFileInfo(width, height, true, maxgray); 126 } else if (identifier2 == PnmConstants.PPM_TEXT_CODE) { 127 final int max = Integer.parseInt(wsr.readtoWhiteSpace()); 128 return new PpmFileInfo(width, height, false, max); 129 } else if (identifier2 == PnmConstants.PPM_RAW_CODE) { 130 final int max = Integer.parseInt(wsr.readtoWhiteSpace()); 131 return new PpmFileInfo(width, height, true, max); 132 } 133 } else if (identifier2 == PnmConstants.PAM_RAW_CODE) { 134 int width = -1; 135 boolean seenWidth = false; 136 int height = -1; 137 boolean seenHeight = false; 138 int depth = -1; 139 boolean seenDepth = false; 140 int maxVal = -1; 141 boolean seenMaxVal = false; 142 final StringBuilder tupleType = new StringBuilder(); 143 boolean seenTupleType = false; 144 145 // Advance to next line 146 wsr.readLine(); 147 String line; 148 while ((line = wsr.readLine()) != null) { 149 line = line.trim(); 150 if (line.charAt(0) == '#') { 151 continue; 152 } 153 final StringTokenizer tokenizer = new StringTokenizer(line, " ", false); 154 final String type = tokenizer.nextToken(); 155 if ("WIDTH".equals(type)) { 156 seenWidth = true; 157 if(!tokenizer.hasMoreTokens()) { 158 throw new ImageReadException("PAM header has no WIDTH value"); 159 } 160 width = Integer.parseInt(tokenizer.nextToken()); 161 } else if ("HEIGHT".equals(type)) { 162 seenHeight = true; 163 if(!tokenizer.hasMoreTokens()) { 164 throw new ImageReadException("PAM header has no HEIGHT value"); 165 } 166 height = Integer.parseInt(tokenizer.nextToken()); 167 } else if ("DEPTH".equals(type)) { 168 seenDepth = true; 169 if(!tokenizer.hasMoreTokens()) { 170 throw new ImageReadException("PAM header has no DEPTH value"); 171 } 172 depth = Integer.parseInt(tokenizer.nextToken()); 173 } else if ("MAXVAL".equals(type)) { 174 seenMaxVal = true; 175 if(!tokenizer.hasMoreTokens()) { 176 throw new ImageReadException("PAM header has no MAXVAL value"); 177 } 178 maxVal = Integer.parseInt(tokenizer.nextToken()); 179 } else if ("TUPLTYPE".equals(type)) { 180 seenTupleType = true; 181 if(!tokenizer.hasMoreTokens()) { 182 throw new ImageReadException("PAM header has no TUPLTYPE value"); 183 } 184 tupleType.append(tokenizer.nextToken()); 185 } else if ("ENDHDR".equals(type)) { 186 break; 187 } else { 188 throw new ImageReadException("Invalid PAM file header type " + type); 189 } 190 } 191 192 if (!seenWidth) { 193 throw new ImageReadException("PAM header has no WIDTH"); 194 } else if (!seenHeight) { 195 throw new ImageReadException("PAM header has no HEIGHT"); 196 } else if (!seenDepth) { 197 throw new ImageReadException("PAM header has no DEPTH"); 198 } else if (!seenMaxVal) { 199 throw new ImageReadException("PAM header has no MAXVAL"); 200 } else if (!seenTupleType) { 201 throw new ImageReadException("PAM header has no TUPLTYPE"); 202 } 203 204 return new PamFileInfo(width, height, depth, maxVal, tupleType.toString()); 205 } 206 throw new ImageReadException("PNM file has invalid prefix byte 2"); 207 } 208 209 private FileInfo readHeader(final ByteSource byteSource) 210 throws ImageReadException, IOException { 211 try (InputStream is = byteSource.getInputStream()) { 212 return readHeader(is); 213 } 214 } 215 216 @Override 217 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 218 throws ImageReadException, IOException { 219 return null; 220 } 221 222 @Override 223 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 224 throws ImageReadException, IOException { 225 final FileInfo info = readHeader(byteSource); 226 227 if (info == null) { 228 throw new ImageReadException("PNM: Couldn't read Header"); 229 } 230 231 return new Dimension(info.width, info.height); 232 } 233 234 @Override 235 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 236 throws ImageReadException, IOException { 237 return null; 238 } 239 240 @Override 241 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 242 throws ImageReadException, IOException { 243 final FileInfo info = readHeader(byteSource); 244 245 if (info == null) { 246 throw new ImageReadException("PNM: Couldn't read Header"); 247 } 248 249 final List<String> comments = new ArrayList<>(); 250 251 final int bitsPerPixel = info.getBitDepth() * info.getNumComponents(); 252 final ImageFormat format = info.getImageType(); 253 final String formatName = info.getImageTypeDescription(); 254 final String mimeType = info.getMIMEType(); 255 final int numberOfImages = 1; 256 final boolean progressive = false; 257 258 // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0); 259 // 260 final int physicalWidthDpi = 72; 261 final float physicalWidthInch = (float) ((double) info.width / (double) physicalWidthDpi); 262 final int physicalHeightDpi = 72; 263 final float physicalHeightInch = (float) ((double) info.height / (double) physicalHeightDpi); 264 265 final String formatDetails = info.getImageTypeDescription(); 266 267 final boolean transparent = info.hasAlpha(); 268 final boolean usesPalette = false; 269 270 final ImageInfo.ColorType colorType = info.getColorType(); 271 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE; 272 273 return new ImageInfo(formatDetails, bitsPerPixel, comments, 274 format, formatName, info.height, mimeType, numberOfImages, 275 physicalHeightDpi, physicalHeightInch, physicalWidthDpi, 276 physicalWidthInch, info.width, progressive, transparent, 277 usesPalette, colorType, compressionAlgorithm); 278 } 279 280 @Override 281 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 282 throws ImageReadException, IOException { 283 pw.println("pnm.dumpImageFile"); 284 285 final ImageInfo imageData = getImageInfo(byteSource); 286 if (imageData == null) { 287 return false; 288 } 289 290 imageData.toString(pw, ""); 291 292 pw.println(""); 293 294 return true; 295 } 296 297 @Override 298 public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params) 299 throws ImageReadException, IOException { 300 try (InputStream is = byteSource.getInputStream()) { 301 final FileInfo info = readHeader(is); 302 303 final int width = info.width; 304 final int height = info.height; 305 306 final boolean hasAlpha = info.hasAlpha(); 307 final ImageBuilder imageBuilder = new ImageBuilder(width, height, 308 hasAlpha); 309 info.readImage(imageBuilder, is); 310 311 final BufferedImage ret = imageBuilder.getBufferedImage(); 312 return ret; 313 } 314 } 315 316 @Override 317 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 318 throws ImageWriteException, IOException { 319 PnmWriter writer = null; 320 boolean useRawbits = true; 321 322 if (params != null) { 323 final Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS); 324 if (useRawbitsParam != null) { 325 if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) { 326 useRawbits = false; 327 } 328 } 329 330 final Object subtype = params.get(PARAM_KEY_FORMAT); 331 if (subtype != null) { 332 if (subtype.equals(ImageFormats.PBM)) { 333 writer = new PbmWriter(useRawbits); 334 } else if (subtype.equals(ImageFormats.PGM)) { 335 writer = new PgmWriter(useRawbits); 336 } else if (subtype.equals(ImageFormats.PPM)) { 337 writer = new PpmWriter(useRawbits); 338 } else if (subtype.equals(ImageFormats.PAM)) { 339 writer = new PamWriter(); 340 } 341 } 342 } 343 344 if (writer == null) { 345 final boolean hasAlpha = new PaletteFactory().hasTransparency(src); 346 if (hasAlpha) { 347 writer = new PamWriter(); 348 } else { 349 writer = new PpmWriter(useRawbits); 350 } 351 } 352 353 // make copy of params; we'll clear keys as we consume them. 354 if (params != null) { 355 params = new HashMap<>(params); 356 } else { 357 params = new HashMap<>(); 358 } 359 360 // clear format key. 361 if (params.containsKey(PARAM_KEY_FORMAT)) { 362 params.remove(PARAM_KEY_FORMAT); 363 } 364 365 // clear rawbits key. 366 if (params.containsKey(PARAM_KEY_PNM_RAWBITS)) { 367 params.remove(PARAM_KEY_PNM_RAWBITS); 368 } 369 370 if (!params.isEmpty()) { 371 final Object firstKey = params.keySet().iterator().next(); 372 throw new ImageWriteException("Unknown parameter: " + firstKey); 373 } 374 375 writer.writeImage(src, os, params); 376 } 377}