PDFlib

高度なPDFアプリケーションの開発を支援する定番プログラムライブラリー Supported by インフォテック株式会社

PDFlib TET サンプル集(クックブック)

本サンプルプログラムは、PDF テキスト抽出ライブラリーの実装である TET の基本的な機能を実際のプログラムで紹介したものです。

本サイトでダウンロードした TET は、一部機能の制限を除き、評価版として無償でお使いいただけます。

テキストの検索と置換

PDFlib TET によりテキストを検索し、白の矩形で隠して、置換テキストを追加することで、簡易的に「検索して置換」機能を実現するサンプルプログラムです。出力ファイルから置き換えたテキストを抽出することも可能です。

このプログラムは、ハイフネーションやドロップキャップにより分断された単語を処理する基本的なアルゴリズムを持っています。

ハイフネーションされた単語の置換は、白地の矩形が大きすぎたり、小さすぎたりすることがあるかもしれません。既存の PDF 文書に対する検索して置換するこのアイデアはあまりお勧めできませんので、印刷文書を予備的に使用する場合や、オンラインの文書のための最後の拠り所としてのみ使用した方が良いでしょう。

必要な製品 : TET 4 ・ PDFlib+PDI 9

必要なデータ: PDF 文書



import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.pdflib.PDFlibException;
import com.pdflib.TET;
import com.pdflib.TETException;
import com.pdflib.pdflib;

/**
 *
 * PDFlib TET によりテキストを検索し、白の矩形で隠して、置換テキストを追加することで、
 * 簡易的に「検索して置換」機能を実現するサンプルプログラムです。
 * 出力ファイルから検索対象のテキストも抽出できることに注意してください。
 * 
 * このプログラムは、ハイフネーションやドロップキャップにより分断された単語を処理する
 * 基本的なアルゴリズムを持っています。
 * 幾つかの状況で好ましくない結果が生じるため、このアプローチの制限を理解することが重
 * 要です。ハイフネーションされた単語の置換は、白地の矩形が大きすぎたり、小さすぎたり
 * する等の好ましくない結果になります。
 * 
 * 既存の PDF 文書に対する検索して置換するこのアイデアは、あまりお勧めできませんので、
 * 印刷文書を予備的に使用する状況や、オンラインの文書のための最後の拠り所としてのみ
 * 使用すべきです。
 * 
 * 必要な製品: TET 4 ・ PDFlib+PDI 9
 * 
 * 必要なデータ: PDF 文書
 *
 * @version
 * $Id: search_and_replace_text.java,v 1.10 2010/07/23 11:11:22 stm Exp $
 */
class search_and_replace_text {
    /**
     * PDIとTETで入力文書を見つけるための、共通の検索パス
     */
    private static final String DOC_SEARCH_PATH = "../input";

    /**
     * グローバルオプションリスト。 相対パス名を指定
     */
    private static final String GLOBAL_OPTLIST =
        "searchpath={../resource/cmap ../resource/glyphlist "
            + DOC_SEARCH_PATH + "}";

    /**
     * 文書の特別なオプションリスト
     */
    private static final String DOC_OPTLIST = "";

    /**
     * ページの特別なオプションリスト。SEARCH_TERM_REGEX で指定される単語と一致す
     * る単語を検索するため、granularity キーワードに"word"を指定する。
     * また、ハイフンの位置についての詳しい情報を得るために、
     * "contentanalysis={keephyphenglyphs}" を指定する。
     */
    private static final String PAGE_OPTLIST = 
        "granularity=word contentanalysis={keephyphenglyphs}";

    /**
     * System.out に送られるエンコーディング。
     * 例えば、コマンドウィンドウでデフォルトエンコーディングと異なる文字コードを
     * 指定することがする
     */
    private static final String OUTPUT_ENCODING = System
            .getProperty("file.encoding");

    /**
     * ベースライン情報の僅かな誤差を無視するために、0.01を設定する
     */
    private static final double BASELINE_EPSILON = 0.01;
    
    /**
     * OUTPUT_ENCODING にて指定されたエンコーディングで System.out より出力
     */
    private static PrintStream out;

    /**
     * 入力文書名
     */
    private String infilename;

