제로에서 PDF 손으로 작성하기 (01): Hello, World——최소한의 사용 가능한 PDF 구축하기
제로에서 PDF 손으로 작성하기 (01): Hello, World——최소한의 사용 가능한 PDF 구축하기
시리즈 목표: PDF를 읽을 수 있는 파일 형식으로 이해하기 - 먼저 "작동하는" 최소 예제부터 시작하여 점차 그래픽, 다중 페이지, 압축 및 자원 재사용으로 확장합니다.
시리즈 목차
- 제 01 편 (본 문서): 최소 PDF(1 페이지 + 1 행 텍스트)를 손으로 작성하고 도구를 사용하여 열 수 있는 표준 PDF로 보완합니다.
- 제 02 편: 콘텐츠 스트림에서 선/사각형 그리기(경로, 테두리, 채우기 이해하기)
- 제 03 편: 다중 페이지 PDF(페이지 트리가 어떻게 생성되는지)
- 제 04 편: 실제 세계에 더 가까워지기(압축 스트림, 자원 재사용, 선택적 구조 등)
왜 PDF의 하위 구조를 이해해야 할까요?
PDF(Portable Document Format, 휴대용 문서 형식)는 오늘날 가장 인기 있는 페이지 설명 언어 중 하나입니다. HTML/CSS와 같은 "내용과 표현 분리, 재흐름 가능" 접근 방식과는 달리, PDF는 **레이아웃 고정, WYSIWYG(What You See Is What You Get)**를 강조합니다 - 어떤 장치에서 열더라도 레이아웃이 일관됩니다.
PDF 하위 구조를 이해하는 데는 몇 가지 실제적인 이점이 있습니다:
- PDF 생성 문제 디버깅: 코드 라이브러리를 사용하여 PDF를 생성할 때 오류가 발생하면, 하위 구조를 이해해야 문제를 빠르게 찾을 수 있습니다.
- 자동화 처리: 대량 텍스트 추출, 문서 병합, 워터마크 추가 등의 작업은 구조를 이해해야 정확하게 수행할 수 있습니다.
- 보안 감사: PDF에 어떤 내용을 삽입할 수 있는지(자바스크립트, 첨부 파일, 양식 등)를 이해하면 보안 분석에 도움이 됩니다.
- 파일 형식 설계 학습: PDF의 "객체 그래프 + 임의 접근" 설계는 고전적인 예로, 배울 가치가 있습니다.
준비 작업
본 문서에서는 먼저 "구조는 불완전하지만 논리는 올바른" hello-broken.pdf를 작성한 후, pdftk를 사용하여 주요 구조를 자동으로 보완하고 hello.pdf로 출력합니다.
- 필요한 도구: pdftk (무료 명령줄 도구, Windows/macOS/Linux 지원)
- 출력 파일:
hello-broken.pdf(손으로 작성),hello.pdf(수정 후 열 수 있음)
핵심 개념: PDF의 세 가지 구조
PDF를 이해하는 데 가장 중요한 것은 세 가지 정신 모델을 구축하는 것입니다:

1. 객체 계층 (Document Content)
PDF 문서는 많은 객체로 구성되어 있으며, 객체 간에는 간접 참조(예: 2 0 R)로 연결되어 있습니다. 일반적인 객체 유형:
| 유형 | 예시 | 설명 |
|---|---|---|
| Name | /Page | /로 시작하는 이름 |
| 정수/실수 | 50, 36.0 | 숫자 |
| 문자열 | (Hello, World!) | 괄호로 묶인 문자열 |
| 배열 | [0 0 612 792] | 순서가 있는 집합 |
| 사전 | << /Type /Page >> | 키-값 쌍의 집합 |
| 간접 참조 | 2 0 R | 객체 2를 참조 (생성 번호 0) |
| 스트림 | stream...endstream | 이진 데이터 (예: 그리기 명령, 이미지) |
2. 콘텐츠 계층 (Page Content)
실제로 "텍스트/그래픽을 페이지에 그리는" 명령어 시퀀스는 일반적으로 stream ... endstream 안에 작성됩니다. 형식은: 작업 수가 앞에, 작업자가 뒤에 있습니다.
/F0 36 Tf ← 작업 수: /F0, 36 작업자: Tf (글꼴 설정)
(Hello, World!) Tj ← 작업 수: 문자열 작업자: Tj (텍스트 그리기)
3. 파일 구조 계층 (File Structure)
리더가 임의 객체에 빠르게 접근할 수 있도록 하여 처음부터 끝까지 읽지 않아도 됩니다:
| 요소 | 역할 |
|---|---|
%PDF-1.x | 파일 헤더, PDF 버전 식별 |
xref | 교차 참조 테이블: 객체 번호 → 바이트 오프셋 |
trailer | 꼬리 사전: 루트 객체 /Root를 가리킴 |
startxref | xref 테이블의 시작 위치를 나타냄 |
%%EOF | 파일 종료 마크 |
최소 PDF에 필요한 객체는 무엇인가요?
"최소하지만 텍스트를 표시할 수 있는" PDF의 객체 간 참조 관계는 다음과 같습니다:

