低レベルインターフェース#

PDFファイルにアクセスして操作するための多くのメソッドが低レベルで利用可能です。正直なところ、「低レベル」機能と「通常」機能の明確な区別は常にできるわけではなく、個人の好みによるところもあります。

また、以前は低レベルと考えられていた機能が後に通常のインターフェースの一部として評価されることもあります。例えば、バージョン1.14.0ではクラス Tools (ツール) についてそのようなことが起きており、現在では「Classes」のセクションで見つけることができます。

何を探しているかに関しては、ドキュメントのどのセクションにあるかはドキュメンテーションのみの問題です。すべての情報は同じインターフェースを介して常に利用可能です。


xref テーブルのイテレーション方法#

PDFの xref テーブルはファイル内で定義されているすべてのオブジェクトのリストです。このテーブルには非常に多くのエントリが含まれることがあります - 例えば Adobe PDFリファレンス マニュアルには127,000のオブジェクトが含まれています。テーブルのエントリ「0」は予約されており、触れてはいけません

>>> xreflen = doc.xref_length()  # length of objects table
>>> for xref in range(1, xreflen):  # skip item 0!
        print("")
        print("object %i (stream: %s)" % (xref, doc.xref_is_stream(xref)))
        print(doc.xref_object(xref, compressed=False))

これにより、以下の出力が生成されます:

object 1 (stream: False)
<<
    /ModDate (D:20170314122233-04'00')
    /PXCViewerInfo (PDF-XChange Viewer;2.5.312.1;Feb  9 2015;12:00:06;D:20170314122233-04'00')
>>

object 2 (stream: False)
<<
    /Type /Catalog
    /Pages 3 0 R
>>

object 3 (stream: False)
<<
    /Kids [ 4 0 R 5 0 R ]
    /Type /Pages
    /Count 2
>>

object 4 (stream: False)
<<
    /Type /Page
    /Annots [ 6 0 R ]
    /Parent 3 0 R
    /Contents 7 0 R
    /MediaBox [ 0 0 595 842 ]
    /Resources 8 0 R
>>
...
object 7 (stream: True)
<<
    /Length 494
    /Filter /FlateDecode
>>
...

PDFオブジェクトの定義は通常のASCII文字列です。


オブジェクトストリームの処理方法#

オブジェクトストリームの処理方法一部のオブジェクトタイプには、オブジェクト定義以外に追加のデータが含まれています。例として、画像、フォント、埋め込みファイル、またはページの外観を記述するコマンドが挙げられます。

これらのタイプのオブジェクトは「ストリームオブジェクト」と呼ばれます。PyMuPDFでは、メソッド Document.xref_stream() を使用して、オブジェクトの xref を引数としてオブジェクトのストリームを読み取ることができます。また、Document.update_stream() を使用して、ストリームの変更されたバージョンを書き戻すことも可能です。

次のスニペットが、PDFのすべてのストリームを読み取るためのものであると仮定します:

>>> xreflen = doc.xref_length() # number of objects in file
>>> for xref in range(1, xreflen): # skip item 0!
        if stream := doc.xref_stream(xref):
            # do something with it (it is a bytes object or None)
            # e.g. just write it back:
            doc.update_stream(xref, stream)

Document.xref_stream() は自動的にバイトオブジェクトとして展開されたストリームを返し、Document.update_stream() は必要に応じて自動的に圧縮されます。


ページ内容の処理方法#

PDFページにはゼロまたは複数の contents オブジェクトが存在できます。これらは、ページ上に何がどこにどのように表示されるかを記述するストリームオブジェクト(テキストや画像など)です。これらは、Adobe PDFリファレンス のページ643の「付録A - オペレーターサマリー」などで説明されている特別なミニ言語で記述されています。

すべてのPDFリーダーアプリケーションは、コンテンツの構文を解釈してページの意図した表示を再現できる必要があります。

複数の contents オブジェクトが提供される場合、それらは複数のコンテンツを連結した場合とまったく同じ方法で、指定された順序で解釈される必要があります。

