80MB 스캔 계약서 PDF를 받았습니다. 이메일에서 반송되고, 채팅 앱에서 거부되며, 시도하는 모든 시스템의 업로드 제한에 걸립니다. 익숙하신가요? PDF 파일이 커지는 데는 이유가 있으며, 이를 안전하게 10분의 1로 줄이는 것은 마법이 아닙니다 — 컨테이너 내부에 무엇이 있는지 이해하고 각 콘텐츠 유형에 올바른 기술을 적용하는 것입니다. 이 글은 PDF 사양의 5가지 핵심 압축 기술을 해부한 다음, 실제 80MB 스캔 계약서를 8.2MB까지 줄이는 과정을 안내합니다.
1. PDF 크기의 원인
PDF는 컨테이너 형식입니다. 내부적으로 페이지 트리, 리소스 사전, 콘텐츠 스트림, 이미지 XObject, 폰트, 메타데이터 등 객체의 모음입니다. 크기 증가는 거의 항상 세 가지 범주로 추적됩니다.
임베디드 이미지. 스캔 문서에서 지배적인 비용입니다. 기본 600DPI로 설정된 스캐너는 약 4960×7016 픽셀의 A4 페이지 비트맵을 생성합니다. 24비트 색 심도에서 단일 페이지의 원시 데이터는 약 100MB입니다. FlateDecode(zlib) 무손실 압축 후에도 각 페이지는 2~5MB입니다. 30페이지 계약서는 60~150MB에 달합니다.
임베디드 폰트. 기기 간 동일한 렌더링을 보장하기 위해 PDF는 전체 폰트 파일을 임베드합니다. 완전한 CJK 폰트(예: Source Han 서체)는 2만 개 이상의 글리프를 가지며, TTF 파일만 10~20MB입니다. OTF는 더 클 수 있습니다. 여러 굵기나 여러 패밀리를 임베드하면 이 범주가 빠르게 증가합니다.
벡터 아트와 중복 객체. 벡터 자체는 작지만, 복잡한 CAD 내보내기, 중첩된 폼 필드, 삭제된 리비전의 리소스, 중복된 메타데이터 스트림이 조용히 축적됩니다. PDF의 증분 업데이트 메커니즘은 악명 높은 범인입니다. 저장할 때마다 이전 객체를 제거하지 않고 새 리비전을 추가하므로, 파일은 편집할 때마다 커집니다.
바이트가 어디에 있는지 알면 해결책이 따라옵니다. 아래의 5단계 방법은 실제 압축 시나리오의 대다수를 다룹니다.
2. 5단계 방법 한눈에 보기
1단계: 이미지 리샘플링
스캔 문서에 가장 효과적인 단일 단계입니다. 600DPI 스캔은 일반 화면에서 150DPI와 구별할 수 없지만, 픽셀 수는 16배 다릅니다(DPI는 선형, 픽셀은 제곱이므로 (600/150)² ≈ 16).
핵심 결정은 타겟 DPI입니다. 문서 콘텐츠(계약서, 보고서, 청구서)는 150DPI에서 잘 보입니다. 인쇄용으로는 200–300DPI로 올립니다. 고충실도 아카이빙 스캔은 400DPI 이상을 유지할 수 있습니다. 리샘플링 알고리즘으로는 Lanczos가 권장됩니다 — 축소 시 텍스트 가장자리 선명도를 바이리니어 보간보다 잘 보존하고 최근접 이웃의 블록 현상을 피합니다.
구현에서는 이미지 XObject의 /Width, /Height, DPI 정보를 /DecodeParms에서 읽고, 비례적으로 스케일한 뒤 다시 씁니다. /Width, /Height, /Intent를 함께 업데이트해야 하며, 그렇지 않으면 렌더링이 어긋납니다.
2단계: 이미지 포맷 변환
PDF는 /Filter 필드로 이미지 인코딩을 표시합니다. 스캔 문서는 일반적으로 /FlateDecode(PNG 방식 무손실)를 사용하지만, 사진이나 스캔 페이지 같은 실제 이미지에서는 /DCTDecode(JPEG)보다 훨씬 비효율적입니다.
FlateDecode에서 4MB를 차지하는 동일한 A4 스캔은 품질 72의 JPEG로 300–500KB로 줄일 수 있습니다 — 화면에서 인지할 수 있는 차이 없이 85% 감소입니다. 주의할 두 가지: 흑백 문서는 인코딩 전에 그레이스케일(/ColorSpace /DeviceGray)로 변환하여 크기를 다시 절반으로 줄이고, 서명이 포함된 페이지는 JPEG 품질을 80+로 올려 펜 획이 온전히 유지되도록 합니다.
/DCTDecode가 유일한 옵션은 아닙니다. 넓은 단색 영역이 있는 차트에는 /JPXDecode(JPEG 2000)가 동일 품질에서 더 작지만, 오래된 리더와의 호환성은 떨어집니다.
3단계: 폰트 서브셋팅
완전한 CJK 폰트 임베딩은 10MB 이상이 들지만, 30페이지 계약서는 실제로 800~1,500개의 고유 문자만 사용할 수 있습니다. 폰트 서브셋팅은 문서에 나타나는 글리프만 유지하고 나머지는 모두 버립니다.
서브셋 폰트는 보통 100–300KB로 줄어듭니다 — 95% 이상 감소입니다. 접근 방식: 각 페이지의 콘텐츠 스트림을 순회하며 Tj 및 TJ 연산자에서 문자를 추출하고, /ToUnicode 맵을 통해 해결하여 사용된 코드 포인트 집합을 얻은 다음, 폰트 도구(fontTools 등)를 사용하여 해당 글리프만 포함된 새 폰트 파일을 구축합니다.
두 가지 주의사항: CID 폰트는 글리프 어긋남을 방지하기 위해 /CIDToGIDMap 처리가 필요하며, 서브셋 폰트는 관례적으로 접두사(ABCDEF+ 등)로 리네임되어 서브셋임을 표시하지만 표시에는 영향이 없습니다.
4단계: 중복 객체 제거
반복된 편집, 병합, 증분 저장 후 PDF는 "고아" 객체를 축적합니다 — 새 리비전으로 덮어쓰여졌지만 크로스 레퍼런스 테이블에서 제거되지 않은 리소스입니다. 전형적인 범인: 어느 페이지에서도 더 이상 참조되지 않는 오래된 이미지, 대체된 폰트 복사본, 빈 리소스 사전, 중복된 메타데이터 스트림.
정리 로직: 문서 카탈로그(/Root)에서 시작하여 페이지 트리와 리소스 참조 체인을 깊이 우선 탐색하고, 도달 가능한 모든 객체를 표시한 다음 크로스 레퍼런스 테이블을 재구축하고 도달 불가능한 것을 버립니다. 이 단계는 여러 번 편집된 문서에서 특히 효과적입니다. 단일 패스로 일반적으로 크기의 5%~15%를 제거하며, 증분 업데이트가 누적된 극단적 경우 30% 이상을 제거합니다.
5단계: 선형화
선형화는 총 바이트 수를 줄이지 않지만, 파일 구조를 재구성하여 Fast Web View를 활성화합니다. 선형화된 PDF는 첫 페이지에 필요한 객체를 파일 앞부분에 배치하고 힌트 테이블을 삽입하여, 리더가 다운로드 완료 전에 첫 페이지를 렌더링할 수 있게 합니다.
선형화가 파일을 직접 줄이지는 않지만, 압축 파이프라인의 자연스러운 마무리 단계입니다. 객체 스트림 압축 및 크로스 레퍼런스 스트림과 함께하면 전체 재구성이 여전히 약간의 크기 감소(1%~3%)를 가져옵니다. 더 중요하게, 선형화된 PDF는 웹 임베딩 및 클라우드 미리보기 시나리오에서 눈에 띄게 더 나은 성능을 보입니다.
3. 전후 비교
80MB 계약서가 각 단계 후 어떻게 변하는지 보여드립니다:
1단계(리샘플링)가 가장 큰 감소를 가져와 80MB에서 15.2MB로 바로 떨어집니다. 2단계(포맷 변환)가 추가로 4MB를 제거합니다. 나머지 세 단계가 합쳐서 약 7MB를 깎아냅니다. 전체 압축 비율은 약 9.8:1입니다.
4. Python 의사코드
아래 의사코드는 완전한 5단계 로직을 보여줍니다. 프로덕션에서는 pikepdf, PyMuPDF, reportlab 같은 라이브러리를 조합할 것입니다.
import io
from PIL import Image
def compress_pdf(input_path, output_path,
target_dpi=150, jpeg_quality=72, grayscale=True):
"""
5단계 PDF 압축 진입점.
:param input_path: 입력 PDF 경로
:param output_path: 출력 PDF 경로
:param target_dpi: 타겟 DPI (기본 150, 화면에 적합)
:param jpeg_quality: JPEG 품질 (기본 72)
:param grayscale: 그레이스케일로 변환 (흑백 문서에 권장)
:return: (원본_크기, 압축_크기)
"""
pdf = open_pdf(input_path)
original_size = file_size(input_path)
# 1단계: 이미지 리샘플링 (600DPI -> 타겟 DPI)
for page in pdf.pages:
for image in page.images:
resampled = resample_image(image, target_dpi)
image.replace(resampled)
# 2단계: 포맷 변환 (FlateDecode -> DCTDecode/JPEG)
for page in pdf.pages:
for image in page.images:
if image.filter == "FlateDecode":
jpeg_data = encode_jpeg(image, jpeg_quality, grayscale)
image.replace(jpeg_data, filter="DCTDecode")
# 3단계: 폰트 서브셋팅 (사용 글리프만 유지)
used_chars = collect_used_chars(pdf)
for font in pdf.fonts:
subset = build_subset(font, used_chars)
font.replace(subset)
# 4단계: 중복 객체 제거
pdf.remove_unreferenced_resources()
pdf.compact_xref() # 크로스 레퍼런스 테이블 재구축
# 5단계: 선형화 (Fast Web View)
pdf.save(output_path, linearize=True,
object_streams=True, compress_streams=True)
pdf.close()
compressed_size = file_size(output_path)
return original_size, compressed_size
def resample_image(image, target_dpi):
"""이미지를 타겟 DPI로 리샘플링합니다."""
src_dpi = image.dpi
if src_dpi <= target_dpi:
return image # 업스케일 금지, 건너뜀
scale = target_dpi / src_dpi
new_w = int(image.width * scale)
new_h = int(image.height * scale)
pil_img = Image.open(io.BytesIO(image.data))
return pil_img.resize((new_w, new_h), Image.LANCZOS)
def encode_jpeg(image, quality, grayscale):
"""그레이스케일로 변환하고 JPEG로 인코딩합니다."""
pil_img = Image.open(io.BytesIO(image.data))
if grayscale and pil_img.mode != "L":
pil_img = pil_img.convert("L") # 그레이스케일로
buf = io.BytesIO()
pil_img.save(buf, format="JPEG", quality=quality, optimize=True)
return buf.getvalue()
def collect_used_chars(pdf):
"""콘텐츠 스트림을 순회하며 실제 사용된 모든 코드 포인트를 수집합니다."""
used = set()
for page in pdf.pages:
for text_op in page.content_stream.text_ops:
used.update(extract_codepoints(text_op))
return used
def build_subset(font, used_chars):
"""사용된 글리프만 포함하는 서브셋 폰트를 구축합니다."""
subset = font.subset(glyphs=used_chars)
subset.name = prefix + "+" + font.name # 서브셋 명명 규칙
return subset
이것은 의사코드입니다 — 에러 처리, CID 폰트 매핑, ToUnicode 재구축은 생략되었습니다 — 하지만 골격은 명확합니다. 리샘플, 변환, 서브셋, 정리, 선형화 순서입니다.
5. 사례 연구: 80MB 스캔 계약서를 8.2MB로
소스 문서는 600DPI로 스캔된 실제 32페이지 중국어 계약서이며, 원본 크기 80.4MB입니다.
문서 프로필:
- 32페이지, A4, 600DPI 흑백 스캔
- 32개 임베디드 이미지, 모두 FlateDecode, 페이지당 평균 2.4MB
- 2개 임베디드 폰트 (Source Han Serif Regular + Bold), 합산 28MB
- 증분 저장 3회, 중복 객체 존재
단계 및 매개변수:
| 단계 | 작업 | 주요 매개변수 | 크기 |
|---|---|---|---|
| 1 | 이미지 리샘플링 | 600DPI → 150DPI, Lanczos | 80.4 → 15.2MB |
| 2 | 포맷 변환 | FlateDecode → JPEG, 품질 72, 그레이스케일 | 15.2 → 11.0MB |
| 3 | 폰트 서브셋팅 | 1,287 사용 글리프 유지 | 11.0 → 10.5MB |
| 4 | 중복 제거 | 크로스 레퍼런스 테이블 재구축 | 10.5 → 9.8MB |
| 5 | 선형화 | Object Stream + Fast Web View | 9.8 → 8.2MB |
결과: 8.2MB, 9.8:1 비율. 텍스트는 선명하고 읽기 쉬우며, 서명은 온전하고, 표 테두리에 깨짐이 없으며, 파일이 깨끗이 인쇄됩니다.
6. 시나리오별 DPI 권장값
타겟 DPI는 압축과 가독성의 균형을 맞추는 주요 레버입니다. 아래 표는 일반적인 사용 사례의 권장값을 나열합니다.
| 사용 사례 | 권장 DPI | 페이지당 이미지 크기 | 비고 |
|---|---|---|---|
| 화면 읽기 / 웹 미리보기 | 150 | 150–300KB | 선명한 텍스트, 빠른 로딩, 이메일에 적합 |
| 표준 인쇄 (A4 레이저/잉크젯) | 200–300 | 400–800KB | 인쇄 선명; 계약서는 300 |
| 아카이브 백업 (고충실도) | 400–600 | 1–3MB | 장기 보관을 위해 스캔 디테일 보존 |
| 흑백 텍스트 전용 | 150–200 | 80–200KB | 그레이스케일 후 극히 작음, 일괄 아카이빙에 적합 |
| 사진 포함 컬러 문서 | 200–300 | 500KB–1.2MB | JPEG 품질 75–85, 색상과 크기 균형 |
실용적 규칙: 화면은 기본 150DPI, 인쇄 시에만 300DPI, 아카이빙용으로 별도의 고해상도 마스터를 유지하세요. 하나의 600DPI 파일로 모든 목적을 해결하려 하지 마세요.
7. FAQ
Q1: 압축된 파일이 원본보다 커졌습니다. 왜 그런가요?
이것은 일반적으로 이미 작은 JPEG 인코딩 이미지를 포함하는 PDF를 재압축할 때 발생합니다. 리샘플링과 재인코딩에는 오버헤드가 있습니다. 이미지가 이미 150DPI JPEG인 경우, 150DPI로 "리샘플링"해도 줄어들지 않으며, 새 JPEG 인코더가 원본보다 비효율적일 수 있습니다. 객체 스트림 재구성으로 인한 메타데이터 오버헤드가 더해지면 파일이 약간 커질 수 있습니다. 해결책: 압축 전 각 이미지의 현재 DPI와 인코딩을 조사하고, 타겟(≤150DPI 및 JPEG)을 이미 충족하는 이미지는 건너뜁니다. 임계값을 실제로 초과하는 객체만 처리합니다.
Q2: 압축이 이미지 품질을 저하시키나요?
네, 하지만 시각적으로 인지할 수 없는 수준으로 유지할 수 있습니다. 손실 압축은 주로 2단계(JPEG 인코딩)에서 발생합니다. 품질 72에서는 화면상 차이가 보이지 않습니다. 인쇄 시 얇은 선이나 작은 폰트 같은 고주파 디테일에 약간의 링잉이 보일 수 있습니다. 품질이 더 중요하면 JPEG 품질을 85로 올리거나 JPEG 2000 무손실 모드(가역 변환 /JPXDecode)로 전환하세요. 폰트 서브셋팅, 중복 제거, 선형화는 모두 무손실이며 표시에 영향이 없습니다.
Q3: 이미 압축된 PDF를 다시 압축할 수 있나요?
네, 하지만 수확 체감이 있습니다. 첫 번째 패스는 손쉬운 과일(고DPI, FlateDecode 인코딩, 전체 폰트)을 수확합니다. 두 번째 패스에는 최적화할 것이 거의 남지 않습니다. 판단 방법: 이미지가 여전히 타겟 DPI를 초과하는지, 비JPEG 인코딩이 남아 있는지, 폰트가 여전히 전체 세트인지 확인하세요. 세 가지 모두 이미 최적이라면, 추가 압축은 주로 중복 제거와 선형화에 의존하며 일반적으로 5%~10%만 절약됩니다. 이미 최적화된 PDF를 반복적으로 압축하지 마세요 — 각 손실 인코딩은 품질 손실을 누적합니다.
8. 매개변수 빠른 참조
| 매개변수 | 권장값 | 비고 |
|---|---|---|
| 타겟 DPI (화면) | 150 | 문서 콘텐츠 표준 |
| 타겟 DPI (인쇄) | 300 | A4 인쇄 선명도 유지 |
| JPEG 품질 (화면) | 70–75 | 크기 우선, 가독성 충분 |
| JPEG 품질 (인쇄) | 80–90 | 품질 우선, 크기 차선 |
| 색상 모드 (흑백 문서) | DeviceGray | 컬러 대비 크기 절반 |
| 폰트 서브셋팅 | 활성화 | CJK 폰트 필수, 95%+ 감소 |
| 선형화 | 활성화 | 웹 미리보기 경험 향상 |
| Object Stream | 활성화 | 객체 메타데이터 압축 |
| 중복 정리 | 활성화 | 반복 편집된 파일에 특히 효과적 |
결론
PDF 압축은 흑마술이 아닙니다 — 파일 내 각 객체 유형에 대한 표적 치료입니다. 5단계 방법은 명확한 논리를 따릅니다. 먼저 가장 큰 기여 요인(이미지, 리샘플링과 포맷 변환 경유)을 처리하고, 다음 폰트(서브셋팅), 다음 구조(중복 제거와 선형화)를 처리합니다. 이 파이프라인을 일반적인 80MB 스캔 계약서에 적용하면, 완전한 가독성과 인쇄 가능성을 유지하면서 8~10MB에 안정적으로 도달합니다.
기억할 세 가지 확인: 스캔은 먼저 DPI를 보세요. 문서는 폰트가 전체 세트인지 확인하세요. 오래된 파일은 중복 객체를 조사하세요. 지배적 비용을 찾으면, 압축 비율은 저절로 따라옵니다.
관련 글:
파일 압축이 필요하신가요? SmartSlim을 사용해 보세요
자체 개발한 Rust 압축 엔진 기반으로 PDF/이미지/비디오/Office/OFD 등 10대 분류 40+ 포맷을 지원합니다. 로컬 압축으로 데이터가 외부로 나가지 않습니다.