Help us understand the problem. What is going on with this article?

LuaTeX で PDF 解析! 〜pdfe ライブラリ〜

LuaTeX 搭載の Lua 処理系で利用可能なライブラリの1つに pdfe ライブラリがあります.このライブラリは LuaTeX が画像として外部の PDF を読み込むのに使用しているロジックへのインターフェースを提供するもので,一般の PDF ファイル解析に利用できます.pdfe ライブラリは,元々 LuaTeX に搭載されていた epdf ライブラリを置き換えるべく比較的最近導入されたもので,TeX Live 収録の LuaTeX バージョンの中では TeX Live 2018 収録のもの (LuaTeX 1.07.0) から利用可能になったようです.

そうした事情もあり,このライブラリの存在と機能は TeX 界隈の人々にもあまり知られていない気がします.そこで,本稿ではチュートリアル的に pdfe ライブラリの基本的な使い方を紹介したいと思います.本稿で扱えていない詳細については LuaTeX リファレンスマニュアル1の13.2節を参照してください.

想定読者と前提環境

本稿の主な想定読者は PDF 解析に興味のある TeX ユーザです.Lua(望ましくは texlua)プログラミングの基礎知識は前提とします.ただし TeX/LaTeX の知識は必要ありません.基本的な PDF 構造は予め知っていることが望ましいですが,本文中で解説文献をいくつか紹介しています.本稿に登場するコード例は,すべて TeX Live 2018 以降の TeX Live フルスキーム環境下で実行することを前提としています.

PDF ファイルの開閉

pdfe ライブラリを利用するには,まずはライブラリを読み込みます.現在の LuaTeX では起動時よりグローバル変数 pdfe にライブラリが読み込まれていますが,この挙動には依拠しない方が無難でしょう.これは LuaTeX に同梱されている他のライブラリについても同様です.

-- pdfe ライブラリを読み込む
local pdfe = require 'pdfe'

pdfe ライブラリを読み込んだら,次は pdfe.open() 関数で操作対象の PDF を開く必要があります.この関数の引数には,単純に開きたい PDF のファイル名(カレントディレクトリからの相対パスまたは絶対パス)を与えてやります.

-- test.pdf を開く
local doc = pdfe.open('test.pdf')

これにより変数 doc<pdfe document> と呼ばれる特殊なデータ構造が格納されます.以降,pdfe ライブラリで PDF のもつ情報にアクセスするためには,基本的にこの変数を対象に操作を行っていくことになります.

pdfe ライブラリは暗号化 PDF にも対応していますが ,内容にアクセスするためには pdfe.unencrypt() 関数を用いて復号化してやる必要があります.

pdfe.unencrypt(doc, user_password, owner_password)

ここで user_passwordowner_password はそれぞれユーザーとオーナーのパスワード(文字列)ですが,いずれかは nil としてもよいようです.

pdfe で開いた PDF の状態は pdfe.getstatus() 関数で確認することができます.

local stat = pdfe.getstatus(doc)

この関数の返り値は PDF の状態を表す整数で,値の意味は次のようになっています.

  • -2: open に失敗
  • -1: 暗号化されている
  • 0: 暗号化されておらず,データにアクセス可能
  • 2: 復号化されており,データにアクセス可能

なお,開いた PDF は次のように宣言することで閉じることができます.特に連続して多数の PDF ファイルを開くような場合,ファイルを閉じ忘れないよう注意しましょう.

-- test.pdf を閉じる
pdfe.close(doc)

基本情報の取得

pdfe でファイルを開くことに成功すると,pdfe のトップレベルに定義された関数を用いて基本情報が取得できるようになります.具体的には,次のような関数が利用できます:

  • pdfe.getsize(<pdfe document>): ファイルサイズを表す整数(バイト単位)を返す
  • pdfe.getversion(<pdfe document>): PDF のメジャー/マイナーバージョンを表す整数を2つ返す
  • pdfe.getnofobjects(<pdfe document>): 文書に含まれるオブジェクトの数を表す整数を返す
  • pdfe.getnofpages(<pdfe document>): ページ数を表す整数を返す
  • pdfe.getmemoryusage(<pdfe document>): メモリの使用状況を表す整数を2つ返す

試しに LuaTeX のリファレンスマニュアル自体を対象に,これらの基本情報を取得してみましょう.

#!texlua
local pdfe = require 'pdfe'
local kpse = require 'kpse'

