기술 문서

PDF 메타데이터, 개요 및 주석 설명

페이지 설명(page description)을 벗겨내면 아무도 인쇄하지 않지만 모든 독자, 인덱서, 아카이브 시스템이 의존하는 얇은 구조 층이 남습니다. 페이지 객체는 자신이 속한 장(chapter), 이를 작성한 저자, 또는 다른 곳으로 연결되는 각주에 대해 아무것도 모릅니다. 이러한 정보는 한 단계 위인 문서 카탈로그에 연결된 세 가지 구조(메타데이터 스트림, 개요 트리, 페이지별 주석 배열)에 존재합니다. 이들은 실수하기 쉬운 공통점을 가지고 있습니다. 어느 것도 페이지에 보이는 표시를 남기지 않기 때문에, 파일이 완벽하게 렌더링되더라도 책갈피가 없거나 자신의 저자 필드와 모순되거나 더 이상 존재하지 않는 페이지 객체를 링크로 가리키고 있을 수 있습니다

이것은 PDF 라이브러리가 문서 속성, 책갈피 API, 링크 또는 주석 호출로 노출하는 계층이며, 검색 크롤러가 문서의 내용을 결정하기 위해 읽는 계층입니다. 그 아래의 객체 모델은 PDF 문서 구조 연습에서 다룹니다. 여기서는 카탈로그에 연결된 내용에만 초점을 맞춥니다

세 가지 구조 모두 카탈로그에 연결됩니다. 이들을 함께 연결한 전체 카탈로그는 다음과 같습니다:

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
   /Outlines 3 0 R
   /Names << /EmbeddedFiles 4 0 R >>
   /Metadata 5 0 R
>>
endobj

4개의 항목, 4개의 독립적인 하위 시스템입니다. /Pages는 보이는 문서입니다; /Outlines는 책갈피 트리입니다; /Metadata는 XMP 스트림을 가리킵니다; /Names는 문서 전체의 이름 딕셔너리에 도달하며, 그 중에는 첨부 파일도 포함됩니다. 각각은 선택 사항이며, 이들 중 아무것도 찾지 못한 리더기도 페이지를 보여줍니다. 이러한 선택성(optionality) 때문에 페이지 내용만 이해하는 도구로 파일을 편집할 때 내비게이션 계층이 가장 먼저 손상됩니다

서로 불일치하는 두 가지 메타데이터 저장소

PDF는 문서 메타데이터를 두 곳에 동시에 저장하며, 두 곳의 내용이 다를 때 문제가 시작됩니다. 원래 메커니즘은 트레일러의 /Info가 참조하는 문서 정보 딕셔너리로, /Title, /Author, /Subject, /Keywords, /Creator, /Producer 및 두 개의 날짜를 위한 평면적인 키-값 쌍의 집합입니다. 이것은 간단하고 모든 뷰어가 이를 읽습니다. PDF 2.0은 두 번째 메커니즘인 XMP 메타데이터 스트림을 위해 이 중 대부분을 폐기(deprecate)했습니다

XMP는 독립적인 XML 문서로, RDF로 작성되며, 카탈로그가 /Metadata를 통해 접근하고 /Type /Metadata /Subtype /XML로 표시된 스트림으로 저장됩니다. PDF 객체 구조 내부에 묻혀 있는 정보 딕셔너리와 달리, XMP 패킷은 PDF에 대해 아무것도 모르는 도구가 자체적으로 추출하고 구문 분석할 수 있도록 설계되었습니다. 대표적인 패킷은 다음과 같습니다:

5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" 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/"
        xmlns:xmp="http://ns.adobe.com/xap/1.0/"
        xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
      <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
      <dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
      <xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
      <xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
      <pdf:Producer>losLab PDF Library</pdf:Producer>
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj

