LoginSignup
15
17

More than 5 years have passed since last update.

Apache PDFBoxでのテキスト装飾(ななめ・斜体・アンダーライン・文字間隔・白抜き文字・TTCフォントファイルの読み込み・ハイパーリンク)

Last updated at Posted at 2016-06-11

Apache PDFBoxが日本語出力できるようになったのもあり、今まで全然使ったことなかったけどPDFBoxどんなことできるの?っていうのを少しずつ調べています。

今回はテキスト出力時の装飾の方法についてです。

TTCフォントファイルの読み込み

TTFフォントファイルの読み込みはこちらで書いたのでここではTTCフォントファイルの読み込みについて書きます。

(Windowsで、C:/Windows/Fonts/msmincho.ttcにフォントがないと動きません。)

    public static void main(String[] args) throws IOException {
        Path path = Paths.get("pdf.pdf");
        try (OutputStream out = Files.newOutputStream(path)) {
            make(out);
        }

        System.out.println(path.toAbsolutePath());
    }

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.A4;

            File file = new File("C:/Windows/Fonts/msmincho.ttc");
            try (TrueTypeCollection collection = new TrueTypeCollection(file)) {//save前にcloseすると埋め込みできないので注意。
                PDFont font = PDType0Font.load(doc, collection.getFontByName("MS-Mincho"), true);//Font名から取得
                float fontHeight = getFontHeight(font, fontSize);

                PDPage page = new PDPage(rectangle);
                doc.addPage(page);

                try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                    contents.beginText();

                    contents.setFont(font, fontSize);

                    contents.setTextMatrix(Matrix.getTranslateInstance(10, rectangle.getHeight() - fontHeight - 10));
                    contents.showText("MS明朝");

                    contents.endText();
                }

                try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                    doc.save(bufferedOutputStream);
                }

            }
        }
    }

    /**
     * フォントの高さを取得
     *
     * @see http://stackoverflow.com/questions/17171815/get-the-font-height-of-a-character-in-pdfbox
     */
    private static float getFontHeight(PDFont font, float fontSize) {
        return font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

TrueTypeCollectionというクラスを使います。
ただこれがCloseableの実装なのでcloseを必要とします。
しかし、PDDocument#saveの前にcloseしてしまうと、フォントの埋め込み1で落ちます。
なので、closeのタイミングは注意が必要です。もしかしたら一回ByteArrayInputStreamにしてしまえばcloseの必要がないかもしれませんが確信はないです。

結果

image

ななめ・斜体

PDFの仕様を理解できれば簡単にできると思いますが、全くわからない僕は苦労しました。
こちら参考にしました。
http://www.pdf-tools.trustss.co.jp/Syntax/text.html
「座標系」とある部分です。

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.A4;

            PDFont font = loadFont(doc);
            float fontHeight = getFontHeight(font, fontSize);

            PDPage page = new PDPage(rectangle);
            doc.addPage(page);

            PDFormXObject formXObject = new PDFormXObject(doc);
            formXObject.setBBox(new PDRectangle(100f, rectangle.getHeight()));

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                contents.beginText();

                contents.setFont(font, fontSize);

                float offset = 0;
                float x = 100;
                float y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(Matrix.getRotateInstance(0, x, y));//傾かない
                contents.showText("テキスト Rotate 0");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(Matrix.getRotateInstance(0.1, x, y));//ななめ
                contents.showText("テキスト Rotate 0.1");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(Matrix.getRotateInstance(-0.1, x, y));//ななめ
                contents.showText("テキスト Rotate -0.1");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(Matrix.getRotateInstance(-Math.PI, x, y));//逆さま
                contents.showText("テキスト Rotate -PI");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(new Matrix(1, 0, 0, 1, x, y));//普通
                contents.showText("テキスト (1, 0, 0, 1, x, y)");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(
                        new Matrix(1, 0, (float) Math.tan(0.5), 1, x, y));//斜体
                contents.showText("テキスト (1, 0, (float) Math.tan(0.5), 1, x, y)");

                y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
                contents.setTextMatrix(
                        new Matrix(1, (float) Math.tan(-0.5), 0, 1, x, y));//縦斜体
                contents.showText("テキスト(1, (float) Math.tan(-0.5), 0, 1, x, y)");

                contents.endText();
            }

            try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                doc.save(bufferedOutputStream);
            }
        }

    }