    /**
     * 出力文書名
     */
    private String outfilename;

    /**
     * 座標(x,y)を出力するためのフォーマット
     */
    private NumberFormat coordFormat;

    /**
     * 検索語を指定する。 "metadata"を検索し大文字に変換して置き換える
     */
    private static final Pattern SEARCH_TERM_REGEX =
        Pattern.compile("(?i)metadata");

    /**
     * 置き換えテキストのフォント名
     */
    private static final String REPLACEMENT_FONT = "Times";

    /**
     * 置き換えたテキストをカウント
     */
    private int replacements = 0;

    /**
     * 断片化された単語をカウント
     */
    private int fragmented = 0;

    /**
     * 矩形に関してより多くの情報を出力するには true を設定する
     */
    private static boolean verbose = false;

    /**
     * 矩形のデータを格納するためのクラス
     */
    private class rectangle {
        rectangle(double baseline, double fontsize,
                double llx, double lly, double urx, double ury, boolean hyphenated) {
            this.llx = llx;
            this.lly = lly;
            this.urx = urx;
            this.ury = ury;

            this.baseline = baseline;
            this.fontsize = fontsize;
            
            this.hyphenated = hyphenated;
        }

        double width() {
            return urx - llx;
        }

        double height() {
            return ury - lly;
        }

        double llx;
        double lly;
        double urx;
        double ury;

        double fontsize;
        double baseline;
        
        boolean hyphenated;
    }

    /**
     * 入力文書の現在のページを、PDIで取込み、出力文書に配置する
     *
     * @param p
     *            PDFlib オブジェクト
     * @param pdiHandle
     *            入力文書のためのPDIハンドル
     * @param pageno
     *            ページ番号
     *
     * @throws PDFlibException
     *             PDFlib API によるエラー
     */
    private boolean importPdiPage(pdflib p, int pdiHandle, int pageno)
            throws PDFlibException {
        /*
         * ページのサイズは、後で入力文書サイズに合わせて調整される
         */
        p.begin_page_ext(10, 10, "");
        int pdiPage = p.open_pdi_page(pdiHandle, pageno, "");

        if (pdiPage == -1) {
            System.err.println("Error: " + p.get_errmsg());
            return false;
        }

        /* 入力文書ページを配置し、ページサイズを調整する */
        p.fit_pdi_page(pdiPage, 0, 0, "adjustpage");
        p.close_pdi_page(pdiPage);

        return true;
    }

    /**
     * 抽出したテキストを分割し、同じベースライン、同じフォントサイズごとの
     * 断片を定義する。これらの値が変化した場合、新しい断片を定義する
     *
     * @param tet
     *            TET オブジェクト
     * @param doc 
     *            入力文書のための TET ドキュメントハンドル
     * @param page
     *            現在のページのページハンドル
     * @param pageno
     *            現在のページのページ番号
     * @param matchedText
     *            一致したテキスト
     *
     * @return 矩形の断片のリスト
     *
     * @throws TETException
     *             TET API によるエラー
     */
    private List<rectangle> analyze_word_fragments(TET tet, final int doc, final int page,
            final int pageno, final String matchedText) throws TETException {
        List<rectangle> result = new LinkedList<rectangle>();
        boolean first = true;
        double llx = 0, lly = 0, urx = 0, ury = 0;
        double baseline = 0, fontsize = 0;

        /*
         * 文字数分ループする。y座標が変化したり、ドロップキャップスのように2行
         * 以上にまたがってフォントサイズが変化する場合、または他の状況を検出して
         * いる
         */
        while (tet.get_char_info(page) != -1) {
            /*
             * アセンダ値とディセンダ値を取得する。この値はフォントサイズの1000分の1を
             * 単位としている。
             * ディセンダ値(負の値)を、y座標に加える。
             */
            final double descender = tet.pcos_get_number(doc, "fonts["
                + tet.fontid + "]/descender") / 1000;
            final double ascender = tet.pcos_get_number(doc, "fonts["
                + tet.fontid + "]/ascender") / 1000;

            if (first) {
                llx = tet.x;
                baseline = tet.y;
                fontsize = tet.fontsize;
                lly = tet.y + descender * tet.fontsize;
                first = false;
            }
            else if (Math.abs(baseline - tet.y) > BASELINE_EPSILON
                    || fontsize != tet.fontsize) {
                /*
                 * y座標やフォントサイズが変わるタイミングで、文字を取り囲む矩形を完成
                 * させる。
                 * attributes はグリフの属性をビットで表しており、ビット6は、直前にハイ
                 * フンで区切られていたことを示す。
                 */
                boolean hyphenated = (tet.attributes & (1 << 6)) != 0;
                result.add(new rectangle(baseline, fontsize,
                        llx, lly, urx, ury, hyphenated));
                baseline = tet.y;
                fontsize = tet.fontsize;
                llx = tet.x;
                lly = tet.y + descender * tet.fontsize;
            }

            urx = tet.x + tet.width;
            ury = tet.y + ascender * tet.fontsize;
        }

        /*
         * 最後に特定された矩形を追加する
         */
        result.add(new rectangle(baseline, fontsize,
                llx, lly, urx, ury, false));

        if (result.size() > 1) {
            fragmented += 1;

            System.err.println("Warning: On page " + pageno
                    + " the search text \""
                    + matchedText
                    + "\" extends over "
                    + "multiple rectangles, starting at "
                    + "x=" + coordFormat.format(llx)
                    + ", y=" + coordFormat.format(lly)
                    + ", result is questionable.");
        }

        return result;
    }