-- texlua で kpse ライブラリの機能を使用する場合,初期化が必要
kpse.set_program_name('luatex')
local luatex_manual = kpse.var_value('TEXMFDIST') .. '/doc/luatex/base/luatex.pdf'

-- PDF ファイルを開く
local doc = pdfe.open(luatex_manual)

-- 基本情報の取得
local bytes = pdfe.getsize(doc)  -- ファイルサイズを取得
local major, minor = pdfe.getversion(doc)  -- PDF バージョンの取得
local nof_objects = pdfe.getnofobjects(doc)  -- オブジェクト数を取得
local nof_pages = pdfe.getnofpages(doc)  -- ページ数を取得
local m_bytes, waste = pdfe.getmemoryusage(doc)  -- メモリ使用状況の取得

-- PDF ファイルを閉じる
pdfe.close(doc)

-- 基本情報の表示
print(string.format('size: %.2f MB', bytes / 1000^2))
print(string.format('version: %d.%d', major, minor))
print(string.format('#objects: %d', nof_objects))
print(string.format('#pages: %d', nof_pages))
print(string.format('memory usage: %.2f MB (waste: %.2f KB)',
                    m_bytes / 1000^2, waste / 1000))
実行結果
size: 1.56 MB
version: 1.7
#objects: 9576
#pages: 294
memory usage: 3.82 MB (waste: 40.37 KB)

コンテンツへのアクセス

pdfe ライブラリを使用すると,前節で紹介したような PDF の基本情報のみならず,そのフルコンテンツへのアクセスも可能です.ただし pdfe はかなり低レベルな API を提供するので,PDF の内容をしっかりと解析するには PDF の構造についての知識は多少なりとも求められます.PDF の構造については,もちろん PDF のリファレンスを読めば完璧ですが,長大でなかなかいきなり読めるものではありません.そこで,取っ掛かりとしてウェブ上にある文献を読んでみるのがよいでしょう.ちょうど最近,鹿野さんによるラムダノートの技術 Advent Calendar 2019の連載第2回の記事で PDF 構造について簡単に解説されていて,サクッと読めるのでまだの方は一読されるとよいかもしれません.より本格的な解説としては itchyny さんの入門記事がおすすめです.

関数を用いる方法

さて,PDF 構造の解説は外部に丸投げして,ここでは pdfe のチュートリアルを続けます.まず大前提として,PDF ドキュメントは木構造なわけですが,pdfe ではこれを <pdfe dictionary> という特殊な辞書を用いて表現します.pdfe で PDF コンテンツへのアクセスを行うにはまず,基本となる <pdfe dictionary> を取得するところから始める必要があります.ここで基本となる辞書は3つあり,それぞれに対応する <pdfe dictionary> を取得する関数が用意されています.

  • pdfe.getcatalog(<pdfe document>): カタログ辞書を返す
  • pdfe.gettrailer(<pdfe document>): トレーラ辞書を返す
  • pdfe.getinfo(<pdfe document>): インフォ辞書を返す

例えば,次のようなコードで変数 catalog, trailer, info にそれぞれ対応する <pdfe dictionary> を代入することができます.

-- PDF ファイルを開く
local doc = pdfe.open('test.pdf')

-- 基本となる辞書を取得する
local catalog = pdfe.getcatalog(doc)
local trailer = pdfe.gettrailer(doc)
local info = pdfe.getinfo(doc)

取得した辞書に対しては,その辞書がもつキーを指定することで,実際の値を取り出すことができます.ただし値の取得に用いる関数は,対象のキーのもつ値の PDF のデータ型によって使い分ける必要があります.

  • pdfe.getstring(<pdfe dictionary>, key): PDF の文字列 (string) を Lua の string として返す
  • pdfe.getinteger(<pdfe dictionary>, key): PDF の整数 (integer) を Lua の number として返す
  • pdfe.getnumber(<pdfe dictionary>, key): PDF の数値 (number) を Lua の number2 として返す
  • pdfe.getboolean(<pdfe dictionary>, key): PDF の真偽値 (boolean) を Lua の boolean として返す
  • pdfe.getname(<pdfe dictionary>, key): PDF の名前 (name) を Lua の string として返す

PDF 仕様が定義されてするデータ型と Lua のそれとは少し違いがありますが,見ての通りそれぞれ適当な読み替えが行なわれます.