이 블록의 세 가지 세부 사항이 메타데이터가 실제 도구와 접촉했을 때 살아남을지를 결정합니다. xpacket 처리 명령어는 장식이 아닙니다: 이들은 추출기가 더 큰 바이트 스트림 내에서 패킷을 찾을 수 있도록 프레임을 구성하며, 닫는 <?xpacket end="w"?>를 생략하는 작성자는 잘 열리지만 엄격한 검증기를 통과하지 못하는 파일을 생성합니다. 속성 데이터 유형도 중요합니다. dc:titlerdf:Alt에 래핑된 언어 대안인 반면, dc:creator는 정렬된 목록이며 rdf:Seq를 취합니다; 둘 중 하나를 단순 텍스트 노드로 내보내는 것은 가장 흔한 XMP 실수이며, 이를 허용하지 않는 하나의 뷰어를 만날 때까지는 대부분의 뷰어에서 허용됩니다. 네임스페이스 접두사는 관례적이지만 그들이 바인딩하는 URI는 규범적입니다: 파서는 접두사가 아닌 URI를 키로 사용합니다

두 저장소에 대한 엄격한 규칙은 그들이 일치해야 한다는 것입니다. 만약 /Info가 저자가 한 사람이라고 말하고 dc:creator가 다른 사람을 명명한다면, 여러분은 동일한 질문에 두 가지 방식으로 대답하는 문서를 배포한 것이며, 어느 답변이 이길지는 소비하는 도구가 어떤 필드를 읽느냐에 달려 있습니다. 라이브러리는 일반적으로 둘 다 기록해주지만, 여러분이 한쪽을 수동으로 편집하거나 다른 생성기에서 나온 파일을 병합하는 순간, 둘은 서로 어긋나기 시작합니다. 정보 딕셔너리는 레거시 호환성을 위한 것으로, XMP를 진실의 출처로 취급하고 독립적으로 패치하는 대신 하나의 값 세트에서 둘 다 재생성하십시오. PDF/A의 경우 이는 적합성 요구 사항이 됩니다: ISO 19005는 XMP를 의무화하고 이에 대응하는 XMP 속성과 모순되는 어떠한 Info 속성도 금지합니다

책갈피 패널 뒤의 개요 트리

뷰어가 책갈피 패널로 보여주는 것은 파일 내에서 문서 개요(document outline)라고 불리는 이중으로 연결된 딕셔너리 트리입니다. 카탈로그는 /Outlines를 통해 루트 개요 딕셔너리를 가리키고; 루트는 첫 번째와 마지막 최상위 항목을 가리키며; 모든 항목은 이웃 항목과 부모 항목으로 연결됩니다. 어디에도 책갈피 배열은 존재하지 않습니다. 전체 구조는 참조를 따라가며 재구성되는데, 이것이 단 하나의 끊어진 링크로 인해 오류 없이 전체 분기가 패널에서 사라질 수 있는 정확한 이유입니다

8 0 obj                                    % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj                                    % top-level: a chapter
<< /Title (Chapter 1: Results)
   /Parent 8 0 R /Count 2
   /First 12 0 R /Last 15 0 R >>
endobj
12 0 obj                                   % first child
<< /Title (Introduction)
   /Parent 9 0 R /Next 15 0 R
   /Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj                                   % second child, last sibling
<< /Title (Methodology)
   /Parent 9 0 R /Prev 12 0 R
   /Dest [3 0 R /Fit] >>
endobj

링크를 읽어보면 불변성이 분명해집니다. 모든 항목은 자신의 /Parent를 다시 가리킵니다. 형제 항목들은 /Prev/Next를 통해 체인을 형성하며, 첫 번째 항목은 /Prev를 생략하고 마지막 항목은 /Next를 생략합니다. 부모는 /First/Last를 통해 첫 번째와 마지막 자식을 명명하고, 그 사이의 자식들은 형제 체인을 걷는 것으로만 접근할 수 있습니다. 하나라도 잘못되면 실패는 조용히 일어납니다: 오래된 /Next는 챕터를 잘라내고, /Last가 체인을 종료하지 않는 부모는 항목을 고아 상태로 남기며, 뷰어는 도달할 수 있는 항목만을 렌더링합니다

/Count 필드는 사람들을 놀라게 하는 상태 정보를 담고 있습니다. 루트나 펼쳐진 항목에서는 현재 보이는 하위 항목의 수를 담고 있고; 접힌 항목에서는 음수이며, 그 절대값은 펼쳤을 때 나타날 하위 항목의 수입니다. 따라서 /Count는 트리에 대한 고정된 구조적 사실이 아니라 저장된 패널의 열림 또는 닫힘 상태이며, 이를 하드 코딩하여 양수 총계로 생성하는 생성기는 작성자가 닫아두려 했던 모든 분기를 다시 엽니다

