PDFlib

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

取得したリンクの座標が見た目と合わない場合の対処法

Q.pCOS 関数を通じて取得したリンクの座標が見た目と合いません。対処法は?

A.ページに Rotate や CropBox が設定されている場合、これらを考慮する必要があります。

pCOS 関数は多くの PDFlib 製品で使用することができ、PDF ファイルから様々な情報を取得することができます。例えば こちらの TET のサンプルのように、ページ上のリンクの座標を取得し、その座標上のテキストを抽出するような用途が考えられます。

しかしながら、ページに Rotate や CropBox が設定されている PDF では、pCOS 関数で取得した座標が一見正しくないように見える場合があります。

Rotate

Rotate は PDF のページに設定されている情報で、表示する際のページの向きを表します。Rotate は右方向の回転角度 (度数法) を表し、0 または 90 の倍数を取ります。ページの回転は表示時に行われるため、リンク等の座標は回転前の座標を持ち続けます。

PDFlib 製品では、ページが Rotate を持つ場合は回転後の座標を基準にします。そのため、例えば Rotate=90 の場合、リンクの座標を 90°回転させた先に本来のリンクテキストが存在していることになります。以下の赤線は、取得したリンクの座標をそのまま配置した場合を表しています。(Rotate=0 の PDFRotate=90 の PDF)

Rotate=0 のページの場合
Rotate=0 のページの場合
Rotate=90 のページの場合
Rotate=90 のページの場合

対処法

Rotate だけリンクの座標も回転させます。Rotate と回転後の座標の関係については、下記に記載しているサンプルコードをご確認ください。

Cropbox

CropBox も Rotate と同様に PDF のページに設定されている情報で、ページの表示領域を表します。CropBox は本来のページサイズを表す MediaBox と同様に各頂点の座標で表されます。CropBox は MediaBox を超えてはならず、また MediaBox を超えた部分については無視されます。

PDFlib 製品では、ページが CropBox を持つ場合は CropBox の領域をページ領域とした座標を基準にします。そのため、CropBox が設定されている場合、CropBox の左下の座標が (0, 0) になるようにリンクの座標も移動させる必要があります。以下の内側の黒線は CropBox の範囲を、赤線は取得したリンクの座標をそのまま配置した場合を表しています。(CropBox を持たない PDFCropBox を持つ PDF)

CropBox を持たないページの場合
CropBox を持たないページの場合
CropBxo を持つページの場合
CropBox を持つページの場合
(実際には内側の部分だけが表示される)

対処法

リンクの座標の各頂点から、CropBox の左下の座標を減算します。これにより、CropBox の左下の座標を (0, 0) にしたのと同じになります。

Rotate と CropBox を両方含むページ

上記の Rotate と CropBox はそれぞれ独立しているため、Rotate と CropBox を両方含むページも存在します。現実的には、Rotate と CropBox のいずれかのみに対応するのではなく、両方に対応する必要があります。

対処法

上記の通り Rotate と CropBox の対処法を順番に適用します。ただし、リンクの座標から CropBox の左下の座標を減算する前に、CropBox も Rotate だけ回転させてから減算します。

これらのケースに対応したリンクテキスト取得のサンプルコードは以下のようになります (ダウンロード) (zip ファイル)。


/*
 * Copyright infoTek K.K. 2018 All rights reserved.
 *
 * 引数の PDF からリンクの座標を取得し、インポートしたページ上に枠を描画して出力します。
 * Rotate と CropBox を考慮します。
 *
 * 動作に必要な製品: PDFlib+PDI 9.0
 *
 */
import java.io.File;
import java.util.Arrays;
import com.pdflib.pdflib;
import com.pdflib.PDFlibException;

public class make_pdf {
    // args[0] のページ内容にリンク枠を追加したものを "output_" + args[0] として出力する
    public static void main(String[] args) throws Exception {
        pdflib p;
        int doc, pages;
        String infile, outfile;

        // 入力ファイル名の取得と出力ファイル名を作成する
        if (args.length < 1) {
            System.out.println("引数に入力元の PDF を指定してください。");
            System.exit(1);
        }
        infile = args[0];
        outfile = "output_" + infile;

        // infile のページ数分だけ makePage() を呼び出しページを作成する
        p = new pdflib();

        p.begin_document(outfile, "");

        doc = p.open_pdi_document(infile, "");
        pages = (int) p.pcos_get_number(doc, "length:pages");

        for (int i = 0; i < pages; i++) {
            makePage(p, doc, i);
        }

        p.close_pdi_document(doc);

        p.end_document("");

        p.delete();
    }