PDF の辞書には,上記に挙げたもの以外に辞書,配列,ストリームが含まれる場合があります.これらについては pdfe ライブラリがそれぞれ対応する userdata 型を用意しており,やはり同様の関数で取得できます.

  • pdfe.getdictionary(<pdfe dictionary>, key): PDF の辞書を Lua の <pdfe dictionary> (userdata) として返す
  • pdfe.getarray(<pdfe dictionary>, key): PDF の配列を Lua の <pdfe array> (userdata) として返す
  • pdfe.getstream(<pdfe dictionary, key>): PDF のストリームを Lua の <pdfe stream> (userdata) として返す.また対応するストリーム辞書を2番目の戻り値として <pdfe dictionary> (userdata) で返す

具体例として先ほど取得したトレーラ辞書からいくつか値を取り出してみましょう.

-- トレーラ辞書からキー Type, Size, Root の値を取得
local t_type = pdfe.getname(trailer, 'Type')
local t_size = pdfe.getinteger(trailer, 'Size')
local t_root = pdfe.getdictionary(trailer, 'Root')

-- それぞれの値とその Lua におけるデータ型を表示
print('Type', t_type, type(t_type))
print('Size', t_size, type(t_size))
print('Root', t_root, type(t_root))
実行結果
Type    XRef    string
Size    9577    number
Root    <pdfe.dictionary 0x108d2e3cf>   userdata

特に難しいところはなかったと思いますが,関数を用いる方法では一点注意が必要なことがあります.それは,あるキーの値を取得する際に,そのキーに対応するデータ型とは異なる関数を用いて値にアクセスしようとすると,特に警告なく nil が返る場合があることです.pdfe.getstring()pdfe.getname() はいずれも Lua 側では string を返しますが,PDF 仕様ではこれらは区別されるため,これらの区分を間違えた場合も戻り値は nil になります.

ページ番号やボックス名を指定して値を取り出す

理論上,ここまでに説明した方法で PDF に含まれるほとんどの値にアクセスできますが,実用上はページ番号やボックス名を指定して,対応するデータを取り出せると便利です.pdfe にはそうした需要に応える手段として pdfe.getpage()pdfe.getbox() 関数が用意されています.

-- PDF ファイル(LuaTeX リファレンスマニュアル)を開く
kpse.set_program_name('luatex')
local luatex_manual = kpse.var_value('TEXMFDIST') .. '/doc/luatex/base/luatex.pdf'
local doc = pdfe.open(luatex_manual)

-- 2ページ目のコンテンツを含む <pdfe dictionary> を取得
local p2 = pdfe.getpage(doc, 2)
print(p2)

-- 2ページ目の MediaBox を取得(MediaBox の4値を持つテーブルが返る)
local p2_mediabox = pdfe.getbox(p2, 'MediaBox')
for _, v in pairs(p2_mediabox) do
  print(v)
end
実行結果
<pdfe.dictionary 0x10369c3b7>
0.0
0.0
595.27559
841.88976

また,pdfe.pagestotable(<pdfe document>) という関数を用いると,ドキュメントから各ページに対応する辞書を Lua の配列3の形で一度に受け取ることができます.この配列の各要素には,いくつかの付加情報をもつ Lua の配列が格納されていますが,その第1要素が目的の辞書です.

-- ページテーブルを取得
local pages_tab = pdfe.pagestotable(doc)

-- 各要素を表示
for _, v in ipairs(pages_tab) do
  print(v[1])
end
実行結果
<pdfe.dictionary 0x1024fe020>
<pdfe.dictionary 0x1024fe3b7>
(以下略)

テーブル記法を用いる方法

実は PDF 辞書 (<pdfe dictionary>) の値の取得には関数を用いる方法の他に代替手段があります.実は <pdfe document><pdfe dictionary> は通常の Lua テーブルと同じ要領で,各キーに対応する値を取得できます.例えば,次の2例ではいずれも pdfe ドキュメント doc のトレーラ辞書 Type キーの値取得に成功します.

local t_type1 = doc['Trailer']['Type']
local t_type2 = doc.Trailer.Type

なお <pdfe document> のトップレベルに定義されているキーは基本辞書に対応する Catalog, Info, TrailerPages です.最後の Pages を用いれば pdfe.getpage() 相当の操作が可能です.

local p2 = doc.Pages[2]

特殊な値の取り扱い