각 항목은 어딘가를 가리킴으로써 자신의 위치를 얻습니다. /Title은 패널이 보여주는 것이고, /Dest는 클릭했을 때 도달하는 곳입니다. 대상은 위와 같이 항목 내에 인라인으로 존재할 수도 있고, 문서의 이름 딕셔너리를 통해 해석되는 이름일 수도 있습니다. 많은 책갈피와 링크가 같은 지점을 목표로 할 때는 이름 딕셔너리가 더 나은 선택입니다. 이동된 대상을 한 곳에서만 수정하면 되기 때문입니다. 라이브러리는 일반적으로 개요 루트 핸들과 자식 항목을 추가하는 메서드 뒤에 이 트리를 숨깁니다. HotPDF에서 문서는 THPDFDocOutlineObject 유형의 OutlineRoot를 노출하고, 항목을 추가할 때 /Prev, /Next, /Parent, /Count 링크를 여러분을 위해 엮어줍니다. 이것은 활용할 가치가 있습니다. 편집 과정을 거치며 이러한 불변성을 수동으로 유지하는 과정에서 개요가 깨지기 때문입니다

대상: 클릭이 향하는 곳의 문법

책갈피와 링크 주석 모두 대상을 가리키며, 대상은 단순한 페이지 번호 이상의 것입니다. 대상을 의미하는 것은 페이지 객체를 명명한 다음 두 번째 슬롯의 동사를 통해 뷰어가 그것을 어떻게 프레임화해야 하는지 지정하는 배열입니다. 가장 일반적이고 가장 오용되는 것은 [page /XYZ left top zoom] 형태의 /XYZ입니다. 세 개의 피연산자는 독립적이며, "독자가 설정한 대로 두기"라는 의미로 null이 될 수 있습니다. 따라서 [page /XYZ null null null]은 스크롤 위치나 확대/축소를 건드리지 않고 해당 페이지로 이동하며, 이는 일반적으로 "페이지로 이동" 링크에서 원하는 바입니다. 숫자는 기본 사용자 공간에 있으며, 페이지 콘텐츠가 사용하는 것과 동일한 좌표계로 왼쪽 아래에서 시작해 위로 갈수록 y가 증가합니다. 화면 레이아웃에 익숙한 작성자들은 반사적으로 위쪽에서부터 측정하여 독자를 페이지의 엉뚱한 끝으로 보냅니다

/Fit 제품군은 탄력성을 위해 정밀한 위치 지정을 포기합니다. [page /Fit]는 전체 페이지를 창에 맞게 조절하고, [page /FitH top]은 주어진 상단 가장자리로 페이지 너비에 맞추며, [page /FitR l b r t]는 보기를 채우기 위해 사각형을 확대합니다. 이들은 고정된 좌표가 아니라 페이지 기하학에서 배율을 계산하기 때문에, 페이지 크기가 조정된 후에도 /Fit 대상은 합리적인 동작을 유지하는 반면, 고정된 확대/축소가 포함된 /XYZ 대상은 독자를 여백만 바라보게 만들 수 있습니다. 목차의 경우, 추측된 확대/축소 비율이 있는 /XYZ보다 섹션의 상단 좌표가 있는 /FitH가 더 낫습니다

주석: 페이지 콘텐츠가 아닌 모든 상호 작용 요소

주석(Annotation)은 페이지 콘텐츠 스트림의 일부가 아니면서 페이지를 오버레이하는 객체입니다. 링크, 스티커 메모, 강조 표시, 폼 위젯, 파일 첨부 아이콘, 도장 등 모두 주석이며, 그들이 위치한 페이지의 /Annots 배열에 나열됩니다. 해당 배열에서 주석을 제거하면 기본 콘텐츠는 건드리지 않고 페이지에서 해당 주석만 제거됩니다. 이것이 핵심입니다: 주석은 그들이 놓인 표시(marks)와 분리된 편집 계층입니다

모든 주석은 작은 뼈대를 공유합니다. /Subtype은 종류를 명명하고, /Rect는 페이지 좌표로 경계 상자를 제공하며, /Contents는 접근 가능한 설명 역할을 겸하는 텍스트를 담고 있습니다. 링크 주석은 연구할 가치가 있는 경우로, 두 가지 형태로 제공되기 때문입니다: 단순한 대상, 그리고 액션(action)입니다

