« ^ »

PDFの返信用注釈機能をPopplerに追加する

所要時間: 約 9分

PDFファイルに触れる機会は非常に多い。外部の人に文書を配布する時の形式ではPDFがよく使用される。モニターと印刷の状態の近さとデータサイズのバランスが良く、また手軽な編集ができないという事が理由だろう1。PDFは編集できないのかと言われると、全くそんな事はない2。PDFを直接編集する方法はいくつかあり、編集機能が対応しているGUIツール使用する、CUIツールを使用するなどが手軽だろう。PDFの仕様は公開されているため、仕様を理解していれば直接ファイルを編集することもできる。

PDFを扱うライブラリというものも存在する。GUI/CUIツールは、それらのライブラリを使用していることも多い。freedesktop.orgが管理しているPDFライブラリにPopplerというものがある。PopplerはPDFを扱うためのライブラリとしてC++で実装されており、特定の環境から利用しやすくしたフロントエンドと呼ばれるAPIを提供している。

通常、Popplerをインストールする場合、各種OS用に提供されているパッケージマネージャからインストールすることが多い。例えばmacOSでHomebrewを使用しているのであれば、 brew install poppler とすれば、何も考えることなくインストールできる。Popplerのソースコードを読んでいて、この中身を修正したくなった3。そのためには自分でソースコードからビルドする必要がある。今回はソースコードを取得し、ビルドするところまで行う。フロントエンドにはglibを利用する。また開発はmacOS上にて行う。

ソースコードの取得

PopplerのソースコードはGitlab上で管理されている。Gitを使ってソースコードを取得する。過去の履歴は不要であったため、 --depth 1 を指定した。

git clone --depth 1 https://gitlab.freedesktop.org/poppler/poppler.git
GitlabからGitを使ってソースコードを取得する

ソースコードが取得できると poppler というディレクトリが作成され、そこにソースコードがある。作業ディレクトリを移動し、各種作業を行う。

cd poppler
作業ディレクトリをpopplerのルートディレクトリに移動する

Popplerをビルドする

ライブラリは動的ローダーと静的ライブラリの2種類の方法で生成できる。ライブラリとして使用するためには、両方の形式でビルドする必要があったため、それぞれの方法でビルドする。ビルドオプションはHomebrewのFormulaを元にする。またフロントエンドは、cppやqtなども選択可能だが今回はglibを使用する。

Popplerのビルドに必要なライブラリについては既にインストール済みとし、手順は省略する。概ね必要なライブラリはcairoとglib、またビルドツールとしてcmakeを用いるため、cmakeも必要になる。詳細は公式のドキュメントを参照して欲しい。

直接環境にインストールしたくないが必要な環境は限定したいため、今回は /opt/ng/poppler_testing にインストールすることにする。

動的ローダー形式でビルドする

動的にリンクを行うライブラリで、所謂共有オブジェクトやDLLと同じ位置付けのものだ。macOSでは最終的には .dyld という拡張子を持つファイルを生成する。まず、ビルド構成ファイルを生成する。

cmake -S . -B build_shared \
      -DBUILD_GTK_TESTS=OFF \
      -DENABLE_BOOST=OFF \
      -DENABLE_CMS=lcms2 \
      -DENABLE_GLIB=ON \
      -DENABLE_QT5=OFF \
      -DENABLE_QT6=OFF \
      -DENABLE_UNSTABLE_API_ABI_HEADERS=ON \
      -DWITH_GObjectIntrospection=ON \
      -DCMAKE_INSTALL_PREFIX=/opt/ng/poppler_testing
動的ローダー形式でビルドするための構成ファイルを生成する

ビルドを実行する。ビルドには少し時間がかかるため、気長に待つ。

cmake --build build_shared
動的ローダーをビルドする

ビルドが完了したら、生成物をインストール先のディレクトリにコピーする。

cmake --install build_shared
生成物をインストール先のディレクトリにコピーする

-DCMAKE_INSTALL_PREFIX=/opt/ng/poppler_testing を指定しているため、 /opt/ng/poppler_testing にインストールされる。

静的ライブラリ形式でビルドする