PDF は基本的に木構造であり,pdfe ではその構造は辞書 <pdfe dictionary> の入れ子構造として表されるということを既に説明しました.辞書内の各キーにアクセスしたときに,それらが「葉」ノードで,単なる数値や文字列といった値の場合は,それを Lua のデータ型として受け取ってあとは好きなようにすればよいですが,辞書の中身を取り出したところ,それが単一の値ではなく特殊な値(データ型)である場合があります.これまでにも何度か登場していますが,そうした特殊な値には辞書 <pdfe dictionary>,配列 <pdfe array>,参照 <pdfe reference>,ストリーム <pdfe stream> があります.pdfe ライブラリには,これらの特殊な値を扱うための機能も用意されています.

辞書 <pdfe dictionary>

PDF には基本となる辞書が3つ(カタログ辞書,トレーラ辞書,info 辞書)あることは既に説明しました.そして,辞書からは pdfe.getナンチャラ(dict, key) を用いることによりキー key に対応する値を取り出せることも説明済みです.

pdfe.getdictionary() 関数の存在からわかるように,辞書の中に別の辞書が格納されている場合もあります.このような場合,取り出した辞書も上記の3つのトップレベルの辞書とまったく同じように扱うことができます.すなわち pdfe.getナンチャラ() 関数で値を取り出したり,テーブル記法を用いて dict['<key>']dict.<key> の形で内部の情報にアクセスしたりすることができます.

また,辞書は pdfe.dictionarytotable() 関数により通常の Lua テーブルに変換することができます.ただし,変換先の Lua テーブルの各項目は値そのものの他にデータ型情報などを含む配列4なので,値そのものはその配列の2番目に格納されています.例えば,単純には次のようにすることでカタログ辞書の内容を一覧にすることができます.

-- カタログ辞書を Lua テーブルに変換
local tab = pdfe.dictionarytotable(doc.Catalog)

-- 変換したテーブルの内容を for ループで表示
for k, v in pairs(tab) do
  -- 実際の値は各要素配列の2番目の要素
  print(k, v[2])
end
実行結果
Type    Catalog
Version 1.7
(以下略)

配列 <pdfe array>

PDF の配列は辞書から pdfe.getarray() を用いて取り出すことができます.辞書から各データ型の値を取り出す時に使用した pdfe.getナンチャラ() 関数は,実は配列から値を取り出す場合にも使用できます.この場合,第2引数にはキーではなくインデックスを渡して pdfe.getナンチャラ(array, index) の形で使用します.配列 <pdfe array> のサイズは通常の Lua テーブルと同様 # 演算子で取得できます.なお,ここで PDF 配列のインデックスは 0-origin であることに注意してください.

local index_arr = pdfe.getarray(doc.Trailer, 'Index')
for i = 0, #index_arr - 1 do
  print(pdfe.getinteger(index_arr, i))
end
実行結果
0
9577

pdfe.getarray() 関数を用いると,配列 <pdfe array> も Lua の配列に変換することができます.この場合,変換後の配列は通常の Lua の習慣にしたがって 1-origin になります.

local index_tab = pdfe.getarray(doc.Trailer, 'Index')
for i = 1, #index_arr do
  print(index_tab[i])
end
実行結果
0
9577

参照 <pdfe reference>

どういうわけか参照については専用の pdfe.getナンチャラ() 関数が用意されていませんが,テーブル記法や辞書の Lua テーブルへの変換などによって辞書の中身を取り出すと,<pdfe reference> という特殊な値が現れる場合があります.これは PDF では別の箇所にあるデータを参照する際に用いるものです.

pdfe ライブラリでは pdfe.getfromreference() 関数の引数に,この <pdfe reference> を渡してやることで,参照しているデータにアクセスすることができます.

ストリーム <pdfe stream>

PDF のストリームはバイナリ列と,そのバイナリ列に関する情報を格納した辞書の組です.先述の通り pdfe.getstream() を用いると辞書からストリームを取り出すことができ,バイナリ列と辞書の両方を取得できます.

pdfe ライブラリでは,ストリームはチャンクごとに読むことも,一気にすべてを読み込むことも可能です.前者の場合,ストリームの開閉はユーザ自身で行う必要があります.

  • pdfe.openstream(<pdfe stream>): ストリームを開く.戻り値は成否を表す真偽値.
  • pdfe.closestream(<pdfe stream>): ストリームを閉じる.戻り値はない.

開いているストリームの内容は pdfe.readfromstream(<pdfe stream>) で読み出すことができます.ストリームはバイナリ列ですが,実質的に ASCII 文字列の場合は単純に文字列として扱ってしまうことが出来ます.試しに LuaTeX リファレンスのカタログ辞書にあるメタデータを読み出してみましょう.

