Python, PyPDF2でPDFの作成者やタイトルなどを取得・削除・変更

PythonのサードパーティライブラリPyPDF2を使うと、PDFファイルのメタデータ(作成者、タイトルなど)の取得や削除、変更ができます。

mstamy2/PyPDF2: A utility to read and write PDFs with Python

ここでは以下の項目について説明します。

PyPDF2のインストール PDFファイルのメタデータの項目 PDFファイルのメタデータを取得 PDFファイルのメタデータを削除 メタデータをすべて削除 メタデータを選択して削除 PDFファイルのメタデータを追加・変更 パスワードが設定されたPDFファイルの処理 XMPによるメタデータを取得(PDF 2.0)

サンプルで使用しているPDFファイルは以下のリンクから。暗号化されているファイルのパスワードはすべてpassword。

python-snippets/notebook/data/src/pdf

すべてのPDFファイルに対して動作を保証するものではない。

PyPDF2のインストール

PyPDF2は外部ライブラリに依存していません。pip(pip3)やcondaでインストールできます。

$ pip install PyPDF2

以下のサンプルコードで使用しているPyPDF2のバージョンは1.26.0。 クラスやメソッドなどの詳細は公式ドキュメントを参照。

PyPDF2 Documentation — PyPDF2 1.26.0 documentation

IssueやPull Requestが溜まっており活発に開発されているという状況ではないが、シンプルなPDFファイルの処理であれば問題ない。

mstamy2/PyPDF2: A utility to read and write PDFs with Python

PDFファイルのメタデータの項目

PDFファイルの規格はISOで標準化されています。

ISO 32000-1 (PDF 1.7) – PDF Association ISO 32000-2 (PDF 2.0) – PDF Association

ISO 32000-1 (PDF 1.7)は無料で公開されており、メタデータについては下記ファイルの「14.3 Metadata」(P548)で説明されています。

PDF32000_2008.pdf

メタデータはMetadata StreamsかDocument Information Dictionaryに格納されます。Document Information Dictionaryのメタデータの項目は以下の通り。

Title: タイトル Author: 作成者 Subject: サブジェクト(主題) Keyword: キーワード Creator: オリジナル文書の作成ツール Producer: 変換ツール Trapped: トラッピングされているか

これらの項目は必須ではなく、また、その他の独自の項目を追加することもできます。 Author, Creator, Producerは似たような項目名だが、文書を作成した個人や団体の名前が入るのがAuthorで、Creator, ProducerにはPDFファイルの作成・変換に使用されたソフトの名前が入る。具体的な例は後述します。 なお、2017年7月に公開されたISO 32000-2 (PDF 2.0)でメタデータに関する仕様が変更され、メタデータはExtensible Metadata Platform(XMP)に格納するようになりました。

PDF 2.0: The worldwide standard for electronic documents has evolved – PDF Association

以下のサンプルコードでは、2019年1月時点で多くのファイルが対応しているPDF 1.7以前の仕様に対する処理をメインで説明します。PDF 2.0に対する処理は最後に少しだけ述べる。

PDFファイルのメタデータの取得

PyPDF2のPdfFileReaderクラスのdocumentInfo属性で、PDFファイルのDocument Information Dictionaryの情報を抽出し取得できます。 PdfFileReaderはコンストラクタにPDFファイルのパスを指定して生成します。documentInfo属性で取得できるのはPyPDF2.pdf.DocumentInformationクラスのオブジェクト。

import PyPDF2

pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')

print(type(pdf.documentInfo))
# <class 'PyPDF2.pdf.DocumentInformation'>

print(isinstance(pdf.documentInfo, dict))
# True

PyPDF2.pdf.DocumentInformationクラスは辞書(dict)のサブクラスなので、[キー名]で値を取得したりkey()などのメソッドを使ったりできます。

ファイルによっては、そのままprint()で出力すると値がIndirectObject(...)となる場合があるが、キーを指定すると中身が確認できます。なお、このサンプルファイルはMacのKeynoteで作成、PDFに変換したもの。

print(pdf.documentInfo)
# {'/Title': IndirectObject(33, 0), '/Producer': IndirectObject(34, 0), '/Creator': IndirectObject(35, 0), '/CreationDate': IndirectObject(36, 0), '/ModDate': IndirectObject(36, 0)}

print(pdf.documentInfo['/Title'])
# sample1

for k in pdf.documentInfo.keys():
    print(k, ':', pdf.documentInfo[k])
# /Title : sample1
# /Producer : macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext
# /Creator : Keynote
# /CreationDate : D:20190114072947Z00'00'
# /ModDate : D:20190114072947Z00'00'

キー名は'/Title'のようにスラッシュ/が付き、最初が大文字なので気をつけてください。

PDFファイルのメタデータを削除

メタデータをすべて削除

以下のような流れでPDFファイルのメタデータを削除して保存できます。

元のPDFファイルからPdfFileReaderオブジェクトを生成 空のPdfFileWriterオブジェクトを作成 PdfFileReaderオブジェクトの中身をコピー PdfFileWriterオブジェクトをPDFファイルとして保存

