PDF는 Word나 RTF와 같은 방식의 문서 형식이 아닙니다. 이러한 형식들은 렌더링 엔진(renderer)이 표시하는 순간에 해석하는 일련의 콘텐츠를 저장하므로, 출력 결과는 그 순간에 존재하는 폰트나 레이아웃 엔진이 무엇인지에 따라 달라집니다. PDF는 그 프로세스의 결과, 즉 정밀한 렌더링 지침, 폰트 프로그램, 압축된 이미지 스트림, 그리고 이것들을 결합하여 각 페이지의 독립적인 설명으로 묶는 객체 그래프(object graph)를 저장합니다. 파일은 호환되는 모든 렌더링 엔진에서 각 페이지를 동일하게 재현하기에 충분한 정보를 전달합니다. 이것이 이 형식의 주요 설계 목표이자 여러분이 이를 프로그래밍 방식으로 생성, 구문 분석(파싱) 또는 수정하려고 할 때 마주치게 되는 대부분의 복잡성의 근원입니다
객체 모델
모든 PDF는 번호가 매겨진 객체들의 모음입니다. 객체는 부울(boolean), 정수(integer), 실수(real number), 이름(name), 문자열(string), 배열(array), 딕셔너리(dictionary), 스트림(stream) 또는 null일 수 있습니다. 흥미로운 거의 모든 것은 키-값 쌍의 집합인 딕셔너리이며, 여기서 키는 이름(names)이고 값은 다른 모든 객체 유형이 되며 객체 번호와 세대(generation) 횟수를 통한 다른 객체의 참조(references)도 포함됩니다. 스트림은 딕셔너리 뒤에 일반적으로 압축된 바이트 시퀀스가 이어지는 형태입니다
카탈로그(catalog) 딕셔너리가 루트(root)입니다. 이것은 페이지 트리를 가리키는데, 평면적인 목록이 아니라 평형 트리(balanced tree) 구조로 페이지 딕셔너리들을 구성합니다. 따라서 10,000페이지짜리 문서의 5,000페이지로 이동하기 위해 이전의 모든 페이지 설명자(descriptors)를 탐색할 필요가 없습니다. 각 페이지 딕셔너리는 해당 콘텐츠 스트림(하나 이상의 페이지 설명 연산자(operators) 시퀀스), 리소스 딕셔너리(폰트 설명자, 색상 공간 및 이미지 XObject를 참조), 그리고 미디어 박스(페이지가 존재하는 좌표 공간)를 참조합니다. 좌표의 원점은 좌측 하단 구석이며, 양의 Y축이 위로 올라가고 단위는 1/72인치입니다
파일의 끝에는 각 객체 번호를 파일 내 바이트 오프셋에 매핑하는 상호 참조 테이블(cross-reference table)이 있습니다. 이 덕분에 무작위 접근(random access)이 가능해집니다. 뷰어는 상호 참조 테이블을 먼저 읽고 나서 필요한 객체로 직접 이동(seek)합니다. PDF 1.5에서는 상호 참조 스트림(cross-reference streams)을 도입했는데, 이는 테이블을 스트림 객체로 압축하고 관련 객체들을 객체 스트림에 포장(pack)하여 많은 수의 작은 객체가 있는 문서의 파일 크기를 눈에 띄게 줄입니다
콘텐츠 스트림 및 그래픽 모델
페이지의 시각적 콘텐츠는 하나 이상의 콘텐츠 스트림(content streams)에 존재합니다. 각 스트림은 PDF 연산자와 피연산자(operands)가 산재된 시퀀스입니다. 텍스트 연산자 BT는 텍스트 객체를 시작하고, Tf는 리소스 딕셔너리에서 폰트와 크기를 선택하고, Td는 텍스트 커서의 위치를 지정하고, Tj나 TJ는 문자열을 그리고(paint), ET는 텍스트 객체를 닫습니다. 벡터 그래픽도 비슷한 패턴을 따릅니다. m은 패스 시작점을 설정하고, l은 선분을 추가하고, c는 베지어 곡선(Bezier curve)을 추가하고, f나 S는 패스를 채우거나 선을 긋습니다(stroke)
그래픽 상태(graphics state)는 연산자 사이에서 일어나는 모든 것, 즉 현재 변환 행렬, 선 너비, 색상 공간, 채우기 색상, 획 색상 및 클리핑 패스를 제어합니다. q나 Q와 같은 연산자는 그래픽 상태를 스택(stack)에 밀어 넣거나 꺼내는데, 이것이 PDF가 주변 컨텍스트에 영향을 주지 않으면서 로컬 좌표 변환과 임시 상태 재정의(overrides)를 구현하는 방법입니다. 폼(Form) XObject는 이를 일반화한 것입니다. 단일 Do 연산자만으로 임의의 위치와 비율로 페이지에 칠할 수 있는 자체 리소스 딕셔너리를 가진 자체 포함형 콘텐츠 스트림입니다
폰트 포함 및 텍스트 추출
PDF는 이름으로 폰트를 참조하고 뷰어가 대체할 수 있도록 의존할 수 있지만, 실제로 공유하려는 문서는 폰트 데이터를 포함(embed)해야 합니다. PDF에 포함된 Type 1 또는 TrueType/OpenType 폰트에는 폰트 파일 스트림을 가리키는 폰트 설명자(descriptor) 딕셔너리가 들어 있습니다. TrueType 폰트의 경우, 해당 스트림에는 바이너리 폰트 프로그램이 들어 있고, Type 1의 경우에는 PFB 데이터입니다. 부분 집합(Subsetting)화는 진지한 PDF 생성기라면 모두 수행하는 것으로 문서에서 참조되지 않은 글리프(glyphs)를 제거하여 거대한 유니코드 폰트라도 파일 크기를 관리 가능한 수준으로 유지합니다
텍스트 추출은 폰트 포함(embedding)이 역효과를 내는 부분입니다. 문자의 시각적 표현은 포함된 폰트 프로그램의 글리프에 의해 결정됩니다. 해당 문자의 유니코드 값은 폰트 딕셔너리에 연결된 ToUnicode CMap 스트림에 의해 결정됩니다. ToUnicode CMap이 누락되거나 잘못된 경우, PDF 뷰어는 텍스트를 읽을 수 있게 렌더링할 수는 있지만 의미 있는 유니코드로 추출할 수 없으며, 이것이 일부 PDF에서 복사-붙여넣기를 할 때 쓰레기 값(garbage)이 나오는 이유입니다. 태그 지정된(Tagged) PDF(ISO 32000 §14.8)는 두 번째 계층, 즉 페이지 콘텐츠를 단락, 제목 및 표 셀과 같은 문서 의미(semantic) 역할에 매핑하는 논리적 구조 트리를 추가합니다. 화면 판독기(Screen readers)와 리플로우 엔진(reflow engines)은 원시 콘텐츠 스트림 순서가 아닌 이 구조 트리를 사용하며, 이것이 시각적으로 잘 레이아웃된 PDF라도 태그가 없거나 잘못된 경우 여전히 액세스할 수 없는 이유를 설명합니다
점진적 업데이트 및 디지털 서명
처음부터 다시 쓰지 않고 기존 PDF의 변경 사항을 저장할 때 새 객체들은 새로운 상호 참조 섹션 및 새로운 트레일러(trailer) 딕셔너리와 함께 원래 파일 본문 뒤에 추가됩니다(append). 업데이트된 트레일러는 새로운 상호 참조 데이터를 가리키며, 대체된 객체들은 파일에 남아 있지만 단순히 새로운 상호 참조 체인(chain)에서 참조되지 않을 뿐입니다. 이것이 점진적 업데이트(incremental update)이며, 여기에는 두 가지 중요한 결과가 따릅니다
첫째, 파일은 저장 주기를 거칠 때마다 커집니다. 문서를 반복적으로 편집하고 저장하면 더 이상 사용되지 않는 객체 계층이 축적됩니다. QPDF와 같은 도구를 사용하면 파일을 선형화하거나 압축-다시쓰기를 통해 그 공간을 회수할 수 있지만 기본값은 축적입니다. 둘째, 디지털 서명(digital signatures)은 그 무결성 모델을 위해 점진적 업데이트에 의존합니다. ISO 32000 서명은 파일의 바이트 범위(일반적으로 서명 값 자체를 위한 자리 표시자 제외)를 포함합니다. 검증 리더(validating reader)는 추가적인 점진적 업데이트로 나타나는 서명 후 변경 사항을 서명 이후에 이뤄진 수정으로 인식하며, 이는 여러분이 원하는 감사 추적(audit trail)과 정확히 일치합니다. 하지만, 이는 승인 서명을 추가하거나 폼 필드를 채우는 등의 특정 수정이 문서의 권한 설정(ISO 32000-2 §12.7.6)에 부합하기만 한다면 원래 서명을 무효화하지 않고 표준에 의해 명시적으로 허용된다는 것을 의미하기도 합니다. 해당 권한을 벗어나는 수정은 무단(unauthorized)으로 신고됩니다. 이러한 구분을 올바르게 이해하는 것은 다운스트림에서 연서(countersigned)될 문서를 생성할 때 중요합니다
적합성 수준 및 ISO 32000의 역사
PDF는 1993년 독점적인 Adobe 형식으로 시작하여 PostScript의 이미징 모델을 흡수했고 15개 버전에 걸쳐 1.1의 암호화, 1.2의 대화형 폼, 1.3의 디지털 서명과 논리적 구조, 1.4의 투명성, 1.5의 객체 스트림, 1.6의 AES 암호화 등의 기능이 축적되었습니다. Adobe는 2007년에 PDF 1.7을 ISO에 제출했고, 그 결과 ISO 32000-1:2008이 제정되었습니다. ISO 32000-2:2020은 PDF 2.0을 다루는데 이 버전은 규정이 불명확했던 여러 영역을 강화하고 AES-256 키 도출 방식(revision 5를 대체하는 revision 6)을 개정했으며, 연관된 파일과 리치 미디어에 대한 명시적 지원을 추가했습니다
하위 표준들은 동일한 기반에서 파생되었습니다. PDF/A(ISO 19005)는 보관(archival)의 안정성을 위해 일부 기능과 타협합니다. 암호화 없음, 외부 콘텐츠 종속성 없음, 모든 폰트 포함, 장치 독립적 색상 공간, XMP 메타데이터 요구 등입니다. PDF/A-1은 PDF 1.4를 기반으로 하고, PDF/A-2는 PDF 1.7을 기반으로 하며, PDF/A-3은 모든 형식의 파일 포함을 허용합니다. PDF/X(ISO 15930)는 인쇄 제작 하위 집합입니다. 출력 인텐트(output intents), 블리드(bleed) 및 트림 박스(trim boxes)가 있으며 이전 적합성 수준에서는 투명성이 허용되지 않습니다. PDF/UA(ISO 14289)는 접근성을 위한 태그가 지정된 구조, 유니코드 매핑, 언어 메타데이터를 강제합니다. 이들은 서로 경쟁하는 형식이 아닙니다. 핵심 PDF 위에 추가적인 제약 조건들의 집합을 둔 것이며, 해당 제약 조건이 상충되지 않는 한 단일 파일이 동시에 두 개 이상에 부합(conform)할 수 있습니다
PDF를 생성하거나 처리하는 코드를 작성하는 사람이라면, 실용적인 기준선은 상호 참조 모델(§7.5), 그래픽 상태(§8.4), 텍스트 상태 연산자(§9.3), 폰트 설명자 및 ToUnicode(§9.6 및 §9.10), 대화형 폼(§12.7), 그리고 디지털 서명(§12.8)을 다루는 섹션에 세심한 주의를 기울인 ISO 32000-2입니다. 이 표준은 방대하지만 프로그래밍 방식의 대부분의 PDF 작업은 그것의 좁은 조각을 반복해서 다루게 됩니다. 객체 모델과 상호 참조 메커니즘을 이해하는 것이 진입점(entry point)이며 그 이후의 모든 것은 거기서부터의 전문화(specialization)일 뿐입니다