12 0 obj                                    % link to a destination
<< /Type /Annot /Subtype /Link
   /Rect [100 200 300 250]
   /Border [0 0 0]
   /Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj                                    % link that runs an action
<< /Type /Annot /Subtype /Link
   /Rect [50 50 200 100]
   /Border [0 0 0]
   /A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj

/Rect는 핫스팟입니다; 그 내부를 클릭하면 개요(outline)가 사용하는 것과 동일한 문법을 재사용하여 리더기를 대상으로 보냅니다. /Border [0 0 0]은 리더기가 링크 주위에 그리는 보기 흉한 기본 사각형을 억제하는 실질적인 역할을 합니다. 두 번째 형태는 단순한 /Dest/A 동작으로 바꾸며, 그 /S 서브타입이 행동을 선택합니다: 이 파일 내의 /GoTo, 다른 파일의 경우 /GoToR, 웹 주소의 경우 /URI, 외부 프로그램을 실행하는 /Launch. 이 마지막 것은 의심의 여지가 있습니다. 실행 파일을 시작하는 /Launch는 PDF를 악성코드의 매개체로 만드는 동작이므로, 규정을 준수하는 뷰어는 이를 차단하거나 큰 경고를 표시하며 대부분의 리더기에서 링크는 실패합니다. /URI/GoTo를 사용하고 /Launch는 그대로 두십시오

강조 표시나 스티커 메모와 같은 마크업 주석과 /Square 같은 도형 주석은 한 가지 문제를 추가합니다: 화면에 나타나는 모습이 그것들의 유형에 의해 함축되지 않는다는 것입니다. 도면 연산자를 담고 있는 양식(form) XObject를 참조하는 외관 스트림(appearance stream), 즉 /AP 항목을 사용해 외관을 고정하지 않으면 뷰어는 자체 버전을 렌더링합니다. 이를 생략하면 동일한 강조 표시가 두 리더기에서 다르거나 편집기를 거치기 전후에 다르게 보일 수 있습니다. 정확한 모양이 문서의 일부인 경우 항상 /AP를 제공하십시오. 참고로, 파일 첨부 기능은 포함된 파일 스트림 및 파일 명세 딕셔너리라는 동일한 메커니즘을 재사용하여 카탈로그의 /Names 아래에 있는 /FileAttachment 주석이나 /EmbeddedFiles 이름 트리로 표면화됩니다

이 계층이 손상되는 위치 및 발견 방법

이 모든 과정에서 반복적으로 발생하는 오류는 끊어진 참조(dangling reference)입니다. 카탈로그에 /Outlines 항목이 없거나 트리 중간에 형제 체인이 끊어지면 책갈피 표시가 멈추고; XMP 스트림에 /Type /Metadata /Subtype /XML 표시가 없거나 xpacket 래퍼 형식이 잘못되면 메타데이터가 무시됩니다. 모든 경우 페이지 콘텐츠는 정상이므로 대충 열어보면 올바른 것처럼 보이며 아무도 확인하지 않은 패널에서만 결함이 나타납니다

두 가지 간단한 습관이 이들 대부분을 잡아냅니다. 완성된 파일을 실제 뷰어에서 열고 책갈피 패널과 샘플 링크를 클릭하여 독자가 하는 방식으로 참조 그래프를 테스트하십시오. 그런 다음 별도의 도구로 메타데이터를 다시 읽고 클릭만으로는 결코 드러나지 않는 한 가지 불일치인 정보 딕셔너리와 XMP가 일치하는지 확인하십시오. 링크 관리를 소유한 라이브러리를 통해 이 계층을 생성하면 이러한 함정의 대부분이 절대 열리지 않습니다. Delphi 및 C++Builder용 HotPDF 컴포넌트는 문서 수준의 API를 통해 개요, 주석 및 메타데이터 구조를 노출하므로 여러분은 책갈피 계층 구조와 링크를 설명하고 라이브러리가 참조를 엮도록 맡길 수 있습니다. 이 구조들이 연결되는 객체 모델의 경우, PDF 파일 구조에 대한 기술 개요에서 이들이 의존하는 카탈로그와 상호 참조 테이블을 다룹니다