複数の contents オブジェクトを持つメリットには、次のような良い技術的理由があります:

  • 新しい contents オブジェクトを追加するだけで、単一の大きなコンテンツオブジェクトを維持するよりもはるかに簡単で高速です(各変更のたびに読み取り、展開、変更、再圧縮、書き直しが必要です)。

  • 増分更新を使用する場合、修正された大きな contents オブジェクトは更新デルタを膨らませ、増分保存の効率を簡単に打ち消す可能性があります。

例えば、PyMuPDFは Page.insert_image()Page.show_pdf_page() 、および Shape(シェイプ) メソッドで新しい小さな contents オブジェクトを追加します。

ただし、単一の contents オブジェクトが有益な状況もあります。それは複数の小さなオブジェクトよりも解釈が容易で、圧縮が効果的です。

以下は、ページの複数のコンテンツを組み合わせる2つの方法です:

>>> # method 1: use the MuPDF clean function
>>> page.clean_contents()  # cleans and combines multiple Contents
>>> xref = page.get_contents()[0]  # only one /Contents now!
>>> cont = doc.xref_stream(xref)
>>> # this has also reformatted the PDF commands

>>> # method 2: extract concatenated contents
>>> cont = page.read_contents()
>>> # the /Contents source itself is unmodified

Page.clean_contents() は、contents オブジェクトを結合するだけでなく、ページのPDFオペレータ構文を修正し最適化し、ページのオブジェクト定義との整合性を保つためにも役立ちます。


PDFカタログへのアクセス方法#

これはPDFの中心的な("ルート")オブジェクトです。これは重要な他のオブジェクトに到達するための出発点として機能し、PDFのいくつかのグローバルオプションも含まれています:

>>> import pymupdf
>>> doc=pymupdf.open("PyMuPDF.pdf")
>>> cat = doc.pdf_catalog()  # get xref of the /Catalog
>>> print(doc.xref_object(cat))  # print object definition
<<
    /Type/Catalog                 % object type
    /Pages 3593 0 R               % points to page tree
    /OpenAction 225 0 R           % action to perform on open
    /Names 3832 0 R               % points to global names tree
    /PageMode /UseOutlines        % initially show the TOC
    /PageLabels<</Nums[0<</S/D>>2<</S/r>>8<</S/D>>]>> % labels given to pages
    /Outlines 3835 0 R            % points to outline tree
>>

注釈

字下げ、改行、コメントは説明のために挿入されており、通常は表示されません。PDFカタログの詳細については、 Adobe PDFリファレンス のページ71のセクション7.7.2を参照してください。


PDFファイルトレーラーへのアクセス方法#

PDFファイルのトレーラーは、ファイルの終わりに位置する dictionary です。特別なオブジェクトと、重要な他の情報へのポインタが含まれています。Adobe PDFリファレンス を参照してください(p. 42)。以下に概要を示します:

キー

タイプ

Value

int

クロスリファレンステーブル内のエントリ数 + 1 の数値。

Prev

int

前の xref セクションへのオフセット(増分更新を示す)。

Root

dictionary

(間接的) カタログへのポインタ。前のセクションを参照してください。

Encrypt

dictionary

(暗号化されたファイルのみ) 暗号化オブジェクトへのポインタ。

Info

dictionary

(間接的) 情報(メタデータ)へのポインタ。

ID

array

2つのバイト文字列からなるファイル識別子。

XRefStm

int

クロスリファレンスストリームのオフセット。Adobe PDFリファレンス を参照してください(p. 49)。

これらの情報には、PyMuPDFを使用して Document.pdf_trailer() または、同等の Document.xref_object() を使用して -1 の代わりに有効な xref 番号を指定することでアクセスします。

>>> import pymupdf
>>> doc=pymupdf.open("PyMuPDF.pdf")
>>> print(doc.xref_object(-1))  # or: print(doc.pdf_trailer())
<<
/Type /XRef
/Index [ 0 8263 ]
/Size 8263
/W [ 1 3 1 ]
/Root 8260 0 R
/Info 8261 0 R
/ID [ <4339B9CEE46C2CD28A79EBDDD67CC9B3> <4339B9CEE46C2CD28A79EBDDD67CC9B3> ]
/Length 19883
/Filter /FlateDecode
>>
>>>

