저수준 인터페이스

PDF 파일에 상당히 낮은 수준으로 액세스하고 조작할 수 있는 많은 메서드가 있습니다. 인정하건대, “낮은 수준”과 “일반” 기능 간의 명확한 구분이 항상 가능한 것은 아니며 개인 취향에 따라 다를 수 있습니다.

이전에 낮은 수준으로 간주되었던 기능이 나중에 일반 인터페이스의 일부로 평가될 수도 있습니다. 이것은 v1.14.0에서 Tools 클래스에 대해 발생했습니다 – 이제 Classes 장에서 항목으로 찾을 수 있습니다.

문서의 어느 장에서 찾고 있는 것을 찾을 수 있는지는 문서화의 문제일 뿐입니다. 모든 것이 사용 가능하며 항상 동일한 인터페이스를 통해 제공됩니다.


xref 테이블을 반복하는 방법

PDF의 xref 테이블은 파일에 정의된 모든 객체의 목록입니다. 이 테이블은 수천 개의 항목을 쉽게 포함할 수 있습니다 – 예를 들어 Adobe PDF 참조 에는 127,000개의 객체가 있습니다. 테이블 항목 “0”은 예약되어 있으며 건드리면 안 됩니다. 다음 스크립트는 xref 테이블을 반복하고 각 객체의 정의를 인쇄합니다:

>>> xreflen = doc.xref_length()  # length of objects table
>>> for xref in range(1, xreflen):  # skip item 0!
        print("")
        print(f"object {xref} (stream: {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 는 객체의 xref 를 인수로 사용하여 Document.xref_stream() 메서드를 통해 객체의 스트림을 읽을 수 있게 합니다. 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() 은 스트림을 자동으로 압축 해제하여 bytes 객체로 반환합니다 – 그리고 Document.update_stream() 은 유리한 경우 자동으로 압축합니다.


페이지 콘텐츠 처리 방법

PDF 페이지는 0개 또는 여러 개의 contents 객체를 가질 수 있습니다. 이것들은 페이지에서 무엇어디에 그리고 어떻게 나타나는지(텍스트 및 이미지와 같은) 설명하는 스트림 객체입니다. 이것들은 Adobe PDF 참조 의 643페이지 “APPENDIX A - Operator Summary” 장에 설명된 특수 미니 언어로 작성됩니다.

모든 PDF 리더 애플리케이션은 페이지의 의도된 모양을 재현하기 위해 콘텐츠 구문을 해석할 수 있어야 합니다.

여러 contents 객체가 제공되는 경우, 여러 개의 연결로 제공된 것과 정확히 동일한 방식으로 지정된 순서로 해석되어야 합니다.

여러 contents 객체를 갖는 것에 대한 좋은 기술적 논거가 있습니다:

  • 하나의 큰 객체를 유지하는 것(각 변경에 대해 읽기, 압축 해제, 수정, 재압축 및 다시 쓰기를 수반함)보다 새로운 contents 객체를 추가하는 것이 훨씬 쉽고 빠릅니다.

  • 증분 업데이트로 작업할 때 수정된 큰 contents 객체는 업데이트 델타를 부풀려서 증분 저장의 효율성을 쉽게 무효화할 수 있습니다.

예를 들어, PyMuPDFPage.insert_image(), Page.show_pdf_page()Shape 메서드에서 새로운 작은 contents 객체를 추가합니다.

그러나 단일 contents 객체가 유리한 상황도 있습니다: 여러 개의 작은 객체보다 해석하기 쉽고 압축률이 더 높습니다.

다음은 페이지의 여러 콘텐츠를 결합하는 두 가지 방법입니다:

>>> # 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 참조 42페이지를 참조하세요. 다음은 개요입니다:

유형

Size

int

교차 참조 테이블의 항목 수 + 1.

Prev

int

이전 xref 섹션으로의 오프셋(증분 업데이트를 나타냄).

Root

dictionary

(간접) 카탈로그에 대한 포인터. 이전 섹션을 참조하세요.

Encrypt

dictionary

암호화 객체에 대한 포인터(암호화된 파일만 해당).

Info

dictionary

(간접) 정보(메타데이터)에 대한 포인터.

ID

array

두 개의 바이트 문자열로 구성된 파일 식별자.

XRefStm

int

교차 참조 스트림의 오프셋. Adobe PDF 참조 49페이지를 참조하세요.

PyMuPDF 에서 Document.pdf_trailer() 또는 동등하게 유효한 xref 번호 대신 -1을 사용하는 Document.xref_object() 를 통해 이 정보에 액세스합니다.

>>> 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 사양을 준수하는지 확인하는 것은 귀하의 책임입니다. 특히 (유니코드) 문자열이어야 합니다. 자세한 내용과 주의사항은 Adobe PDF 참조 의 섹션 14.3(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” 은 Python의 None 에 해당하는 PDF입니다. 해당 값을 가진 키는 지정되지 않은 것으로 처리되며 가비지 수집에서 물리적으로 제거됩니다.


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(f"{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 에 해당합니다. 부울 truefalse 도 유사합니다.

  • 페이지 정의에 회전을 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 딕셔너리는 계층적으로 중첩될 수 있습니다. 다음 페이지 객체 정의에서 FontXObject 는 모두 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 ]
          >>
        >>
      >>
    >>
    
  • 키 값을 설정할 때 기본 PDF 구문 검사 가 MuPDF에 의해 수행됩니다. 예를 들어 새 키는 딕셔너리 아래에만 생성할 수 있습니다. 다음은 이전에 생성된 배열 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.