local metadata, dict = pdfe.getstream(catalog, 'Metadata')
print('* Stream Dictionary')
print_dict(dict)

print('* Reading Stream')
local stat = pdfe.openstream(metadata)
local content, length = pdfe.readfromstream(metadata)
print(string.format('status: %s', stat))
print(string.format('length: %s', length))
print(string.format('content: %s', content))
pdfe.closestream(metadata)
実行結果
* Stream Dictionary
Subtype: "XML"
Type: "Metadata"
Length: 2044
* Reading Stream
status: true
length: 2044
content: <?xpacket begin="<feff>" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:format>application/pdf</dc:format><dc:creator><rdf:Seq><rdf:li xml:lang="x-default"/></rdf:Seq></dc:creator><dc:description><rdf:Alt><rdf:li xml:lang="x-default"/></rdf:Alt></dc:description><dc:title><rdf:Alt><rdf:li xml:lang="x-default">luatex</rdf:li></rdf:Alt></dc:title></rdf:Description><rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/"><pdfx:ID>luatex | 2019-11-08T10:03:40+01:00</pdfx:ID><pdfx:ConTeXt.Jobname>luatex</pdfx:ConTeXt.Jobname><pdfx:ConTeXt.Time>2019-11-08 10:03</pdfx:ConTeXt.Time><pdfx:ConTeXt.Url>www.pragma-ade.com</pdfx:ConTeXt.Url><pdfx:ConTeXt.Support>contextgarden.net</pdfx:ConTeXt.Support><pdfx:ConTeXt.Version>2019.07.04 12:29</pdfx:ConTeXt.Version><pdfx:ConTeXt.LMTX/><pdfx:TeX.Support>tug.org</pdfx:TeX.Support><pdfx:LuaTeX.Version>1.11</pdfx:LuaTeX.Version><pdfx:LuaTeX.Functionality>7223</pdfx:LuaTeX.Functionality><pdfx:LuaTeX.LuaVersion>5.3</pdfx:LuaTeX.LuaVersion><pdfx:LuaTeX.Platform>linux-64</pdfx:LuaTeX.Platform></rdf:Description><rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/"><xmp:CreateDate>2019-11-08T10:03:40+01:00</xmp:CreateDate><xmp:CreatorTool>LuaTeX 1.11 7223 + ConTeXt MkIV 2019.07.04 12:29</xmp:CreatorTool><xmp:ModifyDate>2019-11-08T10:03:40+01:00</xmp:ModifyDate><xmp:MetadataDate>2019-11-08T10:03:40+01:00</xmp:MetadataDate></rdf:Description><rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/"><pdf:Keywords/><pdf:Producer>LuaTeX-1.11</pdf:Producer><pdf:Trapped>False</pdf:Trapped></rdf:Description><rdf:Description rdf:about="" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"><xmpMM:DocumentID>uuid:2c7e4d5f-4ea5-ad1a-28af-03c5cd622751</xmpMM:DocumentID><xmpMM:InstanceID>uuid:b830ca24-41ae-a2ec-e86c-9b4e1e29febe</xmpMM:InstanceID></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>

あるいは,pdfe.readwholestream(<pdfe stream>) を用いてストリームの内容を一気に読み出すこともできます.この場合,ストリームの開閉は必要ありません.

