PDFBoxでPDFからハイライトテキストのみを抽出する

PDFに埋め込まれているテキストのうち、ハイライト部分のみを抽出するプログラムを書きました。

環境

  • Java SE 11
  • STS 4.3.2.RELEASE
  • Maven 3.6.1
  • PDFBox 2.0.24

解説

下記ページの回答に記載されているプログラムを参考にします。
pdf - Java: Apache PDFbox Extract highlighted text - Stack Overflow

QuadPointsについて

1行であれば、PDAnnotation.getRectangleメソッドで注釈の矩形を取得し、それを利用すればテキストを抽出できます。
しかし複数行にまたがる場合、ハイライトの開始位置と終了位置次第では余分な文字まで取得してしまったり、逆に必要な文字が取得できません。
そこで、QuadPointsと呼ばれる各文字が埋め込まれている矩形の四隅の座標を利用することで、ハイライトしたテキストのみを取得することができます。
尚、QuadPointsの順番は、PDFの仕様上では左下、右下、右上、左上の半時計周りに指定することになっていますが、Adobe Acrobatでは左上、右上、左下、右下の順になっているようです。
(参考:annotations - PDF Spec vs Acrobat creation (QuadPoints) - Stack Overflow

QuadPointsの取得方法

  1. PDAnnotation.getCOSObjectメソッドでCOSDictionaryオブジェクトを取得します。
  2. 引数にCOSName.QUADPOINTSを指定したgetCOSArrayメソッドで取得できます。

指定領域のテキスト抽出

指定した領域に含まれるテキストを抽出するには、PDFTextStripperByAreaクラスを利用します。

  1. addRegionメソッドで抽出対象の領域を指定します。
  2. extractRegionsメソッドでテキストを抽出します。
  3. 抽出されたテキストはgetTextForRegionメソッドで取得できます。

領域の指定について

領域には、左上の座標と矩形の幅と高さを指定します。
幅は右上のX座標 - 左下のX座標で算出します。
高さも同様に、右上のY座標 - 左下のY座標で求めます。
左上のX座標は、文字同士の重なりを考慮して、左上X座標 - 1としています。
Y座標は、ページの高さ - 左上Y座標で指定します。このようにしている理由は、おそらくPDFの原点(左下)と一般的な画像処理での原点(左上)の違いによるものかと思われます。

実装例

この例ではPDFファイルのすべてのページから、ハイライトテキストのみを抽出してコンソールに出力します。
実行の際は、抽出対象のPDFファイルのパスを引数に指定する必要があります。
尚、下記のような警告が多数出ますが、内容を見る限り多分無視しても大丈夫でしょう。

7月 12, 2021 9:38:16 午後 org.apache.pdfbox.pdmodel.font.PDCIDFontType2 codeToGID
警告: Using non-embedded GIDs in font MS-Mincho
package xyz.tenohira.pdf_highlight_extractor;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.text.PDFTextStripperByArea;
public class App
{
/**
* ハイライトした文字列のみを抽出する
*
* 下記のURLを参考に実装した
* https://stackoverflow.com/questions/33253757/java-apache-pdfbox-extract-highlighted-text
*
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException
{
// PDFファイル読込み
try (PDDocument doc = PDDocument.load(new File(args[0]))) {
// 全てのページを処理する
for (PDPage page : doc.getPages()) {
// 注釈を全て抽出して処理する
for (PDAnnotation annotation : page.getAnnotations()) {
// Highlightのみ抽出する
if (annotation.getSubtype().equals("Highlight")) {
PDFTextStripperByArea pdfTextStripperByArea = new PDFTextStripperByArea();
// QuadPointsを取得
COSDictionary cosDictionary = annotation.getCOSObject();
COSArray cosArray = cosDictionary.getCOSArray(COSName.QUADPOINTS);
// 座標を表す数値を8個ずつに切り分けてリストに追加
List<List<? extends COSBase>> quadPointList = new ArrayList<>();
for (int i=0; i < cosArray.size(); i+=8) {
quadPointList.add(cosArray.toList().subList(i, i+8));
}
// 文字を抽出する
String extractedText = "";
for (List<? extends COSBase> list : quadPointList) {
// System.out.printf("(%s,%s), (%s,%s), (%s,%s), (%s,%s)",
// ((COSNumber)list.get(0)).floatValue(), // upper left x
// ((COSNumber)list.get(1)).floatValue(), // upper left y
// ((COSNumber)list.get(2)).floatValue(), // upper right x
// ((COSNumber)list.get(3)).floatValue(), // upper right y
// ((COSNumber)list.get(4)).floatValue(), // lower left x
// ((COSNumber)list.get(5)).floatValue(), // lower left y
// ((COSNumber)list.get(6)).floatValue(), // lower right x
// ((COSNumber)list.get(7)).floatValue()); // lower right y
float width = ((COSNumber)list.get(2)).floatValue() - ((COSNumber)list.get(4)).floatValue();
float height = ((COSNumber)list.get(3)).floatValue() - ((COSNumber)list.get(5)).floatValue();
pdfTextStripperByArea.addRegion("highlighted region",
new Rectangle2D.Float(
((COSNumber)list.get(0)).floatValue() - 1,
page.getMediaBox().getHeight() - ((COSNumber)list.get(1)).floatValue(),
width,
height)
);
pdfTextStripperByArea.extractRegions(page);
String text = pdfTextStripperByArea.getTextForRegion("highlighted region");
extractedText += text;
}
System.out.printf("%s\n", extractedText.replaceAll("\r\n", ""));
}
}
}
}
}
}
view raw App.java hosted with ❤ by GitHub