PdfFileReaderとPdfFileWriterをそれぞれのコンストラクタから生成。

import PyPDF2

src_pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')
dst_pdf = PyPDF2.PdfFileWriter()


cloneReaderDocumentRoot()でドキュメントの内容をコピー。
dst_pdf.cloneReaderDocumentRoot(src_pdf)

cloneReaderDocumentRoot()ではメタデータはコピーされないので、このPdfFileWriterをwrite()メソッドでPDFファイルとして保存すればメタデータの情報は削除されます。 PdfFileWriterのwrite()の引数はパス文字列ではなくファイルオブジェクトである必要があるのでopen()を使います。wbで書き込み用のバイナリファイルとしてオープンします。

with open('data/temp/sample1_no_meta.pdf', 'wb') as f:
    dst_pdf.write(f)

出力ファイルのメタデータを確認すると以下のようになります。Producer(変換ツール)の項目がPyPDF2となり、それ以外のメタデータが削除されていることが確認できます。

print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': 'PyPDF2'}

Producerの項目を変更したい場合はPdfFileWriterのaddMetadata()メソッドを使います。空にしたい場合は以下の通り。Producerの項目そのものを削除することはたぶん出来ない。

dst_pdf.addMetadata({'/Producer': ''})

with open('data/temp/sample1_no_meta.pdf', 'wb') as f:
    dst_pdf.write(f)

print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': ''}

関数化すると以下のようになります。

def remove_all_metadata(src_path, dst_path, producer=''):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)
    dst_pdf.addMetadata({'/Producer': producer})
    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

remove_all_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': ''}

例では、関数のデフォルト引数を使って、Producerの項目にはデフォルトで空文字列''が入るようにしています。

メタデータを選択して削除

メタデータの項目を選択して削除したい場合は、元のPDFファイルのdocumentInfo属性を取得して任意の項目を削除してからaddMetadata()メソッドで追加すればOKです。 documentInfo属性の値がIndirectObject(...)である場合、そのままaddMetadata()メソッドの引数に指定するとエラーになるため、ここでは辞書内包表記で新たな辞書を生成する(もっといい方法があるかもしれない)。

src_pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')
dst_pdf = PyPDF2.PdfFileWriter()

d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.keys()}

print(d)
# {'/Title': 'sample1', '/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Creator': 'Keynote', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'"}

上述のようにPyPDF2.pdf.DocumentInformationは辞書(dict)のサブクラスなのでpop()やdelで要素を削除できます。例としてCreator, Producerを削除します。

d.pop('/Creator')
d.pop('/Producer')

print(d)
# {'/Title': 'sample1', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'"}

これをPdfFileWriterオブジェクトのaddMetadata()メソッドで追加し、最後にwrite()メソッドを使用してpdfファイルとして保存します。

dst_pdf.addMetadata(d)

with open('data/temp/sample1_remove_meta.pdf', 'wb') as f:
    dst_pdf.write(f)

出力ファイルのメタデータを確認すると以下のようになります。Producerの項目が削除された場合はデフォルトのPyPDF2が使われる。任意の値を設定したい場合はaddMetadata()メソッドに指定する辞書に要素を追加すれば問題ありません。

print(PyPDF2.PdfFileReader('data/temp/sample1_remove_meta.pdf').documentInfo)
# {'/Producer': 'PyPDF2', '/Title': 'sample1', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'"}

関数化すると以下のようになります。

def remove_metadata(src_path, dst_path, *args, producer=''):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)

    d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.keys()
         if key not in args}

    d.setdefault('/Producer', producer)

    dst_pdf.addMetadata(d)
    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

可変長引数と辞書のsetdefault()メソッドを利用しています。

Producerの項目が残っている場合はその値が、削除した場合はデフォルトでは空文字列''が設定されます。キーワード引数producerで任意の値を設定することもできます。

remove_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf',
                '/Creator', '/ModDate', '/CreationDate')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Title': 'sample1'}

remove_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf',
                '/Creator', '/ModDate', '/CreationDate', '/Producer')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': '', '/Title': 'sample1'}

remove_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf',
                '/Creator', '/ModDate', '/CreationDate', '/Producer', producer='XXX')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': 'XXX', '/Title': 'sample1'}

削除するメタデータの項目ではなく残すメタデータの項目を指定する場合は以下のようになります。辞書内包表記の条件を変更するだけです。こっちのほうが実用的かもしれない。

def select_metadata(src_path, dst_path, *args, producer=''):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)

    d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.keys()
         if key in args}

    d.setdefault('/Producer', producer)

    dst_pdf.addMetadata(d)
    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

使用例は以下の通り。

select_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf',
                '/Title', '/ModDate')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': '', '/Title': 'sample1', '/ModDate': "D:20190114072947Z00'00'"}

select_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_no_meta.pdf',
                '/Title', '/Producer')
print(PyPDF2.PdfFileReader('data/temp/sample1_no_meta.pdf').documentInfo)
# {'/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Title': 'sample1'}

PDFファイルのメタデータを追加・変更