print('* Reading Stream')
local content, length = pdfe.readwholestream(metadata)
print(string.format('length: %s', length))
print(string.format('content: %s', content))
実行結果
* Reading Stream
length: 2044
content: <?xpacket begin="<feff>" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:format>application/pdf</dc:format><dc:creator><rdf:Seq><rdf:li xml:lang="x-default"/></rdf:Seq></dc:creator><dc:description><rdf:Alt><rdf:li xml:lang="x-default"/></rdf:Alt></dc:description><dc:title><rdf:Alt><rdf:li xml:lang="x-default">luatex</rdf:li></rdf:Alt></dc:title></rdf:Description><rdf:Description rdf:about="" xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/"><pdfx:ID>luatex | 2019-11-08T10:03:40+01:00</pdfx:ID><pdfx:ConTeXt.Jobname>luatex</pdfx:ConTeXt.Jobname><pdfx:ConTeXt.Time>2019-11-08 10:03</pdfx:ConTeXt.Time><pdfx:ConTeXt.Url>www.pragma-ade.com</pdfx:ConTeXt.Url><pdfx:ConTeXt.Support>contextgarden.net</pdfx:ConTeXt.Support><pdfx:ConTeXt.Version>2019.07.04 12:29</pdfx:ConTeXt.Version><pdfx:ConTeXt.LMTX/><pdfx:TeX.Support>tug.org</pdfx:TeX.Support><pdfx:LuaTeX.Version>1.11</pdfx:LuaTeX.Version><pdfx:LuaTeX.Functionality>7223</pdfx:LuaTeX.Functionality><pdfx:LuaTeX.LuaVersion>5.3</pdfx:LuaTeX.LuaVersion><pdfx:LuaTeX.Platform>linux-64</pdfx:LuaTeX.Platform></rdf:Description><rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/"><xmp:CreateDate>2019-11-08T10:03:40+01:00</xmp:CreateDate><xmp:CreatorTool>LuaTeX 1.11 7223 + ConTeXt MkIV 2019.07.04 12:29</xmp:CreatorTool><xmp:ModifyDate>2019-11-08T10:03:40+01:00</xmp:ModifyDate><xmp:MetadataDate>2019-11-08T10:03:40+01:00</xmp:MetadataDate></rdf:Description><rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/"><pdf:Keywords/><pdf:Producer>LuaTeX-1.11</pdf:Producer><pdf:Trapped>False</pdf:Trapped></rdf:Description><rdf:Description rdf:about="" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"><xmpMM:DocumentID>uuid:2c7e4d5f-4ea5-ad1a-28af-03c5cd622751</xmpMM:DocumentID><xmpMM:InstanceID>uuid:b830ca24-41ae-a2ec-e86c-9b4e1e29febe</xmpMM:InstanceID></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>

PDF データ構造の可視化

ここまで pdfe ライブラリを用いて PDF の持つ情報にアクセスする方法を説明してきましたが,いずれも「どの辞書のどのキーに何のデータが格納されているか」を熟知していないと使いにくいインターフェースだったと思います.上述の関数では辞書のキーを直接指定しないと内容にアクセスできませんし,さらに値を取り出すためには各キーに対応する値のデータ型も把握していないといけません.

格納している内容が未知の辞書を扱いたい場合,さらに低レベルの関数である pdfe.getfromdictionary() 関数を用いると便利です.この関数は,次のように使用して複数の値5を返します.

i_key, i_type, i_value = pdfe.getfromdictionary(<pdfe dictionary>, index)

戻り値は順にキー(文字列),データ型(数値),値(それぞれ適当な Lua のデータ型)です.2つ目の戻り値は,格納されているデータ型を表す数値で,その読み方は次の表の通りです.

数値 PDF の型 Lua の型
0 none nil
1 null nil
2 boolean boolean
3 integer number (integer)
4 number number (float)
5 name stirng
6 string string
7 array pdfe array (userdata)
8 dictionary pdfe array (userdata)
9 stream pdfe stream (userdata)
10 reference pdfe reference (userdata)

ここで第2引数が index であることに「おや」と思った方もいるでしょう.実は辞書の各項目にはキーの他に整数のインデックスも与えられています6.整数インデックスを用いることで,キーのわからない要素にもアクセスすることができます.なお Lua での習慣にしたがい,<pdfe dictionary> のインデックスも 1-origin です.また通常の Lua テーブルと同様,辞書の大きさは # 演算子で取得できます.したがって,1 から #dict までのインデックスでループを回すことで,辞書 dict の全要素を走査することができます.試しにカタログ辞書の内容をすべて表示してみましょう.

for i = 1, #catalog do
  local i_key, i_type, i_value = pdfe.getfromdictionary(catalog, i)
  print(i_key, i_type, i_value)
end
実行結果
Type    5   Catalog
Pages   10  <pdfe.reference 9573>
Lang    6   en
Metadata    10  <pdfe.reference 8824>
Names   10  <pdfe.reference 9432>
OCProperties    8   <pdfe.dictionary 0x10e618fee>
Outlines    10  <pdfe.reference 8825>
PageLabels  8   <pdfe.dictionary 0x10e61928c>
PageLayout  5   TwoColumnRight
PageMode    5   UseNone
Version 5   1.7
ViewerPreferences   8   <pdfe.dictionary 0x10e619397>

この方法ならば万能で,キーも値のデータ型もわからなくても PDF のコンテンツを列挙していくことができます.

