Delphi용 PDF 라이브러리인 PDFlibPas는 8비트 베이스라인을 넘는 JPEG 압축을 담은 TIFF 이미지를 임포트한다. 12비트 extended sequential(SOF1), Huffman 무손실(SOF3), 산술 부호화(SOF9와 SOF10), 그리고 테이블 오프셋을 지닌 구식 TIFF 6.0 JPEG가 그것이다. PDF DCTDecode는 이런 프로세스를 담을 수 없기 때문에, PDFlibPas는 네이티브 Pascal로 디코딩하고 샘플을 컴포넌트당 8비트 또는 16비트로 무손실 임베딩한다
전체 지원에는 v3.724.0 이상이 필요하며, 겨냥한 곳은 명확하다. 스캐너, 의료·과학 캡처 소프트웨어, 1990년대에 얼어 버린 아카이브 시스템이 내놓는 TIFF다. 아래에서는 이 변형들을 PDF에 있는 그대로 복사할 수 없는 이유, PDFlibPas가 이들을 어떻게 처리하는지, 그리고 그 과정에서 발견된 Pascal 함정들을 다룬다
12비트 JPEG는 왜 PDF에 곧바로 들어가지 못하는가?
12비트 JPEG는 통과시킬 수 없다. 이것을 디코딩하도록 요구받는 PDF 필터가 없기 때문이다. ISO 32000-1 §7.4.8은 DCTDecode를 JPEG 베이스라인 포맷을 기준으로 정의하며, progressive 확장은 PDF 1.3에서 추가되었다. 이 조항 어디에도 12비트 샘플 정밀도, ITU-T T.81 부록 H의 무손실 프로세스, 부록 D의 산술 엔트로피 코더는 없다. DCTDecode 스트림 안에서 SOF1 12비트 프레임이나 SOF9 마커를 만난 리더는 아무것도 그리지 않아도 되고, 대다수 뷰어는 실제로 그렇게 한다
TIFF 속 JPEG의 대부분을 이루는 8비트 Huffman 베이스라인과 progressive 스트립에 대해서는, PDFlibPas가 여전히 모든 스캔을 검증하고 압축된 바이트를 DCTDecode로 통과시켜 PDF를 작게 유지한다. 나머지는 전부 디코딩된다. PDFlibPas는 첫 JPEG 블록의 프레임 마커와 샘플 정밀도를 읽고, SOF3, SOF9, SOF10 또는 8이 아닌 정밀도를 발견하면 페이지를 통과 경로 대신 샘플 디코더로 보낸다. 프레임 마커로 산술 부호화를 탐지하는 일은 들리는 것보다 중요하다. 샘플 정밀도만 보는 검사에서는 클래식 TIFF가 8비트 SOF9 스트립을 DCTDecode 경로로 몰래 넘겨 리더가 표시할 수 없는 PDF를 만들어 내기 때문이다
PDFlibPas가 대신 PDF에 기록하는 것
PDFlibPas는 디코딩된 JPEG 블록을 BitsPerComponent 8 또는 16인 평범한 이미지 XObject로 바꾼다. 샘플은 기본적으로 비압축으로 저장된다. 이미지를 추가하기 전에 CompressImages(1)을 호출하면 PDFlibPas가 FlateDecode로 기록한다. ISO 32000-1 §8.9.5.1은 PDF 1.5부터 컴포넌트당 16비트를 허용하므로, 16비트 샘플 이미지가 추가될 때마다 PDFlibPas는 문서 버전을 최소 1.5로 올린다. multi-strip과 타일 이미지도 포함인데, 이들의 루트 객체는 자체적으로 들여다볼 BitsPerComponent가 없는 Form XObject다
매핑은 JPEG 프로세스별로 고정되어 있다
- 12비트 Huffman JPEG(sequential SOF1, progressive SOF2)는 역양자화되고 역변환된 뒤 16비트 샘플로 넓혀진다
- 산술 부호화 JPEG(sequential SOF9, progressive SOF10)는 8비트 정밀도에서는 8비트 샘플로, 12비트 정밀도에서는 16비트 샘플로 된다
- Huffman 무손실 JPEG(SOF3)는 선언된 정밀도와 정확히 일치하는 8비트 또는 16비트 샘플로 된다
12비트에서 16비트로의 확장은 비트 복제, 즉 (V shl 4) or (V shr 8)을 쓰므로 4095는 65535로 대응되고 흰색은 65520에서 뚜껑이 닫히는 대신 흰색으로 유지된다. PDFlibPas는 여러분 몰래 이런 이미지를 8비트로 떨어뜨리지 않는다. 라이브러리가 자체 표시 면을 위해 그런 페이지를 래스터라이즈할 때는 샘플의 상위 바이트를 쓰지만, PDF에 임베딩된 이미지는 열여섯 비트를 전부 간직한다
대가는 파일 크기다. CompressImages(1)을 켜도 Flate 압축 16비트 샘플은 원래의 DCT 데이터보다 크고, 끄면 더 크다. 진단·아카이브 워크플로라면 이 거래는 대개 옳다. 특정 작업에서 크기가 더 중요하다면 Delphi에서 PDF 파일 크기 줄이기 가이드가 바이트가 실제로 어디로 가는지 보여 준다
TPDFlib로 이런 TIFF 임포트하기
호출 코드는 전혀 바뀌지 않는다. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer는 Options로 1 기반 TIFF 페이지 번호를 받아 이미지 ID를 돌려주고, 페이지를 임포트할 수 없으면 0을 돌려준다. 지원하지 않는 JPEG 변형은 이미지가 삽입되기 전에 거부되므로, 0은 절반쯤 디코딩된 무언가가 문서에 들어갔다는 뜻이 결코 아니다
uses
PDFlibrary;
function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
Lib: TPDFlib;
Page, PageCount, ImageID: Integer;
DPI, W, H: Double;
begin
Result := False;
Lib := TPDFlib.Create;
try
Lib.SetMeasurementUnits(0); // 기본 user space, 1/72인치
Lib.SetOrigin(1); // DrawImage용 좌상단 원점
Lib.CompressImages(1); // 디코딩된 샘플에 Flate 적용
PageCount := Lib.GetImagePageCount(TiffName);
if PageCount = 0 then
Exit;
for Page := 1 to PageCount do
begin
ImageID := Lib.AddImageFromFile(TiffName, Page); // Options = TIFF 페이지
if ImageID = 0 then
Exit; // 지원하지 않거나 손상된 이미지 데이터
Lib.SelectImage(ImageID);
case Lib.ImageResolutionUnits of
2: DPI := Lib.ImageHorizontalResolution; // 인치당 도트 수
3: DPI := Lib.ImageHorizontalResolution * 2.54; // 센티미터당 도트 수
else
DPI := 0;
end;
if DPI <= 0 then
DPI := 300;
W := Lib.ImageWidth * 72 / DPI;
H := Lib.ImageHeight * 72 / DPI;
if Page > 1 then
Lib.NewPage;
Lib.SetPageDimensions(W, H);
Lib.DrawImage(0, 0, W, H);
end;
Result := Lib.SaveToFile(PdfName) = 1;
finally
Lib.Free;
end;
end;
포맷별 분기는 필요 없고, 메모리에 들고 있는 TIFF 데이터에는 AddImageFromStream과 AddImageFromString이 같은 규칙으로 동작한다
구식 JPEG: 오프셋에 있는 테이블, 헤더 없는 스트립
v3.713.0부터 PDFlibPas는 테이블이 JPEGQTables, JPEGDCTables, JPEGACTables 오프셋 태그나 테이블 전용 JPEGInterchangeFormat 스트림에서 오는 구식 JPEG TIFF(Compression = 6)를 임포트한다. TIFF 6.0 Section 22가 이 레이아웃을 정의했고, TIFF Technical Note 2가 나중에 Compression = 7과 공유 JPEGTables 태그로 대체했지만, 스캐너들은 오랫동안 구식 형태를 계속 썼다
컨테이너 측면인 BigTIFF와 타일 레이아웃은 TIFF 디코더 강화에 관한 글이 다루고, 여기서의 초점은 JPEG 페이로드다. 각 테이블 오프셋 태그는 원시 64바이트 양자화 테이블이나, 16개의 counts 뒤에 값들이 붙는 Huffman 테이블을 컴포넌트별 하나씩 가리킨다. PDFlibPas는 이것들을 합성한 JPEGTables 스트림의 DQT와 DHT 세그먼트로 바꾼다. 그다음 벌거벗은 엔트로피 부호화 데이터 스트립마다 자체 헤더를 붙인다. SOI, 그 스트립의 행 수를 담은 프레임 마커, JPEGRestartInterval이 설정됐을 때의 DRI, SOS, 엔트로피 데이터, EOI가 그것이다. 스트립들을 한 스캔으로 붙일 수는 없다. 구식 JPEG는 스트립 경계마다 DC 예측을 재시작하기 때문이다
잘못 잡기 쉬운 디테일이 둘 있다. YCbCr 이미지의 세 컴포넌트는 종종 테이블 오프셋을 같은 바이트로 가리키므로, PDFlibPas는 테이블 번호를 매기기 전에 내용으로 테이블 중복을 제거한다. 한 클래스의 Huffman 테이블이 둘을 넘게 남으면 합성된 프레임은 SOF0 대신 SOF1을 쓴다. 베이스라인 프레임은 DC 테이블 둘과 AC 테이블 둘만 참조할 수 있기 때문이다. JPEGInterchangeFormat이 스캔을 담은 완전한 JPEG 스트림을 가리킬 때는 PDFlibPas가 그 스트림을 이미지 전체로 취급해 표준 방식의 단일 스트립으로 임포트한다. 무손실, 타일, 분리 평면 구식 JPEG는 추측 대신 명시적 오류로 거부하니, 이런 것들은 상류에서 표준 방식 JPEG로 변환하라
TIFF 속 CMYK JPEG가 반전되어 나오는 이유는?
TIFF 속 CMYK JPEG가 반전되어 나오는 것은 임포터가 단독 JPEG 파일에나 맞는 Adobe APP14 관례를 적용할 때다. Photoshop은 단독 JPEG에 반전된 CMYK를 기록하므로 PDF 생산자는 전통적으로 보정하려고 /Decode [1 0 1 0 1 0 1 0]을 추가해 왔다. TIFF PhotometricInterpretation 5는 샘플을 잉크량으로 정의하며 0은 잉크 없음을 뜻하고, 주류 TIFF 리더는 APP14를 보지 않고 디코딩된 JPEG 샘플을 그대로 받아들인다
PDFlibPas는 JPEG 압축 CMYK TIFF 스트립을 반전된 Decode 배열 없이 임베딩한다. APP14 마커가 있든 없든 마찬가지다. APP14 transform 플래그는 여전히 YCCK를 CMYK로 변환할지 결정하고, 그 단계는 DCTDecode가 스스로 처리한다. 단독 4컴포넌트 JPEG 파일은 반전된 Adobe 관례를 유지하므로, 같은 JPEG 바이트가 컨테이너에 따라 다르게 취급된다. 두 포맷이 각각 요구하는 바다
무손실 JPEG 디코딩은 왜 Pascal 시프트에서 깨지는가?
Pascal에서 무손실 JPEG 디코딩이 깨지는 이유는 예측기 5와 6이 산술 우시프트를 필요로 하는데 Pascal의 shr는 논리 시프트이기 때문이다. SOF3는 각 샘플을 왼쪽(Ra), 위(Rb), 좌상단(Rc) 이웃으로부터 예측하고 차이만 부호화한다. T.81 Table H.1의 7개 예측기 중 예측기 5는 Ra + ((Rb - Rc) >> 1), 예측기 6은 Rb + ((Ra - Rc) >> 1)인데, 괄호 안의 차이는 좌상단 이웃이 더 밝을 때마다 음수가 된다. 음수 Integer를 shr에 넣으면 부호 비트가 크기 비트로 바뀌고 이미지는 노이즈로 녹아든다
시프트를 div 2로 바꿔도 해결되지 않는다. div는 0쪽으로 절사하는 반면 산술 시프트는 음의 무한대 쪽으로 내림하고, 둘은 홀수인 음수마다 엇갈린다. Free Pascal은 System 유닛에 SarLongint를 갖고 있지만 Delphi에는 동등한 것이 없어서, PDFlibPas는 양쪽에서 똑같이 컴파일되는 헬퍼를 쓴다
// 1비트 산술 우시프트: floor(Value / 2)
// shr은 논리 시프트이고 div 2는 0쪽으로 절사한다
function HalfDown(Value: Integer): Integer;
begin
if Value >= 0 then
Result := Value shr 1
else
Result := -((1 - Value) shr 1);
end;
// Value = -3인 경우: -3 shr 1 = 2147483646, -3 div 2 = -1, HalfDown(-3) = -2
같은 부류의 문제에서 타입 폭 쪽은 Delphi Linux64의 Longint와 포인터 함정을 보라
무손실 디코더의 나머지 부분에도 고유한 경계 규칙이 있다. 첫 행과 재시작 마커 직후의 첫 행은 왼쪽 이웃으로만 예측하며 정밀도 P와 point transform Pt에 대해 2^(P−Pt−1)에서 시작한다. 그다음 행들은 모두 위쪽 샘플에서 시작한다. 복원은 2^16 모듈로로 감싸고, point transform은 출력에만 스케일을 적용하며 예측 체인은 건드리지 않는다. 추가 비트 없는 32768 차이인 Huffman DC 범주 16은 무손실 모드에서만 합법인데, 그래서 테이블 파서는 프레임이 어떤 프로세스를 선언할지 알기 전에 이것을 받아들일 수 있어야 한다
PDFlibPas는 7개 예측기와 point transform, 행 전체를 커버하는 재시작 인터벌을 지원한다. JPEG 정밀도는 BitsPerSample과 같아야 하고 8 또는 16이어야 하며, 모든 컴포넌트는 1x1로 샘플링되어야 한다. YCbCr 무손실 데이터와 12비트 무손실 JPEG는 명시적 오류로 거부된다
네이티브 Pascal의 12비트 DCT와 산술 부호화
PDFlibPas의 12비트 디코더와 산술 디코더는 역 DCT 하나를 공유하고 엔트로피 디코딩만 다르다. 12비트 Huffman JPEG에서 DC 범주는 15까지, AC 범주는 14까지 이르고, 양자화 테이블은 16비트 항목을 담을 수 있으며, DC 예측은 스캔과 재시작마다 리셋된다. 계수는 역양자화 때까지 지그재그 순서로 유지되고, 역양자화는 같은 지그재그 인덱스의 양자화 값을 써야 하며, 그 이후에야 분리형 2패스 IDCT를 위한 자연 순서로 옮겨간다. 디코딩된 샘플은 독립적인 부동소수점 참조 디코더의 ±1 이내에 들어온다
산술 부호화는 Huffman을 T.81 부록 D의 QM 코더로 대체한다. 114항 확률 상태 표, 64칸 DC 통계 영역, 256칸 AC 영역이 DAC 세그먼트의 조건부 파라미터(기본값 L = 0, U = 1, Kx = 5)에 이끌리는 방식이다. 통계, DC 컨텍스트, 예측은 재시작 인터벌마다, 그리고 모든 스캔 시작마다 리셋된다. PDFlibPas가 산술 이미지용으로 공유 JPEGTables를 정규화할 때 DAC 세그먼트를 유지하는 이유가 바로 이것이다. 버리면 모든 스트립이 엉뚱한 통계 모델로 디코딩된다
v3.723.0은 이 디코딩 경로에 서브샘플링된 컴포넌트와 JFIF 방정식을 쓴 YCbCr-to-RGB 변환을 추가했고, v3.724.0은 12비트 Huffman(SOF2)과 산술(SOF10) 데이터 양쪽에 progressive 스캔을 추가했다. 서브샘플링된 크로마는 부드럽게 하지 않고 전체 해상도로 복제되는데, 이것은 참조 디코더에서 크로마 스무딩을 끈 결과다. 계층형(hierarchical) JPEG와 산술 부호화 무손실 JPEG(SOF11)는 명시적 오류로 거부된다
Delphi에서 바이너리 JPEG 픽스처 테스트하기
Delphi 유닛 테스트에서 손으로 만든 JPEG 픽스처는 디코더 버그처럼 보이는 방식으로 실패할 수 있다. #$xx 문자로 만든 문자열 상수가 255자를 넘으면 UnicodeString으로 타입이 정해지고, AnsiString에 대입하는 순간 활성 코드 페이지를 거쳐 변환된다. 엔트로피 부호화 데이터에는 흔한 $80부터 $9F 바이트가 들어오는 길에 다시 쓰인다. 런타임에 마커 헤더를 조립할 때도 같은 함정이 있다. Delphi에서 AnsiChar + AnsiChar는 UnicodeString을 내놓으므로, AnsiString에는 바이트 하나씩 append하거나 SetLength와 인덱스 대입으로 기록하라. 픽스처라면 헥스 텍스트로 저장하고 런타임에 디코딩하라
uses
SysUtils, PDFlibrary;
function HexToBytes(const Hex: AnsiString): AnsiString;
var
I: Integer;
begin
SetLength(Result, Length(Hex) div 2);
for I := 1 to Length(Result) do
Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;
// 픽스처는 순수 ASCII로 남으므로 어떤 코드 페이지도 건드릴 수 없다
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);
테스트 이미지를 만들 때는 jpegtran -arithmetic이 sequential SOF9를 기록하고, SOF10에는 -progressive도 필요하다는 점을 유념하라. 그리고 0이 아닌 이미지 ID는 임포트가 받아들여졌다는 것만 증명한다. 페이지를 렌더링해 픽셀을 비교하라. 통과시키기 실수는 어떤 뷰어도 그릴 수 없는 잘 구성된 PDF를 낳는다
레거시 스캔 아카이브를 PDF로 옮기기
이런 JPEG 변형들은 아카이브 마이그레이션 도중에 한꺼번에 표면화하는 경향이 있다. PDFlibPas는 12비트, 무손실, 산술, 구식 JPEG TIFF를 다른 모든 것과 같은 AddImageFromFile 호출로 읽어 들이고, PDF 안에 전체 정밀도를 간직하며, 그 집합 밖의 드문 프로세스는 명시적 오류로 거부한다. 라이선스, 전체 기능 목록, 체험판 내려받기는 PDFlibPas Delphi PDF 라이브러리 제품 페이지를 보라