XMLメタデータへのアクセス方法#

PDFには、標準のメタデータ形式に加えてXMLメタデータが含まれている場合があります。実際、ほとんどのPDFビューアや編集ソフトウェアは、PDFを保存する際に(Adobe、Nitro PDF、PDF-XChangeなど)この種の情報を追加します。

しかし、PyMuPDFはXMLの機能を持たないため、この情報を直接解釈または変更する方法はありません。ただし、XMLメタデータは stream オブジェクトとして格納されているため、適切なソフトウェアで読み取り、変更し、書き戻すことができます。

>>> xmlmetadata = doc.get_xml_metadata()
>>> print(xmlmetadata)
<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="3.1-702">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
...
omitted data
...
<?xpacket end="w"?>

あるXMLパッケージを使用して、XMLデータを解釈および/または変更し、それを保存し直すことができます。次の方法もPDFに以前にXMLメタデータがない場合にも機能します:

>>> # write back modified XML metadata:
>>> doc.set_xml_metadata(xmlmetadata)
>>>
>>> # XML metadata can be deleted like this:
>>> doc.del_xml_metadata()

PDFメタデータの拡張方法#

属性 Document.metadata は、すべての サポートされている ドキュメントタイプで同じ方法で機能するように設計されています。これは、固定されたキーと値のセットを持つPython辞書です。同様に、Document.set_metadata() は標準のキーのみを受け入れます。

しかし、PDFにはこのようにアクセスできない項目が含まれている場合があります。また、著作権などの追加情報を保存する理由もあるかもしれません。以下は、PyMuPDFの低レベル関数を使用して任意のメタデータ項目を処理する方法です。

例として、次のPDFの標準メタデータ出力をご覧ください:

# ---------------------
# standard metadata
# ---------------------
pprint(doc.metadata)
{'author': 'PRINCE',
 'creationDate': "D:2010102417034406'-30'",
 'creator': 'PrimoPDF http://www.primopdf.com/',
 'encryption': None,
 'format': 'PDF 1.4',
 'keywords': '',
 'modDate': "D:20200725062431-04'00'",
 'producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, '
             'AppendMode 1.1',
 'subject': '',
 'title': 'Full page fax print',
 'trapped': ''}

以下のコードを使用して、メタデータオブジェクトに保存されているすべてのアイテムを表示します:

# ----------------------------------
# metadata including private items
# ----------------------------------
metadata = {}  # make my own metadata dict
what, value = doc.xref_get_key(-1, "Info")  # /Info key in the trailer
if what != "xref":
    pass  # PDF has no metadata
else:
    xref = int(value.replace("0 R", ""))  # extract the metadata xref
    for key in doc.xref_get_keys(xref):
        metadata[key] = doc.xref_get_key(xref, key)[1]
pprint(metadata)
{'Author': 'PRINCE',
 'CreationDate': "D:2010102417034406'-30'",
 'Creator': 'PrimoPDF http://www.primopdf.com/',
 'ModDate': "D:20200725062431-04'00'",
 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb  9 '
                 "2015;12:00:06;D:20200725062431-04'00'",
 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, '
             'AppendMode 1.1',
 'Title': 'Full page fax print'}
# ---------------------------------------------------------------
# note the additional 'PXCViewerInfo' key - ignored in standard!
# ---------------------------------------------------------------

逆に、PDFにはプライベートなメタデータアイテムを保存することもできます。これらのアイテムがPDF仕様に準拠していることを確認する責任はあなたにあります。特に、これらは(Unicode)文字列である必要があります。詳細や注意事項については、Adobe PDFリファレンスのセクション14.3(p. 548)を参照してください:

what, value = doc.xref_get_key(-1, "Info")  # /Info key in the trailer
if what != "xref":
    raise ValueError("PDF has no metadata")