上記のコード例のように地道に辞書の内容を1階層ずつ手繰っていってもよいのですが,再帰的に木構造を走査することで一気に PDF の構造を表示することもできそうです.最後に,そのような可視化を単純に実装したコード例を紹介します.

local function helper(i_type, i_value)
  if i_type == 0 or i_type == 1 then  -- nil
    return 'leaf', 'None'
  elseif i_type == 2 or i_type == 3 or i_type == 4 then  -- boolean and number
    return 'leaf', i_value
  elseif i_type == 5 or i_type == 6 then  -- string
    return 'leaf', '"' .. i_value .. '"'
  elseif i_type == 7 then  -- array
    return 'array', i_value
  elseif i_type == 8 then  -- dictionary
    return 'dict', i_value
  elseif i_type == 9 then  -- stream
    return 'leaf', 'STREAM'
  elseif i_type == 10 then
    local ref_type, ref_value = pdfe.getfromreference(i_value)
    return helper(ref_type, ref_value)
  else
    return 'leaf', nil
  end
end

function print_dict(pdfe_dict, max_depth, depth, in_array)
  local max_depth = max_depth or 3
  local depth = depth or 0
  local ind

  if depth > max_depth then
    if in_array then
      print(string.format('%s%s', string.rep('  ', depth - 1) .. '- ', pdfe_dict))
    else
      print(string.format('%s%s', string.rep('  ', depth), pdfe_dict))
    end
  else
    for i = 1, #pdfe_dict do
      if i == 1 and in_array then
        ind = string.rep('  ', depth - 1) .. '- '
      else
        ind = string.rep('  ', depth)
      end
      local i_key, i_type, i_value = pdfe.getfromdictionary(pdfe_dict, i)
      local node, str_or_value = helper(i_type, i_value)
      if node == 'leaf' then
        print(string.format('%s%s: %s', ind, i_key, str_or_value))
      elseif node == 'dict' then
        print(string.format('%s%s:', ind, i_key))
        print_dict(str_or_value, max_depth, depth + 1)
      elseif node == 'array' then
        print(string.format('%s%s:', ind, i_key))
        print_array(str_or_value, max_depth, depth + 1)
      end
    end
  end
end

function print_array(pdfe_array, max_depth, depth)
  local max_depth = max_depth or 3
  local depth = depth or 0
  local ind = string.rep('  ', depth)

  if depth > max_depth then
    print(string.format('%s%s', ind, pdfe_array))
  else
    for i = 1, #pdfe_array do
      local i_type, i_value = pdfe.getfromarray(pdfe_array, i)
      local node, str_or_value = helper(i_type, i_value)
      if node == 'leaf' then
        print(string.format('%s- %s', ind, str_or_value))
      elseif node == 'dict' then
        print_dict(str_or_value, max_depth, depth + 1, true)
      elseif node == 'array' then
        -- no support for 2-dim array
        print(string.format('%s- %s', ind, str_or_value))
      end
    end
  end
end

ここで定義した print_dict() 関数を使用すると,第1引数に渡した辞書の内容を,第2引数で指定した階層まで表示します.上限を設定しないと出力が大変なことになってしまうので,第2引数を省略した場合は3階層目まで走査するようになっています.参照は自動で解決して,参照先の内容を表示するようにしています.

なお,この簡易な実装にはいくつか制限があります.まずストリームの内容は取得せず,単に STREAM と表示します.また多次元の配列は,単純に表示方法を思い付かなかったのと,実際のデータに2次元以上の配列の例が見当たらなかったので対応していません.

試しに LuaTeX リファレンスマニュアルについてインフォ辞書とカタログ辞書の構造を表示させてみます.カタログ辞書については長くなるので2階層までに抑えています.

print('* The info table')
print_dict(info)

print('* The catalog table')
print_dict(catalog, 2)
実行結果
* The info table
ConTeXt.Jobname: "luatex"
ConTeXt.Support: "contextgarden.net"
ConTeXt.Time: "2019-11-08 10:03"
ConTeXt.Url: "www.pragma-ade.com"
ConTeXt.Version: "2019.07.04 12:29"
CreationDate: "D:20191108100340+01'00'"
Creator: "feff004c0075006100540065005800200031002e00310031002000370032003200330020002b00200043006f006e00540065005800740020004d006b0049005600200032003000310039002e00300037002e00300034002000310032003a00320039"
ID: "luatex | 2019-11-08T10:03:40+01:00"
ModDate: "D:20191108100340+01'00'"
Producer: "LuaTeX-1.11"
TeX.Support: "tug.org"
Title: "feff006c00750061007400650078"
Trapped: "False"
PTEX.FullBanner: "This is LuaTeX, Version 1.11.2 (TeX Live 2020/dev)"
* The catalog table
Type: "Catalog"
Pages:
  Type: "Pages"
  Count: 294
  Kids:
    - <pdfe.dictionary 0x10b734655>
    - <pdfe.dictionary 0x10b7347f2>
    - <pdfe.dictionary 0x10b73498f>
