기술 문서

Delphi에서 거의 동일한 이미지를 병합해 PDF 용량 줄이기

PDF Library for Delphi는 DeduplicateSimilarImages를 통해 PDF 내부에서 겉보기에는 같지만 바이트 단위로는 동일하지 않은 이미지를 병합할 수 있습니다. 완전 중복 제거는 딕셔너리와 인코딩된 페이로드가 일치하는 이미지만 병합하지만, 이 기능은 지각 해시와 두 가지 픽셀 오차 임계값을 사용해 이미지가 실제로 어떻게 보이는지를 비교하고, 모든 참조를 하나의 정본 객체로 다시 가리킵니다

이 기능이 값어치를 하는 파일들은 우연히 비대해진 파일들입니다. 스캔된 문서 묶음에서 모든 페이지가 서로 다른 스캐너로 다시 인코딩된 같은 로고를 담고 있는 경우. 서로 다른 소스에서 조합되어 각각 조금씩 다른 JPEG 품질로 같은 차트를 임베드한 보고서. 두 도구로 썸네일이 재생성된 카탈로그. 어느 경우든 파일은 하나의 그림을 여러 벌 담고 있으며, 완전 중복 제거는 그중 어느 것도 건드릴 수 없습니다

'충분히 비슷하다'는 것이 무엇을 뜻해야 하는가

두 이미지가 병합되려면 세 가지 임계값이 모두 만족되어야 하며, 각각은 서로 다른 종류의 차이를 잡아냅니다. 지각 해시 거리는 구조적 차이를 제한합니다: 64비트 차이 해시는 디코딩된 이미지의 64x64 샘플로부터 계산되며, 허용되는 해밍 거리는 0부터 7까지입니다. 평균 채널 오차는 전반적인 편차를 제한하는데, 이는 재인코딩이 만들어내는 것입니다. 최대 단일 채널 오차는 국소적인 차이를 제한하는데, 이는 작은 편집이 만들어내는 것입니다

세 가지 모두를 요구하는 것이 이 병합을 정당하게 만듭니다. 한쪽 구석에 워터마크만 다른 이미지는 평균 오차는 낮지만 최대 오차는 높으므로, 최대 오차 게이트가 이를 거부합니다. 재압축된 이미지는 어디서나 작은 오차를 가지므로 통과합니다. 이것이 바로 크기 최적화기가 그어야 할 구분선입니다

거의 동일한 이미지의 병합을 결정하는 세 가지 임계값: 해시 거리, 평균 채널 오차, 최대 단일 채널 오차
세 게이트 모두 디코딩된 픽셀을 사용합니다 — 워터마크만 다른 쌍은 최대 오차 게이트에서 걸리는 반면 재인코딩된 쌍둥이는 무사히 통과합니다
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Stats: TPDFlibImageSimilarityDedupStats;
  Merged: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('scanned-batch.pdf', '');

    // 해시 거리 2, 평균 오차 2, 단일 채널 오차 4
    // 옵션으로 드라이 런을 요청하면 아무것도 수정되지 않음
    Merged := Lib.DeduplicateSimilarImages(2, 2, 4, 0, Stats);
    if Merged < 0 then
      raise Exception.Create('deduplication failed');

    Log(Format('%d scanned, %d merged, %d refs repointed, %d bytes duplicated',
      [Stats.ImagesScanned, Stats.ImagesDeduplicated,
       Stats.ReferencesRepointed, Stats.DuplicatePayloadBytes]));
    Log(Format('worst accepted: hash %d, mean %.2f, channel %d',
      [Stats.MaxAcceptedHashDistance, Stats.MaxAcceptedMeanError,
       Stats.MaxAcceptedChannelError]));
    Lib.SaveToFile('scanned-batch-optimised.pdf');
  finally
    Lib.Free;
  end;
end;

시각적 유사성보다 의미론적 동일성이 먼저다

픽셀을 비교하기 전에 먼저 후보 이미지들은 의미론적으로 일치해야 합니다. 라이브러리는 /Length, /Filter, /DecodeParms를 제외한 전체 이미지 딕셔너리로부터 안정적인 서명을 만들어내므로, 크기, 색공간, 비트 심도, 마스크, /Decode 배열, 보간, 선택적 콘텐츠 소속, 메타데이터가 모두 일치해야 합니다

이 순서는 잘못된 병합 전체를 막아 줍니다. 두 이미지가 64x64 샘플에서는 동일해 보여도 페이지 위에서는 다르게 동작할 수 있습니다: /Interpolate가 참인 것과 그렇지 않은 것은 확대할 때 다르게 부드럽게 처리되며, 선택적 콘텐츠 그룹에 속한 것은 그 레이어가 숨겨지면 사라집니다. 이런 경계를 넘어 병합하면 더 작으면서도 잘못된 문서가 만들어집니다

간접 참조되는 이미지 XObject만 스캔됩니다. 인라인 이미지는 경제성이 다른 별개의 구성체이며, 디코딩할 수 없는 이미지는 근거 없이 병합되는 대신 보수적으로 건너뛰고 개수만 셉니다 — 픽셀 증거가 없으면 병합도 없습니다

검색이 저렴하게 유지되는 방법

모든 이미지를 다른 모든 이미지와 비교하는 것은 이차 시간이며, 수천 개의 이미지를 담은 문서에서는 받아들일 수 없습니다. 그래서 해시는 여덟 개의 8비트 밴드로 나뉘고, 각각에 정렬된 색인이 붙습니다

