ページ更新: 2004-09-29 (水) (5352日前)

関連: 規格/画像/PPM

2004-09-29 作成中

PPM (Portable Pixel Map), PGM (Portable Gray Map) の読み書き。

[編集]

規格 #

  • ヘッダとボディで構成される。
  • ヘッダは、Magic / Width / Height / Depth で構成される。
    • Magic 画像の種別を表す。
      • 'P' + 10進数数値 1桁。

P3 - PPM, Text P6 - PPM, Binary P2 - PGM, Text P5 - PGM, Text

  • Width 画像の幅(ピクセル単位)
    • 10進数で、数字の並び。他の文字の出現で終了。
  • Height 画像の高さ(ピクセル単位)
    • 10進数で、数字の並び。他の文字の出現で終了。
  • Depth 画像のピクセルの最大値。
    • 10進数で、数字の並び。他の文字の出現で終了。
  • ヘッダの各要素は 連続する White Space (Space / TAB / CR / LF) で区切られる
  • コメントは # で開始、行末 (CR / LF) までがコメント。
  • ボディはピクセルの並びで、テキスト形式とバイナリ形式の2種類。
    • ピクセルの順序はラスタ順。
    • PGM の場合、単純にピクセルが並ぶ。
    • PPM の場合、R / G / B の順。
    • テキスト形式は ピクセルの値=10進数数値 区切りは White Space
      • 1行の桁数は 70桁以下にする。
      • 改行コードはなにを使う?
    • バイナリ形式は ピクセルの値=1バイト。
      • よって、値の範囲は 0 〜 255
[編集]

制限 #

  • いまのところPBMは使わないので、実装しない。
  • テキスト形式もまだ使わないので、実装しない。
  • depth >= 256 はサポートしない。
[編集]

ロジック #

- ヘッダ処理
 - 空白を読み飛ばす
 - # が見つかったら、改行コードまで読み飛ばす。
 - 空白以外の文字が見つかったら、以下の処理を行う。
   - S1: 'Pn' (nは数字) であれば、magicNumberとする。S2へ
   - S2: [0-9]+ を取り出して、widthとする。S3へ
   - S3: [0-9]+ を取り出して、heightとする。S4へ
   - S4: [0-9]+ を取り出して、maxとする。S5へ
   - S5: 空白以外であれば、データ開始。
- バイナリデータ処理
 - width x heightの数だけデータを取り出す。
[編集]

作りかけのコード (2005-09-11) #

package jp.discypus.image;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.io.StreamTokenizer;

/**
 * Portable aNy Map Image Loader
 * 
 */
public class PnmLoader {

    private static final String PBM_TEXT = "P1";
    private static final String PGM_TEXT = "P2";
    private static final String PPM_TEXT = "P3";
    private static final String PBM_BINARY = "P4";
    private static final String PGM_BINARY = "P5";
    private static final String PPM_BINARY = "P6";

    private static final int CR = 0x0d;
    private static final int LF = 0x0a;
    
    public static class Header {
        String magicNumber;
        int width;
        int height;
        int max;
    }
    
    public static class Image {
        int width;
        int height;
        int bytesPerPixel;
        byte[][] scanLines;
    }
    
    public Image loadImage(final InputStream in) throws IOException {
        final PushbackInputStream pis = new PushbackInputStream(in);
        
        //
        final Header header = loadHeader(pis);
        
        //
        if (PGM_BINARY.equals(header.magicNumber)) {
            final Image image = loadPgmBody(pis, header);
            return image;
            
        } else if (PPM_BINARY.equals(header.magicNumber)) {
            final Image image = loadPpmBody(pis, header);
            return image;

        } else if (PBM_BINARY.equals(header.magicNumber)) {
            throw new IllegalStateException("Unsupported image type:" + header.magicNumber);
        } else if (PBM_TEXT.equals(header.magicNumber)) {
            throw new IllegalStateException("Unsupported image type:" + header.magicNumber);
        } else if (PGM_TEXT.equals(header.magicNumber)) {
            throw new IllegalStateException("Unsupported image type:" + header.magicNumber);
        } else if (PPM_TEXT.equals(header.magicNumber)) {
            throw new IllegalStateException("Unsupported image type:" + header.magicNumber);

        } else {
            throw new IllegalStateException("Invalid image type='" + header.magicNumber + "'");
        }
    }

    /**
     * @throws IOException 
     */
    private Header loadHeader(final PushbackInputStream in) throws IOException {
        final Header header = new Header();
        
        skipWhiteSpecesAndComments(in);
        byte[] magicNumber = new byte[2];
        in.read(magicNumber);
        header.magicNumber = new String(magicNumber);
        
        skipWhiteSpecesAndComments(in);
        header.width = readNumber(in);

        skipWhiteSpecesAndComments(in);
        header.height = readNumber(in);

        skipWhiteSpecesAndComments(in);
        header.max = readNumber(in);
        
        skipWhiteSpecesAndComments(in);

        return header;
    }