静的ライブラリとしてビルドする方法は、動的ローダーと殆ど同じだが指定するオプションが少し異なる。 -DBUILD_SHARED_LIBS=OFF を指定すると動的ローダーはビルドされない。

cmake -S . -B build_static \
      -DBUILD_GTK_TESTS=OFF \
      -DENABLE_BOOST=OFF \
      -DENABLE_CMS=lcms2 \
      -DENABLE_GLIB=ON \
      -DENABLE_QT5=OFF \
      -DENABLE_QT6=OFF \
      -DENABLE_UNSTABLE_API_ABI_HEADERS=ON \
      -DWITH_GObjectIntrospection=ON \
      -DBUILD_SHARED_LIBS=OFF \
      -DCMAKE_INSTALL_PREFIX=/opt/ng/poppler_testing
静的ライブラリ形式でビルドするための構成ファイルを生成する

構成ファイルを生成したら、静的ライブラリをビルドする。

cmake --build build_static
静的ライブラリをビルドする

静的ライブラリをビルドしたら、インストールパスへコピーする。

cp build_static/libpoppler.a /opt/ng/poppler_testing/lib
cp build_static/glib/libpoppler-glib.a /opt/ng/poppler_testing/lib
静的ライブラリをコピーする

この手順はHomebrewを参考にした。実際フォーミュラではライブラリファイルをコピーしている。

注釈の一覧を表示する

ビルドしたライブラリを使用し、PDF内に存在する注釈を標準出力に表示するプログラムを実装する。

/**

   PDFを開き、注釈の内容を表示する

   参考 https://poppler.freedesktop.org/api/glib/

 */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include "poppler.h"

int main () {
  int fd;
  PopplerDocument *doc;
  PopplerPage *page;
  GList *annots, *item;

  // ファイルを開く
  fd = open("./pdf/annot.pdf", O_RDONLY);
  if (fd < 1) {
    perror("Failed to open file\n");
    return 1;
  }

  // ファイル記述子からPDFのデータを取得しPopplerで扱える形式にする
  doc = poppler_document_new_from_fd(fd, NULL, NULL);
  if (doc == NULL) {
    perror("Failed to read PDF file\n");
    return 2;
  }
  
  // ドキュメントからページを取得
  page = poppler_document_get_page(doc, 0);
  if (page == NULL) {
    perror("Failed to get PDF page\n");
    return 3;
  }

  // ページに含まれる注釈の一覧を取得
  annots = poppler_page_get_annot_mapping(page);
  if (annots == NULL) {
    perror("No annotation\n");
    return 4;
  }

  int annotation_type;
  for (item = annots; item; item = item->next) {
    // 注釈毎に操作するため型変換する
    PopplerAnnotMapping *annot_map = (PopplerAnnotMapping *)item->data;
    annotation_type = poppler_annot_get_annot_type(annot_map->annot);
    if (POPPLER_ANNOT_TEXT != annotation_type) {
      printf("Annotation Type: %d\n", annotation_type);
      continue;
    }

    // 注釈の内容を取得
    gchar *contents;
    contents = poppler_annot_get_contents(annot_map->annot);
    printf("Annotation: %s", contents);
    g_free(contents);
  }
  poppler_page_free_annot_mapping(annots);
  close(fd);
  return 0;
}

ビルドのためのMakefileは手動で記述した。

.PHONY: all
all: main.c
	gcc-12 -m64 \
		-I /usr/local/include/cairo \
		-I /usr/local/include/glib-2.0 \
		-I /usr/local/lib/glib-2.0/include \
		-I /opt/ng/poppler_testing/include/poppler \
		-I /opt/ng/poppler_testing/include/poppler/glib \
		-L /opt/ng/poppler_testing/lib \
		-L /usr/local/lib \
		-l poppler-glib \
	        -l glib-2.0 \
	main.c -o main

ビルドする。

make -f main.mk all

実行する。

./main
Annotation: this is a commentAnnotation: this is reply

ビルドに関しては適切にヘッダファイルやライブラリへのパスを指定する必要がある。また、gccのリンカーオプションに -Wl,-rpath,/opt/ng/poppler_testing/lib のように@rpathを適切に指定しないと、ダイナミックローダーの読み込みに失敗する4

注釈を追加する