최소 객체 목록:
| 객체 | 역할 | 주요 필드 |
|---|---|---|
| Catalog | 루트 객체, 문서 진입점 | /Type /Catalog, /Pages |
| Pages | 페이지 트리 | /Type /Pages, /Kids, /Count |
| Page | 단일 페이지 | /Type /Page, /MediaBox, /Resources, /Contents, /Parent |
| Resources | 자원 컨테이너 | /Font (글꼴 사전) |
| Font | 글꼴 정의 | /Type /Font, /BaseFont, /Subtype |
| Contents | 콘텐츠 스트림 | 그리기 명령의 스트림 |
실전: hello-broken.pdf 손으로 작성하기
새 파일 hello-broken.pdf를 만들고 아래 내용을 완전히 붙여넣습니다:
%PDF-1.0
1 0 obj
<< /Type /Pages
/Count 1
/Kids [2 0 R]
>>
endobj
2 0 obj
<< /Type /Page
/MediaBox [0 0 612 792]
/Resources 3 0 R
/Parent 1 0 R
/Contents [4 0 R]
>>
endobj
3 0 obj
<< /Font
<< /F0
<< /Type /Font
/BaseFont /Times-Italic
/Subtype /Type1 >>
>>
>>
endobj
4 0 obj
<< >>
stream
1. 0. 0. 1. 50. 700. cm
BT
/F0 36. Tf
(Hello, World!) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 6
trailer
<< /Size 6
/Root 5 0 R
>>
startxref
0
%%EOF
왜 이 파일이 "잘못된" 것인가요?
우리는 일부 내용을 의도적으로 생략하거나 잘못 기입했습니다:
| 누락/오류 항목 | 설명 |
|---|---|
xref 오프셋 | 각 객체의 실제 바이트 오프셋을 기입하지 않았습니다. |
startxref | 0으로 기입했으며, 이는 xref의 실제 위치가 아닙니다. |
/Length | 콘텐츠 스트림의 길이를 선언하지 않았습니다. |
| 이진 마크 | 헤더의 이진 식별 행이 누락되었습니다. |
이들은 리더가 필요로 하는 중요한 정보로, 누락되면 열 수 없거나 오류로 열 수 있습니다.
주요 콘텐츠 스트림 명령어 상세 설명
콘텐츠 스트림은 객체 4 0 obj의 stream ... endstream 사이에 있으며, 각 줄을 설명합니다:
1. 0. 0. 1. 50. 700. cm ← 변환 행렬 설정 (주의: 1.은 부동 소수점 수 1.0을 나타냄)
BT ← 텍스트 객체 시작
/F0 36. Tf ← 글꼴 F0 선택, 글꼴 크기 36pt
(Hello, World!) Tj ← 문자열 그리기
ET ← 텍스트 객체 종료
변환 행렬 cm 작업자
1 0 0 1 50 700 cm는 6 요소의 변환 행렬 [a b c d e f]에 해당하며, 다음과 같습니다:
| a b 0 | | 1 0 0 |
| c d 0 | = | 0 1 0 |
| e f 1 | | 50 700 1 |
a=1, b=0, c=0, d=1일 때, 이는 순수 이동 행렬로, 좌표계 원점(즉, 이후 그리기 작업의 (0,0) 점)을 (50, 700)으로 이동시킵니다. 이동하지 않으면 기본 원점은 페이지의 왼쪽 하단에 있습니다.
텍스트 작업자
| 작업자 | 의미 | 예시 |
|---|---|---|
BT | Begin Text, 텍스트 객체 시작 | BT |
ET | End Text, 텍스트 객체 종료 | ET |
Tf | 글꼴 및 글꼴 크기 설정 | /F0 36 Tf |
Tj | 문자열 그리기 | (Hello!) Tj |
pdftk를 사용하여 열 수 있는 PDF로 수정하기
hello-broken.pdf가 있는 디렉토리에서 다음을 실행합니다:
pdftk hello-broken.pdf output hello.pdf
任意 PDF 리더로 hello.pdf를 열면 페이지에 "Hello, World!" (Times-Italic 글꼴, 36pt, 페이지 왼쪽 상단에 위치)가 나타나는 것을 볼 수 있어야 합니다.
pdftk가 보완한 내용은 무엇인가요?
| 보완 항목 | 설명 |
|---|---|
| 이진 마크 행 | %PDF-1.0 뒤에 인쇄할 수 없는 문자를 추가하여 이진 파일로 인식되도록 합니다. |
/Length | 콘텐츠 스트림의 바이트 길이를 계산하고 추가합니다. |
xref 테이블 | 각 객체의 바이트 오프셋을 계산하여 기입합니다. |
startxref | xref 테이블의 실제 시작 위치를 기입합니다. |
왜 xref / trailer / startxref가 필요할까요?
핵심 목적: 임의 접근
500 페이지의 PDF를 상상해 보세요. xref가 없다면 리더는 450 페이지를 표시하기 위해 처음부터 449 페이지까지 해석해야 합니다 - 이는 너무 느립니다.
xref가 있으면 리더는 다음과 같이 할 수 있습니다:
- 먼저
startxref를 읽고 → xref 위치 찾기 trailer를 읽고 → 루트 객체/Root찾기- 루트 객체를 따라가며 → 450 페이지 객체로 직접 점프
- xref를 통해 해당 객체의 바이트 오프셋을 확인하고 → 직접 seek하여 읽기
시간 복잡도가 O(n)에서 O(1)로 감소합니다.
본문 연습
hello-broken.pdf를 실제로 수정한 후, pdftk로 다시 수정하여 효과를 관찰해 보세요:
| 연습 | 수정 내용 | 관찰 포인트 |
|---|---|---|
| A | (Hello, World!)를 다른 영어 문구로 변경 | 텍스트 변화 |
| B | 36을 12 또는 72로 변경 | 글꼴 크기 변화 |
| C | 50 700을 50 100으로 변경 | 위치 하강 (PDF 좌표계 원점은 왼쪽 하단에 있음) |
| D | /Times-Italic을 /Helvetica 또는 /Courier로 변경 | 글꼴 변화 |
| E | /MediaBox [0 0 612 792]을 [0 0 595 842]로 변경 | 용지가 US Letter에서 A4로 변경 |
팁: PDF 좌표계 원점은 페이지의 왼쪽 하단에 있으며, Y축은 위로 향합니다.
(50, 700)은 왼쪽에서 50pt, 아래에서 700pt 떨어진 지점을 나타냅니다.
자주 묻는 질문
Q: 왜 Type1 내장 글꼴을 사용하고 TrueType을 사용하지 않나요?
A: Type1의 14가지 표준 글꼴(타임스, 헬베티카, 쿠리어 등)은 PDF 리더가 반드시 내장해야 하며, 글꼴 파일을 삽입할 필요가 없어 가장 간단합니다. 실제 상황에서는 일반적으로 글꼴을 삽입하여 플랫폼 간 일관성을 보장해야 합니다.
Q: /MediaBox [0 0 612 792]의 숫자는 무엇인가요?
A: 단위는 포인트(1 포인트 = 1/72 인치)입니다. 612 × 792 포인트 = 8.5 × 11 인치 = US Letter 용지. A4는 595 × 842 포인트입니다.
Q: 생성 번호(예: 2 0 R의 0)는 무엇인가요?
A: 증분 업데이트에 사용됩니다. 객체가 수정되면 생성 번호가 1 증가합니다. 새로 생성된 PDF의 모든 객체 생성 번호는 일반적으로 0입니다.
다음 편 예고
제 02 편에서는 "손으로 콘텐츠 스트림 작성" 방식을 계속 사용하여 가장 기본적인 그래픽 경로 작업을 추가합니다:
m(moveto),l(lineto): 경로 정의S(stroke): 테두리 그리기re(rectangle),f(fill): 사각형 그리기 및 채우기
같은 페이지에서 제목 텍스트 + 수평 구분선 + 사각형을 동시에 그려 "글씨를 쓸 수 있는" 단계에서 "그래픽을 그릴 수 있는" 단계로 나아갑니다.
