기술 문서

Delphi MRC: 스캔 PDF 페이지를 JPEG + CCITT로 압축

losLab PDF Library for Delphi는 TPDFlib.CompressPageAsMRC로 스캔 페이지를 Mixed Raster Content 방식으로 압축합니다. 이 메서드는 페이지를 렌더링하고, 풀 해상도 1비트 전경 마스크를 스레숄딩하고, 나머지를 JPEG 배경으로 다운샘플링한 뒤, 페이지를 두 이미지로 다시 만듭니다. 현재 채우기 색으로 칠해진 /DCTDecode 배경 하나와 /CCITTFaxDecode 스텐실 마스크 하나입니다

이 메서드가 존재하는 이유는 모든 문서 보관함에 가득한 바로 그 페이지 때문입니다. 송장이나 계약서의 300 DPI 컬러 스캔이 하나의 큰 JPEG로 저장된 형태죠. 텍스트가 읽히는 품질로 인코딩하면 파일이 커지고, 품질을 낮추면 글자에 띠 모양 후광과 뭉개진 세리프가 번집니다. JPEG가 가장 못 하는 것이 하얀 종이 위의 선명한 검은 경계이기 때문입니다. Mixed Raster Content는 하나의 코덱에 두 가지 일을 시키는 것을 멈춥니다

단일 JPEG는 스캔 텍스트를 왜 흐릴 뿐 아니라 크게 만들까

단일 JPEG가 스캔 텍스트를 못 다루는 이유는 DCT 코덱이 비트를 매끄러운 그라데이션에 쓰고, 양자화가 가장 먼저 버리는 고주파 계수로 딱딱한 경계를 근사하기 때문입니다. 스캔 문서의 텍스트는 거의 전부 딱딱한 경계입니다. 그 경계를 깨끗하게 유지하려면 종이 질감, 로고, 커피 얼룩까지 필요 없는 곳들까지 포함해 페이지 전체에 높은 품질 설정을 강요하게 됩니다

MRC 발상은 페이지를 영역의 실제 정체에 따라 나누는 것입니다. 잉크는 바이레벨(bilevel) 이미지가 되고, CCITT 같은 팩스 코덱이 검은 픽셀과 흰 픽셀의 연속 구간을 무손실로, 아주 조밀하게 기록합니다. 종이 톤, 사진, 컬러 배경 같은 나머지 전부는 연속톤 이미지가 되어 해상도를 몇 분의 일로 줄여 저장할 수 있습니다. 배경을 300 DPI로 읽는 사람은 없으니까요. 바이레벨 레이어는 텍스트를 풀 해상도로 또렷하게 지키고, 배경 레이어는 낮은 비용으로 색을 지킵니다

Delphi에서 PDFlibPas CompressPageAsMRC가 만드는 단일 JPEG 스캔과 Mixed Raster Content 레이어 분할 비교: DCT 코덱은 텍스트의 딱딱한 경계를 뭉개고 페이지 전체에 높은 품질을 강요하는 반면, 잉크는 300 DPI 무손실 1비트 CCITT 스텐실이 되고 종이 톤은 75 DPI DeviceRGB JPEG 배경이 됨
Mixed Raster Content는 하나의 코덱에 두 가지 일을 시키지 않습니다. 바이레벨 레이어가 글자를 풀 해상도로 또렷하게 지키고 배경이 몇 퍼센트 비용으로 색을 지키므로, 품질 50에서 70 사이는 보통 눈에 띄지 않습니다

CompressPageAsMRC는 페이지를 어떻게 두 레이어로 나눌까

TPDFlib.CompressPageAsMRC는 페이지마다 하나의 고정 파이프라인을 실행합니다. 페이지를 요청된 DPI로 24비트 비트맵에 렌더링하고, Rec. 601 가중 휘도(퍼밀 299/587/114)로 모든 픽셀을 Threshold와 비교해 분류한 결과를 같은 해상도의 1비트 마스크에 기록합니다. 동시에 같은 렌더를 BGScale만큼 박스 평균으로 줄여서, BGScale이 4면 픽셀 4×4 블록마다 배경 픽셀 하나가 됩니다

Delphi용 PDFlibPas의 CompressPageAsMRC 데이터 흐름: 300 DPI 24비트 렌더 하나를 Rec. 601 휘도로 스레숄딩해 풀 해상도 CCITTFaxDecode K 1 ImageMask 스텐실을 만들고, BGScale 4 박스 평균으로 DCTDecode DeviceRGB 배경을 만들어, 두 이미지가 마스크를 맨 위에 두고 원래 페이지 연산자를 대체
두 레이어는 하나의 렌더에서 나옵니다. DPI가 스레숄딩에서 살아남는 획의 가늘기를 정하고, BGScale은 사진용 2부터 일반 텍스트용 8까지 배경 디테일을 절충하며, 다시 만들어진 페이지는 스텐실을 현재 채우기 색으로 그립니다

