    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}