기술 문서

PDF 논리적 객체 모델: 유형, 참조 및 구조

PDF 파일은 기본적으로 서로를 가리키는 객체들의 모음입니다. 압축, 상호 참조 기록, 바이트 오프셋을 제거하고 나면, 참조로 연결되고 리더기가 찾을 수 있는 단일 객체에 뿌리를 둔 소수의 유형화된 값들인 그래프만 남습니다. 텍스트 단락에서부터 포함된 글꼴, 디지털 서명에 이르기까지 PDF가 표현할 수 있는 모든 것은 여덟 가지 기본 객체 유형과 한 객체가 다른 객체를 참조할 수 있게 하는 규칙으로 구성됩니다. 이들을 이해하면 파일 형식의 나머지는 수수께끼가 아닌 조합의 결과로 읽힐 것입니다

이것은 ISO 32000-1 7.3절에 정의된 PDF의 논리적 계층이며, 물리적 파일 레이아웃(헤더, 본문, 상호 참조 테이블, 트레일러 등은 PDF 파일 구조에 대한 기술 개요의 주제입니다)의 한 단계 위에 위치합니다. 논리적 모델은 파싱된 바이트의 의미를 나타냅니다. 뷰어는 파일을 역방향으로 읽어 트레일러를 찾고, 이를 따라 루트로 이동하며, 그곳에서부터 객체가 객체를 참조하며 문서가 펼쳐집니다. 잘못된 페이지를 디버깅하거나, 파서를 작성하거나, 문서를 조립하는 라이브러리를 신뢰할 때 추론하게 되는 부분이 바로 이것입니다

여덟 가지 객체 유형, 그리고 그 외에는 없음

PDF는 정확히 여덟 가지 기본 객체 유형을 정의합니다. 문서의 모든 값은 이 중 하나이며, 이것이 광범위한 활용도에도 불구하고 파일 형식을 다루기 쉽게 유지하는 이유입니다

부울(Booleans)truefalse 키워드입니다. 이는 주석 인쇄 여부와 같은 플래그를 켜고 끕니다

숫자(Numbers)는 사양에서 단일 유형으로 취급하는 두 가지 형태를 가집니다: 42와 같은 정수 및 3.14 또는 -0.002와 같은 실수입니다. PDF에는 지수 표기법이 없으므로, 규격에 맞는 파일에서는 1e6을 절대 볼 수 없습니다. 좌표, 글꼴 크기, 회전 각도 등은 모두 숫자입니다

문자열(Strings)은 바이트의 시퀀스를 담으며, 괄호 안의 (Hello) 형태나 꺾쇠 괄호 안의 16진수인 <48656C6C6F> 형태로 작성됩니다. 두 표기법 모두 동일한 내용을 인코딩하며, 16진수는 괄호 안에 넣기 까다로운 바이트를 위한 탈출구 역할을 합니다. 문자열은 텍스트를 전달하지만, 기본적으로는 바이트이며 이는 ASCII를 벗어난 내용을 다루는 순간 중요해집니다

이름(Names)은 슬래시로 시작하는 기본(atomic) 토큰입니다: /Type, /Pages, /MediaBox. 이름은 문자열이 아닙니다; 딕셔너리 키나 열거형 값으로 사용되는 식별자이며, 두 이름은 바이트 단위로 일치할 때만 동일한 것으로 간주됩니다. 슬래시는 구문일 뿐 이름의 일부가 아닙니다. 이는 /Times-Roman(Times-Roman) 문자열을 상호 교환 가능한 것으로 취급하는 초보자들을 혼란스럽게 하지만, 파일 형식은 그렇지 않습니다

배열(Arrays)은 대괄호 안에 들어가는 순서가 지정된 이기종 목록입니다: [0 0 612 792]는 페이지 사각형이며, 배열은 다른 객체에 대한 참조를 포함하여 모든 유형을 자유롭게 혼합할 수 있습니다. 딕셔너리(Dictionaries)는 일등 공신입니다. <<>> 사이에 작성된 딕셔너리는 이름 키를 모든 유형의 값에 매핑하며, 페이지, 카탈로그, 글꼴, 주석 등 PDF의 거의 모든 의미 있는 구조는 자신이 무엇인지 선언하는 /Type 키를 가진 딕셔너리입니다

스트림(Streams)streamendstream 키워드 사이에 원시 바이트 꼬리가 있는 딕셔너리입니다. 딕셔너리는 바이트(길이, 그리고 바이트를 압축하는 FlateDecode와 같은 모든 필터)를 설명하고, 바이트는 인라인으로 위치하기에 너무 크거나 너무 이진 데이터인 페이로드를 전달합니다: 페이지 내용 명령어, 포함된 글꼴 프로그램, 이미지. 스트림은 PDF가 크거나 이진 형식의 데이터를 저장하는 곳입니다