    /**
     * ホワイトスペース (空白、タブ、改行)とコメントを読み飛ばす。
     * 
     * @param in
     * @param header
     * @throws IOException
     */
    private void skipWhiteSpecesAndComments(final PushbackInputStream in) 
    throws IOException {
        /*
         * コメントは#から行末まで。
         */
        for (;;) {
            int ch = in.read();
            if (ch == -1) {
                throw new IllegalStateException("stream is terminated");
            }
            
            if (ch == '#') {
                // 改行まで読み飛ばす
                for (;;) {
                    ch = in.read();
                    if ((ch == CR) || (ch == LF)) {
                        break;
                    }
                }
                // 改行がなくなるまで読み飛ばす
                for (;;) {
                    ch = in.read();
                    if ((ch != CR) && (ch != LF)) {
                        in.unread(ch);
                        break;
                    }
                }
            } else if (isWhitespece(ch)) {
                // 空白を読み飛ばす
                for (;;) {
                    ch = in.read();
                    if (!isWhitespece(ch)) {
                        in.unread(ch);
                        break;
                    }
                }
                
            } else {
                in.unread(ch);
                break;
            }
        }
    }
    
    private boolean isWhitespece(final int ch) {
        return (ch == ' ') || (ch == '\t') || (ch == CR) || (ch == LF);
    }

    private int readNumber(final PushbackInputStream in) throws IOException {
        final StringBuffer buf = new StringBuffer();
        for (;;) {
            int ch = in.read();
            if ((ch < '0') || ('9' < ch)) {
                in.unread(ch);
                break;
            }
            buf.append(ch - '0');
        }
        
        int result = Integer.parseInt(buf.toString());
        return result;
    }

    private Image loadPgmBody(final InputStream in, final Header header) 
    throws IOException {
        final Image image = new Image();
        image.width = header.width;
        image.height = header.height;
        image.bytesPerPixel = 1;
        image.scanLines = new byte[image.height][];
        
        //
        for (int y = 0; y < image.height; y++) {
            final byte[] lineBuf = new byte[image.width * image.bytesPerPixel];
            final int len = in.read(lineBuf);
            image.scanLines[y] = lineBuf;
            if (len < lineBuf.length) {
                break;
            }
        }
        
        return image;
    }

    private Image loadPpmBody(final InputStream in, final Header header) 
    throws IOException {
        final Image image = new Image();
        image.width = header.width;
        image.height = header.height;
        image.bytesPerPixel = 3;
        image.scanLines = new byte[image.height][];

        //
        for (int y = 0; y < image.height; y++) {
            final byte[] lineBuf = new byte[image.width * image.bytesPerPixel];
            final int len = in.read(lineBuf);
            image.scanLines[y] = lineBuf;
            if (len < lineBuf.length) {
                break;
            }
        }
        
        return image;
    }
    
    /**
     * Headerを読み込む。
     * 
     * TODO StreamTokenizerを使って作成中。
     * @throws IOException
     */
    private Header readHeader2(final InputStream in) throws IOException {
        final Reader reader = new InputStreamReader(in);
        final StreamTokenizer st = new StreamTokenizer(reader);
        
        st.commentChar('#');
        st.wordChars('0', '9');
        st.wordChars('a', 'z');
        st.wordChars('A', 'Z');
        st.whitespaceChars(' ', ' ');
        st.whitespaceChars('\n', '\n');
        st.whitespaceChars('\r', '\r');
        st.whitespaceChars('\t', '\t');
        st.eolIsSignificant(false);

        //
        final int MODE_MAGIC_NUMBER_P = 0;
        final int MODE_MAGIC_NUMBER_N = 1;
        final int MODE_WIDTH = 2;
        final int MODE_HEIGHT = 3;
        final int MODE_MAX = 4;
        final int MODE_BODY = 5;

        final Header header = new Header();
        
        int mode = MODE_MAGIC_NUMBER_P;
        loop:
        for (;;) {
            final int type = st.nextToken();
            if (type == StreamTokenizer.TT_EOF) {
                break;
            }
            
            switch (type) {
            default:
                break loop;
            
            case StreamTokenizer.TT_EOL:
                break;
                
            case StreamTokenizer.TT_WORD:
                // Magic Numberの1桁目
                switch (mode) {
                case MODE_MAGIC_NUMBER_P:
                    if ("P".equalsIgnoreCase(st.sval)) {
                        mode = MODE_MAGIC_NUMBER_N;
                    } else {
                        // エラー
                    }
                    break;
                default:
                    // エラー
                    break;
                }
                break;

            case StreamTokenizer.TT_NUMBER:
                // width, height, max, Magic Numberの2桁目
                switch (mode) {
                case MODE_MAGIC_NUMBER_N:
                    header.magicNumber = "P" + (int)st.nval;
                    mode = MODE_WIDTH;
                    break;
                case MODE_WIDTH:
                    header.width = (int)st.nval;
                    mode = MODE_HEIGHT;
                    break;
                case MODE_HEIGHT:
                    header.width = (int)st.nval;
                    mode = MODE_MAX;
                    break;
                case MODE_MAX:
                    header.width = (int)st.nval;
                    mode = MODE_BODY;
                    break loop;
                default:
                    // エラー
                    break;
                }
                break;
            }
        }
        
        if (mode != MODE_BODY) {
            throw new IllegalStateException("");
        }
        
        return header;
    }

    /**
     * 簡易テスト
     */
    public static void main(final String[] args) {
        
        final String filename = "sample.ppm";
        try {
            final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filename));
            try {
                final Image image = new PnmLoader().loadImage(bis);
            } finally {
                if (bis != null) {
                    bis.close();
                }
            }

        } catch (final IOException e) {
            e.printStackTrace();
        }
    }
}