블로그 › PDF를 회전해도 용량이 그대로인 이유
PDF 페이지 편집 도구를 만들면서 20쪽짜리 문서의 모든 쪽을 90도 돌려봤습니다. 회전하기 전 파일이 1,064,740 바이트, 돌린 뒤에도 1,064,740 바이트. 한 바이트도 변하지 않았습니다.
같은 문서에서 20쪽 중 3쪽만 남겼더니 이번에는 1,039.8KB가 156.5KB로 줄었습니다. 85% 감소입니다. 회전은 공짜인데 삭제는 이렇게 극적이라는 게 이상하게 느껴진다면, PDF를 이미지처럼 생각하고 있기 때문입니다.
JPEG나 PNG는 결국 픽셀의 배열입니다. 파일을 열면 "이 자리는 이 색"이 죽 늘어서 있습니다. PDF는 그렇지 않습니다. PDF 파일 안에 들어 있는 것은 서로를 가리키는 객체들의 그래프입니다.
페이지 하나는 그 자체로 그림이 아니라, 몇 가지를 가리키는 참조와 속성의 묶음입니다. 어떤 순서로 무엇을 그릴지 적어 둔 내용 스트림을 가리키고, 그 과정에서 쓸 글꼴과 이미지를 모아 둔 자원 사전을 가리킵니다. 여기에 종이 크기와 회전각 같은 속성이 몇 개 붙어 있습니다.
이 구조를 알고 나면 앞의 두 숫자가 저절로 설명됩니다.
PDF에는 /Rotate라는 페이지 속성이 있습니다. "이 페이지를 화면에 보여줄 때 몇 도 돌려서 보여줘라"를 적어 두는 자리이고, 넣을 수 있는 값은 0·90·180·270 네 개뿐입니다.
그러니 회전은 페이지 내용을 실제로 돌리는 일이 아닙니다. 숫자 하나를 0에서 90으로 바꾸는 일입니다. 내용 스트림은 손대지 않고, 이미지도 다시 압축하지 않습니다. 앞의 측정에서 파일 크기가 정확히 그대로였던 이유도 여기 있습니다 — 한 자리 숫자를 다른 한 자리 숫자로 바꿨으니 길이가 달라질 일이 없었던 것입니다.
대조해 보면 차이가 분명해집니다. 사진 앱에서 JPEG를 돌려 저장하면 대개 픽셀을 실제로 회전시킨 뒤 다시 압축합니다. 돌릴 때마다 화질이 조금씩 깎이죠. PDF에서는 몇 번을 돌려도 원본과 완전히 같은 데이터가 남습니다. 방향을 잘못 눌렀다가 세 번 더 눌러 제자리로 돌려놔도 손해가 없습니다.
여러 PDF를 합칠 때는 페이지 객체를 새 문서로 복사합니다. 그런데 페이지만 달랑 가져올 수는 없습니다. 그 페이지가 가리키던 글꼴과 이미지가 새 문서에도 있어야 같은 모양으로 보이니, 자원까지 함께 따라옵니다.
문제는 이 복사가 중복을 알아보지 못한다는 점입니다. 세 파일이 똑같은 로고 이미지를 쓰고 있어도, 병합기가 보기에는 그저 각자의 자원일 뿐이라 세 벌이 그대로 들어갑니다.
얼마나 차이 나는지 재봤습니다. 같은 53KB짜리 이미지를 쓰는 1쪽 PDF 세 개를 만들어 합친 것과, 처음부터 한 문서에 3쪽을 만들어 같은 이미지를 재사용한 것을 비교했습니다.
다만 오해하기 쉬운 부분이 있어 정확히 적어 둡니다. 이 측정에서 병합 자체가 파일을 부풀린 것은 아닙니다. 원본 세 개의 합계가 157.7KB였고 병합 결과는 156.5KB로, 오히려 아주 조금 작았습니다. 파일 하나당 붙던 머리말과 상호 참조 표가 하나로 줄었기 때문입니다.
그러니 정확한 표현은 "병합하면 커진다"가 아니라 "병합해도 중복은 줄지 않는다"입니다. 같은 서식으로 만든 회사 문서 열 개를 합치면, 같은 로고와 같은 글꼴이 열 벌 들어간 파일이 나옵니다. 그게 정상 동작입니다 — 자원을 하나로 합치려면 두 자원이 정말 같은 것인지 바이트 단위로 대조해야 하고, 조금이라도 잘못 판단하면 문서 모양이 깨집니다.
페이지를 지우는 것도 원본에서 무언가를 덜어내는 방식이 아닙니다. 남기기로 한 페이지만 새 문서에 복사하는 방식입니다. 그러면 남긴 페이지가 가리키지 않는 자원은 애초에 따라오지 않습니다.
쪽마다 다른 사진이 들어간 20쪽 문서에서 3쪽만 남겨봤습니다.
나머지 17쪽이 쓰던 사진이 통째로 빠졌으니 당연한 결과입니다. 여기서 실용적인 결론이 하나 나옵니다 — PDF 용량을 줄이는 가장 확실한 방법은 압축이 아니라 필요 없는 쪽을 지우는 것입니다. 이미지 재압축은 화질을 대가로 치르지만, 안 쓰는 페이지를 빼는 것은 아무것도 잃지 않습니다.
PDF를 합치거나 잘라 본 적이 있다면 목차가 통째로 없어진 경험이 있을 겁니다. 이것도 같은 구조에서 나옵니다. 책갈피는 페이지에 붙어 있는 것이 아니라 문서에 붙어 있습니다. "1장은 이 페이지 객체, 2장은 저 페이지 객체"를 가리키는 목록이 문서 수준에 따로 있는 것이죠. 페이지만 새 문서로 옮기면 그 목록은 남겨지고, 억지로 가져와도 가리키던 대상이 없어 어긋납니다.
전자서명은 더 분명합니다. 서명은 파일의 특정 바이트 범위 전체에 대한 해시입니다. 그 범위 안에서 한 바이트라도 달라지면 서명은 무효가 됩니다. 페이지를 옮기면 객체 번호도 위치도 전부 바뀌니 당연히 깨집니다.
그래서 이건 "보존하지 못하는 한계"가 아니라 "보존되면 안 되는 것"에 가깝습니다. 서명의 존재 이유가 "이 문서는 서명된 뒤로 바뀌지 않았다"를 증명하는 것인데, 내용을 편집했는데도 서명이 멀쩡히 살아 있다면 그 서명은 아무것도 보장하지 못하는 셈입니다.
마지막으로, 페이지 편집기를 만들면서 가장 뜻밖이었던 부분입니다. 브라우저에서 PDF를 편집하려면 라이브러리가 두 개 필요합니다. 하나는 PDF를 고칠 수 있지만 볼 수 없고, 다른 하나는 볼 수 있지만 고칠 수 없기 때문입니다.
지금까지 이야기한 작업 — 회전값 바꾸기, 페이지 복사하기, 자원 따라 붙이기 — 은 전부 객체 그래프를 다루는 일이라 비교적 간단합니다. 하지만 그 페이지를 화면에 그리는 것은 완전히 다른 문제입니다. 내용 스트림의 명령을 하나씩 해석하고, 글꼴 파일을 읽어 글자 모양을 픽셀로 만들고, 색 공간을 변환하고, 투명도를 합성해야 합니다. 사실상 작은 브라우저를 하나 만드는 일입니다.
그 무게가 번들 크기에 그대로 드러납니다. 페이지 편집 도구가 실제로 받아오는 용량은 이렇습니다.
이 분리에는 실용적인 결과가 하나 따라옵니다. 편집기에 보이는 미리보기는 pdf.js가 화면용으로 대충 그린 그림이고, 저장할 때는 pdf-lib이 원본에서 페이지를 그대로 복사합니다. 즉 미리보기가 흐릿해도 결과 파일의 화질과는 아무 상관이 없습니다. 미리보기는 어디까지나 어느 쪽을 남길지 고르기 위한 그림입니다.
결국 전부 한 문장에서 따라 나옵니다. PDF는 픽셀 묶음이 아니라 서로를 가리키는 객체들의 모음입니다. 그래서 어떤 편집은 놀랄 만큼 싸고, 어떤 편집은 애초에 불가능합니다.
직접 확인해 보세요
페이지를 지우고 돌리는 작업은 전부 브라우저 안에서 이루어집니다. 파일은 업로드되지 않습니다.