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 public PnmImageParser() { 071 super(ByteOrder.LITTLE_ENDIAN); 072 } 073 074 private void check(final boolean value, final String type) throws ImagingException { 075 if (!value) { 076 throw new ImagingException("PAM header has no " + type + " value"); 077 } 078 } 079 080 private void checkFound(final int value, final String type) throws ImagingException { 081 check(value != -1, type); 082 } 083 084 private String checkNextTokens(final StringTokenizer tokenizer, final String type) throws ImagingException { 085 check(tokenizer.hasMoreTokens(), type); 086 return tokenizer.nextToken(); 087 } 088 089 private int checkNextTokensAsInt(final StringTokenizer tokenizer, final String type) throws ImagingException { 090 return Integer.parseInt(checkNextTokens(tokenizer, type)); 091 } 092 093 @Override 094 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 095 pw.println("pnm.dumpImageFile"); 096 097 final ImageInfo imageData = getImageInfo(byteSource); 098 if (imageData == null) { 099 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}