    /**
     * 矩形を白で塗りつぶす
     *
     * @param p
     *            PDFlib オブジェクト
     * @param pageno
     *            現在のページ番号
     * @param r
     *            矩形
     * @throws PDFlibException
     *            PDFlib API によるエラー
     */
    private void paint_rectangle(pdflib p, int pageno, rectangle r)
            throws PDFlibException {
        p.save();
        p.setcolor("fillstroke", "gray", 1, 0, 0, 0);
        p.rect(r.llx, r.lly, r.width(), r.height());
        p.fill();
        p.restore();
        if (verbose) {
            out.println("Painted white rectangle at " + "x="
                    + coordFormat.format(r.llx) + ", y="
                    + coordFormat.format(r.lly) + ", width="
                    + coordFormat.format(r.width()) + ", height="
                    + coordFormat.format(r.height()));
        }
    }

    /**
     * テキスト変換のためのメソッド
     *
     * @param matchedText
     *            変換前のテキスト
     * @return 変換後のテキスト
     */
    private String get_replacement_text(String matchedText) {
        return matchedText.toUpperCase();
    }

    /**
     * 矩形を白で塗りつぶし、矩形をテキストで満たす:
     * 
     * - 矩形には少なくとも1文字は入れる
     * - 最後の矩形であれば、残りのテキストを配置する
     * - そうでなければ、矩形のサイズ内に文字を追加して収めていく
     *
     * @param font
     *            フォントハンドル
     * @param p
     *            PDFlib オブジェクト
     * @param pageno
     *            ページ番号
     * @param matchedText
     *            一致したテキスト
     * @param rectangles
     *            単語の断片のリスト
     *
     * @throws PDFlibException
     *             PDFlib API によるエラー
     */