비둘기집 원리가 나머지를 해결합니다: 두 해시가 최대 7비트만 다르다면, 여덟 개 밴드 중 최소 하나에서는 반드시 정확히 일치해야 합니다. 따라서 밴드별로 후보를 조회하면 전수 쌍대 스캔 없이도 허용되는 모든 일치를 찾을 수 있으며, 공개된 한도가 7인 이유는 정확히 그 거리까지만 이 보장이 성립하기 때문입니다

64비트 차이 해시가 여덟 개의 8비트 밴드로 나뉘며, 비둘기집 원리는 7비트가 뒤집혀도 밴드 하나는 여전히 일치함을 보장합니다
여덟 밴드에 걸쳐 7비트가 뒤집혀도 밴드 하나는 손상되지 않아야 하며, 이 덕분에 이차 시간 스캔이 여덟 번의 정렬된 조회로 바뀝니다

후보와 밴드 히트는 객체 번호와 세대 번호로 결정론적으로 정렬되며, 가장 낮은 객체 번호가 정본이 됩니다. 이는 출력을 재현 가능하게 만듭니다: 같은 입력에 대해 최적화기를 두 번 실행해도 같은 정본 선택이 나오며, 이는 결과를 비교하거나 서명할 때 중요합니다

드라이 런과 정직한 보고

이는 병합된 이미지가 동일하지 않았다는 의미에서 손실이 있는 최적화이므로, 합리적인 워크플로는 뛰어들기 전에 먼저 살펴보는 것입니다. 드라이 런 옵션은 객체 그래프를 수정하지 않고도 무엇이 병합될지와 얼마나 많은 페이로드가 회수될지를 보고하므로, 배치 작업이 그 효과를 기록할 수 있고 사람은 파일마다가 아니라 한 번만 임계값을 승인하면 됩니다

보고 내용에는 짚어 둘 만한 정직한 사실이 두 가지 있습니다. 회수 가능 수치는 중복 스트림의 인코딩된 페이로드만 추정하며, 딕셔너리, 간접 객체, 상호 참조 오버헤드는 세지 않으므로, 실제 파일 크기 감소는 보고된 것보다 더 작은 게 아니라 조금 더 큽니다. 그리고 보고서는 받아들여진 최대 오차와 비교 작업량을 노출하므로, 병합이 임계값에 얼마나 가까웠는지 확인할 수 있습니다

고정된 64x64 샘플링 격자는 의도적인 근사이며, 명확히 짚어 둘 만한 결과가 하나 있습니다: 샘플 셀 하나보다 작은 차이는 임계값이 0이더라도 관찰되지 않을 수 있습니다. 스캔된 페이지에서 한 단어만 바뀐 경우 그 샘플링을 통과하지 못할 수도 있습니다. 바이트 수준의 증명이 필요할 때는 대신 완전 중복 제거 경로를 사용하세요 — 이는 진짜로 동일한 리소스만 병합하며 시각적 판단은 전혀 하지 않습니다

크기 축소 파이프라인에서 이것이 있어야 할 위치

위험도 순서로 단계를 배치하세요. 무손실이고 비용이 없으므로 완전 리소스 중복 제거를 먼저 실행하세요. 그다음은 폰트 서브셋팅인데, 이 역시 무손실이며 보통 텍스트가 많은 문서에서 가장 큰 단일 이득을 주며, PDF 파일 크기 최적화와 폰트 서브셋팅에서 다룹니다. 그다음은 대상 매체가 허용한다면 이미지 재압축이나 다운샘플링입니다. 지각 중복 제거는 그 이후에 와야 하는데, 결과가 판단에 좌우되는 유일한 단계이기 때문입니다

무손실 완전 중복 제거와 폰트 서브셋팅에서 시작해 조건부 이미지 재압축, 지각 중복 제거, 검증으로 이어지는 권장 파이프라인 순서
각 단계는 위험을 더하므로 무손실 이득이 먼저 오고 판단이 필요한 병합이 마지막에 실행되며, 그 뒤에 렌더링 후 비교 검사가 이어집니다

문서 종류별로 임계값을 정하세요, 전역적으로 정하지 말고. 모든 페이지가 같은 레터헤드를 담고 있는 스캔 묶음이라면, 해시 거리 2에서 3에 평균 오차 약 2를 두면 레터헤드를 안정적으로 병합할 수 있습니다. 이미지가 데이터를 담고 있는 문서 — 차트, 지도, 의료 영상 — 라면 임계값을 0이나 거의 0으로 두거나, 이 단계를 아예 건너뛰세요. 플롯된 점 하나만 다른 두 차트는 64x64에서 아무리 비슷해 보여도 서로 다른 두 차트이기 때문입니다

최적화 후에는 검증하세요. 전후로 몇 페이지를 렌더링해 비교하고, 페이지 수와 이미지 수가 예상한 대로 바뀌었는지 확인하세요. 문서가 실제로 무엇을 담고 있는지 살펴보는 방법은 카테고리별 PDF 파일 크기 감사하기에서 설명하며, 이는 애초에 이미지가 문제인지 아닌지 알아내는 가장 빠른 방법이기도 합니다

지각 중복 제거, 완전 리소스 중복 제거, 크기 감사는 Delphi, C++Builder, Free Pascal을 위한 동일한 객체 그래프 위에서 동작합니다. 전체 기능 목록은 Delphi용 PDF Library 페이지에서 확인할 수 있습니다