여덟 번째 유형은 널(null) 객체, 즉 키워드 null입니다. 이는 키가 없는 것과는 다른 실제 값입니다. null로 설정된 딕셔너리 항목은 존재하지 않는 것으로 취급되며, 존재하지 않는 객체로 해석되는 참조도 오류를 발생시키는 대신 null을 반환합니다. 이러한 관대한 동작은 의도된 것입니다: 손상된 파일이 열기를 거부하는 대신 점진적으로 기능을 저하시킬 수 있게 합니다. 아홉 번째 유형은 없습니다; PDF가 표현하는 모든 것은 이 여덟 가지가 조합된 결과입니다

직접 값, 간접 객체, 참조

이 여덟 가지 유형 중 어느 것이든 두 가지 방식으로 나타날 수 있습니다. 직접(direct) 객체는 MediaBox 배열 내부의 612처럼 제자리에 작성됩니다. 간접(indirect) 객체는 다른 객체가 자신을 가리킬 수 있도록 식별자를 부여받습니다. 객체 번호와 생성 번호라는 두 개의 정수가 정의를 objendobj로 감쌉니다:

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

이것은 폰트 딕셔너리인 객체 12, 생성 0입니다. 파일의 다른 어디에서든, 다른 객체는 이를 간접 참조(indirect reference)로 가리킵니다: 동일한 두 개의 숫자 뒤에 키워드 R이 붙은 12 0 R입니다. 참조는 포인터입니다. 페이지의 리소스 딕셔너리에 /Font << /F1 12 0 R >>가 있을 때, 이는 페이지 안으로 글꼴 정의를 복사하지 않고 객체 12를 리소스 이름 /F1 뒤의 글꼴로 명명합니다

생성 번호는 삭제 및 재사용을 위해 존재합니다. 객체가 해제되고 그 슬롯이 재사용될 때 생성이 증가하므로, 오래된 12 0 R이 슬롯 12의 새로운 점유자로 해석되지 않습니다. 새로 작성된 파일은 대부분 생성 번호가 0이지만, 많이 편집된 파일은 더 높은 번호를 가질 수 있으며, 생성을 무시하는 파서는 결국 잘못된 객체를 읽게 됩니다

간접 지정은 PDF를 효율적이고 편집 가능하게 만듭니다. 하나의 글꼴, 이미지 또는 색상 공간을 한 번 정의하고 백 개의 페이지에서 참조할 수 있습니다. 작은 변경 사항은 파일을 다시 작성하는 대신 단일 객체를 대체하는 새 수정본으로 추가될 수 있습니다. 상호 참조 테이블은 객체 번호를 바이트 오프셋으로 변환하는 색인으로, 리더기가 스캔 없이 즉시 12 0 obj로 건너뛸 수 있게 해주지만, 이는 물리적 최적화입니다. 논리적으로 알아야 할 것은 12 0 R이 "12 0으로 식별된 객체"를 의미한다는 것뿐입니다

카탈로그: 모든 문서가 시작되는 곳

참조 해석은 어딘가에서 시작되어야 하며, 그 어딘가가 문서 카탈로그(document catalog)를 가리키는 트레일러의 /Root 항목입니다. 카탈로그는 객체 그래프의 루트이자 /Type /Catalog를 가진 딕셔너리입니다. 리더기는 트레일러를 먼저 발견하기 때문에 여기에 가장 먼저 도달하며, 거기서부터 문서의 모든 다른 부분은 참조를 따라 접근할 수 있습니다

카탈로그에는 엄격하게 요구되는 단 두 개의 항목만 있습니다: /Type과 페이지 트리의 루트에 대한 간접 참조인 /Pages입니다. 나머지는 선택 사항이며 내용 자체보다는 문서 전체의 동작을 설명합니다: /Outlines는 책갈피 트리를 가리키고, /Names는 문자열을 키로 하는 이름 트리를 보관하며, /Metadata는 XMP 메타데이터 스트림을 참조하고, /PageMode/PageLayout은 뷰어가 문서를 여는 방식을 제안합니다. 이들 중 어느 것도 페이지를 렌더링하는 데 필요하지 않습니다; 그들은 페이지 주변의 경험을 구성합니다. 카탈로그에 매달린 책갈피, 메타데이터, 주석 구조는 PDF 메타데이터, 책갈피 및 주석에 대한 기사에서 다룹니다

아래 다이어그램은 주변 파일 내부에서 객체 본문이 위치한 곳을 보여줍니다. 카탈로그와 페이지 트리는 본문 내부에 일반적인 간접 객체로 상주하며, 그 주위의 헤더, 상호 참조 테이블, 트레일러는 리더기가 그들을 찾을 수 있게 하는 물리적인 발판입니다

PDF 파일의 4가지 물리적 섹션 다이어그램: 버전 헤더, 카탈로그 및 페이지 트리를 포함한 문서 객체를 보관하는 본문, 객체 오프셋의 상호 참조 테이블, 그리고 루트를 가리키는 트레일러

페이지 트리: 균형 잡힌 페이지 계층 구조

