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