PDFlibPas는 서로 다른 두 백엔드를 통해 바이레벨(bilevel) 이미지를 JBIG2로 인코딩할 수 있습니다. 하나는 항상 존재하는 네이티브 Object Pascal MMR 인코더입니다. 다른 하나는 스캔한 텍스트에서 상당히 작은 출력을 만들어 주는 외부 심볼 사전(symbol-dictionary) 인코더인데, 선택 사항입니다. 존재하려면 프로젝트가 백엔드 유닛을 링크해야 합니다. 이 구분이 이 기능에서 가장 흔한 놀람의 원천이므로 먼저 밝혀 둘 가치가 있습니다. DefaultJBIG2EncodeOptions는 기본적으로 외부 인코더를 요청하며, 백엔드 유닛이 링크되어 있지 않으면 이 요청은 조용히 Pascal MMR 경로로 폴백합니다
Delphi와 C++Builder에서 외부 백엔드는 미리 빌드된 정적 오브젝트 집합입니다. Free Pascal에서는 DLL이 될 수밖에 없었고, 그 결론에 이른 길은 C++ 오브젝트를 Free Pascal 프로그램에 링크해 본 적이 있는 사람이라면 누구에게나 유용한 링커 이야기입니다
등록이 곧 계약입니다
백엔드 유닛은 초기화 섹션에서 RegisterJBIG2EncoderBackend를 호출해 스스로를 등록합니다. 호출자는 값이 4인 옵션 비트 PDF_JBIG2_OPTION_EXTERNAL_ENCODER를 통해서나, 확장 이미지 진입점의 UseExternalEncoder 매개변수를 통해 요청합니다. 라이브러리 엄브렐라 유닛은 의도적으로 백엔드 유닛을 끌어들이지 않습니다. 큰 오브젝트 집합을 안는 것은 각 프로젝트의 결정이어야 하기 때문입니다. 예컨대 C++Builder 트리에서는 원하는 프로젝트가 명시적으로 포함합니다
호출자 입장의 결과는, 외부 인코더를 요청하는 것은 보장이 아니라 선호이며, 유닛을 빠뜨린 빌드는 오류 대신 더 큰 파일을 만든다는 것입니다. 더 나은 인코더를 요청할 만큼 출력 크기가 중요하다면, 정말 그 인코더를 얻었는지 확인할 만큼도 중요합니다
uses
PDFlibrary,
{$IFDEF FPC}
PDFlibJBIG2EncDLL; // Free Pascal용 동적 백엔드
{$ELSE}
PDFlibJBIG2EncC; // Delphi / C++Builder용 정적 오브젝트 집합
{$ENDIF}
var
Pdf: TPDFlib;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
0, 1, 1, 0, 0, 0);
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
유닛 컴파일은 두 줄이었습니다. 심볼이 진짜 작업이었습니다
백엔드 유닛 자체를 Free Pascal 아래에서 컴파일되게 하는 데는 정확히 두 가지 변경이 필요했습니다. 어셈블러 방언을 설정한 것, 그리고 레코드 기반 포맷 설정 생성자를 전역 기본 변수로 바꾼 것입니다. 단순한 Pascal이 두 컴파일러 사이에서 얼마나 이식성이 좋은지를 공정하게 보여 주는 대목입니다
심볼 쪽이 진짜 작업이었습니다. 오브젝트 집합은 C 심볼 176개를 참조합니다. 그중 128개는 유닛 안에 이미 Pascal 구현이 있어 export 이름만 붙이면 되었습니다. Delphi는 함수 이름을 심볼 이름으로 쓰지만 Free Pascal은 명시적 public name 선언을 요구하기 때문입니다. 27개는 JPEG 2000 코덱과 공유되어 정확히 한 곳에서 export되어야 했습니다. 두 번 정의하면 둘을 함께 링크하는 프로그램이 깨지기 때문입니다. 나머지 21개는 플랫폼과 C 런타임 엔트리, 즉 Win32 파일 함수 16개에 표준 라이브러리 호출 몇 개를 더한 것으로, 새 호환성 유닛에 들어갔습니다
어느 것도 개념적으로 어렵지는 않지만, 링커가 시도조차 하려면 전부 필요합니다. 막힌 곳은 바로 링커였습니다
세 가지 링킹 경로, 세 번의 막다른 길
Free Pascal 내부 링커는 오브젝트 파일을 읽지 못합니다. 이 파일들이 연관(associative) COMDAT 섹션을 내보내는 컴파일러로 만들어졌는데, 내부 링커는 이를 지원하지 않는다고 보고하기 때문입니다. 경고가 아니라 완전한 거절입니다
외부 링커로 바꾸는 것이 해답처럼 보였습니다. Free Pascal에 번들된 binutils 링커는 이 아카이브에 섹션 가비지 컬렉션을 적용하다가 그대로 크래시가 납니다. 그리고 그 플래그는 Free Pascal이 64비트 Windows 타깃에 넘기는 고정 파라미터 집합의 일부라 명령줄에서 제거할 수 없고, 이를 억제한다고 문서화된 스위치는 이 경로에서 무시됩니다. 훨씬 새 binutils를 갖다 대면 이번에는 다르게 실패합니다. Free Pascal 링크 스크립트를 전혀 처리하지 못해, 스크립트 없이는 빈 출력을, 스크립트와 함께라면 relocation 오류의 벽을 내놓습니다
링커 문제를 한 번도 겪지 않을 사람에게도 알아 둘 가치가 있는 경계 하나가 과정에서 발견되었습니다. 외부 링커는 오브젝트 파일 경로를 소스 트리가 아니라 실행 파일 출력 디렉터리 기준으로 해석하므로, 상대 경로 include-object 지시문은 출력 디렉터리가 우연히도 컴파일 시점 작업 디렉터리와 같을 때만 동작합니다. 라이브러리는 소비자 프로젝트에 대해 그것을 가정할 수 없으며, 이것만으로도 느슨한 오브젝트보다 링크된 라이브러리를 선호할 이유가 됩니다
다른 C++ 컴파일러가 도움이 되지 않는 이유
다음으로 당연해 보이는 발상은 Free Pascal이 읽을 수 있는 오브젝트를 내는 컴파일러로 C++ 쪽을 다시 빌드하는 것입니다. 이것도 안 되며, 이유는 스위치 문제가 아니라 근본적인 것입니다. 템플릿을 담은 최소 C++ 번역 단위는 코드 생성 기능을 전부 끄고 컴파일해도 weak external 심볼을 내보냅니다. 템플릿과 inline 인스턴스화가 구조적으로 이를 만들어 내기 때문입니다. Free Pascal은 그 심볼 클래스를 완전히 거부합니다. 반대 방향도 실패합니다. 주류 C++ 링커는 같은 COMDAT 섹션 처리 때문에 다른 쪽 컴파일러의 오브젝트를 받아들이지 못합니다
결국 C++ 코드는 어떤 가용 경로로도 오브젝트 형태로 Free Pascal에 전달될 수 없습니다. DLL로는 전달할 수 있고, 실제로 그렇게 되었습니다. 인코더와 그 이미지 처리 의존성이 평면 C 진입점 두 개를 노출하는 라이브러리 하나로 빌드되고, Free Pascal 백엔드 유닛이 이를 동적으로 바인드해 정적 백엔드와 똑같이 자신을 등록합니다. Delphi와 C++Builder 경로는 전혀 건드리지 않았으며, 이것이 올바른 결과입니다. 한 툴체인의 이식성 문제가 이미 동작하던 툴체인을 흔들어서는 안 됩니다
물릴 것은 단 하나, 극성입니다
Windows 바이레벨 비트맵과 JBIG2 인코더 사이에는 어떤 타입 시스템도 잡아 주지 못하는 관례 불일치가 있습니다. 픽셀당 1비트 디바이스 독립 비트맵(DIB) 스캔라인은 설정된 비트를 흰색으로 봅니다. 인코더는 설정된 비트를 검은색으로 봅니다. 스캔라인을 그대로 넘기면 페이지의 사진 네거티브로 된 완전히 유효한 JBIG2 스트림을 받게 됩니다
// 1비트 DIB: 설정된 비트는 흰색. JBIG2 인코더: 설정된 비트는
// 검은색. 들어가는 길에 모든 바이트를 반전합니다
for I := 0 to RowBytes - 1 do
Row[I] := Row[I] xor $FF;
검증 방법은 수정만큼 중요합니다. 압축 스트림 길이를 비교하면 아무것도 알 수 없습니다. 네거티브 이미지도 비슷한 크기로 압축되기 때문입니다. 페이지를 눈으로 보는 것은 눈에 띄게 반전되지 않았다는 것만 증명합니다. 믿을 수 있는 확인은 두 인코딩 경로, 즉 네이티브 Pascal과 외부의 출력을 PNG로 렌더링해 바이트 단위로 비교하는 것입니다. 두 인코더 모두 같은 소스 이미지에서 무손실이므로 정확히 일치하는 것 외의 무엇이든 둘 중 하나의 버그입니다. 이 비교는 이제 상시 회귀 테스트이며, 두 구현이 정확히 일치해야 할 때마다 만들어 둘 가치가 있는 종류의 단정입니다
어느 백엔드를 쓸 것인가
일반적인 바이레벨 콘텐츠, 디더링된 하프톤, 라인 아트, 혼합 그래픽에는 네이티브 Pascal MMR 인코더로 충분하고 배포 비용도 없습니다. JBIG2가 설계된 대상인 스캔 텍스트에는 외부 심볼 사전 인코더가 크기 절감이 사는 곳입니다. 반복되는 글리프 모양을 매번 다시 인코딩하는 대신 사전으로 묶어 내기 때문입니다. 스캔 문서 아카이브를 만든다면 그 차이는 저장 용량 계획을 바꿀 만큼 큽니다
애초에 바이레벨 이미지를 어떻게 만드는가라는 상류 문제도 출력 크기만큼 중요합니다. 영역 기반 모노크롬 렌더링은 모노크롬 영역 렌더링 문서에서, 문서 전체 크기 전략은 PDF 파일 크기 최적화와 폰트 서브세팅에서 다룹니다. 반복되는 페이지가 있는 스캔 세트에는 더 나은 압축보다 중복 제거가 유리한 경우가 많은데, 지각적 이미지 중복 제거가 바로 그 주제입니다. 플랫폼별 툴체인과 백엔드 가용성은 losLab PDF Developer Library 제품 페이지에 정리되어 있습니다