216 DPI와 300 DPI 결과물이 바이트 단위로 같았던 이유
한 페이지짜리인데 높이가 9000pt인 PDF를 이미지로 바꿨더니 작은 글씨가 뭉개진다는 문의를 받았습니다. 해상도가 부족하다고 판단해서 216 DPI를 300 DPI로 올리고 다시 돌렸는데, 결과는 "비슷" 정도가 아니라 완전히 같은 파일이었습니다. 1456×16380 픽셀, 1.10 MB, 4.05초, 세 값이 전부 일치했습니다. 원인은 파싱도 메모리도 아니고 마지막 인코딩 단계에 있었고, 마침 그 부분이 제가 말할 수 있는 영역입니다.
먼저 밝혀두면 제가 맡은 건 클라이언트 쪽 인코딩/디코딩 부분이고 PDF 엔진 자체는 제 담당이 아닙니다. 페이지 파싱 얘기는 "아마 이럴 것이다" 정도로만 말할 수 있고, 캔버스와 인코더 얘기는 실제 숫자를 드릴 수 있습니다. 샘플은 남의 벤치마크를 인용하고 싶지 않아서 직접 만들었습니다. A4 4페이지 도표 혼재 문서 490 KB와 800×9000pt 초장문 1페이지, 이 두 개입니다.
일반 4페이지, 비용은 용량이 아니라 시간
먼저 4페이지 쪽, WebP 전체 페이지 출력. 144 DPI는 페이지당 1192×1686에 합계 317 KB·2.0초, 216 DPI는 1788×2529에 494 KB·3.0초, 300 DPI는 2483×3512에 647 KB·6.1초입니다. 216에서 300으로 가면 픽셀은 93% 늘고 용량은 31%만 늘고 시간은 정확히 두 배가 됩니다. 용량이 주된 비용일 거라고 보고 측정을 시작했는데 아니었습니다. 문서형 페이지는 평평한 흰 영역이 대부분이라 늘어난 픽셀을 인코더가 거의 흡수해 버리고, 정직하게 선형으로 늘어나는 건 시간 쪽이었습니다. 같은 216 DPI에서 포맷만 바꾼 결과도 같은 얘기를 다른 각도에서 합니다. WebP 494 KB·3.0초, AVIF 552 KB·10.3초, PNG 1.28 MB·1.0초, JPG 1.36 MB·1.0초. AVIF가 WebP보다 12% 크고 3.4배 느린 건 예상과 정반대인데, 평평한 면과 날카로운 글자 경계로 이뤄진 페이지에서는 AVIF의 인트라 예측이 별로 먹히지 않으면서 인코딩 시간만 그대로 나가는 것으로 이해하고 있습니다. 무손실인 PNG가 JPEG보다 6% 작은 것도 뒤집어 보면 같은 얘기로, JPEG는 글자 경계의 링잉에 비트레이트를 씁니다. 둘 다 이 샘플에서의 결과이고, 사진 위주 PDF라면 뒤집힐 가능성이 크지만 그건 측정하지 않았으므로 단정하지 않겠습니다.
설정 두 개, 나온 건 같은 파일 하나
흥미로운 건 초장문 페이지 쪽입니다.
설정 | 출력 크기 | 용량 | 시간 |
|---|---|---|---|
216 DPI / 전체 페이지 | 1456×16380 | 1.10 MB | 4.06초 |
300 DPI / 전체 페이지 | 1456×16380 | 1.10 MB | 4.05초 |
300 DPI / 한 장의 긴 이미지 | 1456×16380 | 1.40 MB | 4.05초 |
216 DPI라면 원래 2400×27000이 나와야 하는데 실제 폭은 1456이고, 1456 / (800 / 72)로 역산하면 131 DPI, 선택지 중 가장 낮은 144보다도 낮습니다. 핵심은 16380이라는 숫자로, WebP의 최대 변 길이 16383에서 딱 3픽셀 앞에 맞춰졌습니다. 메모리가 부족해서 품질을 낮춘 게 아니라 인코더가 그보다 긴 변을 아예 받지 않는다는 뜻입니다. 종횡비는 16380/1456 = 11.25로 원본 페이지의 9000/800과 정확히 일치하니 양쪽이 동시에 축소된 것이고, 아래가 잘리거나 늘어나지도 않았습니다. 세 번째 줄은 설명하지 못했습니다. 같은 크기, 같은 내용인데 긴 이미지 경로는 1.40 MB, 전체 페이지 경로는 1.10 MB로 27% 차이가 납니다. "긴 이미지 경로가 더 보수적인 품질 설정을 쓰는 모양"이라고 쓰다가 지웠습니다. 그건 관찰을 말만 바꾼 것이지 설명이 아니니까요.
타일 렌더링이 필수인 이유
타일 렌더링이 필수인 이유도 여기서 이어집니다. 2400×27000은 6480만 픽셀, RGBA면 259 MB짜리 캔버스가 상주하고 모바일 Safari의 캔버스 면적 상한은 이보다 한참 낮습니다. 그래서 페이지를 안전한 높이로 잘라 한 장씩 렌더링·인코딩하고 원래 좌표로 합성하는데, 처음 구현에서 두 가지를 틀렸습니다. 하나는 초기 버전이 전체 타일을 다 렌더링한 뒤 합성해서, 합성 시점에 N개의 타일과 대상 캔버스가 동시에 살아 있어 피크 메모리가 오히려 분할하지 않았을 때보다 나빴던 것. 타일은 그리자마자 즉시 해제하지 않으면 한 번의 큰 할당이 N번의 작은 할당으로 바뀔 뿐입니다. 다른 하나는 이음매로, 각 타일이 자기 높이를 따로 반올림하면 오차가 쌓여 1픽셀 어긋남이나 글자 겹침이 생깁니다. 각 타일의 시작점이 이전 타일의 끝점과 같아지도록 원 좌표계에서 계산하고 반올림은 한 번만 해야 합니다. 둘 다 이음매를 눈으로 따라가며 찾은 것이고, 세련된 디버깅 방법이 있었던 건 아닙니다.
실무적으로는 A4 같은 일반 크기는 216이면 충분하고, 인쇄나 후단 OCR이 있을 때만 300으로 올리되 늘어나는 건 용량보다 시간이라고 잡아두면 됩니다. 초장문 페이지에서 DPI를 올려도 소용없으니 출력 폭을 재서 폭 / (페이지 폭pt / 72)로 실효 DPI를 구하세요. 그게 실제로 받은 해상도이고, 더 얻고 싶으면 페이지를 나누는 수밖에 없습니다. 아직 결론을 못 낸 게 하나 있는데, 상한에 걸렸을 때 타일을 개별 이미지로 내보내고 사용하는 쪽에서 이어 붙이게 하면 해상도는 지킬 수 있지만 "한 페이지 = 한 파일"이라는 출력 형태가 깨진다는 점입니다. 어느 쪽이 덜 나쁜지 아직도 못 정했습니다. 비슷한 걸 만들어 보신 분이 있다면 어느 쪽을 택하셨는지 듣고 싶습니다.
위 수치는 https://imging.ai/pdf-to-image/ 에서 측정했습니다.