Matrixというのを使います。文字列をななめにするだけなら標準のメソッドが用意されています。
斜体は標準メソッドが(たぶん)無いようなので2自力で値を埋めます。

結果

image

アンダーライン

そういうの(たぶん)無いので文字に合わせて下部分に線を引きます。

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.A4;

            PDFont font = loadFont(doc);
            float fontHeight = getFontHeight(font, fontSize);

            PDPage page = new PDPage(rectangle);
            doc.addPage(page);

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                contents.beginText();

                contents.setFont(font, fontSize);

                String text = "テキスト ABC";
                float x = 100;
                float y = rectangle.getHeight() - fontHeight - 10;
                contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
                contents.showText(text);
                contents.endText();

                //アンダーラインを引く
                contents.setLineWidth(fontSize / 20);
                contents.moveTo(x, y);
                contents.lineTo(x + getFontTextWidth(font, text, fontSize), y);
                contents.stroke();
            }

            try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                doc.save(bufferedOutputStream);
            }
        }

    }

    private static float getFontHeight(PDFont font, float fontSize) {
        return font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
    }

    /**
     * Fontで文字列を扱った際の幅を取得
     */
    private static float getFontTextWidth(PDFont font, String text, float fontSize) throws IOException {
        return font.getStringWidth(text) / 1000 * fontSize;
    }

結果

image

文字間隔

PDFBox標準のAPIからは探し出せなかったので自力でPDF構文を埋め込みます。
PDF構文についてはこちら参考にしました。
http://www.pdf-tools.trustss.co.jp/Syntax/text.html

ただ、このPDF構文を埋め込むメソッドはPDPageContentStream#appendRawCommandsというメソッドを利用していて、これはDeprecatedで「いつか消すよ」っぽいことが書かれているので使う際は注意してください。

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.A4;

            PDFont font = loadFont(doc);
            float fontHeight = getFontHeight(font, fontSize);

            PDPage page = new PDPage(rectangle);
            doc.addPage(page);

            PDFormXObject formXObject = new PDFormXObject(doc);
            formXObject.setBBox(new PDRectangle(100f, rectangle.getHeight()));

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                contents.beginText();

                contents.setFont(font, fontSize);

                contents.setLeading(fontHeight);
                float x = 10;
                float y = rectangle.getHeight() - fontHeight - 10;
                contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
                contents.showText("テキスト");

                contents.newLine();
                contents.appendRawCommands("1 Tc ");//文字間隔「1」
                contents.showText("テキスト char spacing 1");

                contents.newLine();
                contents.appendRawCommands("2 Tc ");//文字間隔「2」
                contents.showText("テキスト char spacing 2");

                contents.newLine();
                contents.appendRawCommands("5 Tc ");//文字間隔「5」
                contents.showText("テキスト char spacing 5");

                contents.newLine();
                contents.appendRawCommands("10 Tc ");//文字間隔「10」
                contents.showText("テキスト char spacing 10");

                contents.newLine();
                contents.appendRawCommands("15 Tc ");//文字間隔「15」
                contents.showText("テキスト char spacing 15");

                contents.endText();
            }

            try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                doc.save(bufferedOutputStream);
            }
        }

    }

結果

image

白抜き文字(とか)

こちらもPDFBox標準のAPIからは探し出せなかったので自力でPDF構文を埋め込みます。

下記にも自力でガンバルようなことが書いてあります。
http://stackoverflow.com/questions/17788455/text-rendering-mode-with-pdfbox