그다음 페이지 콘텐츠가 교체됩니다. 원래 연산자들이 제거되고, 다운샘플링된 배경은 /DeviceRGB의 /DCTDecode 이미지로(ISO 32000-1 §7.4.8), 마스크는 라이브러리 CCITT 인코더로 /CCITTFaxDecode 이미지(§7.4.6)로 인코딩되어 /ImageMask true 플래그가 붙으며, 둘 다 마스크를 맨 위에 두고 페이지 박스 전체에 그려집니다. 시그니처는 짧습니다:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page는 1부터 시작하며 PageCount 범위 안이어야 합니다
  • DPI는 원본 래스터의 렌더 해상도로 50에서 1200입니다
  • BGScale은 배경 다운샘플 배율로 1에서 8입니다
  • JPEGQuality는 배경 JPEG 품질로 1에서 100입니다
  • Threshold는 휘도 컷으로 1에서 255이며, 이 값 이상은 종이, 더 어두운 픽셀은 잉크입니다
  • Options는 예약 인자로 0이어야 합니다

스캔 파일 전체를 도는 전형적인 배치는 다음과 같습니다. 각 페이지 직전에 SetFillColor를 호출하는데, 이유는 다음 절에서 설명합니다

uses
  SysUtils, PDFlibrary;

procedure CompressScannedFile(const SrcFile, DstFile: string);
var
  Lib: TPDFlib;
  P, Replaced: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SrcFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
    for P := 1 to Lib.PageCount do
    begin
      Lib.SetFillColor(0, 0, 0);  // 스텐실 레이어의 잉크 색
      // 300 DPI 마스크, 75 DPI 배경, JPEG 품질 60, 휘도 160에서 컷
      Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
      if Replaced = 0 then
        raise Exception.CreateFmt('MRC failed on page %d', [P]);
    end;
    if Lib.SaveToFile(DstFile) <> 1 then
      raise Exception.CreateFmt('Cannot save %s', [DstFile]);
  finally
    Lib.Free;
  end;
end;

전경을 현재 채우기 색으로 칠하는 이유

전경 레이어는 스텐실 마스크이고, 스텐실 마스크에는 자체 색이 없습니다. ISO 32000-1 §8.9.6.2는 /ImageMask true인 이미지를 1비트 모양으로 정의합니다. 샘플 값이 칠해질 위치를 표시하고, 칠해지는 것은 그래픽스 상태의 현재 비스트로킹 색입니다. 레이어가 싸지는 이유가 정확히 여기에 있고, CompressPageAsMRC가 색 매개변수를 받지 않는 이유도 여기 있습니다

TPDFlib.CompressPageAsMRC는 SetFillColor로 설정한 색으로 마스크를 칠하므로 호출 순서가 중요합니다. 페인터는 채우기 색을 게으르게 내보냅니다. SetFillColor는 색을 기록해 변경 표시를 해 두고, 스텐실 드로우는 그 변경이 아직 대기 중일 때에만 자기 q/Q 블록 안에 rg 연산자를 씁니다. 사이에서 채우기 도형을 하나 그리면 대기 중인 변경은 그 도형이 대신 소비합니다. 위 배치 예처럼 각 페이지 바로 앞에서 색을 설정하면 추측이 사라집니다. 검은색이 보통 선택이고, 출력이 복사본처럼 보이지 않길 원하는 파란 잉크 양식이라면 짙은 남색도 쓸 만합니다

Delphi에서 PDFlibPas MRC 스텐실 레이어의 게으른 채우기 색 방출 순서: SetFillColor가 색을 대기 중 변경으로 기록하고, ImageMask 스텐실 드로우는 변경이 대기 중일 때 자기 q와 Q 블록 안에 rg 연산자를 쓰며, 사이에 낀 채우기 도형이 대신 대기 중 변경을 소비
스텐실 마스크는 색을 소유하지 않으므로 모든 잉크 픽셀이 하나의 채우기 색을 받습니다. 빨간 도장, 파란 서명, 검은 본문 텍스트가 한데 뭉치므로, 각 페이지 바로 앞에서 SetFillColor를 호출해야 합니다
// 파란 볼펜 양식: 잉크 레이어를 짙은 남색으로 렌더링
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

배치는 라이브러리 좌표 원점을 따릅니다. CompressPageAsMRC는 GetOrigin을 읽고 두 이미지를 페이지 박스 위쪽 모서리부터 그립니다. 원점이 왼쪽 위면(SetOrigin(1)) 위쪽 좌표 0, 기본값인 왼쪽 아래 원점에서는 위쪽 좌표 PageHeight입니다. 조정할 필요는 전혀 없지만, 파이프라인이 어느 원점을 쓰고 있든 레이어가 올바른 자리에 놓이는 이유가 여기 있습니다