メタデータの項目を選択して追加したり変更したい場合は、元のPDFファイルのdocumentInfo属性を取得して所望のキーの値を追加・変更してからaddMetadata()メソッドで追加すればOKです。 基本的には削除の場合と同じ流れになります。documentInfoで取得したオブジェクトを操作するだけです。

import PyPDF2

src_pdf = PyPDF2.PdfFileReader('data/src/pdf/sample1.pdf')
dst_pdf = PyPDF2.PdfFileWriter()

dst_pdf.cloneReaderDocumentRoot(src_pdf)

d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.keys()}

d['/Title'] = 'new title'
d['/Author'] = 'new author'
d['/XXX'] = 'special data'

dst_pdf.addMetadata(d)

with open('data/temp/sample1_new_meta.pdf', 'wb') as f:
    dst_pdf.write(f)

print(PyPDF2.PdfFileReader('data/temp/sample1_new_meta.pdf').documentInfo)
# {'/Producer': 'macOS バージョン10.14.2(ビルド18C54) Quartz PDFContext', '/Title': 'new title', '/Creator': 'Keynote', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'", '/Author': 'new author', '/XXX': 'special data'}

関数化すると以下のようになります。追加・変更したいメタデータの情報を辞書で引数に指定します。 元のメタデータを保持する場合です。辞書のupdate()メソッドを使います。

def update_metadata(src_path, dst_path, metadata):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)

    d = {key: src_pdf.documentInfo[key] for key in src_pdf.documentInfo.keys()}
    d.update(metadata)

    dst_pdf.addMetadata(d)
    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

update_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_new_meta.pdf',
                {'/Title': 'new title', '/Author': 'new author', '/Producer': ''})
print(PyPDF2.PdfFileReader('data/temp/sample1_new_meta.pdf').documentInfo)
# {'/Producer': '', '/Title': 'new title', '/Creator': 'Keynote', '/CreationDate': "D:20190114072947Z00'00'", '/ModDate': "D:20190114072947Z00'00'", '/Author': 'new author'}

元のメタデータを削除する場合です。

def set_metadata(src_path, dst_path, metadata):
    src_pdf = PyPDF2.PdfFileReader(src_path)
    dst_pdf = PyPDF2.PdfFileWriter()
    dst_pdf.cloneReaderDocumentRoot(src_pdf)
    dst_pdf.addMetadata(metadata)
    with open(dst_path, 'wb') as f:
        dst_pdf.write(f)

set_metadata('data/src/pdf/sample1.pdf', 'data/temp/sample1_new_meta.pdf',
             {'/Title': 'new title', '/Author': 'new author', '/Producer': ''})
print(PyPDF2.PdfFileReader('data/temp/sample1_new_meta.pdf').documentInfo)
# {'/Producer': '', '/Title': 'new title', '/Author': 'new author'}

パスワードが設定されたPDFファイルの処理

パスワード付きの暗号化されたPDFファイルの場合、これまでのサンプルコードではエラーとなります。 PdfFileReaderオブジェクトを生成したあとでdecrypt()メソッドで復号する必要があります。

src_pdf = PyPDF2.PdfFileReader(src_path)
src_pdf.decrypt('password')

また、保存するPDFファイルにパスワードを掛ける場合はPdfFileWriterオブジェクトのwrite()で保存する前にencrypt()メソッドを使います。

dst_pdf.decrypt('password')

パスワードについての詳細は暗号化アルゴリズムはRC4のみに対応しAESに対応していないため、最近のソフトで暗号化されたファイルは解除できない場合が多いといった制約もある(バージョン1.26.0時点)。

XMPによるメタデータを取得(PDF 2.0)

上述のように、2017年7月に公開されたISO 32000-2 (PDF 2.0)でメタデータに関する仕様が変更され、メタデータはExtensible Metadata Platform(XMP)に格納するようになりました。 以下のレポジトリにあるPDF 2.0のサンプルファイルを使って、XMPで格納されたデータを取得する例を紹介します。

pdf-association/pdf20examples: PDF 2.0 example files https://github.com/pdf-association/pdf20examples/raw/master/Simple%20PDF%202.0%20file.pdf

documentInfo属性はNone。

import PyPDF2

pdf = PyPDF2.PdfFileReader('data/temp/Simple PDF 2.0 file.pdf')

print(pdf.documentInfo)
# None

XMPのデータはxmpMetadata属性で取得できます。xmpMetadata属性はXmpInformationクラス。詳細は以下のドキュメント参照。

The XmpInformation Class — PyPDF2 1.26.0 documentation

xmpMetadata属性から各種の情報を取得できます。

print(type(pdf.xmpMetadata))
# <class 'PyPDF2.xmp.XmpInformation'>

print(pdf.xmpMetadata.dc_title)
# {'x-default': 'A simple PDF 2.0 example file'}

print(pdf.xmpMetadata.pdf_keywords)
# PDF 2.0 sample example

print(pdf.xmpMetadata.xmp_metadataDate)
# 2017-07-11 07:55:11

PdfFileWriterクラスのドキュメントを読んだ限りaddMetadata()に相当するようなメソッドはないので、新たなXMPを追加することは出来ない模様(たぶん)。

Last Updated: 6/26/2019, 10:34:03 PM