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