스텐실 의미론의 한 가지 결과는 분명히 말해 둘 가치가 있습니다. 잉크로 분류된 모든 픽셀은 같은 색을 받는다는 것입니다. 빨간 도장, 파란 서명, 검은 본문 텍스트가 전경 레이어에서는 하나의 채우기 색으로 수렴합니다. 원래 색의 희미하고 저해상도인 흔적은 배경 JPEG만 그 아래에 간직합니다

DPI, BGScale, Threshold, JPEG 품질 고르기

DPI 값이 텍스트 선명도를 결정하며, 사무실 스캔이라면 300이 무난한 기본값입니다. 마스크는 풀 렌더 해상도로 유지되므로 DPI가 스레숄딩에서 살아남는 획의 가늠새를 정합니다. 원본 스캔 해상도보다 높게 잡아도 얻는 것은 없습니다. 렌더러가 내장된 스캔 이미지를 업샘플링할 뿐이기 때문입니다. 나머지 제약은 메모리입니다. US Letter 페이지를 300 DPI로 렌더링하면 2550×3300 픽셀, 마스크와 배경을 뽑아 내기 전 24비트 픽셀 데이터 25,245,000바이트이고, 같은 페이지를 최대치인 1200 DPI로 그리면 10200×13200 픽셀, 약 404MB입니다

BGScale은 배경 디테일과 크기를 맞바꿉니다. 300 DPI에서 배율 4는 75 DPI 배경, Letter 기준 638×825 픽셀을 주며, 종이 톤과 단색 영역에는 충분합니다. 독자가 실제로 들여다볼 사진이나 하프톤 삽화가 있는 페이지는 2를 값어치 합니다. 하얀 종이 위 일반 텍스트는 6이나 8도 괜찮습니다. 배경이 색조 이상의 무언가이기 어렵기 때문입니다

Threshold는 페이지의 생김새를 바꾸는 매개변수입니다. 올리면 중간 회색 픽셀이 더 많이 잉크로 분류되어 가는 획과 연필 자국은 굵어지지만, 스캐너 노이즈와 종이 그림자도 점으로 끌어들입니다. 내리면 종이는 깨끗해지고 글자는 가늘어지며 때때로 끊어집니다. 하얀 바탕의 검은 텍스트라면 128에서 170 근처가 무난한 시작 범위입니다. 이 절충은 Delphi에서 PDF 페이지를 1비트 모노크롬으로 렌더링하기에서 다룬 것과 같으며, 거기서도 같은 휘도 스레숄드가 RenderPageToMonochromeFileEx를 움직입니다. 그 메서드와 달리 CompressPageAsMRC에는 잡티 제거 패스가 없으므로, 노이즈 많은 스캔은 스레숄드를 조금 낮추는 편이 낫습니다

JPEGQuality는 단일 이미지 스캔에서만큼 중요하지 않습니다. 배경이 더 이상 텍스트 경계를 담고 있지 않기 때문입니다. 선명한 마스크 뒤 75 DPI 배경에서는 품질 50에서 70 사이가 보통 눈에 띄지 않습니다

반환값이 말해 주는 것과 말하지 않는 것

TPDFlib.CompressPageAsMRC는 교체한 페이지 콘텐츠의 바이트 크기를 반환하고, 뭔가 실패하면 0을 반환합니다. 성공 지표이지 절약량의 척도가 아닙니다. 전형적인 스캔 페이지에서 원본 콘텐츠 스트림은 이미지 하나를 배치하는 데 필요한 연산자만 있는 수십 바이트이므로, 페이지가 메가바이트 단위로 줄어들어도 작은 양수가 정상 결과입니다. 원본 콘텐츠 스트림이 비어 있던 페이지는 재구성되었음에도 0을 보고합니다

실패 동작은 원자적이지 않으며, 설계에서 감안해야 할 부분이 바로 여기입니다. 인자 검사와 렌더, JPEG 인코딩은 페이지를 건드리기 전에 실행되므로, 범위를 벗어난 BGScale이나 101인 JPEGQuality, 렌더 실패는 페이지를 그대로 둡니다. 그러나 원본 콘텐츠가 지워지고 나면 레이어 하나를 임베드하거나 그리는 데 실패해도 0을 반환하는데, 이 시점에서 페이지는 이미 부분적으로 재구성되어 있을 수 있습니다. 0이 나오면 저장하는 대신 메모리상 문서를 버린다는 전제로 다루고, 결과를 소스 파일 위에 덮어 쓰는 일은 절대 하지 마십시오