xref = int(value.replace("0 R", ""))  # extract the metadata xref
# add some private information
doc.xref_set_key(xref, "mykey", pymupdf.get_pdf_str("北京 is Beijing"))
#
# after executing the previous code snippet, we will see this:
pprint(metadata)
{'Author': 'PRINCE',
 'CreationDate': "D:2010102417034406'-30'",
 'Creator': 'PrimoPDF http://www.primopdf.com/',
 'ModDate': "D:20200725062431-04'00'",
 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb  9 '
                  "2015;12:00:06;D:20200725062431-04'00'",
 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, '
             'AppendMode 1.1',
 'Title': 'Full page fax print',
 'mykey': '北京 is Beijing'}

選択したキーを削除するには、doc.xref_set_key(xref, "mykey", "null") を使用します。次のセクションで説明されているように、文字列 "null" はPDFのバージョンでPythonの None に相当します。その値のキーは指定されていないものとして扱われ、ガベージコレクションで物理的に削除されます。


PDFオブジェクトの読み取りと更新方法#

選択したPDF dictionary キーにアクセスし、操作するための粒状で洗練された方法も存在します。

  • Document.xref_get_keys() は、xref のオブジェクトのPDFキーを返します:

    In [1]: import pymupdf
    In [2]: doc = pymupdf.open("pymupdf.pdf")
    In [3]: page = doc[0]
    In [4]: from pprint import pprint
    In [5]: pprint(doc.xref_get_keys(page.xref))
    ('Type', 'Contents', 'Resources', 'MediaBox', 'Parent')
    
  • 完全なオブジェクト定義と比較してください:

    In [6]: print(doc.xref_object(page.xref))
    <<
      /Type /Page
      /Contents 1297 0 R
      /Resources 1296 0 R
      /MediaBox [ 0 0 612 792 ]
      /Parent 1301 0 R
    >>
    
  • 単一のキーは、 Document.xref_get_key() を介して直接アクセスすることもできます。値は常に文字列であり、それを解釈するのに役立つタイプ情報が含まれています:

    In [7]: doc.xref_get_key(page.xref, "MediaBox")
    Out[7]: ('array', '[0 0 612 792]')
    
  • 以下は、上記のページキーの完全な一覧です:

    In [9]: for key in doc.xref_get_keys(page.xref):
    ...:        print("%s = %s" % (key, doc.xref_get_key(page.xref, key)))
    ...:
    Type = ('name', '/Page')
    Contents = ('xref', '1297 0 R')
    Resources = ('xref', '1296 0 R')
    MediaBox = ('array', '[0 0 612 792]')
    Parent = ('xref', '1301 0 R')
    
  • 未定義のキーの問い合わせは、('null', 'null') を返します - PDFオブジェクトタイプ null はPythonの None に対応します。 true および false も同様です。

  • ページ定義に新しいキーを追加して、その回転角を90度に設定しましょう(実際には Page.set_rotation() が存在することを知っているかと思いますが、そうですか?):

    In [11]: doc.xref_get_key(page.xref, "Rotate")  # no rotation set:
    Out[11]: ('null', 'null')
    In [12]: doc.xref_set_key(page.xref, "Rotate", "90")  # insert a new key
    In [13]: print(doc.xref_object(page.xref))  # confirm success
    <<
      /Type /Page
      /Contents 1297 0 R
      /Resources 1296 0 R
      /MediaBox [ 0 0 612 792 ]
      /Parent 1301 0 R
      /Rotate 90
    >>
    
  • このメソッドは、値を null に設定することで xref 辞書からキーを削除するためにも使用できます:次の方法は、ページから回転指定を削除します: doc.xref_set_key(page.xref, "Rotate", "null") 。同様に、ページからすべてのリンク、注釈、およびフィールドを削除するには、 doc.xref_set_key(page.xref, "Annots", "null") を使用します。Annots は定義上配列であるため、doc.xref_set_key(page.xref, "Annots", "[]") という文で空の配列を設定すると、同じ操作が実行されます。

  • PDF辞書は階層的に入れ子にすることができます。次のページオブジェクト定義では、Font (フォント)XObject は両方とも Resources のサブディクショナリです:

    In [15]: print(doc.xref_object(page.xref))
    <<
      /Type /Page
      /Contents 1297 0 R
      /Resources <<
        /XObject <<
          /Im1 1291 0 R
        >>
        /Font <<
          /F39 1299 0 R
          /F40 1300 0 R
        >>
      >>
      /MediaBox [ 0 0 612 792 ]
      /Parent 1301 0 R
      /Rotate 90
    >>
    
  • 上記の状況は、メソッド Document.xref_set_key()Document.xref_get_key() によってサポートされています。必要なキーを指すために、パスのような表記法を使用します。たとえば、上記の Im1 キーの値を取得するには、キー引数にその上位の辞書の完全なチェーン "Resources/XObject/Im1" を指定します:

    In [16]: doc.xref_get_key(page.xref, "Resources/XObject/Im1")
    Out[16]: ('xref', '1291 0 R')
    
  • パス表記法は、値を直接設定するためにも使用できます。以下を使用して、Im1 を異なるオブジェクトを指すように設定します:

    In [17]: doc.xref_set_key(page.xref, "Resources/XObject/Im1", "9999 0 R")
    In [18]: print(doc.xref_object(page.xref))  # confirm success:
    <<
      /Type /Page
      /Contents 1297 0 R
      /Resources <<
        /XObject <<
          /Im1 9999 0 R
        >>
        /Font <<
          /F39 1299 0 R
          /F40 1300 0 R
        >>
      >>
      /MediaBox [ 0 0 612 792 ]
      /Parent 1301 0 R
      /Rotate 90
    >>
    

    ここでは、何の意味的なチェックも行われないことに注意してください。PDFにxref 9999が存在しない場合、この段階では検出されません。

  • キーが存在しない場合、その値を設定することで新しく作成されます。さらに、中間のキーが存在しない場合も、必要に応じて自動的に作成されます。次の例では、既存の辞書 A の下にいくつかの階層下に配列 D を作成しています。中間の辞書 BC も自動的に作成されます:

    In [5]: print(doc.xref_object(xref))  # some existing PDF object:
    <<
      /A <<
      >>
    >>
    In [6]: # the following will create 'B', 'C' and 'D'
    In [7]: doc.xref_set_key(xref, "A/B/C/D", "[1 2 3 4]")
    In [8]: print(doc.xref_object(xref))  # check out what happened:
    <<
      /A <<
        /B <<
          /C <<
            /D [ 1 2 3 4 ]
          >>
        >>
      >>
    >>
    
  • キーの値を設定する際には、MuPDFによって基本的なPDF構文のチェックが行われます。たとえば、新しいキーは辞書の下にのみ作成できます。次の例では、以前に作成された配列 D の下に新しい文字列アイテム E を作成しようとしています:

    In [9]: # 'D' is an array, no dictionary!
    In [10]: doc.xref_set_key(xref, "A/B/C/D/E", "(hello)")
    mupdf: not a dict (array)
    --- ... ---
    RuntimeError: not a dict (array)
    
  • 同様に、上位のキーが「間接」オブジェクト、つまりxrefである場合、キーを作成することはできません。言い換えれば、xrefは直接的に変更できるが、それを参照する他のオブジェクトを通じて暗黙的に変更することはできません:

    In [13]: # the following object points to an xref
    In [14]: print(doc.xref_object(4))
    <<
      /E 3 0 R
    >>
    In [15]: # 'E' is an indirect object and cannot be modified here!
    In [16]: doc.xref_set_key(4, "E/F", "90")
    mupdf: path to 'F' has indirects
    --- ... ---
    RuntimeError: path to 'F' has indirects
    

注意

これらは専門家向けの機能です!有効なPDFオブジェクトやxrefなどが指定されているかどうかの検証はありません。他の低レベルメソッドと同様に、PDF全体またはその一部を利用不能にする可能性があるため注意が必要です。


This software is provided AS-IS with no warranty, either express or implied. This software is distributed under license and may not be copied, modified or distributed except as expressly authorized under the terms of that license. Refer to licensing information at artifex.com or contact Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, United States for further information.

Discord logo