    // 入力元の PDF から page_num + 1 ページ目をインポートし、リンク枠を描画する
    private static void makePage(pdflib p, int doc, int page_num) throws Exception {
        int page, annots, rotate;
        String path, obj_type;
        double[] mediabox, cropbox;

        p.begin_page_ext(0, 0, "");

        p.set_graphics_option("linewidth=2 strokecolor=red");

        // page_num + 1 ページ目を出力のページに貼り付ける
        // (ページの指定は 1 からなので + 1 する)
        page = p.open_pdi_page(doc, page_num + 1, "");
        p.fit_pdi_page(page, 0, 0, "adjustpage");
        p.close_pdi_page(page);

        path = "pages[" + page_num + "]";

        // ページの向きを取得
        rotate = (int) p.pcos_get_number(doc, path + "/Rotate");

        // MediaBox (ページサイズ) と CropBox (ページの表示範囲) を取得
        mediabox = getRect(p, doc, path + "/MediaBox");
        cropbox = getCropBox(p, doc, path + "/CropBox", mediabox);

        // page_num + 1 ページ目に含まれる Annotation の数を取得
        // (pages[] の添字は 0 からなので + 1 しない)
        annots = (int) p.pcos_get_number(doc, "length:pages[" + page_num + "]/annots");

        for (int i = 0; i < annots; i++) {
            double[] rect;
            double width, height;
            String annot_path = path + "/annots[" + i + "]";

            // リンク以外の注釈は無視する
            obj_type = p.pcos_get_string(doc, annot_path + "/Subtype");
            if (obj_type.equals("Link") == false) {
                continue;
            }

            // Rotate および CropBox を考慮した見かけ上のリンクの座標を取得する
            rect = getLinkRect(p, doc, annot_path + "/Rect", rotate, mediabox, cropbox);

            // 取得した座標の矩形パスをページ上に作成する
            width = rect[2] - rect[0];
            height = rect[3] - rect[1];
            p.rect(rect[0], rect[1], width, height);
            p.stroke();
        }

        p.end_page_ext("");
    }

    // path の矩形領域の座標を配列で返す
    private static double[] getRect(pdflib p, int doc, String path) throws Exception {
        double[] rect = new double[4];

        for (int i = 0; i < 4; i++) {
            rect[i] = p.pcos_get_number(doc, path + "[" + i + "]");
        }

        return rect;
    }

    // MediaBox も考慮しながら CropBox を取得する
    private static double[] getCropBox(pdflib p, int doc, String path,
                                       double[] mediabox) throws Exception {
        double[] cropbox;
        String obj_type;

        obj_type = p.pcos_get_string(doc, "type:" + path);
        if (obj_type.equals("null")) {
            // CropBox が存在しない場合、MediaBox の座標を使う
            cropbox = Arrays.copyOf(mediabox, mediabox.length);
        } else {
            cropbox = getRect(p, doc, path);

            // CropBox が MediaBox の範囲を超えた場合は MediaBox の座標を使う
            for (int i = 0; i < 2; i++) {  // 左下の座標
                cropbox[i] = (mediabox[i] > cropbox[i]) ? mediabox[i] : cropbox[i];
            }
            for (int i = 2; i < 4; i++) {  // 右上の座標
                cropbox[i] = (mediabox[i] < cropbox[i]) ? mediabox[i] : cropbox[i];
            }
        }

        return cropbox;
    }

    // リンクの座標を Rotate と CropBox を考慮した座標に変換した配列として返す
    private static double[] getLinkRect(pdflib p, int doc,
                                        String path, int rotate,
                                        double[] mediabox,
                                        double[] cropbox) throws Exception {
        double[] annot_rect;

        // リンクの座標を取得
        annot_rect = getRect(p, doc, path);

        // Rotate に応じてリンクの座標を回転させる
        annot_rect = rotateRect(annot_rect, rotate, mediabox);

        // Rotate に応じて CropBox の座標も回転させる
        cropbox = rotateRect(cropbox, rotate, mediabox);

        // CropBox の左下の座標が (0, 0) になるように Rect の座標を移動させる
        annot_rect = moveRect(annot_rect, cropbox);

        return annot_rect;
    }

    // rotate の値に応じて座標を変換します
    private static double[] rotateRect(double[] rect, int rotate,
                                       double[] mediabox) throws Exception {
        double[] ret;

        if (rotate == 0 || rotate % 360 == 0) {
            return rect;
        }

        ret = new double[4];

        if (rotate % 270 == 0) {
            ret[0] = mediabox[3] - rect[3];
            ret[1] = rect[0];
            ret[2] = mediabox[3] - rect[1];
            ret[3] = rect[2];
        } else if (rotate % 180 == 0) {
            ret[0] = mediabox[2] - rect[2];
            ret[1] = mediabox[3] - rect[3];
            ret[2] = mediabox[2] - rect[0];
            ret[3] = mediabox[3] - rect[1];
        } else if (rotate % 90 == 0) {
            ret[0] = rect[1];
            ret[1] = mediabox[2] - rect[2];
            ret[2] = rect[3];
            ret[3] = mediabox[2] - rect[0];
        } else {
            // PDF の仕様上、90 の倍数以外の値はとれません
            throw new Exception("Specification error (Rotate).");
        }

        return ret;
    }

    // cropbox の左下の座標が (0, 0) になるように rect の座標を移動します
    private static double[] moveRect(double[] rect, double[] cropbox) {
        double[] ret = new double[4];

        ret[0] = rect[0] - cropbox[0];
        ret[1] = rect[1] - cropbox[1];
        ret[2] = rect[2] - cropbox[0];
        ret[3] = rect[3] - cropbox[1];

        return ret;
    }
}
Java 1.7 / PDFlib+PDI 9.1.1
(Jan 30, 2018 - )