Lang: "en"
Metadata: STREAM
Names:
  Dests:
    Kids:
      <pdfe.array 0x10b71e52d>
    Limits:
      <pdfe.array 0x10b71e5bd>
  Type: "Names"
OCProperties:
  D:
    AS:
      <pdfe.array 0x10b734d41>
    BaseState: "ON"
    Name: "Document"
    OFF:
      <pdfe.array 0x10b734dd0>
    ON:
      <pdfe.array 0x10b734df3>
    Order:
      <pdfe.array 0x10b734e69>
  OCGs:
    - <pdfe.dictionary 0x10b71e8fd>
    - <pdfe.dictionary 0x10b71ebc2>
    - <pdfe.dictionary 0x10b71ee88>
    - <pdfe.dictionary 0x10b71f151>
    - <pdfe.dictionary 0x10b71f418>
Outlines:
  Count: 20
  First:
    A:
      <pdfe.dictionary 0x10b6a16bf>
    Next:
      <pdfe.dictionary 0x10b6a19ca>
    Parent:
      <pdfe.dictionary 0x10b6f3dc5>
    Title: "feff0049006e00740072006f00640075006300740069006f006e"
  Last:
    A:
      <pdfe.dictionary 0x10b6f3bd0>
    Parent:
      <pdfe.dictionary 0x10b6f3dc5>
    Prev:
      <pdfe.dictionary 0x10b6f3ad7>
    Title: "feff0053007400610074006900730074006900630073"
  Type: "Outlines"
PageLabels:
  Nums:
    - 0
    - <pdfe.dictionary 0x10b7350be>
    - 1
    - <pdfe.dictionary 0x10b735145>
    - 2
    - <pdfe.dictionary 0x10b7351cc>
PageLayout: "TwoColumnRight"
PageMode: "UseNone"
Version: "1.7"
ViewerPreferences:
  FitWindow: true

おわりに

個人的な感想ですが,pdfe ライブラリのインターフェースは総じてかなり直感的で,使いやすい印象です.かつて doraTeX さんが Mac 環境で PDF のページ数をカウントする9(+1)通りの手法についてまとめられており,その中で pdfTeX を用いた手法も挙げられていましたが,とりわけ「PDF のページ数をカウントする」ぐらいの単純な目的であれば pdfe を備える LuaTeX も有力な選択肢になりそうです.TeX Live さえあればどのプラットフォームでも同じ使い勝手で利用可能であることを考えると,TeX 系開発者にとってはかなり魅力的な PDF 解析ツールだと思います.


  1. 本稿の執筆に際して,LuaTeX リファレンスの pdfe ライブラリの解説には複数の致命的な間違いが見つかりましたが,いずれも上流に報告済みなのでそのうち修正されることでしょう. 

  2. Lua 5.3 以降では整数型が導入されています.math.type() 関数の戻り値は pdfe.getinteger() 関数で取得した値は integerpdfe.getnumber() 関数で取得した値は float となるので区別できます.なお TeX Live 2019 に収録されている LuaTeX に搭載されている Lua 処理系は 5.3.5 なので,この方法が利用可能です. 

  3. 厳密には Lua に配列はありませんので,もちろん実体は連番の整数をキーとするテーブルです.以下でも「Lua の配列」と書いた場合は同様. 

  4. 各配列には後述する pdfe.getfromdictionary() の2つ目以降の戻り値である「データ型(数値)」,「値」,「詳細」がこの順に格納されています. 

  5. pdfe.getfromdictionary() は取り出したデータ型によってはさらに4つ目の戻り値として detail と呼ばれる付加情報を返しますが,本稿ではその解説は省略します. 

  6. ここまで pdfe.getナンチャラ() 関数の第2引数にはキーを指定すると説明してきましたが,いずれの関数についても,辞書の値を取得する際キーの代わりにインデックスを渡すこともできます. 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away