次に注釈を追加する方法を確認する。

/**

   PDFを開き、注釈の内容を表示する

   参考 https://poppler.freedesktop.org/api/glib/

 */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

#include "poppler.h"

int main () {
  int fd;
  PopplerDocument *doc;
  PopplerPage *page;
  GList *annots, *item;

  // ファイルを開く
  fd = open("./pdf/annot.pdf", O_RDONLY);
  if (fd < 1) {
    perror("Failed to open file\n");
    return 1;
  }

  // ファイル記述子からPDFのデータを取得しPopplerで扱える形式にする
  doc = poppler_document_new_from_fd(fd, NULL, NULL);
  if (doc == NULL) {
    perror("Failed to read PDF file\n");
    return 2;
  }
  
  // ドキュメントからページを取得
  page = poppler_document_get_page(doc, 0);
  if (page == NULL) {
    perror("Failed to get PDF page\n");
    return 3;
  }
  // ページに含まれる注釈の一覧を取得
  annots = poppler_page_get_annot_mapping(page);
  if (annots == NULL) {
    perror("No annotation\n");
    return 4;
  }
  // 先頭の注釈を取得する
  item = &annots[0];
  PopplerAnnotMapping *annot_map = (PopplerAnnotMapping *)item->data;

  // 注釈を追加する
  PopplerAnnot *ann;
  PopplerRectangle area = { 0, 0, 0, 0 };
  ann = poppler_annot_text_new(doc, &area);
  poppler_annot_set_contents(ann, "testing");
  poppler_page_add_annot(page, ann);

  // ファイルに書き込む
  gboolean rc;
  GError *err;
  int fd_out;
  fd_out = open("./ddd.pdf", O_WRONLY);
  rc = poppler_document_save_to_fd(doc, fd_out, 1, &err);
  if (!rc) {
    printf("Failed to output pdf file: %s\n", err->message);
  }
  // 注釈用のメモリを開放
  poppler_page_free_annot_mapping(annots);
  close(fd);
  return 0;
}

ビルドのためのMakefileは手動で記述した。

.PHONY: all 
all: addannon.c
	gcc-12 -m64 \
		-I /usr/local/include/cairo \
		-I /usr/local/include/glib-2.0 \
		-I /usr/local/lib/glib-2.0/include \
		-I /opt/ng/poppler_testing/include/poppler \
		-I /opt/ng/poppler_testing/include/poppler/glib \
		-L /opt/ng/poppler_testing/lib \
		-L /usr/local/lib \
		-l poppler-glib \
	        -l glib-2.0 \
	addannon.c -o addannon
	./addannon

ビルドする。

make -f addannon.mk all

実行する。

./addannon

返信注釈を追加する

ここまでは既に存在するPopplerの機能で実装できた。今回の本丸は返信用の注釈を付けることだ。これが既存の機能では実現が難しかった。そこでPoppler自体を修正する必要が出てきた。

AnnotTextのinReplyToを設定できるようにPopplerを拡張する

poppler-glibが提供している関数には、それらしい機能を見つけられなかった。PopplerはC++で実装されており、その中で注釈用に Annot クラスが定義されている。注釈はテキストだけではなく、特定の領域や画像や音声といった形式も挿入可能であり、それらは Annot クラスを継承している。文字系の注釈用に AnnotText が定義されており、それを継承する形でそこから更に様々な注釈用クラスを定義している。


+---------------+    +---------------+    +---------------+
|               |    |               |    |               |
| Annot         +<|--| AnnotText     |<|--| AnnotMarkup   |
|               |    |               |    |               |
+---------------+    +---------------+    +---------------+

AnnotTextにはinReplyToというメンバー変数が宣言されている。ここに返信の元になる注釈の参照を設定することで返信注釈となる。ただinReplyToはprotectedで宣言されているため、外部から直接アクセスすることができない。そこでinReplyToを設定するための、 Annotation.setInReplyTo() メソッドを追加し値を設定できるようにした。またglib用のフロントエンドとして呼び出し可能な poppler_annot_text_reply_new() 関数を追加した。

https://github.com/TakesxiSximada/poppler-symdom-custom/commit/291337c0b76fdc0c7e2493b8496065165de4e046