변환이 실제로 얼마나 아꼈는지는 문서를 측정해 알아냅니다. 전후 SaveToString 길이를 비교하는 것이 가장 빠른 확인이고, 항목별 답이 필요하면 AuditDocumentSpace를 이용한 PDF 파일 크기 감사가 무게가 기대대로 이미지 쪽에서 빠져나갔는지 보여 줍니다

function MRCSavings(Lib: TPDFlib): Int64;
var
  Before: Int64;
  P: Integer;
begin
  Before := Length(Lib.SaveToString);
  for P := 1 to Lib.PageCount do
  begin
    Lib.SetFillColor(0, 0, 0);
    if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
      raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
  end;
  Result := Before - Length(Lib.SaveToString);
end;

이 2레이어 MRC가 ITU-T T.44에 미치지 못하는 지점

CompressPageAsMRC는 의도적으로 단순화한 MRC를 구현합니다. ITU-T T.44의 완전한 모델은 스트라이프마다 세 레이어를 씁니다. 배경 이미지, 잉크 색을 운반하는 전경 이미지, 그리고 픽셀별로 둘을 가려 고르는 바이레벨 마스크이며, 한 페이지에 마스크와 전경 쌍이 여러 개 들어갈 여유도 있습니다. PDF 용어로는 세 번째 레이어가 한 색으로 칠한 스텐실이 아니라 명시적 마스크(§8.9.6.3)를 단 저해상도 컬러 전경 이미지가 됩니다. PDF Library for Delphi는 전경 색 레이어를 건너뛰는데, 결과가 그만큼 작아지는 이유이자 컬러 잉크가 채우기 색으로 수렴하는 이유이기도 합니다

  • 이 메서드는 스캔 종이 위 검은색 또는 단색 텍스트에 어울립니다. 색으로 구분된 문서, 하이라이트된 텍스트, 컬러 도장은 잉크 색을 잃습니다
  • 다시 만들어진 페이지는 순수 래스터입니다. 원본 페이지의 실제 텍스트, 벡터 아트, 선택 가능한 문자가 전부 픽셀이 되므로, born-digital 페이지에는 쓰지 말고 스캔에만 실행하십시오. 그런 페이지에는 폰트 서브세팅과 이미지 다운샘플링이 맞는 도구입니다
  • 마스크는 T.4 2차원 모드의 라이브러리 CCITT 인코더를 쓰며 /K 1로 /CCITTFaxDecode에 기록됩니다. 인코더에는 AddImageCCITTFromBitmapStream으로 쓰는 Group 4 모드도 있지만 CompressPageAsMRC는 이를 노출하지 않고, JBIG2도 사용하지 않습니다
  • 어노테이션과 폼 필드는 평탄화되지 않습니다. 재구성된 콘텐츠 위의 객체로 남습니다
  • 배경은 /DeviceRGB이므로 출력은 PDF/X-1a 워크플로에 맞지 않고, SetFillColor 자체도 PDF/X-1a 모드에서는 거부됩니다
  • OCR이 필요한 스캔은 MRC보다 먼저, 원본 픽셀이 아직 풀 컬러 심도로 존재할 때 인식해야 합니다

FAQ

CompressPageAsMRC는 텍스트를 검색 가능하게 유지하나요?

아니요. CompressPageAsMRC는 페이지 콘텐츠를 두 이미지로 교체하므로, 보이지 않는 OCR 텍스트 레이어를 포함해 원본 페이지의 텍스트 연산자는 모두 함께 제거됩니다. 압축된 페이지에 나중에 OCR을 실행하거나, 압축 후 텍스트 레이어를 다시 얹으십시오

문서의 일부 페이지만 압축할 수 있나요?

네. CompressPageAsMRC는 호출당 1부터 시작하는 한 페이지를 처리하므로 페이지를 직접 고를 수 있고, 페이지마다 BGScale과 Threshold를 다르게 줄 수도 있습니다. 사진 삽화 페이지에는 배율 2, 일반 텍스트 페이지에는 6 같은 식입니다

Options 매개변수는 무엇을 넣어야 하나요?

0을 넣으십시오. Options는 현재 릴리스에서 미래 사용을 위해 예약되어 있고 정의된 비트가 없으므로, 0이 아닌 값은 오늘날 아무 의미가 없습니다

CompressPageAsMRC는 losLab PDF Library for Delphi v3.561.0에 들어갔으며, 이를 조립한 렌더, JPEG, CCITT, 스텐실 마스크 빌딩 블록도 함께 제공됩니다. 파이프라인의 변형이 필요하면 전부 공개 메서드로 쓸 수 있습니다. 라이브러리, C++Builder 지원, 전체 API 레퍼런스는 PDF Library for Delphi 제품 페이지에 있습니다