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 } else { 133 throw new ImageReadException("PNM file has invalid header."); 134 } 135 } else if (identifier2 == PnmConstants.PAM_RAW_CODE) { 136 int width = -1; 137 boolean seenWidth = false; 138 int height = -1; 139 boolean seenHeight = false; 140 int depth = -1; 141 boolean seenDepth = false; 142 int maxVal = -1; 143 boolean seenMaxVal = false; 144 final StringBuilder tupleType = new StringBuilder(); 145 boolean seenTupleType = false; 146 147 // Advance to next line 148 wsr.readLine(); 149 String line; 150 while ((line = wsr.readLine()) != null) { 151 line = line.trim(); 152 if (line.charAt(0) == '#') { 153 continue; 154 } 155 final StringTokenizer tokenizer = new StringTokenizer(line, " ", false); 156 final String type = tokenizer.nextToken(); 157 if ("WIDTH".equals(type)) { 158 seenWidth = true; 159 if(!tokenizer.hasMoreTokens()) { 160 throw new ImageReadException("PAM header has no WIDTH value"); 161 } 162 width = Integer.parseInt(tokenizer.nextToken()); 163 } else if ("HEIGHT".equals(type)) { 164 seenHeight = true; 165 if(!tokenizer.hasMoreTokens()) { 166 throw new ImageReadException("PAM header has no HEIGHT value"); 167 } 168 height = Integer.parseInt(tokenizer.nextToken()); 169 } else if ("DEPTH".equals(type)) { 170 seenDepth = true; 171 if(!tokenizer.hasMoreTokens()) { 172 throw new ImageReadException("PAM header has no DEPTH value"); 173 } 174 depth = Integer.parseInt(tokenizer.nextToken()); 175 } else if ("MAXVAL".equals(type)) { 176 seenMaxVal = true; 177 if(!tokenizer.hasMoreTokens()) { 178 throw new ImageReadException("PAM header has no MAXVAL value"); 179 } 180 maxVal = Integer.parseInt(tokenizer.nextToken()); 181 } else if ("TUPLTYPE".equals(type)) { 182 seenTupleType = true; 183 if(!tokenizer.hasMoreTokens()) { 184 throw new ImageReadException("PAM header has no TUPLTYPE value"); 185 } 186 tupleType.append(tokenizer.nextToken()); 187 } else if ("ENDHDR".equals(type)) { 188 break; 189 } else { 190 throw new ImageReadException("Invalid PAM file header type " + type); 191 } 192 } 193 194 if (!seenWidth) { 195 throw new ImageReadException("PAM header has no WIDTH"); 196 } else if (!seenHeight) { 197 throw new ImageReadException("PAM header has no HEIGHT"); 198 } else if (!seenDepth) { 199 throw new ImageReadException("PAM header has no DEPTH"); 200 } else if (!seenMaxVal) { 201 throw new ImageReadException("PAM header has no MAXVAL"); 202 } else if (!seenTupleType) { 203 throw new ImageReadException("PAM header has no TUPLTYPE"); 204 } 205 206 return new PamFileInfo(width, height, depth, maxVal, tupleType.toString()); 207 } else { 208 throw new ImageReadException("PNM file has invalid prefix byte 2"); 209 } 210 } 211 212 private FileInfo readHeader(final ByteSource byteSource) 213 throws ImageReadException, IOException { 214 try (InputStream is = byteSource.getInputStream()) { 215 return readHeader(is); 216 } 217 } 218 219 @Override 220 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 221 throws ImageReadException, IOException { 222 return null; 223 } 224 225 @Override 226 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 227 throws ImageReadException, IOException { 228 final FileInfo info = readHeader(byteSource); 229 230 if (info == null) { 231 throw new ImageReadException("PNM: Couldn't read Header"); 232 } 233 234 return new Dimension(info.width, info.height); 235 } 236 237 @Override 238 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 239 throws ImageReadException, IOException { 240 return null; 241 } 242 243 @Override 244 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 245 throws ImageReadException, IOException { 246 final FileInfo info = readHeader(byteSource); 247 248 if (info == null) { 249 throw new ImageReadException("PNM: Couldn't read Header"); 250 } 251 252 final List<String> comments = new ArrayList<>(); 253 254 final int bitsPerPixel = info.getBitDepth() * info.getNumComponents(); 255 final ImageFormat format = info.getImageType(); 256 final String formatName = info.getImageTypeDescription(); 257 final String mimeType = info.getMIMEType(); 258 final int numberOfImages = 1; 259 final boolean progressive = false; 260 261 // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0); 262 // 263 final int physicalWidthDpi = 72; 264 final float physicalWidthInch = (float) ((double) info.width / (double) physicalWidthDpi); 265 final int physicalHeightDpi = 72; 266 final float physicalHeightInch = (float) ((double) info.height / (double) physicalHeightDpi); 267 268 final String formatDetails = info.getImageTypeDescription(); 269 270 final boolean transparent = info.hasAlpha(); 271 final boolean usesPalette = false; 272 273 final ImageInfo.ColorType colorType = info.getColorType(); 274 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.NONE; 275 276 return new ImageInfo(formatDetails, bitsPerPixel, comments, 277 format, formatName, info.height, mimeType, numberOfImages, 278 physicalHeightDpi, physicalHeightInch, physicalWidthDpi, 279 physicalWidthInch, info.width, progressive, transparent, 280 usesPalette, colorType, compressionAlgorithm); 281 } 282 283 @Override 284 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 285 throws ImageReadException, IOException { 286 pw.println("pnm.dumpImageFile"); 287 288 final ImageInfo imageData = getImageInfo(byteSource); 289 if (imageData == null) { 290 return false; 291 } 292 293 imageData.toString(pw, ""); 294 295 pw.println(""); 296 297 return true; 298 } 299 300 @Override 301 public BufferedImage getBufferedImage(final ByteSource byteSource, final Map<String, Object> params) 302 throws ImageReadException, IOException { 303 try (InputStream is = byteSource.getInputStream()) { 304 final FileInfo info = readHeader(is); 305 306 final int width = info.width; 307 final int height = info.height; 308 309 final boolean hasAlpha = info.hasAlpha(); 310 final ImageBuilder imageBuilder = new ImageBuilder(width, height, 311 hasAlpha); 312 info.readImage(imageBuilder, is); 313 314 final BufferedImage ret = imageBuilder.getBufferedImage(); 315 return ret; 316 } 317 } 318 319 @Override 320 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 321 throws ImageWriteException, IOException { 322 PnmWriter writer = null; 323 boolean useRawbits = true; 324 325 if (params != null) { 326 final Object useRawbitsParam = params.get(PARAM_KEY_PNM_RAWBITS); 327 if (useRawbitsParam != null) { 328 if (useRawbitsParam.equals(PARAM_VALUE_PNM_RAWBITS_NO)) { 329 useRawbits = false; 330 } 331 } 332 333 final Object subtype = params.get(PARAM_KEY_FORMAT); 334 if (subtype != null) { 335 if (subtype.equals(ImageFormats.PBM)) { 336 writer = new PbmWriter(useRawbits); 337 } else if (subtype.equals(ImageFormats.PGM)) { 338 writer = new PgmWriter(useRawbits); 339 } else if (subtype.equals(ImageFormats.PPM)) { 340 writer = new PpmWriter(useRawbits); 341 } else if (subtype.equals(ImageFormats.PAM)) { 342 writer = new PamWriter(); 343 } 344 } 345 } 346 347 if (writer == null) { 348 final boolean hasAlpha = new PaletteFactory().hasTransparency(src); 349 if (hasAlpha) { 350 writer = new PamWriter(); 351 } else { 352 writer = new PpmWriter(useRawbits); 353 } 354 } 355 356 // make copy of params; we'll clear keys as we consume them. 357 if (params != null) { 358 params = new HashMap<>(params); 359 } else { 360 params = new HashMap<>(); 361 } 362 363 // clear format key. 364 if (params.containsKey(PARAM_KEY_FORMAT)) { 365 params.remove(PARAM_KEY_FORMAT); 366 } 367 368 // clear rawbits key. 369 if (params.containsKey(PARAM_KEY_PNM_RAWBITS)) { 370 params.remove(PARAM_KEY_PNM_RAWBITS); 371 } 372 373 if (!params.isEmpty()) { 374 final Object firstKey = params.keySet().iterator().next(); 375 throw new ImageWriteException("Unknown parameter: " + firstKey); 376 } 377 378 writer.writeImage(src, os, params); 379 } 380 381 /** 382 * Extracts embedded XML metadata as XML string. 383 * <p> 384 * 385 * @param byteSource 386 * File containing image data. 387 * @param params 388 * Map of optional parameters, defined in ImagingConstants. 389 * @return Xmp Xml as String, if present. Otherwise, returns null. 390 */ 391 @Override 392 public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params) 393 throws ImageReadException, IOException { 394 return null; 395 } 396}