Apache PDFBox で折り返しのある文章を表示する

f:id:Naotsugu:20200328020339p:plain

はじめに

Apache PDFBox は PDF を操作する Java ライブラリです。

PDFの作成やテキストの抽出、PDFの分割やマージなどを行うことができます。

Apache PDFBox は比較的低レベルな API セットとなっているため、文章を作成しようとした場合に行の折返し操作を自身で実装する必要があったりします。

ここでは、簡単な HelloWorld からはじめ、折返しのある文章の表示方法について見ていきます。


HelloWorld

まずは簡単な PDF の生成です。

public static void main(String[] args) {

    try (PDDocument doc = new PDDocument()) {

        PDPage page = new PDPage(PDRectangle.A4);
        doc.addPage(page);

        setupText(doc, page);

        doc.save("build/example1.pdf");

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

PDDocument のインスタンスに PDPage を追加して PDF を作成します。


コンテンツは PDPageContentStream に書き込むことで行います。

private static void setupText(PDDocument doc, PDPage page) {

    try (PDPageContentStream content = new PDPageContentStream(doc, page)) {

        content.beginText();

        PDFont font = PDType1Font.HELVETICA_BOLD;
        content.setFont(font, 12);
        content.newLineAtOffset(100, 700);
        content.showText("Hello World");

        content.endText();

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

コンテンツは左下が原点(0, 0) の XY座標系となっているため、文章を作成する場合には行ごとに Y 座標をマイナス方向に増やしていく必要があるため注意が必要です。

ここでは x=100 y=700 の位置にテキストを表示しています。

出力結果は以下のようになります。

f:id:Naotsugu:20200328010523p:plain


True Type Font の指定

先の例では組み込みのフォントを指定しました。

組み込みのフォントは PDType1Font に以下のものが用意されています。

public static final PDType1Font TIMES_ROMAN = new PDType1Font("Times-Roman");
public static final PDType1Font TIMES_BOLD = new PDType1Font("Times-Bold");
public static final PDType1Font TIMES_ITALIC = new PDType1Font("Times-Italic");
public static final PDType1Font TIMES_BOLD_ITALIC = new PDType1Font("Times-BoldItalic");
public static final PDType1Font HELVETICA = new PDType1Font("Helvetica");
public static final PDType1Font HELVETICA_BOLD = new PDType1Font("Helvetica-Bold");
public static final PDType1Font HELVETICA_OBLIQUE = new PDType1Font("Helvetica-Oblique");
public static final PDType1Font HELVETICA_BOLD_OBLIQUE = new PDType1Font("Helvetica-BoldOblique");
public static final PDType1Font COURIER = new PDType1Font("Courier");
public static final PDType1Font COURIER_BOLD = new PDType1Font("Courier-Bold");
public static final PDType1Font COURIER_OBLIQUE = new PDType1Font("Courier-Oblique");
public static final PDType1Font COURIER_BOLD_OBLIQUE = new PDType1Font("Courier-BoldOblique");
public static final PDType1Font SYMBOL = new PDType1Font("Symbol");
public static final PDType1Font ZAPF_DINGBATS = new PDType1Font("ZapfDingbats");


日本語を表示するには、TTF ファイルを指定します。

PDType0Font.load() にてフォントファイルを読み込むことで任意のフォントが利用できます。

private static void setupText(PDDocument doc, PDPage page) throws IOException {

    PDFont font = PDType0Font.load(doc,
            TrueTypeFont.class.getClassLoader().getResourceAsStream("ipagp.ttf"));

    try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {

        cs.beginText();

        cs.setFont(font, 12);
        cs.newLineAtOffset(100, 700);
        cs.showText("IPA Pゴシック");

        cs.endText();

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

ここではIPA Pゴシック をダウンロードして指定しました。

上記にて、指定フォントが PDF に埋め込まれます。 OS添付のフォント含めてフォントは著作物なので、PDF へ埋め込んで利用する場合にはフォントのライセンスをよく確認してください。

出力結果は以下のようになります。

f:id:Naotsugu:20200328011629p:plain


段落文章の表示

ここまでは単純な例でしたが、長い文章を表示した場合には自動的に折り返して次の行に移るようなことはしてくれないため、折返し処理を自前で処理しなくてはなりません。

段落を扱うクラスを以下のように作成します。

import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Paragraph {

    private PDFont font;
    private float width;
    private int fontSize;

    public Paragraph(float width, PDFont font, int fontSize) {
        this.width = width;
        this.font = font;
        this.fontSize = fontSize;
    }

    public Paragraph(float width, PDFont font) {
        this(width, font, 12);
    }

    public void drawString(PDPageContentStream content, String text) {
        try {
            content.setFont(font, fontSize);
            for (String line : lines(text)) {
                content.showText(line);
                content.setLeading(getFontHeight());
                content.newLine();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    protected List<String> lines(String text) {
        List<String> lines = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            sb.append(text.charAt(i));
            if (protrude(sb.toString()) || text.charAt(i) == '\n') {
                if (prohibitedEnd(text.charAt(i))) {
                    sb.deleteCharAt(sb.length() - 1);
                    i--;
                } else if (i + 1 < text.length()
                        && prohibitedHead(text.charAt(i + 1))) {
                    i++;
                    sb.append(text.charAt(i));
                }
                lines.add(sb.toString().replaceAll("\\p{C}", ""));
                sb.setLength(0);
            }
        }
        if (sb.length() > 0) {
            lines.add(sb.toString().replaceAll("\\p{C}", ""));
        }
        return lines;
    }

    public float getFontHeight() {
        return font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

    private boolean protrude(String line) {
        try {
            return (font.getStringWidth(line.replaceAll("\\p{C}", "")) / 1000 * fontSize) > width;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean prohibitedHead(char c) {
        return ("!%)/,.:?]}¢°’”‰′″℃、。々〉》」』】〕ぁぃぅぇぉっゃゅょゎ゛゜ゝゞ" +
                "ァィゥェォッャュョヮヵヶ・ーヽヾ!%),.:;?]}。」、・ァィゥェォャュョッー゙゚¢").contains(Character.toString(c));
    }

    private boolean prohibitedEnd(char c) {
        return "$([{£¥‘“〈《「『【〔$([{「£¥".contains(Character.toString(c));
    }

}

非効率ではありますが、表示する文章を1文字ずつ見ていき、指定の幅を超過する場合に段落を分ける処理をしています。

文章の幅は font.getStringWidth() にて調べることができます。

prohibitedHead(char c) では行頭禁則文字、prohibitedEnd(char c) では行末禁則文字 を調べ、行頭 及び 行末に禁則文字が現れないようにしています。

表示幅で分割した文章は content.setLeading(getFontHeight()) で次行に移り、content.newLine() で行頭に戻すことで段落としての文章を表示しています。

なお、.replaceAll("\\p{C}", "") は ASCII 制御文字を空文字置換しています。改行コードなどが含まれていた場合に対象フォントが存在せずにエラーとなるため、行の分割後にこれらを削除しています。


作成した Paragraph を使って PDF を作成しましょう。

public static void run() {

    try (PDDocument doc = new PDDocument()) {

        PDPage page = new PDPage(PDRectangle.A4);
        doc.addPage(page);

        setupText(doc, page);

        doc.save("build/example3.pdf");

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}


private static void setupText(PDDocument doc, PDPage page) throws IOException {

    PDFont font = PDType0Font.load(doc,
            TrueTypeFont.class.getClassLoader().getResourceAsStream("ipagp.ttf"));

    try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {

        cs.beginText();
        Paragraph para = new Paragraph(PDRectangle.A4.getWidth() - 100, font);
        cs.newLineAtOffset(PDRectangle.A4.getLowerLeftX() + 50,
                PDRectangle.A4.getUpperRightY() - 50);
        para.drawString(cs, getText());

        cs.endText();

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

private static String getText() {
    return  "私はこれから、あまり世間に類例がないだろうと思われる私達夫婦の間柄に就いて、出来るだけ正直に、ざっくばらんに、有り" +
            "のままの事実を書いて見ようと思います。それは私自身に取って忘れがたない貴い記録であると同時に、恐らくは読者諸君に取" +
            "っても、きっと何かの参考資料となるに違いない。殊にこの頃のように日本もだんだん国際的に顔が広くなって来て、内地人と" +
            "外国人とが盛んに交際する、いろんな主義やら思想やらが這入って来る、男は勿論女もどしどしハイカラになる、と云うような" +
            "時勢になって来ると、今まではあまり類例のなかった私たちの如き夫婦関係も、追い追い諸方に生じるだろうと思われますから。" +
            "考えて見ると、私たち夫婦は既にその成り立ちから変っていました。私が始めて現在の私の妻に会ったのは、ちょうど足かけ八" +
            "年前のことになります。尤も何月の何日だったか、委しいことは覚えていませんが、とにかくその時分、彼女は浅草の雷門の近" +
            "くにあるカフエエ・ダイヤモンドと云う店の、給仕女をしていたのです。彼女の歳はやっと数え歳の十五でした。だから私が知" +
            "った時はまだそのカフエエへ奉公に来たばかりの、ほんの新米だったので、一人前の女給ではなく、それの見習い、―――まあ" +
            "云って見れば、ウエイトレスの卵に過ぎなかったのです。";
}

表示結果は以下のようになります。

f:id:Naotsugu:20200328014731p:plain


まとめ

PDFBox を使い、単純な文章の PDF 作成方法について見てきました。

低レベルな API セットとなっており、PDF 自体の知識も多少必要となりますが、用途に合わせたユーティリティなどを作ってしまえば自由に PDF の作成が行えます。



PDF構造解説

PDF構造解説

  • 作者:John Whitington
  • 発売日: 2012/05/25
  • メディア: 単行本(ソフトカバー)