実装後はライブラリをビルドし直す必要があるため、同手順でビルドしインストールパスにファイルをコピーした。

返信用注釈を追加する

先程実装した関数を使い、返信用の注釈を追加する。先頭のページの最初の注釈を取得し、そこに返信用注釈を追加する。

/**

   PDFを開き、注釈の内容を表示する

   参考 https://poppler.freedesktop.org/api/glib/

 */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

#include "poppler.h"

int main () {
  int fd;
  PopplerDocument *doc;
  PopplerPage *page;
  GList *annots, *item;

  // ファイルを開く
  fd = open("./pdf/annot.pdf", O_RDONLY);
  if (fd < 1) {
    perror("Failed to open file\n");
    return 1;
  }

  // ファイル記述子からPDFのデータを取得しPopplerで扱える形式にする
  doc = poppler_document_new_from_fd(fd, NULL, NULL);
  if (doc == NULL) {
    perror("Failed to read PDF file\n");
    return 2;
  }
  
  // ドキュメントからページを取得
  page = poppler_document_get_page(doc, 12);
  if (page == NULL) {
    perror("Failed to get PDF page\n");
    return 3;
  }
  // ページに含まれる注釈の一覧を取得
  annots = poppler_page_get_annot_mapping(page);
  if (annots == NULL) {
    perror("No annotation\n");
    return 4;
  }
  // 先頭の注釈を取得する
  item = &annots[0];
  PopplerAnnotMapping *annot_map = (PopplerAnnotMapping *)item->data;
  PopplerAnnot *ann_parent;
  ann_parent = (PopplerAnnot *)annot_map->annot;
  // printf("%p", ann_parent->annot);

  // 注釈を追加する
  PopplerAnnot *new_poppler_annot;
  gchar *t = "testyaya";
  new_poppler_annot = poppler_annot_text_reply_new(annot_map->annot, t);
  if (new_poppler_annot == NULL) {
    perror("Failed to create new annotation\n");
    return 6;
  }
  poppler_page_add_annot(page, new_poppler_annot);
  printf("Ok!!\n");

  // ファイルに書き込む
  gboolean rc;
  GError *err;
  int fd_out;
  fd_out = open("./pdf/output.pdf", O_WRONLY);
  rc = poppler_document_save_to_fd(doc, fd_out, 1, &err);
  if (!rc) {
    printf("Failed to output pdf file: %s\n", err->message);
  }
  // 注釈用のメモリを開放
  poppler_page_free_annot_mapping(annots);
  close(fd);
  return 0;
}

ビルドのためのMakefileは手動で記述した。

ビルドする。

make -f reply.mk all

実行する。

./reply

返信用注釈が追加されたファイルがpdf/output.pdfに出力される。

修正したコードをHomebrewで入れられるようにする

返信用注釈の追加の為にPopplerに手を加えた。このままでは毎回ソースコードを取得し野良ビルドする必要がある。それは面倒であるため、個人用のHomebrewのTapにフォーミュラを追加し、Homebrewコマンドでインストールできるようにした。以下のコマンドで上記の実装をインストールできる。

brew install TakesxiSximada/homebrew-tap/poppler

https://github.com/TakesxiSximada/homebrew-tap

まとめ

PDFライブラリPopplerの使い方を確認するために、ソースコードからビルドし、注釈一覧と追加の実装方法を確認した。注釈は追加できたが、返信用注釈はPoppler自体を拡張する必要があった。そこで Annotation.setInReplyTo() メソッドと poppler_annot_text_reply_new() 関数を追加し、それらを使って返信用注釈を追加できることを確認した。Popplerへの修正はGithubにアップロードし、Homebrewでインストールできるように整備した。


1

ラスターの形式の場合はほぼ同じ状態になるが、データのサイズが大きくなる。PDFの場合は、構造化されたデータであるためラスターのデータと比較すると、データ量は少なくできる。

2

最近のAdbe製品にはPDFの編集機能が付いている。そもそも、絶対に編集(WRITE)できないファイルというものはない。書き込み出きないというのは、そのセクタなり書き込み領域が破損している時だけだ。実際、テキストエディタやバイナリエディタのようなツールで開けば編集できる。