    private void replace_fragments(int font, pdflib p, int pageno,
            String matchedText, List<rectangle> rectangles) throws PDFlibException {
        /*
         * テキストの全長を計算する
         */
        Iterator<rectangle> i = rectangles.iterator();
        String replacementText = get_replacement_text(matchedText);
        int replacementIndex = 0;
        while (i.hasNext()) {
            rectangle r = (rectangle) i.next();

            paint_rectangle(p, pageno, r);

            int matchedLength = matchedText.length();
            int fragBegin = replacementIndex;
            int fragEnd;

            if (i.hasNext()) {
                /*
                 * 矩形に文字を収める計算は、最後の断片については行わない
                 */
                fragEnd = fragBegin;

                String optlist = "font=" + font + " fontsize=" + r.fontsize;
                double filledWidth = 0;

                /*
                 * 矩形には少なくとも1文字は入れる
                 */
                do {
                    fragEnd += 1;
                    
                    String fragment = matchedText.substring(fragBegin, fragEnd);
                    if (r.hyphenated) {
                    	fragment += "-";
                    }
                    
                    filledWidth = p.info_textline(fragment, "width", optlist);
                }
                while (filledWidth <= r.width() && fragEnd < matchedLength);
            }
            else {
                /*
                 * 残りのテキスト
                 */
                fragEnd = replacementText.length();
            }

            p.save();

            /*
             * テキストは、元のテキストと同じベースラインに垂直に置かれなければならない
             *
             * PDFlibは置き換えテキストがボックスに収まるよう、拡大・縮小率を計算する
             * (fitmethod=auto)
             *
             * setcolor() は置き換えテキストを強調するための処理であり、デフォルトの色を
             * 使用する場合はこの処理は削除する
             */
            p.setcolor("fillstroke", "rgb", 1, 0, 0, 0);

            String replacementFragment = 
                                replacementText.substring(fragBegin, fragEnd);
            if (r.hyphenated) {
                replacementFragment += "-";
            }

            String optlist = "font=" + font + " " + "boxsize={" + r.width()
                    + " " + r.fontsize + "} " + "position={left bottom} "
                    + "fitmethod=auto fontsize=" + r.fontsize + " "
                    + "shrinklimit=65%";
            p.fit_textline(replacementFragment, r.llx, r.baseline, optlist);
            p.restore();
            if (verbose) {
                out.println("Replaced \"" + matchedText + "\" with \""
                        + replacementText + "\"");
            }

            replacementIndex = fragEnd;
        }
    }

    /**
     * 抽出したテキストが置き換えテキストと一致した場合、白い矩形を配置し、その中に置き換え
     * テキストを配置する
     *
     * @param tet
     *            TET オブジェクト
     * @param doc 
     *            入力文書のための TET ドキュメントハンドル
     * @param font
     *            フォントハンドル
     * @param p
     *            PDFlib オブジェクト
     * @param page
     *            現在のページのハンドル
     * @param pageno
     *            現在のページ番号
     * @param word
     *            置き換えられる可能性のある抽出テキスト
     *
     * @throws TETException
     *             TET API によるエラー
     * @throws PDFlibException
     *             PDFlib API によるエラー
     */
    private void replace_text(final TET tet, final int doc, final int font, 
                 final pdflib p, final int page,
            final int pageno, final String word) throws TETException, PDFlibException {
        /*
         * 抽出したテキストを置き換える必要があるかチェックする
         */
        Matcher matcher = SEARCH_TERM_REGEX.matcher(word);

        if (matcher.matches()) {
            replacements += 1;

            String matchedText = matcher.group(0);

            /*
             * 置き換えテキストと一致する単語が属する、矩形の集合リスト
             */
            List<rectangle> rectangles = analyze_word_fragments(tet, doc, page, pageno,
                    matchedText);

            replace_fragments(font, p, pageno, matchedText, rectangles);
        }
    }

    /**
     * ページ処理 : 出力文書の新しいページを生成し、入力文書を出力文書上に配置する。
     * 検索したテキストは全て大文字に置き換える
     *
     * @param tet
     *            TET オブジェクト
     * @param doc
     *            TET ドキュメントハンドル
     * @param font
     *            置き換える単語のフォントハンドル
     * @param p
     *            PDFlib オブジェクト
     * @param pdiHandle
     *            PDI ドキュメントハンドル
     * @param pageno
     *            現在のページ番号
     * @throws TETException
     *             TET API によるエラー
     * @throws PDFlibException
     *             PDFlib APIによるエラー
     */
    private void process_page(TET tet, final int doc, int font, pdflib p,
            int pdiHandle, int pageno) throws TETException, PDFlibException {
        /*
         * 入力文書から出力文書へページをコピーする
         */
        importPdiPage(p, pdiHandle, pageno);

        final int page = tet.open_page(doc, pageno, PAGE_OPTLIST);

        if (page == -1) {
            System.err.println("Error " + tet.get_errnum() + " in "
                    + tet.get_apiname() + "(): " + tet.get_errmsg());
        }
        else {
            /* ページ上の全てのテキストの断片を取得する */
            for (String text = tet.get_text(page); text != null; text = tet
                    .get_text(page)) {
                replace_text(tet, doc, font, p, page, pageno, text);
            }

            if (tet.get_errnum() != 0) {
                System.err.println("Error " + tet.get_errnum() + " in "
                        + tet.get_apiname() + "(): " + tet.get_errmsg());
            }

            /*
             * 入力文書、出力文書のページを閉じる
             */
            p.end_page_ext("");
            tet.close_page(page);
        }
    }