PDF構文についてはこちら参考に(略
http://www.pdf-tools.trustss.co.jp/Syntax/text.html

ただ、このPDF構文を埋め込むメソッドはPDPageContentStream#appendRawCommandsという(略

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.A4;

            PDFont font = loadFont(doc);
            float fontHeight = getFontHeight(font, fontSize);

            PDPage page = new PDPage(rectangle);
            doc.addPage(page);

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                contents.setLineWidth(1F);
                contents.beginText();

                contents.setNonStrokingColor(Color.RED);
                contents.setStrokingColor(Color.BLUE);
                //

                contents.setFont(font, fontSize);
                contents.setLeading(fontHeight);

                contents.setTextMatrix(Matrix.getTranslateInstance(10, rectangle.getHeight() - fontHeight - 10));
                contents.appendRawCommands(RenderingMode.FILL.intValue() + " Tr ");
                contents.showText("テキスト FILL");
                contents.newLine();
                contents.appendRawCommands(RenderingMode.STROKE.intValue() + " Tr ");
                contents.showText("テキスト STROKE");
                contents.newLine();
                contents.appendRawCommands(RenderingMode.FILL_STROKE.intValue() + " Tr ");
                contents.showText("テキスト FILL_STROKE");
                contents.newLine();
                contents.appendRawCommands(RenderingMode.NEITHER.intValue() + " Tr ");
                contents.showText("テキスト NEITHER");
                contents.endText();
            }

            try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                doc.save(bufferedOutputStream);
            }
        }
    }

RenderingModeというenumがあったので値間違えないように、値を取得する為にだけ使っています。

結果

image

最後の"テキスト NEITHER"は見えませんが選択はできます。

ハイパーリンク

下記を参考にしました。
http://stackoverflow.com/questions/21021502/how-to-add-hyperlink-in-pdf-using-pdfbox

    public static void make(OutputStream out) throws IOException {

        try (PDDocument doc = new PDDocument()) {
            float fontSize = 20;
            PDRectangle rectangle = PDRectangle.LETTER;

            PDFont font = loadFont(doc);
            float fontHeight = getFontHeight(font, fontSize);

            PDPage page = new PDPage(rectangle);
            doc.addPage(page);

            String text = "Qiita";
            Color color = Color.BLUE;
            float x = 10;
            float y = rectangle.getHeight() - fontHeight - 10;

            try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
                //テキストの描画
                contents.beginText();
                contents.setFont(font, fontSize);
                contents.setNonStrokingColor(color);
                contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
                contents.showText(text);
                contents.endText();
            }

            //ハイパーリンクを配置
            PDAnnotationLink link = new PDAnnotationLink();

            PDBorderStyleDictionary borderULine = new PDBorderStyleDictionary();
            borderULine.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE);
            link.setBorderStyle(borderULine);

            PDColor pdColor = new PDColor(new float[] { color.getRed(), color.getGreen(), color.getBlue() },
                    PDDeviceRGB.INSTANCE);
            link.setColor(pdColor);

            PDActionURI action = new PDActionURI();
            action.setURI("http://qiita.com");
            link.setAction(action);

            PDRectangle position = new PDRectangle();
            position.setLowerLeftX(x);
            position.setLowerLeftY(y);
            position.setUpperRightX(x + getFontTextWidth(font, text, fontSize));
            position.setUpperRightY(y + fontHeight);
            link.setRectangle(position);

            page.getAnnotations().add(link);

            try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
                doc.save(bufferedOutputStream);
            }
        }

    }

結果

image

クリックしたらQiitaが開きます。


以上です。

TcとTrは自力で埋め込まなくて済む方法ご存知の方ぜひ教えてください。


  1. PDType0Font#loadの第3引数をtrueにするとフォント埋め込みになる様子。 

  2. 普通の斜体ならイタリックフォントで充分ですからね。 

15
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
17