/Pages에서부터 문서는 페이지 트리로 분기하며, 평면 목록 대신 그래프를 선택한 PDF의 장점이 여기서 발휘됩니다. 페이지는 단순한 시퀀스로 저장되지 않습니다; 내부 노드는 페이지 트리 노드(/Type /Pages)이고 리프는 페이지 객체(/Type /Page)인 트리에 매달려 있습니다. 내부 노드는 자식들을 /Kids 배열에 나열하고 /Count에 그 아래 얼마나 많은 리프 페이지가 있는지를 기록합니다. 루트를 제외한 모든 노드는 상위로 돌아가는 /Parent 참조를 가지므로 트리는 어느 방향으로든 탐색 가능합니다

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

여기서 객체 2는 3개의 페이지가 그 아래에 있는 루트입니다: 리프 페이지 3과 내부 노드 4를 통해 도달할 수 있는 두 개의 추가 페이지가 있습니다. 루트의 /Count인 3은 그 아래에 있는 총 리프 수와 동일해야 하며, 실제 구조와 일치하지 않는 개수는 수동으로 편집된 파일이 잘못되는 흔한 원인입니다. 트리의 핵심은 접근의 지역성입니다. 1,000페이지 문서의 900페이지를 여는 리더기는 900개의 객체를 순회하지 않습니다; 잘 형성된 트리는 얕고 균형을 유지하기 때문에 몇 개의 노드만 내려갑니다. 수동으로 이러한 트리를 구축하는 과정은 번거롭지만 처음부터 끝까지 살펴볼 가치가 있으며, 이는 처음부터 PDF 문서 만들기 연습에서 다룹니다

트리는 상속(inheritance)을 통해 두 번째 역할을 합니다. /Resources, /MediaBox, /CropBox, /Rotate 등 소수의 페이지 속성은 내부 노드에 설정되고 개별 페이지에서는 생략될 수 있으며, 이때 페이지는 가장 가까운 조상의 값을 상속합니다. 루트에 한 번 /MediaBox를 설정하면 모든 리프는 반복 없이 동일한 페이지 크기를 얻습니다; 다르게 해야 할 페이지는 자신의 크기를 선언합니다. 이것은 객체 모델 내에서 값의 의미가 객체 자체의 내용뿐만 아니라 트리에서의 위치에 의존하는 유일한 부분입니다

리프 페이지가 실제로 보관하는 것

페이지 객체는 구조적 모델과 보이는 콘텐츠 사이의 결합 지점입니다. 그것의 /Contents 항목은 텍스트와 그래픽을 페이지에 그리는 드로잉 연산자인 하나 이상의 콘텐츠 스트림을 참조합니다. 그것의 /Resources 딕셔너리는 이러한 연산자들이 의존하는 글꼴, 이미지, 색상 공간을 명명하며, 각 항목은 페이지 전체에서 공유되는 객체에 대한 간접 참조입니다. /MediaBox는 페이지 사각형을 포인트(1/72인치) 단위로 제공하고, /Rotate/CropBox와 같은 항목은 그것이 표시되는 방식을 조정합니다

그러한 역할 분담이 전체 모델의 축소판입니다. 페이지 딕셔너리는 구조입니다: 페이지가 무엇이며 어떤 것으로 그려지는지를 알려주는 유형화된 항목과 참조입니다. 콘텐츠 스트림은 명령어입니다: 어떻게 그리는지를 알려주는 분리되고 압축 가능한 블롭(blob)입니다. /F1 뒤의 글꼴은 한 번 정의되어 사용될 때마다 가리켜지는 공유 리소스입니다. 딕셔너리, 스트림, 참조는 하나의 페이지를 렌더링하기 위해 협력하며, 동일한 패턴이 전체 문서로 확장됩니다. 이 블롭 안의 콘텐츠 스트림 연산자는 텍스트 및 글꼴그래픽 및 시각적 요소에 대해 별도로 다룹니다

이 모델을 알아야 할 이유

대부분의 개발자는 무언가가 고장났을 때 객체 모델을 처음 마주합니다: /Contents 참조가 끊어져 페이지가 비어 있거나, 글꼴 리소스가 포함되지 않아 텍스트가 상자로 나오거나, 도구가 찾을 수 있는 페이지와 일치하지 않는 /Count를 보고하는 경우 등입니다. 이들 각각은 그래프에 대한 설명이며, 그래프를 직접 읽는 직접 읽는 것이 추측보다 낫습니다. 여덟 가지 유형과 참조 규칙은 머릿속에 담아두기 충분히 작은 어휘이며, PDF를 객체를 가리키는 객체로 보는 순간 손상된 파일은 더 이상 불투명하지 않습니다

그렇다 하더라도 수동으로 모델을 작성하는 것은 학습 목적 외에는 올바른 선택이 아닙니다. 여러 편집에 걸쳐 상호 참조 오프셋, 생성 번호, 페이지 트리 개수 및 스트림 길이를 일관되게 유지하는 것은 라이브러리가 처리하기 위해 존재하는 부기 작업입니다. 실제 환경에서는 성숙한 PDF 개발 라이브러리가 객체 그래프를 관리하므로 사용자는 페이지와 콘텐츠에만 집중할 수 있습니다. 모델을 아는 것은 여전히 가치가 있습니다: 라이브러리가 내부적으로 무엇을 만들고, 왜 그렇게 하는지를 이해할 수 있기 때문입니다