    private void execute() {
        TET tet = null;
        pdflib p = null;
        int pageno = 0;

        try {
            tet = new TET();
            tet.set_option(GLOBAL_OPTLIST);

            p = new pdflib();
            p.set_option("searchpath={" + DOC_SEARCH_PATH + "}");

            if (p.begin_document(outfilename, "") == -1) {
                System.err.println("Error: " + p.get_errmsg());
                return;
            }

            /* 文書の入力情報を追加する */
            p.set_info("Creator", "Search and Replace TET Cookbook Example");
            p.set_info("Author", "PDFlib GmbH");
            p.set_info("Title", infilename);
            p.set_info("Subject", "Replace text matched by regex \""
                    + SEARCH_TERM_REGEX.pattern()
                    + "\" with its uppercase form" );

            int pdiHandle = p.open_pdi_document(infilename, "");
            if (pdiHandle == -1) {
                System.err.println("Error: " + p.get_errmsg());
                return;
            }

            /*
             * フォントをロードし、要求されたフォントサイズを指定する
             */
            int font = p.load_font(REPLACEMENT_FONT, "unicode", "");
            if (font == -1) {
                System.err.println("Error loading font: " + p.get_errmsg());
                return;
            }

            final int doc = tet.open_document(infilename, DOC_OPTLIST);
            if (doc == -1) {
                System.err.println("Error " + tet.get_errnum() + " in "
                        + tet.get_apiname() + "(): " + tet.get_errmsg());
                return;
            }

            /*
             * 入力文書のページ分ループする
             */
            final int n_pages = (int) tet.pcos_get_number(doc, "length:pages");
            for (pageno = 1; pageno <= n_pages; ++pageno) {
                process_page(tet, doc, font, p, pdiHandle, pageno);
            }

            out.println("Replaced " + replacements + " words, "
                    + fragmented + " words were fragmented");

            p.end_document("");
            p.close_pdi_document(pdiHandle);
            tet.close_document(doc);
        }
        catch (TETException e) {
            if (pageno == 0) {
                System.err.println("Error " + e.get_errnum() + " in "
                        + e.get_apiname() + "(): " + e.get_errmsg() + "\n");
            }
            else {
                System.err.println("Error " + e.get_errnum() + " in "
                        + e.get_apiname() + "() on page " + pageno + ": "
                        + e.get_errmsg() + "\n");
            }
        }
        catch (PDFlibException e) {
            if (pageno == 0) {
                System.err.println("Error " + e.get_errnum() + " in "
                        + e.get_apiname() + "(): " + e.get_errmsg() + "\n");
            }
            else {
                System.err.println("Error " + e.get_errnum() + " in "
                        + e.get_apiname() + "() on page " + pageno + ": "
                        + e.get_errmsg() + "\n");
            }
        }
        finally {
            tet.delete();
            p.delete();
        }
    }

    /**
     * @param infilename
     *            入力文書のファイル名
     * @param outfilename
     *            出力文書のファイル名
     */
    private search_and_replace_text(String infilename, String outfilename) {
        this.infilename = infilename;
        this.outfilename = outfilename;

        this.coordFormat = NumberFormat.getInstance();
        coordFormat.setMinimumFractionDigits(0);
        coordFormat.setMaximumFractionDigits(2);
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println("Using output encoding \"" + OUTPUT_ENCODING + "\"");
        out = new PrintStream(System.out, true, OUTPUT_ENCODING);

        if (args.length != 2) {
            out.println("usage: search_and_replace_text <infilename> <outfilename>");
            return;
        }

        search_and_replace_text t = new search_and_replace_text(args[0], args[1]);
        t.execute();
    }
}


(May 6, 2010 - May 20, 2015)