PDF를 압축할 때 실제로 일어나는 일
PDF123 압축은 qpdf 스트림 재압축과 객체 스트림 패킹을 실행하며 이미지 다운샘플링, 손실 JPEG 재인코딩, 폰트 서브셋팅은 하지 않는다. 선형화는 파일 크기를 거의 바꾸지 않는다.

화면에서 똑같아 보이는 PDF 두 개가 50 KB와 50 MB처럼 세 자릿수만큼 차이 나는 일이 있다. 그 격차는 거의 텍스트 연산자에서 나오지 않는다. 스무 쪽짜리 계약서라면 텍스트 연산자는 보통 수십 KB에 불과하고, 나머지는 전부 이미지 샘플과 통째로 임베드된 폰트 프로그램, 그리고 스트림과 객체가 얼마나 촘촘히 패킹됐는지다.
여기서 미리 짚어 둘 결론이 하나 있다. "PDF 압축"은 하나의 동작이 아니라 네 가지다. 줄이는 대상도 다르고 치르는 값도 다르다. 이걸 한 덩어리로 취급하면 경험이 서로 모순된다. 압축했는데 작아지지 않거나, 작아졌는데 화면이 흐려지는 식이다.
크기는 어디서 오는가
"이 PDF는 왜 이렇게 큰가"라는 물음은 대부분 촬영 크기 그대로 임베드된 고해상도 스캔이나 사진에서 시작한다. 이 산수는 한 번 해 둘 만하다. Letter 한 쪽을 8.5 × 11인치, 300 DPI로 스캔하면 약 2550 × 3300픽셀이고, 픽셀당 3바이트면 비압축 상태로 약 25 MB다. 그런 쪽이 스무 장이면 누구도 압축을 건드리기 전에 60~80 MB에 이르는 일이 흔하다.
두 번째 원천은 폰트다. PDF는 폰트 프로그램 전체를 임베드해 어느 기기에서나 텍스트가 똑같이 렌더되게 할 수 있는데, 그 비용은 실제로 읽히는 텍스트의 양이 아니라 글리프 테이블의 크기로 매겨진다. 그래서 폰트 서브셋팅이 별도의 크기 레버가 된다.
세 번째는 패킹 그 자체다. 같은 내용이라도 낭비적인 압축 설정으로 저장될 수 있고, 객체가 파일 곳곳에 흩어져 각자 부기 오버헤드를 안고 있을 수 있다. 이 부분은 픽셀도 글리프도 바꾸지 않지만, 바로 스트림 재압축이 닿을 수 있는 부분이다.
"압축"은 서로 다른 네 가지다
크기를 움직이는 레버는 네 개이고, 각기 다른 층에서 작동하며 비용은 거의 겹치지 않는다. 잘못된 레버를 고르는 것이 압축이 아무 일도 하지 않은 것처럼 보이는 가장 흔한 이유다.
| 레버 | 건드리는 것 | 크기 이득 | 비용 |
|---|---|---|---|
| 스트림 재압축과 객체 스트림 | 콘텐츠 스트림의 압축 방식, 객체의 저장과 패킹 | 원본이 얼마나 느슨했는지에 달림 | 페이지 모양 그대로 |
| 이미지 다운샘플링 | 픽셀 수, 즉 해상도 | 큼 | 해상도가 영구히 낮아짐 |
| 손실 이미지 재인코딩 | 비트맵의 바이트 표현 | 큼 | 화질 저하 |
| 폰트 서브셋팅 | 임베드된 글리프 테이블 | 중간 | 쓰이지 않은 글리프는 더 이상 편집 불가 |
| 선형화 | 객체의 순서 | 거의 없음 | 대신 첫 페이지 로드 시간을 얻음 |
앞의 네 줄은 모두 넓은 의미로 "압축"이라 불리지만, 내용을 건드리지 않는 것은 첫 줄뿐이다. 효과를 가장 오판하기 쉬운 줄이기도 하다. 크기 대부분이 가공되지 않은 이미지 샘플인 스캔에는 제거할 중복이 별로 없다. 그것을 크게 줄여 줄 레버는 다운샘플링과 손실 재인코딩이고, 대가는 그 해상도와 화질을 영영 잃는 것이다.
PDF123 압축이 구현하는 레버는 하나뿐
PDF 압축(POST /api/v1/misc/compress-pdf)이 하는 일은 첫 줄뿐이다. qpdf를 압축되지 않은 스트림의 압축(--compress-streams=y), 이미 Flate로 압축된 스트림에 대한 두 번째 패스(--recompress-flate), 객체를 객체 스트림으로 패킹(--object-streams=generate)과 함께 돌린다.
이미지를 다운샘플링하지 않고, 비트맵을 더 낮은 JPEG 품질로 재인코딩하지도 않고, 폰트를 서브셋팅하지도 않는다. 페이지는 같아 보여야 하며, 절감은 Flate 재압축과 객체 스트림 패킹에서 나온다.
실패 조건도 같은 선명도로 적어 둘 가치가 있다. 파일 크기가 대부분 이미 JPEG로 압축된 이미지 데이터라면 Flate가 짜낼 것이 남아 있지 않다. 그 이미지 바이트는 위의 세 스위치가 닿는 범위 밖에 있기 때문이다. 그래서 스캔 묶음은 몇 퍼센트만 작아지는데 비슷한 크기의 텍스트 위주 PDF는 눈에 띄게 더 줄어드는 일이 생긴다.
반대 경로는 PDF 압축 풀기이며, 검사를 위해 스트림을 펼치고(--qdf, 객체 스트림 비활성화) 마찬가지로 페이지 모양을 바꾸지 않는다.
다른 경로를 골라야 할 때
| 필요한 것 | 쓸 경로 |
|---|---|
| 모양은 그대로, 패킹 오버헤드만 덜고 싶다 | 압축 |
| 스캔 묶음이 정말 크고 화질 손실을 감수할 수 있다 | 다운샘플링이나 손실 재인코딩. 이 압축이 아니다 |
| 편집은 계속 하고 싶은데 다음 글자가 입력되지 않는다 | 내보내기 설정이나 원본 파일을 바꾼다. 폰트 서브셋팅 참고 |
| 브라우저가 첫 페이지를 더 빨리 보여 주길 원한다 | 선형화가 해결하는 것은 첫 페인트이며 크기가 아니다 |
| 파일이 아예 열리지 않는다 | 복구. 크기가 아니라 구조 문제 |
마지막 줄에 함정이 하나 있다. 여기서 PDF 선형화는 별도의 구현이 아니라 압축의 별칭이다. 둘 다 같은 작업을 가리키며 정규 id는 misc/compress-pdf이고 linearize-pdf는 별칭일 뿐이다. 포털은 이 도구에 항상 linearize=true와 optimizeLevel=1을 보내지만 서버는 두 필드를 모두 읽지 않고, qpdf에 --linearize가 전달되는 일도 없다. 실제로 파일을 선형화하는 작업은 보호가 암호화할 때 일어난다.
그래서 모양은 그대로 두고 파일만 작게 만들고 싶다면 압축으로 충분하다. 손실 이미지 축소나 폰트 서브셋팅을 원한다면 이 경로는 잘못된 레버다. 파일은 계정 없이 곧바로 돌아온다. 객체와 스트림, 상호 참조 테이블이 파일 어디에 있는지 보려면 PDF 안에는 무엇이 있는가로 이어가면 된다.